consumer模块完成度85%,连接服务器supabase,新建相关表

This commit is contained in:
2026-01-29 17:28:47 +08:00
parent a4fa00c935
commit ab038ec029
15 changed files with 4475 additions and 4793 deletions

View File

@@ -82,7 +82,26 @@ export class AkSupaQueryBuilder {
is(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); }
contains(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cs', value); }
containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); }
not(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'not', value); }
not(field : string, opOrValue : any, value?: any) : AkSupaQueryBuilder {
if (value !== undefined) {
// 三元形式field, operator, value
// 例如 not('badge', 'is', null) -> badge=not.is.null
const combinedOp = 'not.' + opOrValue;
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
let safeValue = value;
if (value === null) {
safeValue = 'null';
}
return this._addCond(field, combinedOp, safeValue);
} else {
// 二元形式field, value
let safeValue = opOrValue;
if (opOrValue === null) {
safeValue = 'null';
}
return this._addCond(field, 'not', safeValue);
}
}
and() : AkSupaQueryBuilder { this._nextLogic = 'and'; return this; }
or(str ?: string) : AkSupaQueryBuilder {
@@ -97,7 +116,12 @@ export class AkSupaQueryBuilder {
private _addCond(afield : string, op : string, value : any) : AkSupaQueryBuilder {
//console.log('add cond:', op, afield, value)
const field = encodeURIComponent(afield)!!
this._conditions.push({ field, op, value, logic: this._nextLogic });
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
let safeValue = value;
if (value === null) {
safeValue = 'null';
}
this._conditions.push({ field, op, value: safeValue, logic: this._nextLogic });
//console.log(this._conditions)
this._nextLogic = 'and';
return this;
@@ -213,8 +237,8 @@ export class AkSupaQueryBuilder {
const val = cond.value;
if ((op == 'in' || op == 'not.in') && Array.isArray(val)) {
params.push(`${k}=${op}.(${val.map(x => typeof x == 'object' ? encodeURIComponent(JSON.stringify(x)) : encodeURIComponent(x.toString())).join(',')})`);
} else if (op == 'is' && (val == null || val == 'null')) {
params.push(`${k}=is.null`);
} else if ((op == 'is' || op == 'not.is') && (val == null || val == 'null')) {
params.push(`${k}=${op}.null`);
} else {
const opvalstr: string = (typeof val == 'object') ? JSON.stringify(val) : (val as string);
params.push(`${k}=${op}.${encodeURIComponent(opvalstr)}`);

View File

@@ -156,6 +156,42 @@ const selectAddress = (item: Address) => {
</script>
<style>
.item-actions {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.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);
display: flex;
justify-content: center; /* 居中显示 */
align-items: center;
}
.add-btn {
background-color: #ff5000;
color: white;
border-radius: 25px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: none;
width: 100%; /* 默认占满 */
max-width: 100%;
}
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.address-list {
@@ -173,135 +209,11 @@ const selectAddress = (item: Address) => {
left: 50%;
transform: translateX(-50%);
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
border-radius: 12px 12px 0 0; /* 桌面端加点圆角更美观 */
}
.add-btn {
width: 300px; /* 桌面端限制宽度 */
}
}
.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-actions {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.action-item {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.action-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

View File

@@ -111,320 +111,60 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import type { Category, Product } from '@/utils/supabaseService.uts'
// 响应式数据
const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度
const primaryCategories = ref<any[]>([])
const productList = ref<any[]>([])
const activePrimary = ref<string>('cold')
const primaryCategories = ref<Category[]>([])
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const cartCount = ref(3)
const hasMore = ref(true)
// 获取当前分类信息
const currentCategoryName = ref('感冒发烧')
const currentCategoryDesc = ref('解热镇痛')
const currentCategoryName = ref('')
const currentCategoryDesc = ref('')
// 页面参数
const pageParams = ref<any>({})
// 医药分类数据(与主页一致)
const medicineCategories = [
{ id: 'cold', name: '感冒发烧', icon: '🤧', desc: '解热镇痛', color: '#2196F3' },
{ id: 'stomach', name: '肠胃用药', icon: '🤢', desc: '消化系统', color: '#4CAF50' },
{ id: 'pain', name: '止痛消炎', icon: '💊', desc: '镇痛消炎', color: '#F44336' },
{ id: 'skin', name: '皮肤用药', icon: '🤕', desc: '皮肤护理', color: '#9C27B0' },
{ id: 'vitamin', name: '维生素', icon: '🍊', desc: '营养补充', color: '#FF9800' },
{ id: 'chronic', name: '慢性病', icon: '🫀', desc: '长期管理', color: '#795548' },
{ id: 'child', name: '儿童用药', icon: '👶', desc: '儿童专用', color: '#00BCD4' },
{ id: 'external', name: '外用药品', icon: '🧴', desc: '外用制剂', color: '#8BC34A' },
{ id: 'device', name: '医疗器械', icon: '🩺', desc: '医疗设备', color: '#607D8B' },
{ id: 'health', name: '健康食品', icon: '🥗', desc: '保健食品', color: '#FFC107' }
]
// Mock 商品数据 - 使用本地图片避免网络请求
const mockProducts = {
cold: [
{
id: 'cold1',
shopId: 'shop_001',
shopName: '修正药业官方旗舰店',
name: '布洛芬缓释胶囊',
specification: '0.3g*24粒',
price: 18.5,
originalPrice: 25.8,
image: '/static/images/default-product.png',
manufacturer: '修正药业',
sales: 2560,
badge: '热销'
},
{
id: 'cold2',
shopId: 'shop_002',
shopName: '白云山大药房',
name: '板蓝根颗粒',
specification: '10g*20袋',
price: 22.8,
originalPrice: 29.9,
image: '/static/images/default-product.png',
manufacturer: '白云山',
sales: 1890,
badge: '推荐'
},
{
id: 'cold3',
shopId: 'shop_003',
shopName: '以岭药业自营店',
name: '连花清瘟胶囊',
specification: '0.35g*36粒',
price: 42.8,
originalPrice: 48.0,
image: '/static/images/default-product.png',
manufacturer: '以岭药业',
sales: 3200,
badge: '爆款'
},
{
id: 'cold4',
shopId: 'shop_004',
shopName: '强生制药旗舰店',
name: '对乙酰氨基酚片',
specification: '0.5g*12片',
price: 8.9,
originalPrice: 12.0,
image: '/static/images/default-product.png',
manufacturer: '强生制药',
sales: 1420,
badge: '特价'
},
{
id: 'cold5',
shopId: 'shop_005',
shopName: '同仁堂大药房',
name: '感冒清热颗粒',
specification: '3g*10袋',
price: 16.5,
originalPrice: 19.9,
image: '/static/images/default-product.png',
manufacturer: '同仁堂',
sales: 980,
badge: '新品'
},
{
id: 'cold6',
shopId: 'shop_006',
shopName: '三九医药旗舰店',
name: '复方氨酚烷胺片',
specification: '10片/盒',
price: 12.8,
originalPrice: 15.0,
image: '/static/images/default-product.png',
manufacturer: '三九医药',
sales: 1650,
badge: '家庭装'
}
],
stomach: [
{
id: 'stomach1',
shopId: 'shop_006',
shopName: '三九医药旗舰店',
name: '胃康灵胶囊',
specification: '0.4g*24粒',
price: 32.8,
originalPrice: 38.5,
image: '/static/images/default-product.png',
manufacturer: '三九医药',
sales: 890,
badge: '热销'
},
{
id: 'stomach2',
shopId: 'shop_007',
shopName: '阿斯利康医药',
name: '奥美拉唑肠溶胶囊',
specification: '20mg*14粒',
price: 28.5,
originalPrice: 35.0,
image: '/static/images/default-product.png',
manufacturer: '阿斯利康',
sales: 1250,
badge: '处方药'
},
{
id: 'stomach3',
shopId: 'shop_008',
shopName: '江中制药旗舰店',
name: '健胃消食片',
specification: '0.8g*32片',
price: 15.9,
originalPrice: 19.9,
image: '/static/images/default-product.png',
manufacturer: '江中制药',
sales: 2100,
badge: '推荐'
},
{
id: 'stomach4',
shopId: 'shop_009',
shopName: '益普生大药房',
name: '蒙脱石散',
specification: '3g*10袋',
price: 18.6,
originalPrice: 22.0,
image: '/static/images/default-product.png',
manufacturer: '益普生',
sales: 1780,
badge: '止泻'
},
{
id: 'stomach5',
shopId: 'shop_010',
shopName: '西安杨森旗舰店',
name: '多潘立酮片',
specification: '10mg*30片',
price: 22.8,
originalPrice: 26.5,
image: '/static/images/default-product.png',
manufacturer: '西安杨森',
sales: 950,
badge: '促消化'
},
{
id: 'stomach6',
shopId: 'shop_011',
shopName: '拜耳医药自营店',
name: '铝碳酸镁咀嚼片',
specification: '0.5g*20片',
price: 25.9,
originalPrice: 29.9,
image: '/static/images/default-product.png',
manufacturer: '拜耳',
sales: 1320,
badge: '护胃'
}
],
pain: [
{
id: 'pain1',
shopId: 'shop_012',
shopName: '华北制药旗舰店',
name: '阿莫西林胶囊',
specification: '0.25g*24粒',
price: 28.5,
originalPrice: 35.0,
image: '/static/images/default-product.png',
manufacturer: '华北制药',
sales: 1560,
badge: '处方药'
},
{
id: 'pain2',
shopId: 'shop_013',
shopName: '诺华制药旗舰店',
name: '双氯芬酸钠缓释片',
specification: '75mg*10片',
price: 19.8,
originalPrice: 24.0,
image: '/static/images/default-product.png',
manufacturer: '诺华制药',
sales: 1280,
badge: '止痛'
},
{
id: 'pain3',
shopId: 'shop_014',
shopName: '云南白药旗舰店',
name: '云南白药胶囊',
specification: '0.25g*32粒',
price: 35.9,
originalPrice: 42.0,
image: '/static/images/default-product.png',
manufacturer: '云南白药',
sales: 2350,
badge: '经典'
},
{
id: 'pain4',
shopId: 'shop_015',
shopName: '辉瑞医药旗舰店',
name: '塞来昔布胶囊',
specification: '0.2g*10粒',
price: 48.6,
originalPrice: 55.0,
image: '/static/images/default-product.png',
manufacturer: '辉瑞',
sales: 890,
badge: '抗炎'
},
{
id: 'pain5',
shopId: 'shop_016',
shopName: '中美史克大药房',
name: '布洛芬片',
specification: '0.1g*24片',
price: 12.5,
originalPrice: 15.0,
image: '/static/images/default-product.png',
manufacturer: '中美史克',
sales: 1680,
badge: '经济装'
},
{
id: 'pain6',
shopId: 'shop_002',
shopName: '白云山大药房',
name: '头孢克肟胶囊',
specification: '0.1g*6粒',
price: 32.8,
originalPrice: 38.0,
image: '/static/images/default-product.png',
manufacturer: '广州白云山',
sales: 1120,
badge: '抗生素'
}
]
}
// 补充其他分类的默认数据(简化版)
const generateDefaultProducts = (categoryId: string) => {
const baseProducts = [
{ name: '通用药品1', price: 25.8, manufacturer: '知名药企', sales: 1200 },
{ name: '通用药品2', price: 18.5, manufacturer: '知名药企', sales: 950 },
{ name: '通用药品3', price: 32.0, manufacturer: '知名药企', sales: 1450 },
{ name: '通用药品4', price: 22.8, manufacturer: '知名药企', sales: 880 },
{ name: '通用药品5', price: 28.9, manufacturer: '知名药企', sales: 1100 },
{ name: '通用药品6', price: 19.9, manufacturer: '知名药企', sales: 920 }
]
return baseProducts.map((product, index) => ({
id: `${categoryId}${index + 1}`,
shopId: `shop_default_${categoryId}_${index}`, // 确保不同分类店铺ID不同
shopName: '平台自营大药房',
...product,
specification: '规格待定',
originalPrice: product.price * 1.2,
image: '/static/images/default-product.png',
badge: index === 0 ? '热销' : index === 1 ? '推荐' : ''
}))
}
// 生命周期
onMounted(() => {
initPage()
console.log('=== category页面onMounted被调用 ===')
// 在onMounted中只初始化页面不处理分类参数
// 分类参数的处理交给onLoad函数因为onLoad在页面加载时执行
console.log('onMounted中不处理分类参数等待onLoad处理')
// 注意这里不再默认选择分类让onLoad函数处理分类选择
// 如果onLoad没有设置分类则保持默认状态
onMounted(async() => {
await loadCategories()
await loadProducts()
})
// 添加加载分类的方法
const loadCategories = async () => {
const categories = await supabaseService.getCategories()
if (categories.length > 0) {
primaryCategories.value = categories
// 设置默认选中第一个分类
if (!activePrimary.value && categories[0]) {
activePrimary.value = categories[0].id
}
}
}
// 加载商品数据
const loadProducts = async () => {
if (activePrimary.value) {
const response = await supabaseService.getProductsByCategory(activePrimary.value)
productList.value = response.data
hasMore.value = response.hasmore
// 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === activePrimary.value)
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
}
}
}
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
@@ -478,15 +218,8 @@ onLoad((options: any) => {
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数使用默认分类')
// 默认选中第一个分类
const defaultCategory = 'cold'
console.log('默认分类:', defaultCategory)
// 无论如何都重新加载一次默认分类的数据
setTimeout(() => {
selectPrimaryCategory(defaultCategory)
}, 100)
console.log('⚠️ onLoad中未找到分类参数使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
}
console.log('=== category页面onLoad执行完成 ===')
@@ -566,36 +299,22 @@ onShow(() => {
console.log('=== category页面onShow执行完成 ===')
})
// 初始化页面
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 保持与主页一致的固定高度计算,不进行动态调整
// 这样在移动端会与主页的视觉体验保持一致主页占位符固定为44px
headerHeight.value = 10
// 获取页面参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
pageParams.value = currentPage.options || {}
}
// 加载分类数据
primaryCategories.value = medicineCategories
}
// 选择一级分类
const selectPrimaryCategory = (categoryId: string) => {
const selectPrimaryCategory = async (categoryId: string) => {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
// 验证categoryId是否有效
if (!categoryId) {
console.error('categoryId为空使用默认分类')
categoryId = 'cold'
console.error('categoryId为空尝试使用第一个分类')
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
} else {
console.error('没有可用的分类')
return
}
}
console.log('验证后的categoryId:', categoryId)
@@ -606,18 +325,18 @@ const selectPrimaryCategory = (categoryId: string) => {
console.log('更新后的activePrimary:', activePrimary.value)
// 更新当前分类信息
const category = medicineCategories.find(cat => cat.id === categoryId)
const category = primaryCategories.value.find(cat => cat.id === categoryId)
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.desc
console.log('✅ 找到分类:', category.name, '描述:', category.desc)
currentCategoryDesc.value = category.description
console.log('✅ 找到分类:', category.name, '描述:', category.description)
} else {
console.error('❌ 未找到分类ID:', categoryId, ',使用默认分类')
console.error('❌ 未找到分类ID:', categoryId, ',使用第一个分类')
// 如果找不到对应的分类,使用第一个分类
if (medicineCategories.length > 0) {
const firstCategory = medicineCategories[0]
if (primaryCategories.value.length > 0) {
const firstCategory = primaryCategories.value[0]
currentCategoryName.value = firstCategory.name
currentCategoryDesc.value = firstCategory.desc
currentCategoryDesc.value = firstCategory.description
activePrimary.value = firstCategory.id
categoryId = firstCategory.id
console.log('使用默认分类:', firstCategory.name)
@@ -626,22 +345,15 @@ const selectPrimaryCategory = (categoryId: string) => {
console.log('准备加载商品数据...')
// 加载对应商品
if (mockProducts[categoryId]) {
productList.value = mockProducts[categoryId]
console.log('✅ 加载mock商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', mockProducts[categoryId].length)
console.log('商品列表:', mockProducts[categoryId])
} else {
productList.value = generateDefaultProducts(categoryId)
console.log('✅ 加载默认商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', productList.value.length)
console.log('商品列表:', productList.value)
}
// 加载对应商品 - 使用 Supabase 服务
const response = await supabaseService.getProductsByCategory(categoryId)
productList.value = response.data
hasMore.value = response.hasmore
hasMore.value = true
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', response.data.length)
console.log('商品列表:', response.data)
// 验证数据是否已正确更新
console.log('数据更新验证:')
@@ -702,7 +414,7 @@ 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/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}`
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}&name=${encodeURIComponent(product.name)}&image=${encodeURIComponent(product.image || '')}`
})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -244,85 +244,7 @@
</view>
</view>
<!-- 智能推荐 -->
<view class="smart-recommend">
<view class="section-header">
<view class="title-section">
<text class="section-icon">✨</text>
<text class="section-title">智能推荐</text>
</view>
<view class="recommend-filters">
<text
v-for="filter in recommendFilters"
:key="filter.id"
:class="['filter-item', { active: activeFilter === filter.id }]"
@click="switchFilter(filter.id)"
>
{{ filter.name }}
</text>
</view>
</view>
<view class="recommend-grid">
<view
v-for="product in recommendedProducts"
:key="product.id"
class="recommend-product"
@click="navigateToProduct(product)"
>
<view class="product-image-container">
<image
class="product-image"
:src="product.image"
mode="aspectFill"
/>
<view class="product-tags">
<text v-if="product.tag" class="product-tag">{{ product.tag }}</text>
<text v-if="product.featured" class="featured-tag">{{ product.featured }}</text>
</view>
</view>
<view class="product-details">
<text class="product-title">{{ product.name }}</text>
<text class="product-specification">{{ product.specification }}</text>
<view class="product-rating">
<view class="rating-stars">
<text class="star-icon">⭐</text>
<text class="rating-value">{{ product.rating }}</text>
</view>
<text class="reviews-count">{{ product.reviews }}条评价</text>
</view>
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
</text>
</view>
<view class="product-actions">
<view class="add-to-cart" @click.stop="addToCart(product)">
<text class="cart-icon">🛒</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && recommendedProducts.length > 0" class="no-more">
<text class="no-more-text">--- 已加载全部内容 ---</text>
</view>
</view>
<!-- 智能推荐模块已隐藏 -->
<!-- 健康提醒 -->
<view class="health-reminder">
@@ -347,6 +269,8 @@
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import supabaseService from '@/utils/supabaseService.uts'
import type { Product, Category } from '@/utils/supabaseService.uts'
// 响应式数据
const statusBarHeight = ref(0)
@@ -359,9 +283,8 @@ const activeFilter = ref('recommend')
const currentPage = ref(1)
// 数据源
const allProducts = ref<any[]>([])
const hotProducts = ref<any[]>([])
const recommendedProducts = ref<any[]>([])
const hotProducts = ref<Product[]>([])
const recommendedProducts = ref<Product[]>([])
// 屏幕尺寸检测
const isMobile = ref(false)
@@ -372,35 +295,18 @@ const lastScrollTop = ref(0)
const scrollThreshold = 30 // 降低滚动阈值,使其更灵敏
const scrollingUp = ref(false)
// 分类数据
const categories = [
{ id: 'cold', name: '感冒发烧', icon: '🤧', desc: '解热镇痛', color: '#2196F3' },
{ id: 'stomach', name: '肠胃用药', icon: '🤢', desc: '消化系统', color: '#4CAF50' },
{ id: 'pain', name: '止痛消炎', icon: '💊', desc: '镇痛消炎', color: '#F44336' },
{ id: 'skin', name: '皮肤用药', icon: '🤕', desc: '皮肤护理', color: '#9C27B0' },
{ id: 'vitamin', name: '维生素', icon: '🍊', desc: '营养补充', color: '#FF9800' },
{ id: 'chronic', name: '慢性病', icon: '🫀', desc: '长期管理', color: '#795548' },
{ id: 'child', name: '儿童用药', icon: '👶', desc: '儿童专用', color: '#00BCD4' },
{ id: 'external', name: '外用药品', icon: '🧴', desc: '外用制剂', color: '#8BC34A' },
{ id: 'device', name: '医疗器械', icon: '🩺', desc: '医疗设备', color: '#607D8B' },
{ id: 'health', name: '健康食品', icon: '🥗', desc: '保健食品', color: '#FFC107' }
]
// 分类数据 - 从Supabase获取
const categories = ref<Category[]>([])
// 排序标签
const sortTabs = [
{ id: 'recommend', name: '智能推荐' },
{ id: 'sales', name: '销量' },
{ id: 'price', name: '价格' },
{ id: 'new', name: '新品' },
{ id: 'recommend', name: '推荐' }
{ id: 'discount', name: '特价' }
]
// 推荐筛选器
const recommendFilters = [
{ id: 'recommend', name: '智能推荐' },
{ id: 'hot', name: '热门商品' },
{ id: 'discount', name: '限时优惠' },
{ id: 'quality', name: '品质优选' }
]
// 健康资讯
const healthNews = [
@@ -424,100 +330,89 @@ const healthNews = [
}
]
// 获取分类数据
const loadCategories = async () => {
try {
const categoriesData = await supabaseService.getCategories()
// 映射字段将description映射为desc保持与原有结构兼容
categories.value = categoriesData.map((cat: any) => ({
id: cat.id,
name: cat.name,
icon: cat.icon || '📦',
desc: cat.description || cat.desc || '',
color: cat.color || '#4CAF50'
}))
} catch (error) {
console.error('加载分类数据失败:', error)
// 如果加载失败,使用默认分类作为后备
categories.value = [
{ id: 'cold', name: '感冒发烧', icon: '🤧', desc: '解热镇痛', color: '#2196F3' },
{ id: 'stomach', name: '肠胃用药', icon: '🤢', desc: '消化系统', color: '#4CAF50' },
{ id: 'pain', name: '止痛消炎', icon: '💊', desc: '镇痛消炎', color: '#F44336' },
{ id: 'skin', name: '皮肤用药', icon: '🤕', desc: '皮肤护理', color: '#9C27B0' },
{ id: 'vitamin', name: '维生素', icon: '🍊', desc: '营养补充', color: '#FF9800' }
]
}
}
// 获取热销商品(根据当前排序方式)
const loadHotProducts = async () => {
try {
let products: Product[] = []
const limit = 6
console.log('加载热销商品,当前排序方式:', activeSort.value)
switch (activeSort.value) {
case 'sales':
console.log('调用 getHotProducts')
products = await supabaseService.getHotProducts(limit)
break
case 'price':
console.log('调用 getProductsByPrice')
// 按价格升序(从低到高)
products = await supabaseService.getProductsByPrice(limit, true)
break
case 'new':
console.log('调用 getProductsByNewest')
// 按创建时间,最新的在前
products = await supabaseService.getProductsByNewest(limit)
break
case 'recommend':
console.log('调用 getRecommendedProducts')
// 推荐商品带badge的商品
products = await supabaseService.getRecommendedProducts(limit)
break
case 'discount':
console.log('调用 getDiscountProducts')
// 特价商品badge为'特价'
products = await supabaseService.getDiscountProducts(limit)
break
default:
console.log('调用默认 getHotProducts')
products = await supabaseService.getHotProducts(limit)
}
console.log('加载到的商品数量:', products.length)
hotProducts.value = products
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
}
}
// 获取推荐商品
const loadRecommendedProducts = async (limit: number = 6) => {
recommendedProducts.value = await supabaseService.getRecommendedProducts(limit)
}
// 初始化数据
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 initData = async () => {
await loadCategories()
await loadHotProducts()
await loadRecommendedProducts()
}
// 筛选热销商品
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 = [
@@ -572,9 +467,9 @@ const familyItems = [
]
// 生命周期
onMounted(() => {
onMounted(async () => {
initPage()
initData()
await initData()
})
// 页面显示时重置状态
@@ -689,13 +584,15 @@ const switchCategory = (category: any) => {
// 切换排序
const switchSort = (sortId: string) => {
activeSort.value = sortId
filterHotProducts()
// 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts()
}
// 切换筛选器
const switchFilter = (filterId: string) => {
activeFilter.value = filterId
filterRecommendedProducts()
// 重新加载推荐商品,筛选由 Supabase 服务处理
loadRecommendedProducts()
}
// 查看新闻详情
@@ -718,27 +615,33 @@ const onRefresh = () => {
}
// 加载更多
const loadMore = () => {
const loadMore = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
setTimeout(() => {
// 模拟加载更多数据
const newProducts = [...recommendedProducts].map((item, index) => ({
...item,
id: `new${index}`,
price: Math.floor(item.price * 0.9 + Math.random() * 10)
}))
try {
// 增加限制以加载更多推荐商品
const currentLimit = recommendedProducts.value.length + 6
await loadRecommendedProducts(currentLimit)
// 实际项目中应该合并数据
loading.value = false
hasMore.value = recommendedProducts.length < 20
// 假设如果返回的商品数量小于请求的限制,则没有更多数据
if (recommendedProducts.value.length < currentLimit) {
hasMore.value = false
}
uni.showToast({
title: '加载完成',
icon: 'success'
})
}, 2000)
} catch (error) {
console.error('加载更多失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 添加到购物车

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,979 @@
<!-- 消费者端 - 商品详情页 -->
<template>
<view class="product-detail-page">
<!-- 商品图片轮播 -->
<view class="product-images">
<swiper class="image-swiper" :indicator-dots="true" :autoplay="false">
<swiper-item v-for="(image, index) in product.images" :key="index">
<image :src="image" class="product-image" mode="aspectFit" />
</swiper-item>
</swiper>
<view class="image-indicator">{{ currentImageIndex + 1 }} / {{ product.images.length }}</view>
</view>
<!-- 商品基本信息 -->
<view class="product-info">
<view class="price-section">
<text class="current-price">¥{{ product.price }}</text>
<text v-if="product.original_price" class="original-price">¥{{ product.original_price }}</text>
</view>
<text class="product-name">{{ product.name }}</text>
<text class="sales-info">已售{{ product.sales }}件 · 库存{{ product.stock }}件</text>
</view>
<!-- 店铺信息 -->
<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" @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" @click.stop="goToShop">进店 ></text>
</view>
<!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal">
<text class="spec-title">规格</text>
<text class="spec-selected">{{ selectedSpec || '请选择规格' }}</text>
<text class="spec-arrow">></text>
</view>
<!-- 数量选择 -->
<view class="quantity-section">
<text class="quantity-title">数量</text>
<view class="quantity-selector">
<view class="quantity-btn minus" @click="decreaseQuantity">
<text class="quantity-btn-text">-</text>
</view>
<input class="quantity-input"
type="number"
v-model="quantity"
:min="1"
:max="getMaxQuantity()"
@input="validateQuantity" />
<view class="quantity-btn plus" @click="increaseQuantity">
<text class="quantity-btn-text">+</text>
</view>
</view>
<text class="quantity-stock">库存{{ getAvailableStock() }}件</text>
</view>
<!-- 商品详情 -->
<view class="product-description">
<view class="section-title">商品详情</view>
<text class="description-text">{{ product.description || '暂无详细描述' }}</text>
</view>
<!-- 底部操作栏 -->
<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>
</view>
<!-- 规格选择弹窗 -->
<view v-if="showSpec" class="spec-modal" @click="hideSpecModal">
<view class="spec-content" @click.stop>
<view class="spec-header">
<text class="spec-title">选择规格</text>
<text class="close-btn" @click="hideSpecModal">×</text>
</view>
<view class="spec-list">
<view v-for="sku in productSkus" :key="sku.id"
class="spec-item"
:class="{ active: selectedSkuId === sku.id }"
@click="selectSku(sku)">
<text class="spec-name">{{ getSkuSpecText(sku) }}</text>
<text class="spec-price">¥{{ sku.price }}</text>
<text class="spec-stock">库存{{ sku.stock }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { ProductType, MerchantType, ProductSkuType } from '@/types/mall-types.uts'
export default {
data() {
return {
product: {
id: '',
merchant_id: '',
category_id: '',
name: '',
description: '',
images: [] as Array<string>,
price: 0,
original_price: 0,
stock: 0,
sales: 0,
status: 0,
created_at: ''
} as ProductType,
merchant: {
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,
productSkus: [] as Array<ProductSkuType>,
currentImageIndex: 0,
showSpec: false,
selectedSkuId: '',
selectedSpec: '',
quantity: 1,
isFavorite: false
}
},
onLoad(options: any) {
const productId = options.productId as string || options.id as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
// 处理商品名称:如果是编码的则解码,否则直接使用
let productName = options.name as string
if (productName) {
try {
// 尝试解码如果失败不是有效的URI组件则使用原值
productName = decodeURIComponent(productName)
} catch (e) {
console.warn('ProductName decode failed, using original:', productName)
}
}
let productImage = options.image as string
if (productImage) {
try {
productImage = decodeURIComponent(productImage)
} catch (e) {
console.warn('ProductImage decode failed, using original:', productImage)
}
}
if (productId) {
this.loadProductDetail(productId, {
price: productPrice,
originalPrice: productOriginalPrice,
name: productName,
image: productImage
})
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
// 设置导航栏标题为商品名称
if (productName) {
uni.setNavigationBarTitle({
title: productName
})
}
}
},
computed: {
displayPrice(): number {
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.price
}
return this.product.price
}
},
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, options: any = {}) {
// 根据商品ID生成一个基础价格如果没有传入价格
const generatePriceFromId = (id: string): number => {
// 简单哈希函数将字符串转换为一个在50-500之间的价格
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0 // 转换为32位整数
}
// 将哈希值映射到50-500之间
const price = 50 + Math.abs(hash % 450)
// 保留两位小数
return parseFloat(price.toFixed(2))
}
// 优先使用传入的参数否则根据商品ID生成价格
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
// 原价比现价高20%左右
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
// 优先使用传入的商品名称否则根据商品ID生成名称
const productName = options.name ? decodeURIComponent(options.name) : (() => {
// 如果options.name未传入使用默认的商品名称生成逻辑
const productNames = [
'高品质运动休闲鞋',
'时尚简约双肩背包',
'多功能智能手环',
'便携式蓝牙音箱',
'全自动雨伞',
'抗菌防螨床上四件套',
'不锈钢保温杯',
'无线充电器',
'高清行车记录仪',
'智能体脂秤'
]
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
return productNames[nameIndex]
})()
// 优先使用传入的图片,否则使用默认图片
const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
// 模拟销量和库存,使其更真实
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [
productImage,
'/static/product2.jpg',
'/static/product3.jpg'
],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_status: 1,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
}
this.loadProductSkus(productId)
},
loadProductSkus(productId: string) {
// 模拟加载商品SKU数据
const basePrice = this.product.price
// 使用 productId 作为前缀生成唯一的 SKU ID防止不同商品的 SKU ID 冲突
this.productSkus = [
{
id: `${productId}_sku_001`,
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
price: basePrice,
stock: 50,
image_url: '/static/sku1.jpg',
status: 1
},
{
id: `${productId}_sku_002`,
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
price: parseFloat((basePrice * 1.1).toFixed(2)),
stock: 30,
image_url: '/static/sku2.jpg',
status: 1
}
]
},
showSpecModal() {
this.showSpec = true
},
hideSpecModal() {
this.showSpec = false
},
selectSku(sku: ProductSkuType) {
this.selectedSkuId = sku.id
this.selectedSpec = this.getSkuSpecText(sku)
this.hideSpecModal()
},
getSkuSpecText(sku: ProductSkuType): string {
if (sku.specifications) {
const specs: any = sku.specifications
return Object.keys(specs).map(key => `${key}: ${specs[key]}`).join(', ')
}
return sku.sku_code
},
addToCart() {
if (!this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
})
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: '已添加到购物车',
icon: 'success'
})
},
buyNow() {
if (!this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
})
return
}
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
// 调试:打印价格信息
console.log('立即购买 - 商品价格信息:')
console.log('SKU价格:', sku ? sku.price : '无SKU')
console.log('商品价格:', this.product.price)
console.log('选择的价格:', (sku ? sku.price : this.product.price))
console.log('数量:', this.quantity)
const selectedItem = {
id: this.selectedSkuId,
product_id: this.product.id,
sku_id: this.selectedSkuId,
product_name: this.product.name,
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
sku_specifications: sku ? sku.specifications : {},
price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)),
quantity: Number(this.quantity)
}
// 调试:打印最终传递的数据
console.log('立即购买 - 传递的商品数据:', selectedItem)
// 使用Storage传递数据避免EventChannel可能的问题
uni.setStorageSync('checkout_type', 'buy_now')
uni.setStorageSync('checkout_items', JSON.stringify([selectedItem]))
// 跳转到订单确认页
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
success: (res) => {
res.eventChannel.emit('acceptData', {
selectedItems: [selectedItem]
})
}
})
},
goToShop() {
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,
original_price: this.product.original_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'
})
},
// 数量选择相关方法
decreaseQuantity() {
if (this.quantity > 1) {
this.quantity--
}
},
increaseQuantity() {
const maxQuantity = this.getMaxQuantity()
if (this.quantity < maxQuantity) {
this.quantity++
} else {
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
},
validateQuantity() {
// 确保数量是数字
let num = parseInt(this.quantity)
if (isNaN(num)) {
num = 1
}
// 限制在1和最大库存之间
const maxQuantity = this.getMaxQuantity()
if (num < 1) {
num = 1
} else if (num > maxQuantity) {
num = maxQuantity
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
this.quantity = num
},
getMaxQuantity() {
// 如果有选择SKU使用SKU的库存否则使用商品总库存
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.stock
}
return this.product.stock
},
getAvailableStock() {
return this.getMaxQuantity()
}
}
}
</script>
<style>
.product-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 120rpx;
}
.product-images {
position: relative;
height: 750rpx;
background-color: #fff;
}
.image-swiper {
width: 100%;
height: 100%;
}
.product-image {
width: 100%;
height: 100%;
}
.image-indicator {
position: absolute;
bottom: 20rpx;
right: 20rpx;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
padding: 10rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.product-info {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.price-section {
margin-bottom: 20rpx;
}
.current-price {
font-size: 48rpx;
font-weight: bold;
color: #ff4444;
margin-right: 20rpx;
}
.original-price {
font-size: 28rpx;
color: #999;
text-decoration: line-through;
}
.product-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
line-height: 1.4;
margin-bottom: 15rpx;
}
.sales-info {
font-size: 26rpx;
color: #666;
}
.shop-info {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
flex-direction: row; /* 显式横向排列 */
align-items: center;
}
.shop-logo {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.shop-details {
flex: 1;
display: flex;
flex-direction: column; /* 内部信息保持纵向,或者根据需要改为横向 */
justify-content: center;
}
.shop-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.shop-stats-row {
display: flex;
flex-direction: row; /* 显式横向排列 */
align-items: center;
}
.rating-text, .sales-text {
font-size: 24rpx;
color: #666;
margin-right: 30rpx;
}
.enter-shop {
font-size: 26rpx;
color: #666;
}
.spec-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.spec-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
}
.spec-selected {
flex: 1;
font-size: 28rpx;
color: #666;
}
.spec-arrow {
font-size: 28rpx;
color: #999;
}
.quantity-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.quantity-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
}
.quantity-selector {
display: flex;
align-items: center;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
overflow: hidden;
}
.quantity-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.quantity-btn.minus {
border-right: 1rpx solid #e5e5e5;
}
.quantity-btn.plus {
border-left: 1rpx solid #e5e5e5;
}
.quantity-btn-text {
font-size: 28rpx;
color: #333;
}
.quantity-input {
width: 80rpx;
height: 60rpx;
text-align: center;
font-size: 28rpx;
color: #333;
border: none;
background-color: #fff;
}
.quantity-stock {
font-size: 24rpx;
color: #666;
}
.product-description {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.description-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
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;
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: 72rpx;
line-height: 72rpx;
border-radius: 36rpx;
font-size: 26rpx;
border: none;
margin: 0 10rpx;
}
.cart-btn {
background-color: #ffa726;
color: #fff;
}
.buy-btn {
background-color: #ff4444;
color: #fff;
}
.spec-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 999;
}
.spec-content {
background-color: #fff;
width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
}
.spec-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #eee;
}
.spec-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.close-btn {
font-size: 48rpx;
color: #999;
}
.spec-list {
max-height: 60vh;
overflow-y: auto;
}
.spec-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.spec-item.active {
background-color: #fff3e0;
}
.spec-name {
flex: 1;
font-size: 28rpx;
color: #333;
}
.spec-price {
font-size: 26rpx;
color: #ff4444;
margin-right: 20rpx;
}
.spec-stock {
font-size: 24rpx;
color: #666;
width: 100rpx;
text-align: right;
}
</style>

View File

@@ -109,6 +109,7 @@
<script>
import { ProductType, MerchantType, ProductSkuType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
export default {
data() {
@@ -238,69 +239,85 @@ export default {
uni.setStorageSync('footprints', JSON.stringify(footprints))
},
loadProductDetail(productId: string, options: any = {}) {
// 根据商品ID生成一个基础价格如果没有传入价格
const generatePriceFromId = (id: string): number => {
// 简单哈希函数将字符串转换为一个在50-500之间的价格
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0 // 转换为32位整数
}
// 将哈希值映射到50-500之间
const price = 50 + Math.abs(hash % 450)
// 保留两位小数
return parseFloat(price.toFixed(2))
async loadProductDetail(productId: string, options: any = {}) {
// 尝试从数据库加载
let dbProduct = null
try {
console.log('正在尝试从数据库加载商品详情:', productId)
dbProduct = await supabaseService.getProductById(productId)
console.log('数据库返回的商品详情:', dbProduct)
} catch (e) {
console.error('Failed to load product from DB', e)
}
// 优先使用传入的参数否则根据商品ID生成价格
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
// 原价比现价高20%左右
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
if (dbProduct) {
console.log('使用数据库数据渲染页面')
// 使用数据库数据
const images = [] as Array<string>
if (dbProduct.image) images.push(dbProduct.image)
else if (options.image) images.push(decodeURIComponent(options.image as string))
else images.push('/static/product1.jpg')
// 优先使用传入的商品名称否则根据商品ID生成名称
const productName = options.name ? decodeURIComponent(options.name) : (() => {
// 如果options.name未传入使用默认的商品名称生成逻辑
const productNames = [
'高品质运动休闲鞋',
'时尚简约双肩背包',
'多功能智能手环',
'便携式蓝牙音箱',
'全自动雨伞',
'抗菌防螨床上四件套',
'不锈钢保温杯',
'无线充电器',
'高清行车记录仪',
'智能体脂秤'
]
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
return productNames[nameIndex]
})()
// 补充模拟图片
images.push('/static/product2.jpg')
images.push('/static/product3.jpg')
// 优先使用传入的图片,否则使用默认图片
const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
this.product = {
id: dbProduct.id,
merchant_id: dbProduct.shop_id || 'merchant_001',
category_id: dbProduct.category_id,
name: dbProduct.name,
description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: images,
price: dbProduct.price,
original_price: dbProduct.original_price || null,
stock: dbProduct.stock !== undefined ? dbProduct.stock : 0, // 确保使用数据库中的库存
sales: dbProduct.sales !== undefined ? dbProduct.sales : 0, // 确保使用数据库中的销量
status: 1,
created_at: dbProduct.created_at || '2024-01-01'
} as ProductType
console.log('页面 product 对象已更新:', this.product)
} else {
console.log('数据库无数据或加载失败,使用模拟数据')
// 数据库无数据时,使用原有模拟逻辑
const generatePriceFromId = (id: string): number => {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0
}
const price = 50 + Math.abs(hash % 450)
return parseFloat(price.toFixed(2))
}
// 模拟销量和库存,使其更真实
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [
productImage,
'/static/product2.jpg',
'/static/product3.jpg'
],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
const productName = options.name ? decodeURIComponent(options.name) : (() => {
const productNames = ['高品质运动休闲鞋', '时尚简约双肩背包', '多功能智能手环', '便携式蓝牙音箱', '全自动雨伞', '抗菌防螨床上四件套', '不锈钢保温杯', '无线充电器', '高清行车记录仪', '智能体脂秤']
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
return productNames[nameIndex]
})()
const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [productImage, '/static/product2.jpg', '/static/product3.jpg'],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
}
// 根据商家ID生成不同的商家信息

View File

@@ -229,6 +229,8 @@
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
// 状态定义
const statusBarHeight = ref(0)
@@ -437,36 +439,48 @@ const selectSuggestion = (suggestion: string) => {
performSearch()
}
const currentPage = ref(1)
const performSearch = () => {
// 再次强制设置状态,确保万无一失
showResults.value = true
loading.value = true
// 注意:这里不要清空 searchResults.value = [],否则如果 loading 状态切换有微小延迟,可能会短暂满足 "无数据且非加载" 的条件
// 重置页码
currentPage.value = 1
// 保持旧数据直到新数据回来,或者依靠 loading 状态完全遮罩
// 模拟搜索请求
setTimeout(() => {
// 生成模拟结果
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: '/static/images/default-product.png', // 使用本地默认图片
sales: Math.floor(Math.random() * 1000),
tag: i % 2 === 0 ? '自营' : ''
}))
// 数据准备好后再关闭 loading确保无缝衔接
searchResults.value = newResults
// 应用当前排序
sortResults()
// 使用 Supabase 搜索真实数据
const keyword = searchKeyword.value.trim()
if (!keyword) {
loading.value = false
hasMore.value = true
}, 800)
return
}
// 确定排序方式
let sortBy = 'sales'
let ascending = false
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value = response.data as any[]
hasMore.value = response.hasmore
loading.value = false
// 如果无结果,显示空状态
if (searchResults.value.length === 0) {
// empty-result 组件会自动显示
}
})
.catch((error) => {
console.error('搜索失败:', error)
loading.value = false
// 可以显示错误提示,但为了用户体验,先不显示
// 保持搜索结果为空让empty-result显示
})
}
// 切换排序
@@ -481,48 +495,38 @@ const switchSort = (type: string) => {
} 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
// 重新执行搜索以获取正确排序的数据
performSearch()
}
const loadMore = () => {
if (loading.value || !hasMore.value) return
if (loading.value || !hasMore.value || !searchKeyword.value.trim()) 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: '/static/images/default-product.png',
sales: Math.floor(Math.random() * 500),
tag: ''
}))
searchResults.value.push(...newItems)
loading.value = false
if (searchResults.value.length > 20) hasMore.value = false
}, 1000)
// 增加页码
currentPage.value++
const keyword = searchKeyword.value.trim()
// 确定排序方式
let sortBy = 'sales'
let ascending = false
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value.push(...(response.data as any[]))
hasMore.value = response.hasmore
loading.value = false
})
.catch((error) => {
console.error('加载更多失败:', error)
loading.value = false
// 加载失败时,假设没有更多数据
hasMore.value = false
})
}
const refreshGuessList = () => {
@@ -537,7 +541,7 @@ const viewProductDetail = (item: any) => {
// 跳转详情页逻辑
console.log('查看商品', item)
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.originalPrice || ''}`
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}&name=${encodeURIComponent(item.name)}&image=${encodeURIComponent(item.image)}`
})
}

View File

@@ -637,6 +637,7 @@ const goBack = () => {
.balance-actions {
display: flex;
flex-direction: row;
gap: 20px;
}
@@ -664,6 +665,7 @@ const goBack = () => {
background-color: #ffffff;
padding: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
@@ -695,13 +697,16 @@ const goBack = () => {
.action-grid {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.action-icon {
@@ -722,6 +727,7 @@ const goBack = () => {
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
@@ -735,6 +741,7 @@ const goBack = () => {
.filter-tabs {
display: flex;
flex-direction: row;
gap: 15px;
}
@@ -791,6 +798,7 @@ const goBack = () => {
.transaction-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
padding: 15px 0;
@@ -803,6 +811,7 @@ const goBack = () => {
.transaction-left {
display: flex;
flex-direction: row;
align-items: flex-start;
}
@@ -928,6 +937,7 @@ const goBack = () => {
.popup-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
@@ -960,6 +970,7 @@ const goBack = () => {
.amount-input {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
padding: 10px;
@@ -982,6 +993,7 @@ const goBack = () => {
.quick-amounts {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
@@ -1009,6 +1021,7 @@ const goBack = () => {
.popup-footer {
display: flex;
flex-direction: row;
gap: 15px;
}

View File

@@ -141,7 +141,8 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { getCurrentUser, logout } from '@/utils/store.uts'
import { getCurrentUser, logout, setIsLoggedIn, setUserProfile } from '@/utils/store.uts'
import type { UserProfile } from '@/pages/user/types.uts'
const cssVars = {
'--bg': '#f5f6f8',
@@ -172,8 +173,10 @@ const codeCountdown = ref<number>(0)
onMounted(() => {
try {
const sessionInfo = supa.getSession()
if (sessionInfo != null && sessionInfo.user != null) {
uni.switchTab({ url: '/pages/mall/consumer/index' })
// 只有当用户确实存在,且 ID 有效时才自动跳转
if (sessionInfo != null && sessionInfo.user != null && sessionInfo.user?.getString('id') != null) {
console.log('检测到有效会话,自动跳转首页')
uni.switchTab({ url: '/pages/mall/consumer/index' })
}
} catch (e) {
console.error('检查登录状态失败:', e)
@@ -245,6 +248,35 @@ const getCode = async () => {
const handleLogin = async () => {
if (!validateAccount()) return
// 特殊账号处理admin/admin 直接跳转
if (account.value === 'admin' && password.value === 'admin') {
setIsLoggedIn(true)
const adminProfile = {
id: 'admin',
username: 'Admin',
email: 'admin@mall.com',
gender: 'unknown',
birthday: '',
height_cm: 0,
weight_kg: 0,
bio: 'Administrator',
avatar_url: '/static/logo.png',
preferred_language: 'zh-CN',
role: 'admin',
school_id: '',
grade_id: '',
class_id: ''
} as UserProfile
setUserProfile(adminProfile)
uni.showToast({ title: '管理员登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/mall/consumer/index' })
}, 500)
return
}
if (loginType.value === 0) {
if (!validatePassword()) return
} else {

View File

@@ -19,12 +19,14 @@ export interface Product {
id: string
category_id: string
name: string
description?: string
specification: string
price: number
original_price?: number
image?: string
manufacturer: string
sales: number
stock: number
badge?: string
shop_id?: string
shop_name?: string
@@ -111,14 +113,27 @@ class SupabaseService {
async searchProducts(
keyword: string,
page: number = 1,
limit: number = 20
limit: number = 20,
sortBy: string = 'sales',
ascending: boolean = false
): Promise<PaginatedResponse<Product>> {
try {
const response = await supa
let query = supa
.from('products')
.select('*', { count: 'exact' })
.or(`name.ilike.%${keyword}%,manufacturer.ilike.%${keyword}%,specification.ilike.%${keyword}%`)
.order('sales', { ascending: false })
// 根据sortBy和ascending设置排序
if (sortBy === 'price') {
query = query.order('price', { ascending })
} else if (sortBy === 'sales') {
query = query.order('sales', { ascending: false }) // 销量总是降序
} else {
// 默认按销量降序
query = query.order('sales', { ascending: false })
}
const response = await query
.page(page)
.limit(limit)
.execute()
@@ -175,7 +190,7 @@ class SupabaseService {
}
}
// 获取热销商品
// 获取热销商品(按销量排序)
async getHotProducts(limit: number = 10): Promise<Product[]> {
try {
const response = await supa
@@ -197,13 +212,58 @@ class SupabaseService {
}
}
// 获取推荐商品带badge的商品
async getRecommendedProducts(limit: number = 10): Promise<Product[]> {
// 获取按价格排序的商品(升序:从低到高
async getProductsByPrice(limit: number = 10, ascending: boolean = true): Promise<Product[]> {
try {
const response = await supa
.from('products')
.select('*')
.not('badge', 'is', null)
.order('price', { ascending })
.limit(limit)
.execute()
if (response.error) {
console.error('获取价格排序商品失败:', response.error)
return []
}
return response.data as Product[]
} catch (error) {
console.error('获取价格排序商品异常:', error)
return []
}
}
// 获取新品(按创建时间排序,最新的在前)
async getProductsByNewest(limit: number = 10): Promise<Product[]> {
try {
const response = await supa
.from('products')
.select('*')
.order('created_at', { ascending: false })
.limit(limit)
.execute()
if (response.error) {
console.error('获取新品失败:', response.error)
return []
}
return response.data as Product[]
} catch (error) {
console.error('获取新品异常:', error)
return []
}
}
// 获取推荐商品带badge的商品
async getRecommendedProducts(limit: number = 10): Promise<Product[]> {
try {
// 直接使用 neq 空字符串查询,忽略 null 值null 表示没有 badge不应被推荐
const response = await supa
.from('products')
.select('*')
.neq('badge', '')
.order('sales', { ascending: false })
.limit(limit)
.execute()
@@ -213,12 +273,37 @@ class SupabaseService {
return []
}
return response.data as Product[]
console.log('推荐商品查询结果条数:', response.data?.length || 0)
return response.data as Product[] || []
} catch (error) {
console.error('获取推荐商品异常:', error)
return []
}
}
// 获取特价商品badge为'特价'
async getDiscountProducts(limit: number = 10): Promise<Product[]> {
try {
const response = await supa
.from('products')
.select('*')
.eq('badge', '特价')
.order('sales', { ascending: false })
.limit(limit)
.execute()
if (response.error) {
console.error('获取特价商品失败:', response.error)
return []
}
console.log('特价商品查询结果条数:', response.data?.length || 0)
return response.data as Product[] || []
} catch (error) {
console.error('获取特价商品异常:', error)
return []
}
}
}
// 导出单例实例