前端各页面对接数据
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
// Supabase 配置
|
||||
// 内网环境 - 本地部署的 Supabase
|
||||
// IP: 192.168.1.61 (Ubuntu服务器)
|
||||
// IP: 192.168.1.62
|
||||
// Kong HTTP Port: 8000
|
||||
export const SUPA_URL: string = 'http://192.168.1.61:8000'
|
||||
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg'
|
||||
export const SUPA_URL: string = 'http://192.168.1.61:18000'
|
||||
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
|
||||
|
||||
// WebSocket 实时连接(内网使用 ws:// 而非 wss://)
|
||||
export const WS_URL: string = 'ws://192.168.1.61:8000/realtime/v1/websocket'
|
||||
export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
|
||||
|
||||
// 备用配置(已注释,如需切换可取消注释)
|
||||
// 开发环境 - 其他内网地址
|
||||
@@ -26,4 +26,4 @@ export const WS_URL: string = 'ws://192.168.1.61:8000/realtime/v1/websocket'
|
||||
|
||||
// 路由配置
|
||||
export const HOME_REDIRECT: string = '/pages/mall/consumer/index'
|
||||
export const TABORPAGE: string = '/pages/mall/consumer/index'
|
||||
export const TABORPAGE: string = '/pages/mall/consumer/index'
|
||||
38
ak/configbackup.uts
Normal file
38
ak/configbackup.uts
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
// export const SUPA_URL: string = 'http://192.168.0.150:8080'
|
||||
// export const SUPA_ANON_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
|
||||
// export const SUPA_SERVICE_ROLE_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
|
||||
// export const SUPA_KEY = SUPA_ANON_KEY
|
||||
// export const WS_URL: string = 'ws://'+'/192.168.0.150:8080'+'/realtime/v1/websocket';
|
||||
export const SUPA_URL: string = 'https://ak3.oulog.com';
|
||||
export const SUPA_KEY: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
|
||||
export const SUPA_SERVICE_KEY: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
export const WS_URL: string = 'wss://'+'ak3.oulog.com'+'/realtime/v1/websocket';
|
||||
// Optional: Edge Function or API endpoint that returns S3 presigned POST
|
||||
// Expected response: { url: string, fields: object, publicUrl?: string }
|
||||
export const S3_PRESIGN_URL: string = ''
|
||||
// Optional: Public base URL for your S3/CND to build final URLs when presign response has no publicUrl
|
||||
export const S3_PUBLIC_BASE: string = ''
|
||||
export const RAG_API_KEY: string ='ragflow-lkZmNjMzI2YzRiNjExZWY4ZGIwMDI0Mm';
|
||||
export const RAG_BASE_URL: string ='https://rag.oulog.com';
|
||||
export const RAG_AGENT_ID: string ='15b01b26128111f08cd30242ac120006';
|
||||
export const TABORPAGE:boolean = false
|
||||
|
||||
// export const HOME_REDIRECT :string = '/pages/ec/health/ecalert'
|
||||
//export const HOME_REDIRECT :string = '/pages/sport/index'
|
||||
// export const HOME_REDIRECT :string = '/pages/sport/teacher/dashboard'
|
||||
// export const HOME_REDIRECT :string = '/pages/test/multi_device_monitor'
|
||||
|
||||
|
||||
|
||||
// export const HOME_REDIRECT :string = '/pages/ec/admin/dashboard'
|
||||
// export const HOME_REDIRECT :string = '/pages/sense/healthble'
|
||||
//export const HOME_REDIRECT :string = '/pages/ec/elder/dashboard'
|
||||
// export const HOME_REDIRECT :string = '/pages/ec/caregiver/dashboard'
|
||||
// export const HOME_REDIRECT :string = '/pages/ec/doctor/dashboard'
|
||||
// export const HOME_REDIRECT :string = '/pages/ec/family/dashboard'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
29
ak/configme.uts
Normal file
29
ak/configme.uts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Supabase 配置
|
||||
// 内网环境 - 本地部署的 Supabase
|
||||
// 家里通过端口映射访问公司内网Supabase
|
||||
// 本地映射端口:HTTP 18000, WebSocket 13000
|
||||
export const SUPA_URL: string = 'http://192.168.1.61:18000'
|
||||
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg1234567890'
|
||||
//export const SUPA_URL: string = 'https://ak3.oulog.com'
|
||||
//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
|
||||
|
||||
// WebSocket 实时连接(内网使用 ws:// 而非 wss://)
|
||||
export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
|
||||
|
||||
// 备用配置(已注释,如需切换可取消注释)
|
||||
// 开发环境 - 其他内网地址
|
||||
// export const SUPA_URL: string = 'http://192.168.0.150:8080'
|
||||
// export const SUPA_KEY: string = 'your-anon-key'
|
||||
// export const WS_URL: string = 'ws://192.168.0.150:8080/realtime/v1/websocket'
|
||||
|
||||
// 生产环境 - Supabase 云服务(已注释)
|
||||
// export const SUPA_URL: string = 'https://ak3.oulog.com'
|
||||
// export const SUPA_KEY: string = 'your-anon-key'
|
||||
// export const WS_URL: string = 'wss://ak3.oulog.com/realtime/v1/websocket'
|
||||
|
||||
// 指向你的 Supabase 服务(开发/私有部署)
|
||||
// export const SUPA_URL: string = 'http://192.168.1.64:3000'
|
||||
// export const SUPA_KEY: string = 'your-anon-key'
|
||||
// export const WS_URL: string = 'ws://192.168.1.64:3000/realtime/v1'
|
||||
|
||||
//export const HOME_REDIRECT :string = '/pages/mall/consumer/index'
|
||||
@@ -800,6 +800,13 @@ async select(table : string, filter ?: string | null, options ?: AkSupaSelectOpt
|
||||
headers['Prefer'] = 'return=representation,single-object';
|
||||
}
|
||||
}
|
||||
|
||||
// 确保有 select 参数
|
||||
if (options.columns == null) {
|
||||
params.push('select=*');
|
||||
} else if (options.columns == "") {
|
||||
params.push('select=*');
|
||||
}
|
||||
} else {
|
||||
params.push('select=*');
|
||||
}
|
||||
|
||||
@@ -1,54 +1,34 @@
|
||||
// /components/supadb/aksupainstance.uts
|
||||
import { createClient } from './aksupa.uts'
|
||||
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
|
||||
|
||||
// 创建并导出 Supabase 客户端实例
|
||||
const supabaseUrl = 'https://your-project.supabase.co' // 替换为你的 Supabase URL
|
||||
const supabaseAnonKey = 'your-anon-key' // 替换为你的匿名密钥
|
||||
// 创建单一真实的 Supabase 客户端实例 (使用 config.uts 配置)
|
||||
// Create single source of truth client using config
|
||||
const supaInstance = createClient(SUPA_URL, SUPA_KEY)
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
// 导出默认实例 (供 login.uvue 等使用)
|
||||
export default supaInstance
|
||||
|
||||
// 导出 Supabase 实例就绪状态
|
||||
// 导出命名实例 'supabase' (供 store.uts 使用)
|
||||
export const supabase = supaInstance
|
||||
|
||||
// 导出 isSupabaseReady 状态
|
||||
export const isSupabaseReady = true
|
||||
|
||||
// 兼容 ensureSupabaseReady
|
||||
export async function ensureSupabaseReady() {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查连接状态的函数
|
||||
export function checkConnection() {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
// 兼容 supaReady Promise
|
||||
export const supaReady = Promise.resolve(true)
|
||||
|
||||
// 如果有其他需要导出的函数,可以这样导出:
|
||||
export function initializeSupabase(url: string, key: string) {
|
||||
return createClient(url, key)
|
||||
}
|
||||
|
||||
// 检查连接状态的函数
|
||||
export function checkConnection() {
|
||||
return new Promise((resolve) => {
|
||||
// 模拟连接检查
|
||||
setTimeout(() => {
|
||||
resolve(true)
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
// 不再使用 supaready 变量,而是提供函数
|
||||
export async function ensureSupabaseReady() {
|
||||
return await checkConnection()
|
||||
}
|
||||
import AkSupa from './aksupa.uts'
|
||||
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
|
||||
|
||||
const supa = new AkSupa(SUPA_URL, SUPA_KEY)
|
||||
|
||||
// Do not perform hard-coded auto sign-in during page preload (development mode may preload pages).
|
||||
// Instead, mark supa as ready if an existing session is present; otherwise defer sign-in to explicit user action.
|
||||
const supaReady: Promise<boolean> = (async () => {
|
||||
try {
|
||||
const sess = supa.getSession();
|
||||
if (sess != null && sess.session != null) {
|
||||
return true;
|
||||
}
|
||||
// No session found — do not auto sign-in with hard-coded credentials.
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Supabase instance init failed', err)
|
||||
return false;
|
||||
}
|
||||
})()
|
||||
|
||||
export { supaReady }
|
||||
export default supa
|
||||
19
pages.json
19
pages.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/mall/consumer/index",
|
||||
"path": "pages/user/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
"navigationBarTitleText": "用户登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"path": "pages/user/boot",
|
||||
@@ -45,11 +45,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/login",
|
||||
"path": "pages/mall/consumer/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/change-password",
|
||||
|
||||
487
pages/mall/consumer/Supabase Snippet SQL Query.csv
Normal file
487
pages/mall/consumer/Supabase Snippet SQL Query.csv
Normal file
File diff suppressed because one or more lines are too long
@@ -168,6 +168,8 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import supabaseService from '@/utils/supabaseService.uts'
|
||||
import type { CartItem } from '@/utils/supabaseService.uts'
|
||||
|
||||
// 响应式数据
|
||||
const cartItems = ref<any[]>([])
|
||||
@@ -289,23 +291,46 @@ onShow(() => {
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadCartData = () => {
|
||||
const loadCartData = async () => {
|
||||
loading.value = true
|
||||
|
||||
// 从本地存储加载购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems.value = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
try {
|
||||
// 从Supabase获取购物车数据
|
||||
const dbCartItems = await supabaseService.getCartItems()
|
||||
console.log('从Supabase获取购物车数据:', dbCartItems)
|
||||
|
||||
// 将数据库CartItem映射为页面CartItem格式
|
||||
cartItems.value = dbCartItems.map((item: CartItem) => ({
|
||||
id: item.id, // 购物车项ID
|
||||
productId: item.product_id, // 商品ID
|
||||
shopId: item.shop_id || 'shop_default', // 店铺ID
|
||||
shopName: item.shop_name || '自营店铺', // 店铺名称
|
||||
name: item.product_name || '商品名称', // 商品名称
|
||||
price: item.product_price || 0, // 商品价格
|
||||
image: item.product_image || '/static/images/default-product.png', // 商品图片
|
||||
spec: item.product_specification || '默认规格', // 商品规格
|
||||
quantity: item.quantity, // 数量
|
||||
selected: item.selected // 是否选中
|
||||
}))
|
||||
|
||||
// 同时更新本地存储作为缓存
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems.value))
|
||||
console.log('购物车数据已从数据库加载,数量:', cartItems.value.length)
|
||||
} catch (error) {
|
||||
console.error('从Supabase加载购物车数据失败:', error)
|
||||
// 如果数据库加载失败,尝试从本地存储恢复
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems.value = JSON.parse(cartData as string) as any[]
|
||||
console.log('使用本地存储购物车数据,数量:', cartItems.value.length)
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
cartItems.value = []
|
||||
}
|
||||
} else {
|
||||
cartItems.value = []
|
||||
}
|
||||
} else {
|
||||
// 如果本地没有数据,使用Mock数据(可选,或者直接为空)
|
||||
// 为了演示效果,这里可以保留一部分Mock数据,或者初始化为空
|
||||
// cartItems.value = [...mockCartItems]
|
||||
cartItems.value = []
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -315,22 +340,42 @@ const loadCartData = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 监听购物车数据变化并保存到本地存储
|
||||
// 保存购物车数据到本地存储(作为缓存)
|
||||
const saveCartData = () => {
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems.value))
|
||||
}
|
||||
|
||||
// 商品操作 - 增加保存逻辑
|
||||
const toggleSelect = (itemId: string) => {
|
||||
// 商品操作 - 切换选择状态
|
||||
const toggleSelect = async (itemId: string) => {
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
cartItems.value[index].selected = !cartItems.value[index].selected
|
||||
cartItems.value = [...cartItems.value] // 触发响应式更新
|
||||
saveCartData()
|
||||
const newSelected = !cartItems.value[index].selected
|
||||
|
||||
try {
|
||||
// 更新数据库中的选择状态
|
||||
const success = await supabaseService.updateCartItemSelection(itemId, newSelected)
|
||||
if (success) {
|
||||
cartItems.value[index].selected = newSelected
|
||||
cartItems.value = [...cartItems.value] // 触发响应式更新
|
||||
saveCartData()
|
||||
} else {
|
||||
console.error('更新购物车项选择状态失败')
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新购物车项选择状态异常:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleShopSelect = (shopId: string) => {
|
||||
const toggleShopSelect = async (shopId: string) => {
|
||||
const group = cartGroups.value.find((g: any) => g.shopId === shopId)
|
||||
if (!group) return
|
||||
|
||||
@@ -338,56 +383,164 @@ const toggleShopSelect = (shopId: string) => {
|
||||
const isAllShopSelected = (group.items as any[]).every((item: any) => item.selected)
|
||||
const newState = !isAllShopSelected
|
||||
|
||||
// 更新该店铺下所有商品的状态
|
||||
cartItems.value.forEach(item => {
|
||||
if (item.shopId === shopId) {
|
||||
item.selected = newState
|
||||
// 获取该店铺下所有购物车项的ID
|
||||
const cartItemIds = cartItems.value
|
||||
.filter(item => item.shopId === shopId)
|
||||
.map(item => item.id)
|
||||
|
||||
if (cartItemIds.length === 0) return
|
||||
|
||||
try {
|
||||
// 批量更新数据库中的选择状态
|
||||
const success = await supabaseService.batchUpdateCartItemSelection(cartItemIds, newState)
|
||||
if (success) {
|
||||
// 更新本地状态
|
||||
cartItems.value.forEach(item => {
|
||||
if (item.shopId === shopId) {
|
||||
item.selected = newState
|
||||
}
|
||||
})
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
} else {
|
||||
console.error('批量更新购物车项选择状态失败')
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
const newSelectedState = !allSelected.value
|
||||
cartItems.value = cartItems.value.map(item => ({
|
||||
...item,
|
||||
selected: newSelectedState
|
||||
}))
|
||||
saveCartData()
|
||||
}
|
||||
|
||||
const increaseQuantity = (itemId: string) => {
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
cartItems.value[index].quantity++
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
} catch (error) {
|
||||
console.error('批量更新购物车项选择状态异常:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const decreaseQuantity = (itemId: string) => {
|
||||
const toggleSelectAll = async () => {
|
||||
const newSelectedState = !allSelected.value
|
||||
|
||||
// 获取所有购物车项的ID
|
||||
const cartItemIds = cartItems.value.map(item => item.id)
|
||||
|
||||
if (cartItemIds.length === 0) return
|
||||
|
||||
try {
|
||||
// 批量更新数据库中的选择状态
|
||||
const success = await supabaseService.batchUpdateCartItemSelection(cartItemIds, newSelectedState)
|
||||
if (success) {
|
||||
// 更新本地状态
|
||||
cartItems.value = cartItems.value.map(item => ({
|
||||
...item,
|
||||
selected: newSelectedState
|
||||
}))
|
||||
saveCartData()
|
||||
} else {
|
||||
console.error('批量更新全选状态失败')
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量更新全选状态异常:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const increaseQuantity = async (itemId: string) => {
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
const newQuantity = cartItems.value[index].quantity + 1
|
||||
|
||||
try {
|
||||
// 更新数据库中的数量
|
||||
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
|
||||
if (success) {
|
||||
cartItems.value[index].quantity = newQuantity
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
} else {
|
||||
console.error('更新购物车项数量失败')
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新购物车项数量异常:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decreaseQuantity = async (itemId: string) => {
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
if (cartItems.value[index].quantity > 1) {
|
||||
cartItems.value[index].quantity--
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
const newQuantity = cartItems.value[index].quantity - 1
|
||||
|
||||
try {
|
||||
// 更新数据库中的数量
|
||||
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
|
||||
if (success) {
|
||||
cartItems.value[index].quantity = newQuantity
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
} else {
|
||||
console.error('更新购物车项数量失败')
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新购物车项数量异常:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 数量为1时,询问是否删除
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要从购物车移除该商品吗?',
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
cartItems.value.splice(index, 1)
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
|
||||
uni.showToast({
|
||||
title: '已移除',
|
||||
icon: 'none'
|
||||
})
|
||||
try {
|
||||
// 从数据库删除购物车项
|
||||
const success = await supabaseService.deleteCartItem(itemId)
|
||||
if (success) {
|
||||
cartItems.value.splice(index, 1)
|
||||
cartItems.value = [...cartItems.value]
|
||||
saveCartData()
|
||||
|
||||
uni.showToast({
|
||||
title: '已移除',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
console.error('删除购物车项失败')
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除购物车项异常:', error)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -395,8 +548,8 @@ const decreaseQuantity = (itemId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除商品 - 增加保存逻辑
|
||||
const deleteSelectedItems = () => {
|
||||
// 删除商品 - 批量删除选中的商品
|
||||
const deleteSelectedItems = async () => {
|
||||
if (selectedCount.value === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择要删除的商品',
|
||||
@@ -408,70 +561,75 @@ const deleteSelectedItems = () => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定要删除选中的 ${selectedCount.value} 件商品吗?`,
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
cartItems.value = cartItems.value.filter(item => !item.selected)
|
||||
saveCartData()
|
||||
// 获取选中的购物车项ID
|
||||
const selectedCartItemIds = cartItems.value
|
||||
.filter(item => item.selected)
|
||||
.map(item => item.id)
|
||||
|
||||
// 如果购物车删空了,退出管理模式
|
||||
if (cartItems.value.length === 0) {
|
||||
isManageMode.value = false
|
||||
try {
|
||||
// 批量从数据库删除购物车项
|
||||
const success = await supabaseService.batchDeleteCartItems(selectedCartItemIds)
|
||||
if (success) {
|
||||
// 更新本地状态
|
||||
cartItems.value = cartItems.value.filter(item => !item.selected)
|
||||
saveCartData()
|
||||
|
||||
// 如果购物车删空了,退出管理模式
|
||||
if (cartItems.value.length === 0) {
|
||||
isManageMode.value = false
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
console.error('批量删除购物车项失败')
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除购物车项异常:', error)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addToCart = (product: any) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let currentItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
currentItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
const addToCart = async (product: any) => {
|
||||
try {
|
||||
// 调用SupabaseService添加商品到购物车
|
||||
const success = await supabaseService.addToCart(product.id, 1, product.skuId)
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载购物车数据
|
||||
loadCartData()
|
||||
} else {
|
||||
console.error('添加商品到购物车失败')
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在 (使用商品ID匹配,因为推荐商品没有SKU)
|
||||
const existingItem = currentItems.find((item: any) =>
|
||||
item.productId === product.id || item.id === product.id
|
||||
)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
} else {
|
||||
// 添加新商品
|
||||
currentItems.push({
|
||||
id: product.id, // 商品ID(因为没有SKU)
|
||||
productId: product.id, // 同样存储商品ID
|
||||
shopId: product.shopId || 'shop_recommend',
|
||||
shopName: product.shopName || '推荐好物',
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
spec: product.specification || '默认规格', // 优先使用商品自带的规格
|
||||
quantity: 1,
|
||||
selected: true
|
||||
} catch (error) {
|
||||
console.error('添加商品到购物车异常:', error)
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(currentItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 立即刷新当前列表
|
||||
loadCartData()
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
|
||||
@@ -523,52 +523,32 @@ const deleteSelectedItems = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const addToCart = (product: any) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let currentItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
currentItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
const addToCart = async (product: any) => {
|
||||
try {
|
||||
// 调用SupabaseService添加商品到购物车
|
||||
const success = await supabaseService.addToCart(product.id, 1, product.skuId)
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载购物车数据
|
||||
loadCartData()
|
||||
} else {
|
||||
console.error('添加商品到购物车失败')
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在 (使用商品ID匹配,因为推荐商品没有SKU)
|
||||
const existingItem = currentItems.find((item: any) =>
|
||||
item.productId === product.id || item.id === product.id
|
||||
)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
} else {
|
||||
// 添加新商品
|
||||
currentItems.push({
|
||||
id: product.id, // 商品ID(因为没有SKU)
|
||||
productId: product.id, // 同样存储商品ID
|
||||
shopId: product.shopId || 'shop_recommend',
|
||||
shopName: product.shopName || '推荐好物',
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
spec: product.specification || '默认规格', // 优先使用商品自带的规格
|
||||
quantity: 1,
|
||||
selected: true
|
||||
} catch (error) {
|
||||
console.error('添加商品到购物车异常:', error)
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(currentItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 立即刷新当前列表
|
||||
loadCartData()
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
|
||||
1371
pages/mall/consumer/cart药品.uvue
Normal file
1371
pages/mall/consumer/cart药品.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,7 @@ const productList = ref<Product[]>([])
|
||||
const activePrimary = ref<string>('')
|
||||
const cartCount = ref(3)
|
||||
const hasMore = ref(true)
|
||||
const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
|
||||
|
||||
// 获取当前分类信息
|
||||
const currentCategoryName = ref('')
|
||||
@@ -134,34 +135,67 @@ const pageParams = ref<any>({})
|
||||
// 生命周期
|
||||
onMounted(async() => {
|
||||
await loadCategories()
|
||||
await loadProducts()
|
||||
// 等待分类加载完成后,再检查是否需要加载默认分类的商品
|
||||
// 延迟一点时间,确保页面参数处理完成
|
||||
setTimeout(async () => {
|
||||
if (!hasLoadedFromParams.value && activePrimary.value) {
|
||||
await loadProducts()
|
||||
}
|
||||
}, 300)
|
||||
})
|
||||
|
||||
// 添加加载分类的方法
|
||||
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
|
||||
try {
|
||||
const categories = await supabaseService.getCategories()
|
||||
console.log('加载分类数据成功,数量:', categories.length)
|
||||
if (categories.length > 0) {
|
||||
primaryCategories.value = categories
|
||||
// 如果没有通过参数设置分类,则设置默认选中第一个分类
|
||||
if (!activePrimary.value && categories[0]) {
|
||||
activePrimary.value = categories[0].id
|
||||
console.log('设置默认分类为:', categories[0].name, 'ID:', categories[0].id)
|
||||
}
|
||||
} else {
|
||||
console.warn('从Supabase获取的分类数据为空')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载分类数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载商品数据
|
||||
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
|
||||
try {
|
||||
if (activePrimary.value) {
|
||||
console.log('开始加载商品,分类ID:', activePrimary.value)
|
||||
const response = await supabaseService.getProductsByCategory(activePrimary.value)
|
||||
console.log('商品加载结果:', {
|
||||
dataCount: response.data.length,
|
||||
total: response.total,
|
||||
hasmore: response.hasmore
|
||||
})
|
||||
|
||||
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 || ''
|
||||
console.log('当前分类信息:', category.name, '描述:', category.description)
|
||||
} else {
|
||||
console.warn('未找到对应的分类信息,分类ID:', activePrimary.value)
|
||||
}
|
||||
|
||||
console.log('商品列表加载完成,数量:', productList.value.length)
|
||||
} else {
|
||||
console.warn('activePrimary为空,无法加载商品')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商品数据失败:', error)
|
||||
productList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +234,7 @@ onLoad((options: any) => {
|
||||
|
||||
// 如果有找到分类ID,则选中对应的分类
|
||||
if (categoryId) {
|
||||
hasLoadedFromParams.value = true
|
||||
console.log('✅ 准备选中分类:', categoryId)
|
||||
console.log('分类名称:', categoryName || '未指定')
|
||||
|
||||
@@ -244,6 +279,7 @@ onShow(() => {
|
||||
|
||||
// 检查是否有分类参数
|
||||
if (pageOptions.categoryId) {
|
||||
hasLoadedFromParams.value = true
|
||||
const categoryId = pageOptions.categoryId
|
||||
const categoryName = pageOptions.name || ''
|
||||
|
||||
@@ -288,6 +324,7 @@ onShow(() => {
|
||||
const params = new URLSearchParams(queryString)
|
||||
const urlCategoryId = params.get('categoryId')
|
||||
if (urlCategoryId) {
|
||||
hasLoadedFromParams.value = true
|
||||
console.log('✅ 从URL解析到分类参数:', urlCategoryId)
|
||||
selectPrimaryCategory(urlCategoryId)
|
||||
}
|
||||
|
||||
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
@@ -772,64 +772,10 @@ const loadDefaultAddress = async () => {
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
// 尝试从多个可能的键名获取用户ID
|
||||
const possibleKeys = ['user_id', 'userId', 'uid', 'user_uuid', 'userID', 'user.id']
|
||||
|
||||
for (const key of possibleKeys) {
|
||||
const value = uni.getStorageSync(key)
|
||||
console.log(`getCurrentUserId: 尝试键名 ${key}:`, value)
|
||||
if (value) {
|
||||
console.log(`getCurrentUserId: 从 ${key} 获取到用户ID:`, value)
|
||||
return value as string
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从userInfo对象获取
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
console.log('getCurrentUserId: 从userInfo获取:', userInfo)
|
||||
if (userInfo) {
|
||||
// userInfo可能是字符串(需要解析)或对象
|
||||
let userInfoObj: any = userInfo
|
||||
if (typeof userInfo === 'string') {
|
||||
try {
|
||||
userInfoObj = JSON.parse(userInfo)
|
||||
} catch (e) {
|
||||
console.error('解析userInfo失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试多个可能的属性名
|
||||
const possibleProps = ['id', 'userId', 'uid', 'user_id', 'uuid', 'user_uuid']
|
||||
for (const prop of possibleProps) {
|
||||
if (userInfoObj && userInfoObj[prop]) {
|
||||
console.log(`getCurrentUserId: 从userInfo.${prop} 获取到用户ID:`, userInfoObj[prop])
|
||||
return userInfoObj[prop] as string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从auth获取(如果使用Supabase Auth)
|
||||
const authData = uni.getStorageSync('supabase.auth.token')
|
||||
if (authData) {
|
||||
console.log('getCurrentUserId: 从supabase.auth.token获取:', authData)
|
||||
try {
|
||||
const authObj = typeof authData === 'string' ? JSON.parse(authData) : authData
|
||||
if (authObj.currentSession && authObj.currentSession.user && authObj.currentSession.user.id) {
|
||||
console.log('getCurrentUserId: 从auth session获取用户ID:', authObj.currentSession.user.id)
|
||||
return authObj.currentSession.user.id as string
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析auth数据失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 打印所有存储键,用于调试
|
||||
console.log('getCurrentUserId: 所有Storage键:')
|
||||
const allKeys = uni.getStorageInfoSync().keys
|
||||
console.log('Storage keys:', allKeys)
|
||||
|
||||
console.log('getCurrentUserId: 未找到用户ID')
|
||||
return ''
|
||||
// 使用 SupabaseService 获取当前用户ID
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
console.log('getCurrentUserId: 从SupabaseService获取到用户ID:', userId)
|
||||
return userId ?? ''
|
||||
}
|
||||
|
||||
// 用户登录状态
|
||||
@@ -1280,65 +1226,89 @@ const selectCoupon = () => {
|
||||
|
||||
// 提交订单
|
||||
const submitOrder = async () => {
|
||||
if (!selectedAddress.value) {
|
||||
uni.showToast({
|
||||
title: '请选择收货地址',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
// 校验地址
|
||||
if (!selectedAddress.value) {
|
||||
uni.showToast({
|
||||
title: '请选择收货地址',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 校验商品
|
||||
if (checkoutItems.value.length === 0) {
|
||||
uni.showToast({
|
||||
title: '订单中没有商品',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
|
||||
// MOCK ORDER SUBMISSION
|
||||
// 模拟创建成功
|
||||
try {
|
||||
const mockOrderId = `order_${Date.now()}`
|
||||
const userId = getCurrentUserId()
|
||||
// 确保使用当前登录用户ID (如果本地存储为空,可能需要处理)
|
||||
if (!userId) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建订单对象
|
||||
const newOrder = {
|
||||
id: mockOrderId,
|
||||
order_no: generateOrderNo(),
|
||||
user_id: getCurrentUserId() || 'user_001',
|
||||
merchant_id: checkoutItems.value[0]?.product_id || 'merchant_001', // 简化处理,取第一个商品的merchant
|
||||
status: 1, // 待支付
|
||||
total_amount: totalAmount.value,
|
||||
discount_amount: discountAmount.value,
|
||||
delivery_fee: deliveryFee.value,
|
||||
actual_amount: actualAmount.value,
|
||||
payment_method: 0,
|
||||
payment_status: 0,
|
||||
delivery_address: selectedAddress.value,
|
||||
items: checkoutItems.value,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
// 准备订单项数据
|
||||
// 注意:需根据 checkoutItems 的实际结构转换为 createOrder 需要的 CartItem 结构
|
||||
// 假设 checkoutItems 已经包含了 product_id, quantity, price, name, image 等字段
|
||||
const orderItems = checkoutItems.value.map((item: any): any => ({
|
||||
id: item.id || '', // 这是一个临时ID或者购物车ID,createOrder 中会使用 product_id
|
||||
product_id: item.product_id || item.id, // 确保有 product_id
|
||||
quantity: item.quantity,
|
||||
price: item.price,
|
||||
product_name: item.name,
|
||||
product_image: item.image,
|
||||
spec: item.spec,
|
||||
checked: true
|
||||
}))
|
||||
|
||||
// 保存到本地存储
|
||||
const storedOrders = uni.getStorageSync('orders')
|
||||
let orders: any[] = []
|
||||
if (storedOrders) {
|
||||
try {
|
||||
orders = JSON.parse(storedOrders as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析订单数据失败', e)
|
||||
}
|
||||
}
|
||||
orders.unshift(newOrder)
|
||||
uni.setStorageSync('orders', JSON.stringify(orders))
|
||||
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
// 调用 Supabase 服务创建订单
|
||||
const result = await supabaseService.createOrder(
|
||||
userId,
|
||||
selectedAddress.value!.id, // 地址ID
|
||||
actualAmount.value, // 实付金额
|
||||
orderItems
|
||||
)
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
// 携带价格详情跳转
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('创建订单失败:', err)
|
||||
uni.showToast({
|
||||
title: '订单创建失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// 清除购买的商品 (如果来自购物车,应该在 createOrder 成功后清除,或者这里手动清除本地存储)
|
||||
// 这里我们假设购物车清理逻辑可能在 createOrder 后端处理,或者需要在这里清除本地
|
||||
try {
|
||||
uni.removeStorageSync('checkout_items')
|
||||
} catch(e) {
|
||||
console.error('清除结算商品失败', e)
|
||||
}
|
||||
|
||||
const activeOrderId = result.data as string
|
||||
|
||||
// 跳转支付页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/payment?orderId=${activeOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
|
||||
})
|
||||
} else {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
uni.hideLoading()
|
||||
console.error('创建订单失败:', err)
|
||||
uni.showToast({
|
||||
title: err.message || '订单创建失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
type Product = {
|
||||
id: string
|
||||
@@ -48,61 +49,46 @@ onMounted(() => {
|
||||
loadFavorites()
|
||||
})
|
||||
|
||||
const addToCart = (product: Product) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在
|
||||
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
const addToCart = async (product: Product) => {
|
||||
uni.showLoading({ title: '添加中' })
|
||||
const success = await supabaseService.addToCart(product.id, 1)
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||
} else {
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: product.id,
|
||||
shopId: product.shopId || 'shop_favorite_default',
|
||||
shopName: product.shopName || '收藏店铺',
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
spec: '默认规格',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
})
|
||||
uni.showToast({ title: '添加失败', icon: 'none' })
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const loadFavorites = () => {
|
||||
// 从本地存储获取收藏列表
|
||||
const storedFavorites = uni.getStorageSync('favorites')
|
||||
if (storedFavorites) {
|
||||
try {
|
||||
favorites.value = JSON.parse(storedFavorites as string) as Product[]
|
||||
} catch (e) {
|
||||
console.error('Failed to parse favorites', e)
|
||||
favorites.value = []
|
||||
}
|
||||
} else {
|
||||
favorites.value = []
|
||||
}
|
||||
const loadFavorites = async () => {
|
||||
const res = await supabaseService.getFavorites()
|
||||
|
||||
// Map response
|
||||
favorites.value = res.map((item: any): Product => {
|
||||
const prod = item.ml_products
|
||||
let image = '/static/default-product.png'
|
||||
if (prod) {
|
||||
if (prod.main_image_url) image = prod.main_image_url
|
||||
else if (prod.image_url) image = prod.image_url
|
||||
else if (prod.image_urls) {
|
||||
// Try parse
|
||||
try {
|
||||
const arr = JSON.parse(prod.image_urls)
|
||||
if (Array.isArray(arr) && arr.length > 0) image = arr[0]
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: prod?.id || item.target_id,
|
||||
name: prod?.name || '未知商品',
|
||||
price: prod?.price || 0,
|
||||
image: image,
|
||||
sales: prod?.sales || 0,
|
||||
shopId: '',
|
||||
shopName: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goShopping = () => {
|
||||
@@ -111,26 +97,29 @@ const goShopping = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const goToDetail = (product: Product) => {
|
||||
const goToDetail = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.original_price || ''}`
|
||||
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
const removeFavorite = (id: string) => {
|
||||
const removeFavorite = async (id: string) => {
|
||||
uni.showModal({
|
||||
title: '取消收藏',
|
||||
content: '确定要取消收藏该商品吗?',
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const index = favorites.value.findIndex(item => item.id === id)
|
||||
if (index !== -1) {
|
||||
favorites.value.splice(index, 1)
|
||||
uni.setStorageSync('favorites', JSON.stringify(favorites.value))
|
||||
uni.showToast({
|
||||
title: '已取消收藏',
|
||||
icon: 'none'
|
||||
})
|
||||
const success = await supabaseService.toggleFavorite(id) // Toggle removes if exists
|
||||
if (success) {
|
||||
// Remove from local list
|
||||
const index = favorites.value.findIndex(item => item.id === id)
|
||||
if (index !== -1) {
|
||||
favorites.value.splice(index, 1)
|
||||
}
|
||||
uni.showToast({
|
||||
title: '已取消收藏',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,12 +271,14 @@ 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'
|
||||
import { getCurrentUser } from '@/utils/store.uts'
|
||||
|
||||
// 响应式数据
|
||||
const statusBarHeight = ref(0)
|
||||
const scrollHeight = ref(0)
|
||||
const refreshing = ref(false)
|
||||
const loading = ref(false)
|
||||
const isFirstShow = ref(true)
|
||||
const hasMore = ref(true)
|
||||
const activeSort = ref('sales')
|
||||
const activeFilter = ref('recommend')
|
||||
@@ -334,13 +336,13 @@ const healthNews = [
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const categoriesData = await supabaseService.getCategories()
|
||||
// 映射字段:将description映射为desc,保持与原有结构兼容
|
||||
// 映射字段:根据ml_categories表结构映射
|
||||
categories.value = categoriesData.map((cat: any) => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
icon: cat.icon || '📦',
|
||||
desc: cat.description || cat.desc || '',
|
||||
color: cat.color || '#4CAF50'
|
||||
icon: cat.icon_url || '📦', // 使用icon_url字段
|
||||
desc: cat.description || '', // 使用description字段
|
||||
color: '#4CAF50' // 默认颜色,表中可能没有color字段
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('加载分类数据失败:', error)
|
||||
@@ -408,6 +410,13 @@ const loadRecommendedProducts = async (limit: number = 6) => {
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
// 首先确保用户资料已加载
|
||||
try {
|
||||
await getCurrentUser()
|
||||
console.log('主页初始化:用户资料加载完成')
|
||||
} catch (error) {
|
||||
console.error('加载用户资料失败:', error)
|
||||
}
|
||||
await loadCategories()
|
||||
await loadHotProducts()
|
||||
await loadRecommendedProducts()
|
||||
@@ -489,6 +498,22 @@ onShow(() => {
|
||||
// 让分类页面在成功读取后自行清除
|
||||
// 这样可以确保分类页面能正确读取到传递的数据
|
||||
|
||||
// 每次页面显示时尝试更新用户资料
|
||||
if (!isFirstShow.value) {
|
||||
getCurrentUser().then(profile => {
|
||||
if (profile) {
|
||||
console.log('主页onShow:用户资料更新成功')
|
||||
} else {
|
||||
console.log('主页onShow:用户资料为空,可能未登录')
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('主页onShow:加载用户资料失败:', error)
|
||||
})
|
||||
} else {
|
||||
isFirstShow.value = false
|
||||
console.log('主页首次显示,跳过onShow中的用户资料检查,交由initData处理')
|
||||
}
|
||||
|
||||
console.log('=== index页面onShow执行完成 ===')
|
||||
})
|
||||
|
||||
|
||||
2158
pages/mall/consumer/index药品.uvue
Normal file
2158
pages/mall/consumer/index药品.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -156,9 +156,11 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { onShow, onLoad } from '@dcloudio/uni-app'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
// 响应式数据
|
||||
const orders = ref<any[]>([])
|
||||
const allOrdersList = ref<any[]>([]) // Store all fetched orders for client-side filtering
|
||||
const loading = ref<boolean>(false)
|
||||
const loadingMore = ref<boolean>(false)
|
||||
const hasMore = ref<boolean>(true)
|
||||
@@ -169,144 +171,16 @@ const searchKeyword = ref<string>('')
|
||||
|
||||
// 订单标签页
|
||||
const orderTabs = reactive([
|
||||
{ id: 'all', name: '全部', count: 12 },
|
||||
{ id: 'pending', name: '待付款', count: 2 },
|
||||
{ id: 'shipping', name: '待发货', count: 1 },
|
||||
{ id: 'delivering', name: '待收货', count: 3 },
|
||||
{ id: 'completed', name: '已完成', count: 5 },
|
||||
{ id: 'cancelled', name: '已取消', count: 1 }
|
||||
{ id: 'all', name: '全部', count: 0 },
|
||||
{ id: 'pending', name: '待付款', count: 0 },
|
||||
{ id: 'shipping', name: '待发货', count: 0 },
|
||||
{ id: 'delivering', name: '待收货', count: 0 },
|
||||
{ id: 'completed', name: '已完成', count: 0 },
|
||||
{ id: 'cancelled', name: '已取消', count: 0 }
|
||||
])
|
||||
|
||||
// Mock 订单数据
|
||||
const mockOrders = [
|
||||
{
|
||||
id: '202311230001',
|
||||
order_no: '202311230001',
|
||||
status: 1, // 1:待付款 2:待发货 3:待收货 4:已完成 5:已取消
|
||||
create_time: '2023-11-23 14:30:22',
|
||||
product_amount: 378.00,
|
||||
shipping_fee: 0.00,
|
||||
total_amount: 378.00,
|
||||
products: [
|
||||
{
|
||||
id: '1001',
|
||||
name: '无线蓝牙耳机 降噪版',
|
||||
price: 299.00,
|
||||
image: 'https://picsum.photos/80/80?random=1',
|
||||
spec: '白色',
|
||||
quantity: 1
|
||||
},
|
||||
{
|
||||
id: '1002',
|
||||
name: '耳机保护套',
|
||||
price: 29.00,
|
||||
image: 'https://picsum.photos/80/80?random=2',
|
||||
spec: '黑色',
|
||||
quantity: 1
|
||||
},
|
||||
{
|
||||
id: '1003',
|
||||
name: '数据线',
|
||||
price: 19.00,
|
||||
image: 'https://picsum.photos/80/80?random=3',
|
||||
spec: '1米',
|
||||
quantity: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '202311220001',
|
||||
order_no: '202311220001',
|
||||
status: 2,
|
||||
create_time: '2023-11-22 10:15:33',
|
||||
product_amount: 199.00,
|
||||
shipping_fee: 10.00,
|
||||
total_amount: 209.00,
|
||||
products: [
|
||||
{
|
||||
id: '2001',
|
||||
name: '运动T恤 速干面料',
|
||||
price: 79.00,
|
||||
image: 'https://picsum.photos/80/80?random=4',
|
||||
spec: '黑色 L',
|
||||
quantity: 2
|
||||
},
|
||||
{
|
||||
id: '2002',
|
||||
name: '运动短裤',
|
||||
price: 59.00,
|
||||
image: 'https://picsum.photos/80/80?random=5',
|
||||
spec: '黑色 M',
|
||||
quantity: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '202311210001',
|
||||
order_no: '202311210001',
|
||||
status: 3,
|
||||
create_time: '2023-11-21 16:45:12',
|
||||
product_amount: 299.00,
|
||||
shipping_fee: 0.00,
|
||||
total_amount: 299.00,
|
||||
products: [
|
||||
{
|
||||
id: '3001',
|
||||
name: '智能手环 心率监测',
|
||||
price: 199.00,
|
||||
image: 'https://picsum.photos/80/80?random=6',
|
||||
spec: '黑色',
|
||||
quantity: 1
|
||||
},
|
||||
{
|
||||
id: '3002',
|
||||
name: '手环腕带',
|
||||
price: 29.00,
|
||||
image: 'https://picsum.photos/80/80?random=7',
|
||||
spec: '蓝色',
|
||||
quantity: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '202311200001',
|
||||
order_no: '202311200001',
|
||||
status: 4,
|
||||
create_time: '2023-11-20 09:30:45',
|
||||
product_amount: 99.00,
|
||||
shipping_fee: 0.00,
|
||||
total_amount: 99.00,
|
||||
products: [
|
||||
{
|
||||
id: '4001',
|
||||
name: '保温杯 500ml',
|
||||
price: 49.00,
|
||||
image: 'https://picsum.photos/80/80?random=8',
|
||||
spec: '白色',
|
||||
quantity: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '202311190001',
|
||||
order_no: '202311190001',
|
||||
status: 5,
|
||||
create_time: '2023-11-19 14:20:18',
|
||||
product_amount: 599.00,
|
||||
shipping_fee: 0.00,
|
||||
total_amount: 599.00,
|
||||
products: [
|
||||
{
|
||||
id: '5001',
|
||||
name: '蓝牙音箱 便携式',
|
||||
price: 199.00,
|
||||
image: 'https://picsum.photos/80/80?random=9',
|
||||
spec: '黑色',
|
||||
quantity: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
// Removed Mock Data
|
||||
|
||||
|
||||
// 计算属性:根据当前标签筛选订单
|
||||
const filteredOrders = computed(() => {
|
||||
@@ -349,90 +223,79 @@ onShow(() => {
|
||||
// 加载订单数据
|
||||
const loadOrders = async () => {
|
||||
loading.value = true
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
const userId = userStore?.id
|
||||
|
||||
if (!userId) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 从本地存储获取订单
|
||||
const ordersStr = uni.getStorageSync('orders')
|
||||
let localOrders: any[] = []
|
||||
if (ordersStr) {
|
||||
localOrders = JSON.parse(ordersStr as string) as any[]
|
||||
}
|
||||
|
||||
// 如果本地存储为空,使用 Mock 数据
|
||||
if (localOrders.length === 0) {
|
||||
localOrders = mockOrders
|
||||
// 可选:将 Mock 数据写入本地存储,以便后续操作生效
|
||||
// uni.setStorageSync('orders', JSON.stringify(mockOrders))
|
||||
}
|
||||
// Fetch all orders from Supabase (status=0)
|
||||
const fetchedOrders = await supabaseService.getOrders(0)
|
||||
|
||||
// 过滤当前用户的订单
|
||||
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
|
||||
// 暂时显示所有订单用于测试
|
||||
let userOrders = localOrders
|
||||
|
||||
// 根据标签页过滤
|
||||
let filtered = userOrders
|
||||
const statusMap: Record<string, number> = {
|
||||
'pending': 1,
|
||||
'shipping': 2,
|
||||
'delivering': 3,
|
||||
'completed': 4,
|
||||
'cancelled': 5
|
||||
}
|
||||
|
||||
if (activeTab.value !== 'all') {
|
||||
const targetStatus = statusMap[activeTab.value]
|
||||
filtered = userOrders.filter((o: any) => o.status === targetStatus)
|
||||
}
|
||||
|
||||
// 按时间倒序
|
||||
filtered.sort((a: any, b: any) => {
|
||||
const timeA = new Date(a.created_at || a.create_time).getTime()
|
||||
const timeB = new Date(b.created_at || b.create_time).getTime()
|
||||
return timeB - timeA
|
||||
})
|
||||
|
||||
// 处理数据格式以适配当前页面
|
||||
orders.value = filtered.map((order: any) => ({
|
||||
// Map to View Model
|
||||
const mappedOrders = fetchedOrders.map((order: any) => ({
|
||||
id: order.id,
|
||||
order_no: order.order_no,
|
||||
status: order.status,
|
||||
create_time: order.created_at || order.create_time,
|
||||
product_amount: order.total_amount,
|
||||
status: order.order_status,
|
||||
create_time: order.created_at,
|
||||
product_amount: order.product_amount || order.actual_amount,
|
||||
shipping_fee: order.delivery_fee,
|
||||
total_amount: order.actual_amount,
|
||||
products: (order.items || order.products || []).map((item: any) => ({
|
||||
id: item.product_id || item.id,
|
||||
name: item.product_name || item.name,
|
||||
products: (order.ml_order_items || []).map((item: any) => ({
|
||||
id: item.product_id,
|
||||
name: item.product_name,
|
||||
price: item.price,
|
||||
image: item.product_image || item.image || '/static/default-product.png',
|
||||
spec: item.sku_specifications ? formatSpec(item.sku_specifications) : (item.spec || ''),
|
||||
image: item.product_image,
|
||||
spec: item.spec || '',
|
||||
quantity: item.quantity
|
||||
}))
|
||||
}))
|
||||
|
||||
// Sort by created_at desc
|
||||
mappedOrders.sort((a: any, b: any) => {
|
||||
const timeA = new Date(a.create_time).getTime()
|
||||
const timeB = new Date(b.create_time).getTime()
|
||||
return timeB - timeA
|
||||
})
|
||||
|
||||
allOrdersList.value = mappedOrders
|
||||
|
||||
// 更新统计数据
|
||||
orderTabs[0].count = userOrders.length
|
||||
orderTabs[1].count = userOrders.filter((o: any) => o.status === 1).length
|
||||
orderTabs[2].count = userOrders.filter((o: any) => o.status === 2).length
|
||||
orderTabs[3].count = userOrders.filter((o: any) => o.status === 3).length
|
||||
orderTabs[4].count = userOrders.filter((o: any) => o.status === 4).length
|
||||
orderTabs[5].count = userOrders.filter((o: any) => o.status === 5).length
|
||||
// Update tab counts
|
||||
updateTabsCounts(mappedOrders)
|
||||
|
||||
// Apply current tab filter
|
||||
filterOrdersByTab()
|
||||
|
||||
} catch (err) {
|
||||
console.error('加载订单异常:', err)
|
||||
uni.showToast({ title: '加载订单失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateTabsCounts = (allOrders: any[]) => {
|
||||
orderTabs[0].count = allOrders.length
|
||||
orderTabs[1].count = allOrders.filter((o: any) => o.status === 1).length
|
||||
orderTabs[2].count = allOrders.filter((o: any) => o.status === 2).length
|
||||
orderTabs[3].count = allOrders.filter((o: any) => o.status === 3).length
|
||||
orderTabs[4].count = allOrders.filter((o: any) => o.status === 4).length
|
||||
orderTabs[5].count = allOrders.filter((o: any) => o.status === 5).length
|
||||
}
|
||||
|
||||
const filterOrdersByTab = () => {
|
||||
const statusMap: Record<string, number> = {
|
||||
'pending': 1,
|
||||
'shipping': 2,
|
||||
'delivering': 3,
|
||||
'completed': 4,
|
||||
'cancelled': 5
|
||||
}
|
||||
|
||||
if (activeTab.value === 'all') {
|
||||
orders.value = allOrdersList.value
|
||||
} else {
|
||||
const targetStatus = statusMap[activeTab.value]
|
||||
orders.value = allOrdersList.value.filter((o: any) => o.status === targetStatus)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (isoString: string): string => {
|
||||
if (!isoString) return ''
|
||||
const date = new Date(isoString)
|
||||
@@ -483,9 +346,7 @@ const performSearch = () => {
|
||||
}
|
||||
|
||||
const getCurrentOrderData = () => {
|
||||
// 这里应该从本地存储或API获取完整订单数据
|
||||
// 暂时返回当前orders.value
|
||||
return orders.value
|
||||
return allOrdersList.value
|
||||
}
|
||||
|
||||
const formatSpec = (specs: any): string => {
|
||||
@@ -499,9 +360,7 @@ const formatSpec = (specs: any): string => {
|
||||
// 切换标签
|
||||
const switchTab = (tabId: string) => {
|
||||
activeTab.value = tabId
|
||||
page.value = 1
|
||||
orders.value = []
|
||||
loadOrders()
|
||||
filterOrdersByTab()
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
|
||||
@@ -34,6 +34,23 @@
|
||||
<text class="enter-shop" @click.stop="goToShop">进店 ></text>
|
||||
</view>
|
||||
|
||||
<!-- 功能主治(药品功能) -->
|
||||
<view class="function-section" v-if="product.usage">
|
||||
<text class="function-title">功能主治</text>
|
||||
<text class="function-content">{{ product.usage }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 商品参数 -->
|
||||
<view class="params-section" @click="showParamsModal">
|
||||
<text class="params-title">商品参数</text>
|
||||
<view class="params-summary">
|
||||
<text class="params-item" v-if="product.specification">规格: {{ product.specification }}</text>
|
||||
<text class="params-item" v-if="product.expiry_date">有效期: {{ product.expiry_date }}</text>
|
||||
<text class="params-item" v-if="product.approval_number">批准文号: {{ product.approval_number }}</text>
|
||||
</view>
|
||||
<text class="params-arrow">></text>
|
||||
</view>
|
||||
|
||||
<!-- 规格选择 -->
|
||||
<view class="spec-section" @click="showSpecModal">
|
||||
<text class="spec-title">规格</text>
|
||||
@@ -113,6 +130,50 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品参数弹窗 -->
|
||||
<view v-if="showParams" class="params-modal" @click="hideParamsModal">
|
||||
<view class="params-content" @click.stop>
|
||||
<view class="params-header">
|
||||
<text class="params-title">商品参数</text>
|
||||
<text class="close-btn" @click="hideParamsModal">×</text>
|
||||
</view>
|
||||
<view class="params-list">
|
||||
<view class="params-item" v-if="product.specification">
|
||||
<text class="params-label">规格</text>
|
||||
<text class="params-value">{{ product.specification }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.usage">
|
||||
<text class="params-label">功能主治</text>
|
||||
<text class="params-value">{{ product.usage }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.side_effects">
|
||||
<text class="params-label">副作用</text>
|
||||
<text class="params-value">{{ product.side_effects }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.precautions">
|
||||
<text class="params-label">注意事项</text>
|
||||
<text class="params-value">{{ product.precautions }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.expiry_date">
|
||||
<text class="params-label">有效期</text>
|
||||
<text class="params-value">{{ product.expiry_date }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.storage_conditions">
|
||||
<text class="params-label">储存条件</text>
|
||||
<text class="params-value">{{ product.storage_conditions }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.approval_number">
|
||||
<text class="params-label">批准文号</text>
|
||||
<text class="params-value">{{ product.approval_number }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.tags && product.tags.length > 0">
|
||||
<text class="params-label">标签</text>
|
||||
<text class="params-value">{{ product.tags.join(', ') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -157,7 +218,8 @@ export default {
|
||||
selectedSkuId: '',
|
||||
selectedSpec: '',
|
||||
quantity: 1,
|
||||
isFavorite: false
|
||||
isFavorite: false,
|
||||
showParams: false
|
||||
}
|
||||
},
|
||||
onLoad(options: any) {
|
||||
@@ -415,7 +477,16 @@ export default {
|
||||
stock: stock,
|
||||
sales: sales,
|
||||
status: 1,
|
||||
created_at: dbProduct.created_at || '2024-01-01'
|
||||
created_at: dbProduct.created_at || '2024-01-01',
|
||||
// 药品相关字段
|
||||
specification: dbProduct.specification || null,
|
||||
usage: dbProduct.usage || null,
|
||||
side_effects: dbProduct.side_effects || null,
|
||||
precautions: dbProduct.precautions || null,
|
||||
expiry_date: dbProduct.expiry_date || null,
|
||||
storage_conditions: dbProduct.storage_conditions || null,
|
||||
approval_number: dbProduct.approval_number || null,
|
||||
tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
|
||||
} as ProductType
|
||||
console.log('页面 product 对象已更新:', this.product)
|
||||
console.log('商品图片数组:', this.product.images)
|
||||
@@ -781,6 +852,14 @@ export default {
|
||||
current: index,
|
||||
urls: this.product.images
|
||||
})
|
||||
},
|
||||
|
||||
showParamsModal() {
|
||||
this.showParams = true
|
||||
},
|
||||
|
||||
hideParamsModal() {
|
||||
this.showParams = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1149,6 +1228,131 @@ export default {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 功能主治样式 */
|
||||
.function-section {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.function-title {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.function-content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 商品参数样式 */
|
||||
.params-section {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.params-title {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
width: 120rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.params-summary {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.params-item {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 5rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.params-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
/* 商品参数弹窗样式 */
|
||||
.params-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: 1000;
|
||||
}
|
||||
|
||||
.params-content {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.params-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.params-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.params-list {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.params-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.params-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
width: 150rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.params-value {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 商品详情图片样式 */
|
||||
.detail-images {
|
||||
margin-top: 30rpx;
|
||||
@@ -1160,4 +1364,29 @@ export default {
|
||||
border-radius: 10rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 电脑端适配 */
|
||||
@media (min-width: 768px) {
|
||||
.params-section {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.params-summary {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.params-item {
|
||||
flex: 1;
|
||||
margin-right: 0;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.params-arrow {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -373,7 +373,18 @@ export default {
|
||||
// 尝试多种方式访问属性
|
||||
const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined)
|
||||
const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined)
|
||||
const priceValue = dbProduct.price !== undefined ? dbProduct.price : (dbProduct['price'] !== undefined ? dbProduct['price'] : undefined)
|
||||
|
||||
// 价格字段兼容性处理:优先查找 price,其次查找 base_price
|
||||
let priceValue = dbProduct.price
|
||||
if (priceValue === undefined || priceValue === null) {
|
||||
priceValue = dbProduct.base_price
|
||||
}
|
||||
if (priceValue === undefined || priceValue === null) {
|
||||
priceValue = dbProduct['price']
|
||||
}
|
||||
if (priceValue === undefined || priceValue === null) {
|
||||
priceValue = dbProduct['base_price']
|
||||
}
|
||||
|
||||
const hasId = idValue !== undefined && idValue !== null
|
||||
const hasName = nameValue !== undefined && nameValue !== null
|
||||
@@ -396,33 +407,27 @@ export default {
|
||||
// 数据库Product接口和本地ProductType接口字段可能不同
|
||||
const images = [] as Array<string>
|
||||
|
||||
// 处理图片字段:优先使用images字段,其次使用image字段
|
||||
console.log('处理数据库图片字段:')
|
||||
console.log('dbProduct.images:', dbProduct.images, '类型:', typeof dbProduct.images)
|
||||
console.log('dbProduct.image:', dbProduct.image, '类型:', typeof dbProduct.image)
|
||||
// 处理图片字段:优先使用image_urls字段,其次使用main_image_url
|
||||
console.log('处理数据库图片字段')
|
||||
|
||||
// 尝试从数据库的images字段获取图片(可能是字符串或数组)
|
||||
if (dbProduct.images) {
|
||||
// 尝试从数据库的image_urls字段获取图片(JSON字符串或对象)
|
||||
if (dbProduct.image_urls) {
|
||||
let imagesArray: any[] = []
|
||||
if (typeof dbProduct.images === 'string') {
|
||||
if (typeof dbProduct.image_urls === 'string') {
|
||||
try {
|
||||
imagesArray = JSON.parse(dbProduct.images)
|
||||
console.log('解析images字符串成功:', imagesArray)
|
||||
imagesArray = JSON.parse(dbProduct.image_urls)
|
||||
} catch (e) {
|
||||
console.error('解析images字段失败:', e, dbProduct.images)
|
||||
// 如果不是JSON,尝试按逗号分割
|
||||
if (dbProduct.images.includes(',')) {
|
||||
imagesArray = dbProduct.images.split(',').map((img: string) => img.trim())
|
||||
} else if (dbProduct.images) {
|
||||
imagesArray = [dbProduct.images]
|
||||
console.error('解析image_urls字段失败:', e, dbProduct.image_urls)
|
||||
// 尝试逗号分割
|
||||
if (dbProduct.image_urls.includes(',')) {
|
||||
imagesArray = dbProduct.image_urls.split(',').map((img: string) => img.trim())
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(dbProduct.images)) {
|
||||
imagesArray = dbProduct.images
|
||||
} else if (Array.isArray(dbProduct.image_urls)) {
|
||||
imagesArray = dbProduct.image_urls
|
||||
}
|
||||
|
||||
if (imagesArray.length > 0) {
|
||||
console.log('从数据库images字段获取图片数组:', imagesArray)
|
||||
for (const img of imagesArray) {
|
||||
if (typeof img === 'string' && img) {
|
||||
images.push(img)
|
||||
@@ -430,11 +435,18 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有从images字段获取到图片,尝试使用image字段
|
||||
|
||||
// 如果没有获取到相册图,但有主图,放入相册
|
||||
if (dbProduct.main_image_url) {
|
||||
// 如果相册里没有这张图,把它加到第一位
|
||||
if (!images.includes(dbProduct.main_image_url)) {
|
||||
images.unshift(dbProduct.main_image_url)
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧字段 image
|
||||
if (images.length === 0 && dbProduct.image) {
|
||||
console.log('使用单张图片字段:', dbProduct.image)
|
||||
images.push(dbProduct.image)
|
||||
images.push(dbProduct.image)
|
||||
}
|
||||
|
||||
// 如果仍然没有图片,使用传入的图片或默认图片
|
||||
@@ -461,9 +473,33 @@ export default {
|
||||
const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001'
|
||||
|
||||
// 确保数值字段有效
|
||||
const price = typeof dbProduct.price === 'number' ? dbProduct.price : 0
|
||||
const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : 100
|
||||
const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : 50
|
||||
// 优先使用 price,不存在则使用 base_price
|
||||
let productPrice = 0
|
||||
if (typeof dbProduct.price === 'number') {
|
||||
productPrice = dbProduct.price
|
||||
} else if (typeof dbProduct.base_price === 'number') {
|
||||
productPrice = dbProduct.base_price
|
||||
} else if (priceValue !== undefined) {
|
||||
// 使用上面校验时获取到的 priceValue
|
||||
productPrice = Number(priceValue)
|
||||
}
|
||||
|
||||
const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : ((dbProduct.total_stock != null && !isNaN(Number(dbProduct.total_stock))) ? Math.floor(Number(dbProduct.total_stock)) : 100)
|
||||
const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : ((dbProduct.sale_count != null && !isNaN(Number(dbProduct.sale_count))) ? Math.floor(Number(dbProduct.sale_count)) : 50)
|
||||
|
||||
// 解析 attributes
|
||||
let attributes: any = {}
|
||||
if (dbProduct.attributes) {
|
||||
try {
|
||||
if (typeof dbProduct.attributes === 'string') {
|
||||
attributes = JSON.parse(dbProduct.attributes)
|
||||
} else {
|
||||
attributes = dbProduct.attributes
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析 attributes 失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.product = {
|
||||
id: dbProduct.id || productId,
|
||||
@@ -472,20 +508,20 @@ export default {
|
||||
name: dbProduct.name || '商品名称',
|
||||
description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
|
||||
images: images,
|
||||
price: price,
|
||||
original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : null,
|
||||
price: productPrice,
|
||||
original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : ((dbProduct.market_price != null && !isNaN(Number(dbProduct.market_price))) ? Number(dbProduct.market_price) : null),
|
||||
stock: stock,
|
||||
sales: sales,
|
||||
status: 1,
|
||||
created_at: dbProduct.created_at || '2024-01-01',
|
||||
// 药品相关字段
|
||||
specification: dbProduct.specification || null,
|
||||
usage: dbProduct.usage || null,
|
||||
side_effects: dbProduct.side_effects || null,
|
||||
precautions: dbProduct.precautions || null,
|
||||
expiry_date: dbProduct.expiry_date || null,
|
||||
storage_conditions: dbProduct.storage_conditions || null,
|
||||
approval_number: dbProduct.approval_number || null,
|
||||
specification: attributes.specification || dbProduct.specification || null,
|
||||
usage: attributes.usage || dbProduct.usage || null,
|
||||
side_effects: attributes.side_effects || dbProduct.side_effects || null,
|
||||
precautions: attributes.precautions || dbProduct.precautions || null,
|
||||
expiry_date: attributes.expiry_date || dbProduct.expiry_date || null,
|
||||
storage_conditions: attributes.storage_conditions || dbProduct.storage_conditions || null,
|
||||
approval_number: attributes.approval_number || dbProduct.approval_number || null,
|
||||
tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
|
||||
} as ProductType
|
||||
console.log('页面 product 对象已更新:', this.product)
|
||||
@@ -535,37 +571,109 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据商家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'
|
||||
// 尝试加载真实商户信息
|
||||
let realMerchantLoaded = false
|
||||
// 只有当 ID 是 UUID 格式(包含-)或者是真实数据时才尝试查询
|
||||
if (this.product.merchant_id && (this.product.merchant_id.includes('-') || !this.product.merchant_id.startsWith('merchant_'))) {
|
||||
console.log('尝试加载商户信息:', this.product.merchant_id)
|
||||
try {
|
||||
const shop = await supabaseService.getShopByMerchantId(this.product.merchant_id)
|
||||
if (shop) {
|
||||
console.log('加载到商户信息:', shop.shop_name)
|
||||
|
||||
// 确保字段存在,避免 undefined 导致构造失败
|
||||
this.merchant = {
|
||||
id: shop.id || '',
|
||||
user_id: shop.merchant_id || '',
|
||||
shop_name: shop.shop_name || '未命名店铺',
|
||||
shop_logo: shop.shop_logo || '/static/default-shop.png',
|
||||
shop_banner: shop.shop_banner || '/static/default-banner.png',
|
||||
shop_description: shop.description || '',
|
||||
contact_name: shop.contact_name || '店主',
|
||||
contact_phone: shop.contact_phone || '',
|
||||
shop_status: 1,
|
||||
// 优先使用 avg_rating,没有则使用默认值
|
||||
rating: shop.rating_avg !== undefined && shop.rating_avg !== null ? shop.rating_avg : 4.8,
|
||||
// 使用 order_count 或 product_count 作为销量/活跃度指标,如果没有则默认 0
|
||||
total_sales: shop.total_sales !== undefined ? shop.total_sales : (shop.order_count !== undefined ? shop.order_count : 0),
|
||||
created_at: shop.created_at || new Date().toISOString()
|
||||
} as MerchantType
|
||||
realMerchantLoaded = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载商户信息失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!realMerchantLoaded) {
|
||||
// 根据商家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) {
|
||||
async loadProductSkus(productId: string) {
|
||||
// 尝试从数据库加载SKU
|
||||
try {
|
||||
const skus = await supabaseService.getProductSkus(productId)
|
||||
if (skus.length > 0) {
|
||||
console.log('加载到商品SKU:', skus.length)
|
||||
this.productSkus = skus.map((sku): ProductSkuType => {
|
||||
let specs: UTSJSONObject = {}
|
||||
if (sku.specifications) {
|
||||
try {
|
||||
if (typeof sku.specifications === 'string') {
|
||||
specs = JSON.parse(sku.specifications) as UTSJSONObject
|
||||
} else {
|
||||
// 假设已经是对象
|
||||
specs = sku.specifications as unknown as UTSJSONObject
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('解析SKU规格失败', e)
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: sku.id,
|
||||
product_id: sku.product_id,
|
||||
sku_code: sku.sku_code,
|
||||
specifications: specs,
|
||||
price: sku.price,
|
||||
stock: sku.stock !== undefined ? sku.stock : 0,
|
||||
image_url: sku.image_url || '',
|
||||
status: sku.status !== undefined ? sku.status : 1
|
||||
} as ProductSkuType
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fetch SKUs error', e)
|
||||
}
|
||||
|
||||
// 模拟加载商品SKU数据
|
||||
const basePrice = this.product.price
|
||||
|
||||
@@ -620,7 +728,7 @@ export default {
|
||||
return sku.sku_code
|
||||
},
|
||||
|
||||
addToCart() {
|
||||
async addToCart() {
|
||||
if (!this.selectedSkuId) {
|
||||
uni.showToast({
|
||||
title: '请选择规格',
|
||||
@@ -629,50 +737,42 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在 (同一SKU)
|
||||
const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity += this.quantity
|
||||
} else {
|
||||
// 查找SKU信息
|
||||
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
|
||||
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID
|
||||
productId: this.product.id,
|
||||
shopId: this.merchant.id,
|
||||
shopName: this.merchant.shop_name,
|
||||
name: this.product.name,
|
||||
price: sku ? sku.price : this.product.price,
|
||||
image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
|
||||
spec: this.selectedSpec,
|
||||
quantity: this.quantity,
|
||||
selected: true
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
// 模拟添加到购物车
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
// 显示加载中
|
||||
uni.showLoading({
|
||||
title: '添加中...'
|
||||
})
|
||||
|
||||
try {
|
||||
// 调用 Supabase 服务添加到购物车
|
||||
// 传递 productId, quantity, skuId
|
||||
const success = await supabaseService.addToCart(
|
||||
this.product.id,
|
||||
this.quantity,
|
||||
this.selectedSkuId
|
||||
)
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
console.error('添加购物车返回失败')
|
||||
uni.showToast({
|
||||
title: '添加失败,请登录重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('添加购物车异常', e)
|
||||
uni.showToast({
|
||||
title: '添加异常',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
buyNow() {
|
||||
@@ -787,6 +887,14 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
goToShop() {
|
||||
if (this.merchant.user_id) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.user_id}`
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
goToCart() {
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/cart'
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { MerchantType, ProductType } from '@/types/mall-types.uts'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
const merchant = ref<MerchantType>({
|
||||
id: '',
|
||||
@@ -74,73 +75,81 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const loadShopData = (id: string) => {
|
||||
// 模拟加载店铺数据
|
||||
merchant.value = {
|
||||
id: id,
|
||||
user_id: 'user_001',
|
||||
shop_name: '优质好店',
|
||||
shop_logo: '/static/shop-logo.png',
|
||||
shop_banner: '/static/shop-banner.png',
|
||||
shop_description: '专注品质生活,为您提供最优质的商品和服务。',
|
||||
contact_name: '店主小王',
|
||||
contact_phone: '13800138000',
|
||||
shop_status: 1,
|
||||
rating: 4.8,
|
||||
total_sales: 15680,
|
||||
created_at: '2023-06-01'
|
||||
const loadShopData = async (id: string) => {
|
||||
const shop = await supabaseService.getShopByMerchantId(id)
|
||||
if (shop) {
|
||||
merchant.value = {
|
||||
id: shop.id,
|
||||
user_id: shop.merchant_id, // 映射关系
|
||||
shop_name: shop.shop_name,
|
||||
shop_logo: shop.shop_logo || '/static/default-shop.png',
|
||||
shop_banner: shop.shop_banner || '/static/default-banner.png',
|
||||
shop_description: shop.description || '',
|
||||
contact_name: shop.contact_name || '',
|
||||
contact_phone: shop.contact_phone || '',
|
||||
shop_status: 1, // 默认正常
|
||||
rating: shop.rating_avg || 5.0,
|
||||
total_sales: shop.total_sales || 0,
|
||||
created_at: shop.created_at || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadShopProducts = (id: string) => {
|
||||
// 模拟加载店铺商品列表
|
||||
products.value = [
|
||||
{
|
||||
id: 'prod_001',
|
||||
merchant_id: id,
|
||||
category_id: 'cat_001',
|
||||
name: '精选好物商品 A',
|
||||
description: '商品描述 A',
|
||||
images: ['/static/product1.jpg'],
|
||||
price: 199.99,
|
||||
original_price: 299.99,
|
||||
stock: 100,
|
||||
sales: 1256,
|
||||
status: 1,
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 'prod_002',
|
||||
merchant_id: id,
|
||||
category_id: 'cat_001',
|
||||
name: '精选好物商品 B',
|
||||
description: '商品描述 B',
|
||||
images: ['/static/product2.jpg'],
|
||||
price: 299.00,
|
||||
original_price: 399.00,
|
||||
stock: 50,
|
||||
sales: 856,
|
||||
status: 1,
|
||||
created_at: '2024-01-16'
|
||||
},
|
||||
{
|
||||
id: 'prod_003',
|
||||
merchant_id: id,
|
||||
category_id: 'cat_002',
|
||||
name: '精选好物商品 C',
|
||||
description: '商品描述 C',
|
||||
images: ['/static/product3.jpg'],
|
||||
price: 99.00,
|
||||
original_price: 129.00,
|
||||
stock: 200,
|
||||
sales: 3256,
|
||||
status: 1,
|
||||
created_at: '2024-01-17'
|
||||
}
|
||||
]
|
||||
const loadShopProducts = async (id: string) => {
|
||||
const res = await supabaseService.getProductsByMerchantId(id)
|
||||
if (res.data.length > 0) {
|
||||
products.value = res.data.map((item): ProductType => {
|
||||
// 解析图片数组
|
||||
let images: string[] = []
|
||||
if (item.image_urls) {
|
||||
try {
|
||||
const rawUrl = item.image_urls
|
||||
if (Array.isArray(rawUrl)) {
|
||||
// 已经是数组
|
||||
images = rawUrl as string[]
|
||||
} else if (typeof rawUrl === 'string') {
|
||||
if (rawUrl.startsWith('[')) {
|
||||
images = JSON.parse(rawUrl) as string[]
|
||||
} else {
|
||||
// 单个图片路径字符串
|
||||
images = [rawUrl]
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('解析图片数组失败:', e)
|
||||
// 降级处理:尝试直接作为单个图片
|
||||
if (typeof item.image_urls === 'string') {
|
||||
images = [item.image_urls!]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (images.length === 0 && item.image) {
|
||||
images.push(item.image!)
|
||||
}
|
||||
if (images.length === 0 && item.main_image_url) {
|
||||
images.push(item.main_image_url!)
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
merchant_id: item.merchant_id,
|
||||
category_id: item.category_id,
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
images: images,
|
||||
price: item.price,
|
||||
original_price: item.original_price || item.price,
|
||||
stock: item.stock || 0,
|
||||
sales: item.sales || 0,
|
||||
status: 1,
|
||||
created_at: item.created_at || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFollow = () => {
|
||||
// TODO: Implement actual follow logic with Supabase
|
||||
isFollowed.value = !isFollowed.value
|
||||
uni.showToast({
|
||||
title: isFollowed.value ? '关注成功' : '已取消关注',
|
||||
@@ -148,47 +157,24 @@ const toggleFollow = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const addToCart = (product: ProductType) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
const addToCart = async (product: ProductType) => {
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
const success = await supabaseService.addToCart(product.id, 1)
|
||||
|
||||
// 检查商品是否已存在
|
||||
const existingItem = cartItems.find((item: any) => item.productId === product.id)
|
||||
uni.hideLoading()
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: product.id, // 简单使用产品ID作为购物车ID,实际可能有规格
|
||||
productId: product.id,
|
||||
shopId: merchant.value.id,
|
||||
shopName: merchant.value.shop_name,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.images[0],
|
||||
spec: '默认规格',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
uni.showToast({
|
||||
title: '添加失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const goToProduct = (id: string) => {
|
||||
|
||||
@@ -329,6 +329,16 @@ const handleLogin = async () => {
|
||||
console.error('获取用户信息失败(忽略,不阻塞登录):', e)
|
||||
}
|
||||
|
||||
// 显式保存用户ID到本地存储,确保页面刷新或重启后 SupabaseService 能恢复身份
|
||||
const currentSession = supa.getSession()
|
||||
if (currentSession.user != null) {
|
||||
const uid = currentSession.user?.getString('id')
|
||||
if (uid != null) {
|
||||
uni.setStorageSync('user_id', uid)
|
||||
console.log('用户ID已保存到本地存储:', uid)
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
|
||||
690
pages/user/loginn.uvue
Normal file
690
pages/user/loginn.uvue
Normal file
@@ -0,0 +1,690 @@
|
||||
<template>
|
||||
<view class="page-wrapper">
|
||||
<scroll-view class="login-container" scroll-y="true" show-scrollbar="false">
|
||||
<!-- Language switch button -->
|
||||
<view class="language-switch">
|
||||
<button class="language-btn" @click="toggleLanguage">
|
||||
{{ currentLocale === 'zh-CN' ? 'EN' : '中' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- Unified content container -->
|
||||
<view class="content-wrapper">
|
||||
<!-- Logo section -->
|
||||
<view class="logo-section">
|
||||
<text class="app-title">Trainning Monitor</text>
|
||||
<text class="page-title">{{ $t('user.login.title') }}</text>
|
||||
<text class="page-subtitle">{{ $t('user.login.subtitle') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Form container -->
|
||||
<view class="form-container">
|
||||
<form @submit="onSubmit">
|
||||
<!-- Email input -->
|
||||
<view class="input-group" :class="{ 'input-error': emailError }">
|
||||
<text class="input-label">{{ $t('user.login.email') }}</text>
|
||||
<input class="input-field" name="email" type="text" :value="email"
|
||||
:placeholder="$t('user.login.email_placeholder')" @blur="validateEmail" />
|
||||
<text v-if="emailError" class="error-text">{{ emailError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Password input -->
|
||||
<view class="input-group" :class="{ 'input-error': passwordError }">
|
||||
<text class="input-label">{{ $t('user.login.password') }}</text>
|
||||
<view class="password-input-container">
|
||||
<input class="input-field" name="password" :type="showPassword ? 'text' : 'password'"
|
||||
:value="password" :placeholder="$t('user.login.password_placeholder')"
|
||||
@blur="validatePassword" />
|
||||
<view class="password-toggle" @click="showPassword = !showPassword">
|
||||
<text class="toggle-icon">{{ showPassword ? '👁' : '🙈' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="passwordError" class="error-text">{{ passwordError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Additional options -->
|
||||
<view class="options-row">
|
||||
<view class="remember-me">
|
||||
<checkbox value="rememberMe" color="#2196f3" />
|
||||
<text class="remember-me-text">{{ $t('user.login.remember_me') }}</text>
|
||||
</view>
|
||||
<text class="forgot-password" @click="navigateToForgotPassword">
|
||||
{{ $t('user.login.forgot_password') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Login button -->
|
||||
<button form-type="submit" class="login-button" :disabled="isLoading" :loading="isLoading">
|
||||
{{ $t('user.login.login_button') }}
|
||||
</button>
|
||||
|
||||
<!-- General error message -->
|
||||
<text v-if="generalError" class="general-error">{{ generalError }}</text>
|
||||
</form>
|
||||
|
||||
<!-- Social login options -->
|
||||
<view class="social-login">
|
||||
<text class="social-login-text">{{ $t('user.login.or_login_with') }}</text>
|
||||
<view class="social-buttons">
|
||||
<button class="social-button wechat" @click="socialLogin('WeChat')">
|
||||
<text class="social-icon">🟩</text>
|
||||
</button>
|
||||
<button class="social-button qq" @click="socialLogin('QQ')">
|
||||
<text class="social-icon">🔵</text>
|
||||
</button>
|
||||
<button class="social-button sms" @click="socialLogin('SMS')">
|
||||
<text class="social-icon">✉️</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Register option -->
|
||||
<view class="register-option">
|
||||
<text class="register-text">{{ $t('user.login.no_account') }}</text>
|
||||
<text class="register-link"
|
||||
@click="navigateToRegister">{{ $t('user.login.register_now') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import {HOME_REDIRECT,TABORPAGE} from '@/ak/config.uts'
|
||||
import type { AkReqOptions, AkReqResponse, AkReqError } from '@/uni_modules/ak-req/index.uts';
|
||||
import supa from '@/components/supadb/aksupainstance.uts';
|
||||
import { getCurrentUser, logout } from '@/utils/store.uts';
|
||||
import { switchLocale, getCurrentLocale } from '@/utils/utils.uts';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// email: "akoo@163.com",
|
||||
// password: "Hf2152111",
|
||||
email: "am@163.com",
|
||||
password: "kookoo",
|
||||
emailError: "",
|
||||
passwordError: "",
|
||||
generalError: "",
|
||||
isLoading: false,
|
||||
showPassword: false,
|
||||
rememberMe: false,
|
||||
currentLocale: getCurrentLocale()
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
// Try to restore saved email if "remember me" was selected
|
||||
// this.tryRestoreEmail();
|
||||
console.log('akkkk')
|
||||
}, methods: {
|
||||
|
||||
toggleLanguage() {
|
||||
const newLocale = this.currentLocale === 'zh-CN' ? 'en-US' : 'zh-CN';
|
||||
switchLocale(newLocale);
|
||||
this.currentLocale = newLocale;
|
||||
|
||||
uni.showToast({
|
||||
title: this.$t('user.login.language_switched'),
|
||||
icon: 'success'
|
||||
});
|
||||
}, tryRestoreEmail() {
|
||||
|
||||
try {
|
||||
const savedEmail = uni.getStorageSync('rememberEmail') as string;
|
||||
if (savedEmail.trim() !== "") {
|
||||
this.email = savedEmail;
|
||||
this.rememberMe = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error restoring email:", e);
|
||||
}
|
||||
},
|
||||
onSubmit(e : UniFormSubmitEvent) {
|
||||
this.handleLogin();
|
||||
},
|
||||
validateEmail() {
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (this.email.trim() === "") {
|
||||
this.emailError = this.$t('user.login.email_required');
|
||||
return false;
|
||||
} else if (!emailRegex.test(this.email)) {
|
||||
this.emailError = this.$t('user.login.email_invalid');
|
||||
return false;
|
||||
} else {
|
||||
this.emailError = '';
|
||||
return true;
|
||||
}
|
||||
}, validatePassword() {
|
||||
if (this.password.trim() === "") {
|
||||
this.passwordError = this.$t('user.login.password_required');
|
||||
return false;
|
||||
} else if (this.password.length < 6) {
|
||||
this.passwordError = this.$t('user.login.password_too_short');
|
||||
return false;
|
||||
} else {
|
||||
this.passwordError = '';
|
||||
return true;
|
||||
}
|
||||
},
|
||||
validateForm() {
|
||||
const emailValid = this.validateEmail();
|
||||
const passwordValid = this.validatePassword();
|
||||
return emailValid && passwordValid;
|
||||
},
|
||||
async handleLogin() {
|
||||
this.generalError = '';
|
||||
// 清除旧用户数据
|
||||
logout();
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// Save email if remember me is checked
|
||||
if (this.rememberMe) {
|
||||
uni.setStorageSync('rememberEmail', this.email);
|
||||
} else {
|
||||
uni.removeStorageSync('rememberEmail');
|
||||
} // Call signin method from Supabase
|
||||
const result = await supa.signIn(
|
||||
this.email,
|
||||
this.password);
|
||||
if (result.user !== null) {
|
||||
// 获取并更新当前用户,确保数据已生效
|
||||
const profile = await getCurrentUser();
|
||||
if (profile==null) throw new Error(this.$t('user.login.profile_update_failed'));
|
||||
// 登录成功提示
|
||||
uni.showToast({ title: this.$t('user.login.login_success'), icon: 'success' });
|
||||
// 跳转至运动页面
|
||||
if(TABORPAGE)
|
||||
{
|
||||
uni.switchTab({ url: HOME_REDIRECT });
|
||||
}
|
||||
else{
|
||||
uni.navigateTo({ url: HOME_REDIRECT });
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
} else {
|
||||
throw new Error(this.$t('user.login.login_failed'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
|
||||
// Show error dialog to user
|
||||
let errorMessage = this.$t('user.login.login_failed');
|
||||
if (err !== null && typeof err === 'object') {
|
||||
const error = err as Error;
|
||||
if (error.message !== null && error.message.trim() !== '') {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: this.$t('user.login.error_title'),
|
||||
content: errorMessage,
|
||||
showCancel: false,
|
||||
confirmText: this.$t('user.login.confirm'),
|
||||
success: () => {
|
||||
// Clear any existing error states
|
||||
this.generalError = '';
|
||||
this.emailError = '';
|
||||
this.passwordError = '';
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
socialLogin(provider : string) {
|
||||
// This would be implemented to handle OAuth login with different providers
|
||||
uni.showToast({
|
||||
title: `${provider} ${this.$t('user.login.coming_soon')}`,
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
navigateToRegister() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/register'
|
||||
});
|
||||
},
|
||||
navigateToForgotPassword() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/forgot-password'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Page wrapper for full screen */
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.login-container {
|
||||
width: 100%;
|
||||
background-image: linear-gradient(to bottom right, #f8f9fa, #e9ecef);
|
||||
}
|
||||
|
||||
/* Content wrapper - single scrollable container */
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 20rpx 15rpx;
|
||||
min-height: 800rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 30rpx 25rpx;
|
||||
min-height: 1000rpx;
|
||||
/* #endif */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Language switch button - adjusted for single container */
|
||||
.language-switch {
|
||||
position: absolute;
|
||||
/* #ifdef APP-PLUS */
|
||||
top: 30rpx;
|
||||
right: 25rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
top: 40rpx;
|
||||
right: 35rpx;
|
||||
/* #endif */
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.language-btn {
|
||||
/* #ifdef APP-PLUS */
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
font-size: 18rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 35rpx;
|
||||
font-size: 24rpx;
|
||||
/* #endif */
|
||||
background-color: rgba(33, 150, 243, 0.8);
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
text-align: center;
|
||||
box-shadow: 0 3rpx 10rpx rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
/* Logo section - very compact */
|
||||
.logo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin: 15rpx 0 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin: 25rpx 0 30rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.app-title {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 36rpx;
|
||||
/* #endif */
|
||||
font-weight: bold;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 32rpx;
|
||||
margin-top: 8rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 48rpx;
|
||||
margin-top: 15rpx;
|
||||
/* #endif */
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
margin-top: 4rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Form container - optimized for tablets */
|
||||
.form-container {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
max-width: 620rpx;
|
||||
padding: 18rpx;
|
||||
margin: 0 auto;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
max-width: 560rpx;
|
||||
padding: 32rpx;
|
||||
margin: 0 auto;
|
||||
/* #endif */
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Input groups - more compact */
|
||||
.input-group {
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-bottom: 18rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-bottom: 25rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.input-label {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
margin-bottom: 6rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 8rpx;
|
||||
/* #endif */
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 22rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
height: 80rpx;
|
||||
padding: 0 25rpx;
|
||||
font-size: 26rpx;
|
||||
/* #endif */
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #ddd;
|
||||
background-color: #f9f9f9;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-error .input-field {
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
margin-top: 5rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-top: 6rpx;
|
||||
/* #endif */
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* Password input */
|
||||
.password-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
/* #ifdef APP-PLUS */
|
||||
right: 12rpx;
|
||||
top: 16rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
right: 18rpx;
|
||||
top: 22rpx;
|
||||
/* #endif */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 26rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Options row - more compact */
|
||||
.options-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin: 10rpx 0 18rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin: 15rpx 0 25rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.remember-me-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
margin-left: 6rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-left: 8rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
/* #endif */
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
/* Login button - more compact */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
margin: 12rpx 0;
|
||||
border-radius: 30rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
height: 80rpx;
|
||||
font-size: 30rpx;
|
||||
margin: 18rpx 0; border-radius: 40rpx;
|
||||
/* #endif */
|
||||
background-image: linear-gradient(to right, #2196f3, #03a9f4);
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 16rpx rgba(3, 169, 244, 0.2);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
background: #ccc;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* General error */
|
||||
.general-error {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #f44336;
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
margin-top: 10rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-top: 15rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* Social login - very compact */
|
||||
.social-login {
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-top: 15rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-top: 25rpx;
|
||||
/* #endif */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.social-login-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
margin-bottom: 10rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 15rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
}
|
||||
.social-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.social-buttons .social-button {
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-left: 10rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-left: 12rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.social-buttons .social-button:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.social-button {
|
||||
/* #ifdef APP-PLUS */
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 35rpx;
|
||||
/* #endif */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 32rpx;
|
||||
/* #endif */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.google {
|
||||
background-color: #db4437;
|
||||
}
|
||||
|
||||
.facebook {
|
||||
background-color: #4267B2;
|
||||
}
|
||||
|
||||
.apple {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.wechat {
|
||||
background-color: #1aad19;
|
||||
}
|
||||
|
||||
.qq {
|
||||
background-color: #498ad5;
|
||||
}
|
||||
|
||||
.sms {
|
||||
background-color: #ffb300;
|
||||
}
|
||||
|
||||
/* Register option - compact */
|
||||
.register-option {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-top: 15rpx;
|
||||
margin-bottom: 5rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-top: 25rpx;
|
||||
margin-bottom: 10rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.register-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
margin-right: 4rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-right: 6rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
/* #endif */
|
||||
color: #2196f3;
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts';
|
||||
import { SUPA_URL } from '@/ak/config.uts';
|
||||
|
||||
// token 持久化 key
|
||||
const ACCESS_TOKEN_KEY = 'akreq_access_token';
|
||||
@@ -75,7 +76,7 @@ export class AkReq {
|
||||
headers = Object.assign({}, headers, { 'apikey': apikey }) as UTSJSONObject;
|
||||
} try {
|
||||
const res = await this.request({
|
||||
url: 'https://ak3.oulog.com/auth/v1/token?grant_type=refresh_token',
|
||||
url: SUPA_URL + '/auth/v1/token?grant_type=refresh_token',
|
||||
method: 'POST',
|
||||
data: ({ refresh_token: refreshToken } as UTSJSONObject),
|
||||
headers: headers,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createClient } from '@/components/supadb/aksupa.uts'
|
||||
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { AkReqResponse } from '@/uni_modules/ak-req/index.uts'
|
||||
|
||||
// 创建 Supabase 客户端
|
||||
const supa = createClient(SUPA_URL, SUPA_KEY)
|
||||
// 使用单例 Supabase 客户端
|
||||
// const supa = createClient(SUPA_URL, SUPA_KEY)
|
||||
|
||||
// 类型定义
|
||||
export interface Category {
|
||||
@@ -18,18 +17,46 @@ export interface Category {
|
||||
export interface Product {
|
||||
id: string
|
||||
category_id: string
|
||||
merchant_id: string
|
||||
name: string
|
||||
description?: string
|
||||
specification: string
|
||||
specification?: string
|
||||
price: number
|
||||
base_price?: number
|
||||
original_price?: number
|
||||
market_price?: number
|
||||
image?: string
|
||||
manufacturer: string
|
||||
main_image_url?: string
|
||||
image_urls?: string // JSON string
|
||||
manufacturer?: string
|
||||
sales?: number
|
||||
sale_count?: number
|
||||
stock?: number
|
||||
available_stock?: number
|
||||
badge?: string
|
||||
shop_id?: string
|
||||
shop_name?: string
|
||||
attributes?: string // JSON string
|
||||
created_at?: string
|
||||
expiry_date?: string
|
||||
approval_number?: string
|
||||
usage?: string
|
||||
side_effects?: string
|
||||
}
|
||||
|
||||
export interface Shop {
|
||||
id: string
|
||||
merchant_id: string
|
||||
shop_name: string
|
||||
shop_logo?: string
|
||||
shop_banner?: string
|
||||
description?: string
|
||||
contact_name?: string
|
||||
contact_phone?: string
|
||||
rating_avg?: number
|
||||
total_sales?: number
|
||||
product_count?: number
|
||||
total_sales_count?: number
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
@@ -73,10 +100,33 @@ export interface PaginatedResponse<T> {
|
||||
hasmore: boolean
|
||||
}
|
||||
|
||||
export interface ProductSku {
|
||||
id: string
|
||||
product_id: string
|
||||
sku_code: string
|
||||
specifications: string // JSON string
|
||||
price: number
|
||||
market_price?: number
|
||||
cost_price?: number
|
||||
stock?: number
|
||||
warning_stock?: number
|
||||
image_url?: string
|
||||
weight?: number
|
||||
status?: number
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
class SupabaseService {
|
||||
// 获取当前用户ID
|
||||
private getCurrentUserId(): string | null {
|
||||
public getCurrentUserId(): string | null {
|
||||
try {
|
||||
// 优先从 Supabase 会话获取
|
||||
const session = supa.getSession()
|
||||
if (session && session.user) {
|
||||
return session.user.getString('id')
|
||||
}
|
||||
|
||||
// 后备:尝试从本地存储获取 (兼容旧逻辑)
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
return userId ? userId as string : null
|
||||
} catch (e) {
|
||||
@@ -89,7 +139,7 @@ class SupabaseService {
|
||||
async getCategories(): Promise<Category[]> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('categories')
|
||||
.from('ml_categories')
|
||||
.select('*')
|
||||
.order('name', { ascending: true })
|
||||
.execute()
|
||||
@@ -114,10 +164,10 @@ class SupabaseService {
|
||||
): Promise<PaginatedResponse<Product>> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('category_id', categoryId)
|
||||
.order('sales', { ascending: false })
|
||||
.order('sale_count', { ascending: false })
|
||||
.page(page)
|
||||
.limit(limit)
|
||||
.execute()
|
||||
@@ -152,6 +202,28 @@ class SupabaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据商品ID获取SKU列表
|
||||
async getProductSkus(productId: string): Promise<ProductSku[]> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_product_skus')
|
||||
.select('*')
|
||||
.eq('product_id', productId)
|
||||
.eq('status', 1)
|
||||
.execute()
|
||||
|
||||
if (response.error) {
|
||||
console.error('获取商品SKU失败:', response.error)
|
||||
return []
|
||||
}
|
||||
|
||||
return response.data as ProductSku[]
|
||||
} catch (error) {
|
||||
console.error('获取商品SKU异常:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索商品
|
||||
async searchProducts(
|
||||
keyword: string,
|
||||
@@ -162,18 +234,18 @@ class SupabaseService {
|
||||
): Promise<PaginatedResponse<Product>> {
|
||||
try {
|
||||
let query = supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*', { count: 'exact' })
|
||||
.or(`name.ilike.%${keyword}%,manufacturer.ilike.%${keyword}%,specification.ilike.%${keyword}%`)
|
||||
|
||||
// 根据sortBy和ascending设置排序
|
||||
if (sortBy === 'price') {
|
||||
query = query.order('price', { ascending })
|
||||
} else if (sortBy === 'sales') {
|
||||
query = query.order('sales', { ascending: false }) // 销量总是降序
|
||||
query = query.order('base_price', { ascending })
|
||||
} else if (sortBy === 'sales' || sortBy === 'sale_count') {
|
||||
query = query.order('sale_count', { ascending: false }) // 销量总是降序
|
||||
} else {
|
||||
// 默认按销量降序
|
||||
query = query.order('sales', { ascending: false })
|
||||
query = query.order('sale_count', { ascending: false })
|
||||
}
|
||||
|
||||
const response = await query
|
||||
@@ -215,7 +287,7 @@ class SupabaseService {
|
||||
async getProductById(productId: string): Promise<Product | null> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.eq('id', productId)
|
||||
.single()
|
||||
@@ -233,11 +305,80 @@ class SupabaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据商户ID获取店铺信息
|
||||
async getShopByMerchantId(merchantId: string): Promise<Shop | null> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_shops')
|
||||
.select('*')
|
||||
.eq('merchant_id', merchantId)
|
||||
.single()
|
||||
.executeAs<Shop>()
|
||||
|
||||
if (response.error) {
|
||||
console.error('获取店铺信息失败:', response.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = response.data
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length > 0) return data[0] as Shop
|
||||
return null
|
||||
}
|
||||
return data as Shop
|
||||
} catch (error) {
|
||||
console.error('获取店铺信息异常:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 根据商户ID获取商品列表
|
||||
async getProductsByMerchantId(merchantId: string, page: number = 1, limit: number = 20): Promise<PaginatedResponse<Product>> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_products')
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('merchant_id', merchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.page(page)
|
||||
.limit(limit)
|
||||
.execute()
|
||||
|
||||
if (response.error) {
|
||||
console.error('获取商户商品失败:', response.error)
|
||||
return {
|
||||
data: [],
|
||||
total: 0,
|
||||
page,
|
||||
limit,
|
||||
hasmore: false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data as Product[],
|
||||
total: response.total || 0,
|
||||
page,
|
||||
limit,
|
||||
hasmore: response.hasmore || false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商户商品异常:', error)
|
||||
return {
|
||||
data: [],
|
||||
total: 0,
|
||||
page,
|
||||
limit,
|
||||
hasmore: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取热销商品(按销量排序)
|
||||
async getHotProducts(limit: number = 10): Promise<Product[]> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.order('sales', { ascending: false })
|
||||
.limit(limit)
|
||||
@@ -259,7 +400,7 @@ class SupabaseService {
|
||||
async getProductsByPrice(limit: number = 10, ascending: boolean = true): Promise<Product[]> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.order('price', { ascending })
|
||||
.limit(limit)
|
||||
@@ -281,7 +422,7 @@ class SupabaseService {
|
||||
async getProductsByNewest(limit: number = 10): Promise<Product[]> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(limit)
|
||||
@@ -304,7 +445,7 @@ class SupabaseService {
|
||||
try {
|
||||
// 直接使用 neq 空字符串查询,忽略 null 值(null 表示没有 badge,不应被推荐)
|
||||
const response = await supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.neq('badge', '')
|
||||
.order('sales', { ascending: false })
|
||||
@@ -328,7 +469,7 @@ class SupabaseService {
|
||||
async getDiscountProducts(limit: number = 10): Promise<Product[]> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('products')
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.eq('badge', '特价')
|
||||
.order('sales', { ascending: false })
|
||||
@@ -369,14 +510,13 @@ class SupabaseService {
|
||||
selected,
|
||||
created_at,
|
||||
updated_at,
|
||||
products!inner (
|
||||
ml_products!inner (
|
||||
id,
|
||||
name,
|
||||
image,
|
||||
price,
|
||||
specification,
|
||||
shop_id,
|
||||
shop_name
|
||||
merchant_id
|
||||
)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
@@ -387,12 +527,59 @@ class SupabaseService {
|
||||
console.error('获取购物车失败:', response.error)
|
||||
return []
|
||||
}
|
||||
|
||||
const cartData = response.data as any[]
|
||||
|
||||
// 调试日子:打印购物车数据第一条结构,确认产品字段名
|
||||
if (cartData && Array.isArray(cartData) && cartData.length > 0) {
|
||||
console.log('Cart Item Structure:', JSON.stringify(cartData[0]))
|
||||
}
|
||||
|
||||
const merchantIds: string[] = []
|
||||
|
||||
if (cartData && Array.isArray(cartData)) {
|
||||
for (const item of cartData) {
|
||||
// PostgREST 返回的关联字段通常与表名一致
|
||||
// 尝试获取ml_products,如果为空则尝试products
|
||||
let product = item['ml_products'] as any
|
||||
if (!product) {
|
||||
product = item['products'] as any
|
||||
}
|
||||
|
||||
if (product && product.merchant_id && !merchantIds.includes(product.merchant_id)) {
|
||||
merchantIds.push(product.merchant_id as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查询店铺信息
|
||||
const shopMap = new Map<string, any>()
|
||||
if (merchantIds.length > 0) {
|
||||
const shopRes = await supa
|
||||
.from('ml_shops')
|
||||
.select('id, merchant_id, shop_name')
|
||||
.in('merchant_id', merchantIds)
|
||||
.execute()
|
||||
|
||||
if (!shopRes.error && shopRes.data != null) {
|
||||
const shops = shopRes.data as any[]
|
||||
for (const shop of shops) {
|
||||
shopMap.set(shop.merchant_id as string, shop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理返回数据,构建CartItem数组
|
||||
const cartItems: CartItem[] = []
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
for (const item of response.data) {
|
||||
const product = item.products as any
|
||||
if (cartData && Array.isArray(cartData)) {
|
||||
for (const item of cartData) {
|
||||
let product = item['ml_products'] as any
|
||||
if (!product) {
|
||||
product = item['products'] as any
|
||||
}
|
||||
|
||||
const merchantId = product?.merchant_id as string
|
||||
const shopInfo = shopMap.get(merchantId)
|
||||
|
||||
cartItems.push({
|
||||
id: item.id as string,
|
||||
@@ -405,8 +592,8 @@ class SupabaseService {
|
||||
product_image: product?.image as string,
|
||||
product_price: product?.price as number,
|
||||
product_specification: product?.specification as string,
|
||||
shop_id: product?.shop_id as string,
|
||||
shop_name: product?.shop_name as string,
|
||||
shop_id: shopInfo ? (shopInfo['id'] as string) : (merchantId || 'unknown_shop'),
|
||||
shop_name: shopInfo ? (shopInfo['shop_name'] as string) : '未知店铺',
|
||||
created_at: item.created_at as string,
|
||||
updated_at: item.updated_at as string
|
||||
})
|
||||
@@ -430,27 +617,59 @@ class SupabaseService {
|
||||
}
|
||||
|
||||
// 检查商品是否已在购物车中
|
||||
const existingResponse = await supa
|
||||
// 注意:必须处理 sku_id 为空的情况,使用 is.null 过滤器
|
||||
let query = supa
|
||||
.from('ml_shopping_cart')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.eq('product_id', productId)
|
||||
.eq('sku_id', skuId || '')
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (skuId && skuId.length > 0) {
|
||||
query = query.eq('sku_id', skuId)
|
||||
} else {
|
||||
query = query.is('sku_id', null)
|
||||
}
|
||||
|
||||
const existingResponse = await query.single().execute()
|
||||
|
||||
let existingItem: any | null = null
|
||||
|
||||
if (existingResponse.data != null) {
|
||||
const rawData = existingResponse.data as any
|
||||
if (Array.isArray(rawData)) {
|
||||
if (rawData.length > 0) {
|
||||
existingItem = rawData[0]
|
||||
}
|
||||
} else {
|
||||
existingItem = rawData
|
||||
}
|
||||
}
|
||||
|
||||
let response
|
||||
if (existingResponse.data) {
|
||||
if (existingItem != null) {
|
||||
// 商品已存在,更新数量
|
||||
const existingItem = existingResponse.data as any
|
||||
response = await supa
|
||||
.from('ml_shopping_cart')
|
||||
.update({
|
||||
quantity: (existingItem.quantity || 0) + quantity,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', existingItem.id)
|
||||
.execute()
|
||||
console.log('Found existing cart item:', JSON.stringify(existingItem))
|
||||
|
||||
// 确保 existingItem.id 存在
|
||||
const itemId = existingItem['id']
|
||||
const itemQty = existingItem['quantity']
|
||||
|
||||
if (itemId != null) {
|
||||
const currentQty = typeof itemQty === 'number' ? itemQty : parseInt(String(itemQty || 0))
|
||||
const newQty = currentQty + quantity
|
||||
|
||||
response = await supa
|
||||
.from('ml_shopping_cart')
|
||||
.update({
|
||||
quantity: newQty,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', itemId)
|
||||
.execute()
|
||||
} else {
|
||||
console.error('购物车已有商品但缺少ID,无法更新. Data:', JSON.stringify(existingItem))
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// 商品不存在,添加新记录
|
||||
response = await supa
|
||||
@@ -671,7 +890,7 @@ class SupabaseService {
|
||||
|
||||
const response = await supa
|
||||
.from('ml_user_addresses')
|
||||
.select('*')
|
||||
.select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
|
||||
.eq('user_id', userId)
|
||||
.order('is_default', { ascending: false })
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -700,7 +919,7 @@ class SupabaseService {
|
||||
|
||||
const response = await supa
|
||||
.from('ml_user_addresses')
|
||||
.select('*')
|
||||
.select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
|
||||
.eq('id', addressId)
|
||||
.eq('user_id', userId)
|
||||
.single()
|
||||
@@ -745,12 +964,12 @@ class SupabaseService {
|
||||
.from('ml_user_addresses')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
recipient_name: address.recipient_name,
|
||||
phone: address.phone,
|
||||
receiver_name: address.recipient_name,
|
||||
receiver_phone: address.phone,
|
||||
province: address.province,
|
||||
city: address.city,
|
||||
district: address.district,
|
||||
detail_address: address.detail_address,
|
||||
address_detail: address.detail_address,
|
||||
postal_code: address.postal_code || null,
|
||||
is_default: address.is_default || false,
|
||||
created_at: new Date().toISOString(),
|
||||
@@ -792,13 +1011,22 @@ class SupabaseService {
|
||||
if (address.is_default) {
|
||||
await this.clearDefaultAddress(userId)
|
||||
}
|
||||
|
||||
// 构造更新数据,映射字段名到数据库列名
|
||||
const updateData = {}
|
||||
if (address.recipient_name != null) updateData['receiver_name'] = address.recipient_name
|
||||
if (address.phone != null) updateData['receiver_phone'] = address.phone
|
||||
if (address.province != null) updateData['province'] = address.province
|
||||
if (address.city != null) updateData['city'] = address.city
|
||||
if (address.district != null) updateData['district'] = address.district
|
||||
if (address.detail_address != null) updateData['address_detail'] = address.detail_address
|
||||
if (address.postal_code != null) updateData['postal_code'] = address.postal_code
|
||||
if (address.is_default != null) updateData['is_default'] = address.is_default
|
||||
updateData['updated_at'] = new Date().toISOString()
|
||||
|
||||
const response = await supa
|
||||
.from('ml_user_addresses')
|
||||
.update({
|
||||
...address,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.update(updateData)
|
||||
.eq('id', addressId)
|
||||
.eq('user_id', userId)
|
||||
.execute()
|
||||
@@ -843,6 +1071,298 @@ class SupabaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// 清除默认地址(内部使用)
|
||||
private async clearDefaultAddress(userId: string): Promise<void> {
|
||||
try {
|
||||
await supa
|
||||
.from('ml_user_addresses')
|
||||
.update({
|
||||
is_default: false,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('user_id', userId)
|
||||
.eq('is_default', true)
|
||||
.execute()
|
||||
} catch (error) {
|
||||
console.error('清除默认地址异常:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户资料
|
||||
async getUserProfile(): Promise<any | null> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return null
|
||||
|
||||
// 联合查询 auth user 和 profile
|
||||
// 由于 Supabase auth table 不可直接访问,这里查询 ml_user_profiles
|
||||
const response = await supa
|
||||
.from('ml_user_profiles')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (response.error) {
|
||||
// 如果不存在 profile,可能只有 auth user,这里暂时返回空或创建默认
|
||||
return null
|
||||
}
|
||||
return response.data
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
async createOrder(orderData: {
|
||||
merchant_id: string,
|
||||
product_amount: number,
|
||||
shipping_fee: number,
|
||||
total_amount: number,
|
||||
shipping_address: any,
|
||||
items: any[]
|
||||
}): Promise<string | null> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return null
|
||||
|
||||
// 生成订单号
|
||||
const orderNo = 'ORD' + Date.now() + Math.floor(Math.random() * 1000)
|
||||
|
||||
// 1. 创建主订单
|
||||
const orderResponse = await supa
|
||||
.from('ml_orders')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
merchant_id: orderData.merchant_id,
|
||||
order_no: orderNo,
|
||||
product_amount: orderData.product_amount,
|
||||
shipping_fee: orderData.shipping_fee,
|
||||
total_amount: orderData.total_amount,
|
||||
paid_amount: 0,
|
||||
shipping_address: JSON.stringify(orderData.shipping_address),
|
||||
order_status: 1, // 待付款
|
||||
payment_status: 1, // 未支付
|
||||
shipping_status: 1, // 未发货
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (orderResponse.error) {
|
||||
console.error('创建订单失败:', orderResponse.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const orderId = orderResponse.data['id'] as string
|
||||
|
||||
// 2. 创建订单项
|
||||
const orderItems = orderData.items.map((item: any) => ({
|
||||
order_id: orderId,
|
||||
product_id: item.product_id,
|
||||
sku_id: item.sku_id || null,
|
||||
product_name: item.product_name,
|
||||
sku_name: item.sku_name || '',
|
||||
specifications: item.specifications ? JSON.stringify(item.specifications) : '{}',
|
||||
image_url: item.image_url,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
total_amount: item.price * item.quantity,
|
||||
created_at: new Date().toISOString()
|
||||
}))
|
||||
|
||||
const itemsResponse = await supa
|
||||
.from('ml_order_items')
|
||||
.insert(orderItems)
|
||||
.execute()
|
||||
|
||||
if (itemsResponse.error) {
|
||||
console.error('创建订单项失败:', itemsResponse.error)
|
||||
// 此时应该回滚订单,但这里简化处理
|
||||
return null
|
||||
}
|
||||
|
||||
// 3. 清除购物车中已购买的商品(如果是从购物车购买)
|
||||
// 这一步通常在前端调用 removeCartItem 或在此处根据参数处理
|
||||
|
||||
return orderId
|
||||
} catch (error) {
|
||||
console.error('创建订单异常:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
async getOrders(status: number = 0): Promise<any[]> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return []
|
||||
|
||||
let query = supa
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
ml_order_items (*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (status > 0) {
|
||||
query = query.eq('order_status', status)
|
||||
}
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error) {
|
||||
console.error('获取订单列表失败:', response.error)
|
||||
return []
|
||||
}
|
||||
|
||||
return response.data || []
|
||||
} catch (error) {
|
||||
console.error('获取订单列表异常:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
async getOrderDetail(orderId: string): Promise<any | null> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return null
|
||||
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
ml_order_items (*),
|
||||
ml_shops (shop_name, id)
|
||||
`)
|
||||
.eq('id', orderId)
|
||||
.eq('user_id', userId)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (response.error) {
|
||||
return null
|
||||
}
|
||||
return response.data
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏相关
|
||||
async checkFavorite(productId: string): Promise<boolean> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return false
|
||||
|
||||
const response = await supa
|
||||
.from('ml_user_favorites')
|
||||
.select('id')
|
||||
.eq('user_id', userId)
|
||||
.eq('target_id', productId)
|
||||
.eq('target_type', 1) // 1 for product
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
return !!response.data
|
||||
} catch(e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async toggleFavorite(productId: string): Promise<boolean> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return false
|
||||
|
||||
// Check if exists
|
||||
const exists = await this.checkFavorite(productId)
|
||||
|
||||
if (exists) {
|
||||
// Delete
|
||||
await supa
|
||||
.from('ml_user_favorites')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
.eq('target_id', productId)
|
||||
.eq('target_type', 1)
|
||||
.execute()
|
||||
return false // Now not favorite
|
||||
} else {
|
||||
// Add
|
||||
await supa
|
||||
.from('ml_user_favorites')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
target_id: productId,
|
||||
target_type: 1,
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
.execute()
|
||||
return true // Now favorite
|
||||
}
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getFavorites(): Promise<any[]> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return []
|
||||
|
||||
// 需要关联查询商品信息
|
||||
const response = await supa
|
||||
.from('ml_user_favorites')
|
||||
.select(`
|
||||
id,
|
||||
target_id,
|
||||
created_at,
|
||||
ml_products!target_id (
|
||||
id, name, image_urls, main_image_url, price, sales
|
||||
)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.eq('target_type', 1)
|
||||
.order('created_at', { ascending: false })
|
||||
.execute()
|
||||
|
||||
if (response.error) return []
|
||||
return response.data || []
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getAddressList(): Promise<UserAddress[]> {
|
||||
try {
|
||||
const userId = this.getCurrentUserId()
|
||||
if (!userId) return []
|
||||
|
||||
const response = await supa
|
||||
.from('ml_user_addresses')
|
||||
.select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
|
||||
.eq('user_id', userId)
|
||||
.order('is_default', { ascending: false })
|
||||
.order('created_at', { ascending: false })
|
||||
.execute()
|
||||
|
||||
if (response.error) {
|
||||
console.error('获取地址列表失败:', response.error)
|
||||
return []
|
||||
}
|
||||
return response.data as UserAddress[]
|
||||
} catch (e) {
|
||||
console.error('获取地址列表异常:', e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认地址
|
||||
async setDefaultAddress(addressId: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -877,31 +1397,6 @@ class SupabaseService {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 清除用户的默认地址(内部方法)
|
||||
private async clearDefaultAddress(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_user_addresses')
|
||||
.update({
|
||||
is_default: false,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('user_id', userId)
|
||||
.eq('is_default', true)
|
||||
.execute()
|
||||
|
||||
if (response.error) {
|
||||
console.error('清除默认地址失败:', response.error)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('清除默认地址异常:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
|
||||
Reference in New Issue
Block a user