consumer模块完成度85%,连接服务器supabase,新建相关表
This commit is contained in:
@@ -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)}`);
|
||||
@@ -1051,4 +1075,4 @@ export function createClient(url : string, key : string) : AkSupa {
|
||||
return new AkSupa(url, key);
|
||||
}
|
||||
|
||||
export default AkSupa;
|
||||
export default AkSupa;
|
||||
|
||||
@@ -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
@@ -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 || '')}`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1416,4 +1128,4 @@ const onScan = () => {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
1131
pages/mall/consumer/category完成分类及商品数据获取.uvue
Normal file
1131
pages/mall/consumer/category完成分类及商品数据获取.uvue
Normal file
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
@@ -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
979
pages/mall/consumer/product-detail - 副本.uvue
Normal file
979
pages/mall/consumer/product-detail - 副本.uvue
Normal 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>
|
||||
@@ -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))
|
||||
|
||||
// 优先使用传入的商品名称,否则根据商品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'
|
||||
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')
|
||||
|
||||
// 补充模拟图片
|
||||
images.push('/static/product2.jpg')
|
||||
images.push('/static/product3.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 basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
|
||||
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
|
||||
|
||||
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生成不同的商家信息
|
||||
|
||||
@@ -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)}`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1230,4 +1234,4 @@ const goBack = () => {
|
||||
.safe-area {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
|
||||
Reference in New Issue
Block a user