consumer模块完成度95%,安卓端大部分页面能正常获取数据,页面样式显示基本正常,逐渐完善;消费者端的积分、余额、评价、优惠券等小模块正在完善

This commit is contained in:
cyh666666
2026-03-02 17:21:19 +08:00
parent df84fd8642
commit 7e74b88e1e
34 changed files with 17088 additions and 1751 deletions

View File

@@ -1,13 +1,88 @@
<script lang="uts">
import { setIsLoggedIn, setUserProfile, getCurrentUser } from '@/utils/store.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 自动登录凭据(开发测试用)
const AUTO_LOGIN_EMAIL = 'test@mall.com'
const AUTO_LOGIN_PASSWORD = 'Hf2152111'
export default {
onLaunch: function () {
console.log('App Launch')
// 尝试自动登录并跳转(使用 Promise 链)
this.tryAutoLogin()
},
onShow: function () {
console.log('App Show')
},
onHide: function () {
console.log('App Hide')
},
methods: {
tryAutoLogin: function(): void {
// 检查是否已有有效会话
const session = supa.getSession()
if (session.user != null) {
console.log('已有有效会话,跳过自动登录')
setIsLoggedIn(true)
uni.reLaunch({ url: '/pages/mall/consumer/index' })
return
}
// 检查本地存储的登录状态
const savedUserId = uni.getStorageSync('user_id')
if (savedUserId != null && savedUserId != '') {
console.log('本地存储中有用户ID尝试恢复会话')
getCurrentUser().then((profile) => {
if (profile != null) {
console.log('会话恢复成功')
setIsLoggedIn(true)
uni.reLaunch({ url: '/pages/mall/consumer/index' })
return
}
// 恢复失败,执行自动登录
this.doAutoLogin()
}).catch(() => {
console.log('会话恢复失败,尝试自动登录')
this.doAutoLogin()
})
return
}
// 执行自动登录
this.doAutoLogin()
},
doAutoLogin: function(): void {
console.log('开始自动登录...')
supa.signIn(AUTO_LOGIN_EMAIL, AUTO_LOGIN_PASSWORD).then((result) => {
if (result.user != null) {
console.log('自动登录成功')
setIsLoggedIn(true)
// 保存用户ID到本地存储
const uid = result.user.getString('id')
if (uid != null) {
uni.setStorageSync('user_id', uid)
console.log('用户ID已保存:', uid)
}
// 获取用户资料
getCurrentUser().then(() => {
console.log('获取用户资料成功')
}).catch((e) => {
console.log('获取用户资料失败(忽略)')
})
// 直接跳转到首页
uni.reLaunch({ url: '/pages/mall/consumer/index' })
} else {
console.log('自动登录失败,用户需要手动登录')
}
}).catch((e) => {
console.error('自动登录异常:', e)
})
}
}
}
</script>

View File

@@ -87,19 +87,22 @@
"path": "pages/mall/consumer/messages",
"style": {
"navigationBarTitleText": "消息",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/cart",
"style": {
"navigationBarTitleText": "购物车"
"navigationBarTitleText": "购物车",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/profile",
"style": {
"navigationBarTitleText": "我的"
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
}
],
@@ -911,32 +914,32 @@
{
"pagePath": "pages/mall/consumer/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
"iconPath": "static/tabbar/home.svg",
"selectedIconPath": "static/tabbar/home-active.svg"
},
{
"pagePath": "pages/mall/consumer/category",
"text": "分类",
"iconPath": "static/tabbar/category.png",
"selectedIconPath": "static/tabbar/category-active.png"
"iconPath": "static/tabbar/category.svg",
"selectedIconPath": "static/tabbar/category-active.svg"
},
{
"pagePath": "pages/mall/consumer/messages",
"text": "消息",
"iconPath": "static/tabbar/messages.png",
"selectedIconPath": "static/tabbar/messages-active.png"
"iconPath": "static/tabbar/messages.svg",
"selectedIconPath": "static/tabbar/messages-active.svg"
},
{
"pagePath": "pages/mall/consumer/cart",
"text": "购物车",
"iconPath": "static/tabbar/cart.png",
"selectedIconPath": "static/tabbar/cart-active.png"
"iconPath": "static/tabbar/cart.svg",
"selectedIconPath": "static/tabbar/cart-active.svg"
},
{
"pagePath": "pages/mall/consumer/profile",
"text": "我的",
"iconPath": "static/tabbar/profile.png",
"selectedIconPath": "static/tabbar/profile-active.png"
"iconPath": "static/tabbar/profile.svg",
"selectedIconPath": "static/tabbar/profile-active.svg"
}
]
},

View File

@@ -59,8 +59,10 @@ const loadAddresses = async () => {
const supabaseAddresses = await supabaseService.getAddresses()
// 转换数据格式以匹配前端界面
const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress): Address => {
return {
const transformedAddresses: Address[] = []
for (let i = 0; i < supabaseAddresses.length; i++) {
const item = supabaseAddresses[i]
const addr: Address = {
id: item.id,
name: item.recipient_name,
phone: item.phone,
@@ -69,9 +71,10 @@ const loadAddresses = async () => {
district: item.district,
detail: item.detail_address,
isDefault: item.is_default,
label: '' // Supabase表没有label字段
label: ''
} as Address
})
transformedAddresses.push(addr)
}
addresses.value = transformedAddresses

View File

@@ -14,11 +14,17 @@
</view>
</view>
<!-- 导航栏占位符 - 已移除 -->
<!-- <view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view> -->
<!-- 导航栏占位符 - 需要包含statusBarHeight + 导航栏高度44px -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 购物车内容 -->
<scroll-view scroll-y class="cart-content" :style="{ paddingTop: (statusBarHeight + 10) + 'px' }">
<scroll-view
:scroll-y="true"
class="cart-content"
:show-scrollbar="false"
:enhanced="true"
:bounces="true"
>
<!-- 空购物车 -->
<view v-if="!loading && cartItems.length === 0" class="empty-cart">
<text class="empty-icon">🛒</text>
@@ -726,28 +732,28 @@ const goToCheckout = () => {
<style>
.cart-page {
width: 100%;
height: 100%;
flex: 1; /* 使用 Flex 撑满 */
height: 100%; /* 兼容性考虑,部分环境需要 */
background-color: #f5f5f5;
display: flex;
flex-direction: column;
overflow: hidden; /* 防止整页滚动 */
}
/* 智能导航栏 */
.smart-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.smart-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #4CAF50;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
display: flex;
flex-direction: column;
justify-content: flex-start;
flex-shrink: 0;
}
.nav-container {
padding: 0 16px;
@@ -809,7 +815,9 @@ const goToCheckout = () => {
/* 内容区 */
.cart-content {
flex: 1;
height: 0; /* 配合 flex: 1 实现自适应滚动 */
/* 必须设置 height: 0 或 overflow: hidden 可以在 flex 容器中正确收缩 */
height: 0px;
width: 100%;
padding-bottom: 60px; /* 为底部结算栏留出空间 */
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -25,19 +25,24 @@
</view>
</view>
<!-- 导航栏占位 - 修复只使用44px高度因为search-bar的paddingTop已处理statusBarHeight -->
<view class="navbar-placeholder" :style="{ height: '44px' }"></view>
<!-- 导航栏占位 - 需要包含statusBarHeight + 搜索框高度44px -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 分类内容区 -->
<view class="category-content">
<!-- 左侧一级分类 -->
<scroll-view scroll-y class="primary-category">
<scroll-view
:scroll-y="true"
class="primary-category"
:scroll-top="scrollTop"
:scroll-with-animation="true"
>
<view
v-for="item in primaryCategories"
:key="item.id"
:class="['primary-item', { active: activePrimary === item.id }]"
:class="['primary-item', { active: isPrimaryActive(item.id) }]"
@click="selectPrimaryCategory(item.id)"
:style="{ backgroundColor: activePrimary === item.id ? item.color : 'transparent' }"
:style="{ backgroundColor: getPrimaryItemBgColor(item) }"
>
<text class="primary-icon">{{ item.icon }}</text>
<text class="primary-name">{{ item.name }}</text>
@@ -46,7 +51,7 @@
<!-- 右侧商品列表 -->
<scroll-view
scroll-y
:scroll-y="true"
class="product-content"
@scrolltolower="loadMore"
:lower-threshold="50"
@@ -57,6 +62,21 @@
<text class="category-desc">{{ currentCategoryDesc }}</text>
</view>
<!-- 二级分类 -->
<view v-if="subCategories.length > 0" class="sub-category-section">
<view class="sub-category-list">
<view
v-for="sub in subCategories"
:key="sub.id"
:class="['sub-category-item', { active: isSubActive(sub.id) }]"
@click="selectSubCategory(sub.id)"
>
<text class="sub-category-icon">{{ sub.icon }}</text>
<text class="sub-category-name">{{ sub.name }}</text>
</view>
</view>
</view>
<!-- 商品网格 -->
<view v-if="productList.length > 0" class="product-grid">
<view
@@ -98,6 +118,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import supabaseService from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
@@ -111,15 +132,20 @@ type LocalCategory = {
// 响应式数据
const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度
const headerHeight = ref(44)
const primaryCategories = ref<LocalCategory[]>([])
const subCategories = ref<LocalCategory[]>([]) // 二级分类列表
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const activeSubCategory = ref<string>('') // 当前选中的二级分类
const selectedParentId = ref<string>('') // 当前选中的一级分类ID用于高亮显示
const cartCount = ref(3)
const hasMore = ref(true)
const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
const hasLoadedFromParams = ref(false)
const currentPage = ref(1)
const loading = ref(false)
const scrollTop = ref(0)
const pendingCategoryId = ref('') // 待处理的分类ID从其他页面跳转过来时暂存
// 获取当前分类信息
const currentCategoryName = ref('')
@@ -147,7 +173,7 @@ async function loadProducts(): Promise<void> {
page: currentPage.value
})
if (currentPage.value === 1) {
if (currentPage.value == 1) {
productList.value = response.data
} else {
productList.value.push(...response.data)
@@ -155,75 +181,303 @@ async function loadProducts(): Promise<void> {
hasMore.value = response.hasmore
// 更新当前分类信息
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === activePrimary.value)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
// 更新当前分类信息 - 先在一级分类中查找,再在二级分类中查找
let foundCat: LocalCategory | null = null
for (let i = 0; i < primaryCategories.value.length; i++) {
if (primaryCategories.value[i].id == activePrimary.value) {
foundCat = primaryCategories.value[i]
break
}
}
if (foundCat == null) {
for (let i = 0; i < subCategories.value.length; i++) {
if (subCategories.value[i].id == activePrimary.value) {
foundCat = subCategories.value[i]
break
}
}
}
if (foundCat != null) {
currentCategoryName.value = foundCat.name
currentCategoryDesc.value = foundCat.description
}
console.log('商品列表加载完成,当前总数量:', productList.value.length)
} catch (error) {
console.error('加载商品数据失败:', error)
if (currentPage.value === 1) {
if (currentPage.value == 1) {
productList.value = []
}
} finally {
loading.value = false
loading.value = false
}
}
// 加载二级分类
async function loadSubCategories(parentId: string): Promise<void> {
console.log('加载二级分类父级ID:', parentId)
try {
const subCats = await supabaseService.getSubCategories(parentId)
console.log('获取到二级分类数量:', subCats.length)
const categories: LocalCategory[] = []
for (let i = 0; i < subCats.length; i++) {
const cat = subCats[i]
categories.push({
id: cat.id,
name: cat.name,
icon: cat.icon,
description: cat.description,
color: cat.color
})
}
subCategories.value = categories
} catch (e) {
console.error('加载二级分类失败:', e)
subCategories.value = []
}
}
// 判断一级分类是否选中
function isPrimaryActive(categoryId: string): boolean {
return selectedParentId.value == categoryId
}
// 判断二级分类是否选中
function isSubActive(subCategoryId: string): boolean {
return activeSubCategory.value == subCategoryId || activePrimary.value == subCategoryId
}
// 获取一级分类的背景色
function getPrimaryItemBgColor(item: LocalCategory): string {
if (isPrimaryActive(item.id)) {
return item.color
}
return 'transparent'
}
// 选择二级分类
async function selectSubCategory(subCategoryId: string): Promise<void> {
console.log('选择二级分类:', subCategoryId)
activeSubCategory.value = subCategoryId
// 使用二级分类ID加载商品
currentPage.value = 1
hasMore.value = true
activePrimary.value = subCategoryId // 临时设置为二级分类ID用于加载商品
await loadProducts()
}
// 选择一级分类 - 必须在 loadCategories 之前定义
// originalCategoryId: 可能是一级分类ID也可能是二级分类ID
async function selectPrimaryCategory(originalCategoryId: string): Promise<void> {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', originalCategoryId)
if (originalCategoryId == '') {
console.error('categoryId为空尝试使用第一个分类')
if (primaryCategories.value.length > 0) {
originalCategoryId = primaryCategories.value[0].id
} else {
console.error('没有可用的分类')
return
}
}
// 检查传入的是否是一级分类ID
let targetParentId = originalCategoryId
let targetSubId = ''
console.log('当前一级分类列表长度:', primaryCategories.value.length)
let foundInPrimary: LocalCategory | null = null
for (let i = 0; i < primaryCategories.value.length; i++) {
if (primaryCategories.value[i].id == originalCategoryId) {
foundInPrimary = primaryCategories.value[i]
break
}
}
console.log('在一级分类中查找结果:', foundInPrimary != null)
if (foundInPrimary == null) {
// 传入的可能是二级分类ID需要查找其父级分类
console.log('传入的ID不在一级分类中可能是二级分类ID尝试查找父级分类')
// 从服务器获取分类信息以确定父级
try {
const categoryInfo = await supabaseService.getCategoryById(originalCategoryId)
if (categoryInfo != null && categoryInfo.parent_id != null && categoryInfo.parent_id != '') {
console.log('找到父级分类ID:', categoryInfo.parent_id)
// 检查父级分类ID是否在一级分类列表中
console.log('查找父级分类ID:', categoryInfo.parent_id)
let parentInPrimary: LocalCategory | null = null
for (let i = 0; i < primaryCategories.value.length; i++) {
if (primaryCategories.value[i].id == categoryInfo.parent_id) {
parentInPrimary = primaryCategories.value[i]
break
}
}
console.log('父级分类查找结果:', parentInPrimary != null)
if (parentInPrimary != null) {
console.log('父级分类在列表中找到:', parentInPrimary.name)
targetParentId = categoryInfo.parent_id!
targetSubId = originalCategoryId // 记住要选中的二级分类
} else {
console.log('父级分类不在列表中,使用第一个分类')
// 打印当前列表中的所有分类ID
for (let i = 0; i < primaryCategories.value.length; i++) {
console.log('列表中的分类:', primaryCategories.value[i].id, primaryCategories.value[i].name)
}
if (primaryCategories.value.length > 0) {
targetParentId = primaryCategories.value[0].id
}
}
} else {
console.log('未找到父级分类,使用第一个分类')
if (primaryCategories.value.length > 0) {
targetParentId = primaryCategories.value[0].id
}
}
} catch (e) {
console.error('获取分类信息失败:', e)
if (primaryCategories.value.length > 0) {
targetParentId = primaryCategories.value[0].id
}
}
}
console.log('最终选中的一级分类ID:', targetParentId)
console.log('需要选中的二级分类ID:', targetSubId)
// 设置一级分类高亮
selectedParentId.value = targetParentId
activePrimary.value = targetParentId
// 加载二级分类
await loadSubCategories(targetParentId)
// 如果有要选中的二级分类
if (targetSubId != '') {
activeSubCategory.value = targetSubId
} else {
// 如果没有指定二级分类,但有二级分类列表,默认选中第一个
if (subCategories.value.length > 0) {
activeSubCategory.value = subCategories.value[0].id
targetSubId = subCategories.value[0].id
console.log('默认选中第一个二级分类:', subCategories.value[0].name)
} else {
activeSubCategory.value = ''
}
}
// 自动滚动到选中位置
let foundIndex = -1
for (let i = 0; i < primaryCategories.value.length; i++) {
if (primaryCategories.value[i].id == targetParentId) {
foundIndex = i
break
}
}
if (foundIndex != -1) {
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()
let itemHeight = 70
if (systemInfo.windowWidth > 1025) {
itemHeight = 80
}
const scrollViewHeight = systemInfo.windowHeight - systemInfo.statusBarHeight - 44
const targetScrollTop = (foundIndex * itemHeight) - (scrollViewHeight / 2) + (itemHeight / 2)
scrollTop.value = Math.max(0, targetScrollTop)
console.log(`滚动左侧菜单: index=${foundIndex}, target=${scrollTop.value}`)
}
// 查找分类信息
let foundCategory: LocalCategory | null = null
for (let i = 0; i < primaryCategories.value.length; i++) {
if (primaryCategories.value[i].id == targetParentId) {
foundCategory = primaryCategories.value[i]
break
}
}
if (foundCategory != null) {
currentCategoryName.value = foundCategory.name
currentCategoryDesc.value = foundCategory.description
} else {
console.log('分类信息未找到,使用第一个分类的信息')
if (primaryCategories.value.length > 0) {
const firstCategory = primaryCategories.value[0]
currentCategoryName.value = firstCategory.name
currentCategoryDesc.value = firstCategory.description
}
}
currentPage.value = 1
hasMore.value = true
// 如果有选中的二级分类使用二级分类ID加载商品否则使用一级分类ID
const categoryIdForProducts = (targetSubId != '') ? targetSubId : targetParentId
activePrimary.value = categoryIdForProducts // 临时设置为要加载的分类ID
await loadProducts()
}
async function loadCategories(): Promise<void> {
try {
const categoriesData = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categoriesData.length)
// 只获取一级分类parent_id 为 null 的分类)
const categoriesData = await supabaseService.getParentCategories()
console.log('加载一级分类数据成功,数量:', categoriesData.length)
// 映射数据并添加默认颜色,防止选中时背景透明导致文字看不清
// 过滤掉医药健康相关分类
const categories: LocalCategory[] = []
const rawList = categoriesData as any[]
for (let i = 0; i < rawList.length; i++) {
const raw = rawList[i]
const catObj = (raw instanceof UTSJSONObject) ? (raw as UTSJSONObject) : (JSON.parse(JSON.stringify(raw)) as UTSJSONObject)
const name = catObj.getString('name') ?? ''
for (let i = 0; i < categoriesData.length; i++) {
const cat = categoriesData[i]
const name = cat.name
console.log('一级分类:', cat.id, name)
if (name.includes('医药') || name.includes('健康')) {
console.log('过滤掉分类:', name)
continue
}
const id = catObj.getString('id') ?? ''
const description = catObj.getString('description') ?? ''
const icon = catObj.getString('icon') ?? catObj.getString('icon_url') ?? '📦'
const color = catObj.getString('color') ?? '#4CAF50'
categories.push({
id,
name,
icon,
description,
color
id: cat.id,
name: cat.name,
icon: cat.icon,
description: cat.description,
color: cat.color
})
}
console.log('最终一级分类列表数量:', categories.length)
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中一个分类
if (activePrimary.value == '') {
// 优先查找"厨具"相关的分类作为默认
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('厨具')) ?? categories[0]
activePrimary.value = defaultCategory.id
console.log('设置默认分类为:', defaultCategory.name, 'ID:', defaultCategory.id)
currentCategoryName.value = defaultCategory.name
currentCategoryDesc.value = defaultCategory.description
} else {
// 如果已经选中了分类可能来自Storage更新显示信息
const current = categories.find((c: LocalCategory): boolean => c.id == activePrimary.value)
if (current != null) {
currentCategoryName.value = current.name
currentCategoryDesc.value = current.description
// 如果此时没有商品列表(且没有正在加载),可能需要加载
if (productList.value.length === 0 && !loading.value) {
loadProducts()
}
}
// 检查是否有待处理的分类ID从其他页面跳转过来时暂存
if (pendingCategoryId.value != '') {
console.log('发现待处理的分类ID:', pendingCategoryId.value)
// 直接调用 selectPrimaryCategory它会处理一级或二级分类ID
const idToSelect = pendingCategoryId.value
pendingCategoryId.value = '' // 清除暂存
selectPrimaryCategory(idToSelect)
return
}
// 检查是否有预设的分类ID
if (activePrimary.value != '') {
console.log('有预设的分类ID:', activePrimary.value)
const target = categories.find((c: LocalCategory): boolean => c.id == activePrimary.value)
if (target != null) {
console.log('找到目标分类,执行选中:', target.name)
selectPrimaryCategory(activePrimary.value)
return
}
}
// 默认选中第一个分类或"厨具"分类
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('厨具')) ?? categories[0]
if (defaultCategory != null) {
console.log('设置默认分类:', defaultCategory.name)
selectPrimaryCategory(defaultCategory.id)
}
} else {
console.warn('从Supabase获取的分类数据为空')
@@ -241,71 +495,6 @@ function loadMore(): void {
}
}
// 选择一级分类
async function selectPrimaryCategory(categoryId: string): Promise<void> {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
// 验证categoryId是否有效
if (categoryId == '') {
console.error('categoryId为空尝试使用第一个分类')
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
} else {
console.error('没有可用的分类')
return
}
}
console.log('验证后的categoryId:', categoryId)
console.log('当前activePrimary的值:', activePrimary.value)
// 更新活动分类
activePrimary.value = categoryId
console.log('更新后的activePrimary:', activePrimary.value)
// 更新当前分类信息
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === categoryId)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
console.log('✅ 找到分类:', category.name, '描述:', category.description)
} else {
console.error('❌ 未找到分类ID:', categoryId, ',使用第一个分类')
// 如果找不到对应的分类,使用第一个分类
if (primaryCategories.value.length > 0) {
const firstCategory = primaryCategories.value[0]
currentCategoryName.value = firstCategory.name
currentCategoryDesc.value = firstCategory.description
activePrimary.value = firstCategory.id
categoryId = firstCategory.id
console.log('使用默认分类:', firstCategory.name)
}
}
console.log('准备加载商品数据...')
// 重置分页并加载
currentPage.value = 1
hasMore.value = true
await loadProducts()
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', productList.value.length)
console.log('商品列表:', productList.value)
// 验证数据是否已正确更新
console.log('数据更新验证:')
console.log('activePrimary:', activePrimary.value)
console.log('currentCategoryName:', currentCategoryName.value)
console.log('currentCategoryDesc:', currentCategoryDesc.value)
console.log('productList长度:', productList.value.length)
console.log('=== selectPrimaryCategory函数执行完成 ===')
}
// 生命周期
onMounted(() => {
loadCategories().then(() => {
@@ -317,12 +506,42 @@ onMounted(() => {
})
})
// 页面加载时处理参数 - 这是处理分类切换的主要入口
// 页面显示时检查是否有参数传递过来
onShow(() => {
console.log('=== category页面onShow被调用 ===')
// 检查是否有存储的分类选择
const savedCategoryId = uni.getStorageSync('selectedCategory')
console.log('onShow检查Storage:', savedCategoryId)
if (savedCategoryId != null && savedCategoryId != '') {
const targetId = savedCategoryId as string
console.log('onShow发现存储的分类ID:', targetId)
// 清除存储,避免下次进入默认选中
uni.removeStorageSync('selectedCategory')
// 确保分类数据已加载
if (primaryCategories.value.length > 0) {
// 如果当前未选中或选中的不是目标分类,则切换
if (activePrimary.value != targetId) {
console.log('onShow执行切换分类:', targetId)
selectPrimaryCategory(targetId)
} else {
console.log('当前已是目标分类:', targetId)
}
} else {
// 如果分类数据未加载暂存ID等待loadCategories完成后处理
console.log('分类数据尚未加载暂存ID等待加载')
pendingCategoryId.value = targetId
}
}
})
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
console.log('=== category页面onLoad被调用 ===')
let categoryId = ''
let categoryName = ''
@@ -381,72 +600,6 @@ onLoad((options: any) => {
console.log('=== category页面onLoad执行完成 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
// 1. 优先检查 Storage 中的参数 (由首页传入)
const storageCategoryId = (uni.getStorageSync('selectedCategory') as string) ?? ''
if (storageCategoryId !== '') {
console.log('✅ onShow中找到Storage分类参数:', storageCategoryId)
hasLoadedFromParams.value = true
// 清除Storage防止下次误读
uni.removeStorageSync('selectedCategory')
if (activePrimary.value !== storageCategoryId) {
selectPrimaryCategory(storageCategoryId)
}
// 如果分类还没加载完这里设置了ID等loadCategories完成后会自动匹配信息
return
}
// 在onShow中我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时可能不会触发onLoad
// 而是触发onShow
// 获取当前页面实例和参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('onShow中获取参数:', rawPageOptions)
const pageOptObj = (rawPageOptions instanceof UTSJSONObject) ? (rawPageOptions as UTSJSONObject) : (JSON.parse(JSON.stringify(rawPageOptions)) as UTSJSONObject)
// 检查是否有分类参数
const pageCategoryId = pageOptObj.getString('categoryId') ?? ''
if (pageCategoryId !== '') {
hasLoadedFromParams.value = true
const categoryId = pageCategoryId
const categoryName = pageOptObj.getString('name') ?? ''
console.log('✅ onShow中找到分类参数:', categoryId, categoryName)
console.log('URL中的时间戳参数:', pageOptObj.getString('timestamp') ?? '')
console.log('URL中的随机参数:', pageOptObj.getString('random') ?? '')
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
}
}
console.log('=== category页面onShow执行完成 ===')
})
// 添加到购物车
async function addToCart(product: Product): Promise<void> {
@@ -574,7 +727,7 @@ function onScan(): void {
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
background-color: #4CAF50;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
}
@@ -714,32 +867,31 @@ function onScan(): void {
/* 左侧一级分类 */
.primary-category {
width: 120px;
height: 100%; /* 占满父容器高度 */
margin-right: 20px; /* gap replacement */
width: 63px;
height: 100%;
margin-right: 6px;
background: white;
border-radius: 12px;
padding: 12px 0;
border-radius: 8px;
padding: 6px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
}
.primary-item {
display: flex;
flex-direction: column; /* 图标和文字垂直排列 */
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12px 8px;
margin: 4px 8px;
padding: 12px 4px;
margin: 4px 6px;
border-radius: 8px;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease;
color: #666;
text-align: center;
}
.primary-item:hover {
transform: translateY(-2px); /* 悬停时向上浮动 */
transform: translateY(-2px);
}
.primary-item.active {
@@ -748,24 +900,22 @@ function onScan(): void {
}
.primary-icon {
font-size: 24px;
margin-bottom: 6px;
margin-right: 0; /* 移除右边距 */
font-size: 20px;
margin-bottom: 4px;
margin-right: 0;
text-align: center;
/* display: block; removed for uniapp-x support */
}
.primary-name {
font-size: 13px;
line-height: 1.4;
/* display: block; removed for uniapp-x support */
font-size: 11px;
line-height: 1.25;
}
/* 右侧内容区 */
.product-content {
flex: 1;
height: 100%; /* 占满父容器高度 */
padding: 0; /* 移除内边距,交给内部元素 */
height: 100%;
padding: 0;
}
.category-header {
@@ -789,6 +939,56 @@ function onScan(): void {
color: #666;
}
/* 二级分类 */
.sub-category-section {
padding: 8px 8px;
background: white;
margin-bottom: 8px;
}
.sub-category-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
justify-content: space-between;
}
.sub-category-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px 4px;
background: #f5f5f5;
border-radius: 8px;
flex: 1;
min-width: 50px;
}
.sub-category-item.active {
background: #4CAF50;
}
.sub-category-item.active .sub-category-name {
color: white;
}
.sub-category-icon {
font-size: 16px;
margin-bottom: 4px;
}
.sub-category-name {
font-size: 11px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
width: 100%;
}
/* 商品网格 */
.product-grid {
display: flex;
@@ -954,52 +1154,47 @@ function onScan(): void {
}
.category-content {
padding: 0 8px;
padding: 0 4px;
}
.primary-category {
width: 80px; /* 减小宽度 */
/* display: flex; 移除flex布局保持默认 */
/* flex-wrap: wrap; 移除换行 */
padding: 8px 0;
margin-right: 10px; /* Gap replacement */
width: 56px;
padding: 4px 0;
margin-right: 4px;
border-radius: 8px;
}
.primary-item {
/* width: calc(25% - 8px); 移除百分比宽度 */
width: auto; /* 恢复自动宽度 */
margin: 4px;
padding: 8px 4px;
/* text-align: center; 已经在通用样式中设置 */
margin: 2px 2px;
padding: 4px 2px;
border-radius: 6px;
}
.primary-icon {
margin-right: 0;
margin-bottom: 4px;
font-size: 20px;
margin-bottom: 2px;
font-size: 16px;
}
.primary-name {
font-size: 11px;
font-size: 9px;
line-height: 1.1;
}
.product-grid {
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 8px; REMOVED */
padding: 0 4px 20px 4px; /* 增加底部内边距 */
padding: 0 4px 20px 4px;
}
.product-card {
width: 48%; /* 2 columns for mobile */
width: 48%;
margin: 1%;
}
/* 手机端商品卡片极简模式 - 仿照主页样式 */
.product-spec,
.manufacturer,
.original-price,
.sales-info,
.product-badge { /* 分类页也隐藏角标,保持整洁 */
.product-badge {
display: none;
}
@@ -1008,7 +1203,7 @@ function onScan(): void {
}
.product-image {
height: 100px; /* 由于分类页右侧空间更窄,图片高度设得更小一点 */
height: 100px;
}
.product-name {
@@ -1018,10 +1213,7 @@ function onScan(): void {
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
/* display: -webkit-box; REMOVED for support */
/* -webkit-line-clamp: 2; REMOVED for support */
/* -webkit-box-orient: vertical; REMOVED for support */
lines: 2; /* UTS text truncation */
lines: 2;
}
.price-section {
@@ -1040,30 +1232,52 @@ function onScan(): void {
}
.product-meta {
display: none; /* 隐藏整个元数据行 */
}
.search-container {
padding: 0 12px;
height: 44px;
}
.search-box {
padding: 8px 16px;
display: none;
}
.category-header {
padding: 12px 4px 0 4px;
padding: 8px 4px 0 4px;
}
.category-title {
font-size: 14px;
}
.category-desc {
font-size: 11px;
}
}
/* 中屏手机/小平板 (415px-768px) */
/* 中屏手机 (415px-768px) */
@media screen and (min-width: 415px) and (max-width: 768px) {
.search-container {
padding: 0 16px;
height: 44px;
}
.primary-category {
width: 62px;
padding: 6px 0;
margin-right: 6px;
border-radius: 8px;
}
.primary-item {
margin: 2px 2px;
padding: 5px 2px;
border-radius: 6px;
}
.primary-icon {
font-size: 18px;
margin-bottom: 3px;
}
.primary-name {
font-size: 10px;
line-height: 1.1;
}
.product-card {
width: 46%;
margin: 2%;
@@ -1077,6 +1291,26 @@ function onScan(): void {
height: 44px;
}
.primary-category {
width: 100px;
padding: 10px 0;
margin-right: 16px;
}
.primary-item {
margin: 4px 4px;
padding: 10px 6px;
}
.primary-icon {
font-size: 22px;
margin-bottom: 5px;
}
.primary-name {
font-size: 12px;
}
.product-card {
width: 30%;
margin: 1.5%;
@@ -1097,7 +1331,7 @@ function onScan(): void {
.primary-category {
width: 160px;
padding: 16px 0;
margin-right: 30px; /* Gap replacement */
margin-right: 30px;
}
.primary-item {
@@ -1132,14 +1366,9 @@ function onScan(): void {
font-size: 15px;
}
.product-grid {
/* grid-template-columns: repeat(4, 1fr); REMOVED */
/* gap: 24px; REMOVED */
}
.product-card {
border-radius: 14px;
width: 22%; /* 4 columns */
width: 22%;
margin: 1.5%;
}
@@ -1164,12 +1393,11 @@ function onScan(): void {
@media screen and (min-width: 1400px) {
.category-content {
max-width: 1600px;
/* gap: 40px; REMOVED */
padding: 0 32px;
}
.primary-category {
margin-right: 40px; /* Gap replacement */
margin-right: 40px;
}
.primary-category {
@@ -1209,14 +1437,9 @@ function onScan(): void {
font-size: 16px;
}
.product-grid {
/* grid-template-columns: repeat(5, 1fr); REMOVED */
/* gap: 28px; REMOVED */
}
.product-card {
border-radius: 16px;
width: 17%; /* 5 columns */
width: 17%;
margin: 1.5%;
}

View File

@@ -165,6 +165,8 @@ function getCurrentTime(): string {
function setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...')
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
const filter = ({
event: 'INSERT',
schema: 'public',
@@ -173,41 +175,80 @@ function setupRealtimeSubscription(): void {
realtimeChannel = supa.channel('public:ml_chat_messages')
.on('postgres_changes', filter, (payload: any) => {
console.log('=== 收到实时订阅回调 ===')
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) return
if (newMsgAny == null) {
console.log('newMsgAny 为空,跳过')
return
}
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
console.log('收到新消息:', newMsg)
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
const msgId = newMsg.getString('id') ?? ''
const content = newMsg.getString('content') ?? ''
console.log('=== 消息详情 ===')
console.log('消息ID:', msgId)
console.log('发送者ID:', senderId)
console.log('接收者ID:', receiverId)
console.log('当前用户ID:', currentUserId.value)
console.log('商家ID:', merchantId.value)
console.log('消息内容:', content)
if (senderId === currentUserId.value) {
// 检查消息是否已经在列表中(避免重复)
for (let i = 0; i < messages.value.length; i++) {
if (messages.value[i].id == msgId) {
console.log('消息已存在,跳过')
return
}
}
// 判断消息类型
const isMyMessage = (senderId == currentUserId.value)
const isForMe = (receiverId == currentUserId.value)
const isRelatedToCurrentChat = (senderId == merchantId.value || receiverId == merchantId.value)
console.log('=== 条件判断 ===')
console.log('isMyMessage:', isMyMessage)
console.log('isForMe:', isForMe)
console.log('isRelatedToCurrentChat:', isRelatedToCurrentChat)
// 如果消息与当前聊天无关,跳过
if (!isRelatedToCurrentChat) {
console.log('消息与当前聊天无关,跳过')
return
}
if (receiverId === currentUserId.value) {
if (merchantId.value != '' && senderId !== merchantId.value) {
return
}
// 如果是自己发送的消息,或者是发给自己的消息,都显示
if (isMyMessage || isForMe) {
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
const date = new Date(createdAt)
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const incomingMsg: UiChatMessage = {
id: newMsg.getString('id') ?? Date.now().toString(),
type: 'received',
content: newMsg.getString('content') ?? '',
id: msgId,
type: isMyMessage ? 'sent' : 'received',
content: content,
time: timeStr
}
console.log('=== 添加新消息到列表 ===')
console.log('消息类型:', incomingMsg.type)
console.log('消息内容:', incomingMsg.content)
messages.value.push(incomingMsg)
scrollToBottom()
} else {
console.log('条件不满足,不添加消息')
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态:', status)
if (err != null) {
console.log('订阅错误:', err)
}
})
}
@@ -221,21 +262,27 @@ async function loadChatHistory(): Promise<void> {
return
}
messages.value = rawMsgs.reverse().map((m: ChatMessage): UiChatMessage => {
// 使用 for 循环替代 map
const uiMessages: UiChatMessage[] = []
for (let i = rawMsgs.length - 1; i >= 0; i--) {
const m = rawMsgs[i]
const date = new Date(m.created_at ?? new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const sender = m.sender_id ?? ''
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
const rawId = (m.id ?? '').toString()
const msgId = rawId !== '' ? rawId : Date.now().toString()
return {
const msgId = rawId != '' ? rawId : Date.now().toString() + i.toString()
const uiMsg: UiChatMessage = {
id: msgId,
type: msgType,
content: m.content ?? '',
time: timeStr
}
})
uiMessages.push(uiMsg)
}
messages.value = uiMessages
setTimeout(() => {
scrollToBottom()
@@ -288,22 +335,13 @@ const sendMessage = async () => {
const content = inputMessage.value.trim()
if (content == '') return
// 添加发送的消息 (乐观更新)
const newMessage: UiChatMessage = {
id: Date.now().toString(),
type: 'sent',
content: content,
time: getCurrentTime()
}
messages.value.push(newMessage)
// 清空输入框
inputMessage.value = ''
// 滚动到底部
scrollToBottom()
// 发送到 Supabase
if (merchantId.value != '') {
// 不使用乐观更新,等待实时订阅推送
// 这样可以确保多端同步
const success = await supabaseService.sendMessage(merchantId.value, content)
if (!success) {
uni.showToast({

View File

@@ -39,18 +39,22 @@ const loadCoupons = async () => {
uni.showLoading({ title: '加载中...' })
try {
const userCoupons = await supabaseService.getUserCoupons(1)
coupons.value = userCoupons.map((item: UserCoupon): Coupon => {
const couponList: Coupon[] = []
for (let i = 0; i < userCoupons.length; i++) {
const item = userCoupons[i]
const amountVal = item.amount ?? 0
const expiryVal = (item.expire_at != null && item.expire_at !== '')
? item.expire_at.substring(0, 10)
: '长期有效'
return {
const coupon: Coupon = {
id: item.id,
title: (item.template_name != null && item.template_name !== '') ? item.template_name : '优惠券',
amount: `¥${amountVal}`,
expiry: expiryVal
} as Coupon
})
couponList.push(coupon)
}
coupons.value = couponList
} catch (e) {
console.error('加载优惠券失败', e)
coupons.value = []

View File

@@ -0,0 +1,235 @@
# 智能推荐系统文档
## 一、系统概述
智能推荐系统基于用户行为数据,为用户提供个性化的商品推荐。系统综合分析用户的搜索历史、浏览历史,结合全站热销商品数据,生成智能推荐列表。
## 二、推荐逻辑架构
```
┌─────────────────────────────────────────────────────────────┐
│ 智能推荐系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 用户搜索历史 │ │ 用户浏览历史 │ │ 全站热销商品 │ │
│ │ (权重高) │ │ (权重中) │ │ (权重低) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 关键词匹配 │ │ 分类匹配 │ │ 销量排序 │ │
│ │ 相关商品 │ │ 相似商品 │ │ 热门商品 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 去重 & 合并结果 │ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 返回推荐商品列表 │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
## 三、权重分配策略
### 3.1 数据源权重
| 数据源 | 权重 | 说明 |
|--------|------|------|
| 用户搜索历史 | 50% | 用户主动搜索的关键词最能反映购买意向 |
| 用户浏览历史 | 30% | 用户浏览过的商品分类反映兴趣偏好 |
| 热销商品 | 20% | 全站热销商品作为兜底和补充 |
### 3.2 推荐优先级
```
优先级 1: 用户搜索历史匹配的商品最多占推荐列表的50%
优先级 2: 用户浏览过的分类下的商品最多占推荐列表的30%
优先级 3: 全站热销商品(填充剩余位置)
```
## 四、核心算法详解
### 4.1 智能推荐主流程
```typescript
async getSmartRecommendations(limit: number): Promise<Product[]> {
const products: Product[] = []
const addedIds = new Set<string>() // 用于去重
// 步骤1: 根据用户搜索历史推荐(权重最高)
const searchHistory = await getUserSearchHistory(5)
if (searchHistory.length > 0) {
const keywordProducts = await searchProductsByKeywords(searchHistory, limit)
// 添加到结果列表,去重
}
// 步骤2: 根据用户浏览历史推荐(权重中)
if (products.length < limit) {
const browseCategories = await getUserBrowseCategories(3)
if (browseCategories.length > 0) {
const categoryProducts = await getProductsByCategories(browseCategories, limit - products.length)
// 添加到结果列表,去重
}
}
// 步骤3: 补充热销商品(权重低)
if (products.length < limit) {
const hotProducts = await getHotProducts(limit - products.length + 5)
// 添加到结果列表,去重
}
return products.slice(0, limit)
}
```
### 4.2 搜索历史匹配算法
```typescript
// 根据用户搜索关键词匹配商品
async searchProductsByKeywords(keywords: string[], limit: number): Promise<Product[]> {
// 1. 获取商品数据
// 2. 遍历商品,检查名称和描述是否包含关键词
// 3. 匹配成功的商品加入推荐列表
for (product in products) {
for (keyword in keywords) {
if (product.name.contains(keyword) || product.description.contains(keyword)) {
matched = true
break
}
}
}
}
```
### 4.3 浏览历史分类匹配算法
```typescript
// 根据用户浏览过的商品分类推荐
async getProductsByCategories(categoryIds: string[], limit: number): Promise<Product[]> {
// 1. 获取用户浏览过的商品分类ID列表
// 2. 查询这些分类下的商品
// 3. 按销量排序返回
}
```
## 五、热搜词系统
### 5.1 热搜词计算
```typescript
async getHotKeywords(limit: number): Promise<string[]> {
// 1. 获取最近100条搜索记录
// 2. 统计每个关键词的出现频率
// 3. 按频率降序排序
// 4. 返回前N个高频关键词
}
```
### 5.2 热搜词展示规则
| 排名 | 样式 | 说明 |
|------|------|------|
| 1-3名 | 红色背景 | 热度最高,突出显示 |
| 4-10名 | 灰色背景 | 普通热度 |
## 六、用户行为记录
### 6.1 搜索行为记录
```typescript
// 在用户执行搜索时调用
async recordSearch(keyword: string, resultCount: number): Promise<void> {
// 记录字段:
// - user_id: 用户ID可选支持匿名
// - keyword: 搜索关键词
// - result_count: 搜索结果数量
// - created_at: 搜索时间
}
```
### 6.2 浏览行为记录
```typescript
// 在用户查看商品详情时调用
async recordBrowse(productId: string, duration: number): Promise<void> {
// 记录字段:
// - user_id: 用户ID
// - product_id: 商品ID
// - browse_duration: 浏览时长(秒)
// - created_at/updated_at: 时间戳
}
```
## 七、数据表结构
### 7.1 搜索历史表 (ml_search_history)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| user_id | UUID | 用户ID可空 |
| keyword | VARCHAR(200) | 搜索关键词 |
| result_count | INTEGER | 搜索结果数量 |
| created_at | TIMESTAMP | 搜索时间 |
### 7.2 浏览历史表 (ml_browse_history)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| user_id | UUID | 用户ID |
| product_id | UUID | 商品ID |
| browse_duration | INTEGER | 浏览时长(秒) |
| created_at | TIMESTAMP | 首次浏览时间 |
| updated_at | TIMESTAMP | 最近浏览时间 |
## 八、API接口列表
| API | 方法 | 说明 |
|-----|------|------|
| getSmartRecommendations | GET | 获取智能推荐商品 |
| getHotKeywords | GET | 获取热搜词列表 |
| getUserSearchHistory | GET | 获取用户搜索历史 |
| getUserBrowseCategories | GET | 获取用户浏览分类 |
| recordSearch | POST | 记录搜索行为 |
| recordBrowse | POST | 记录浏览行为 |
## 九、性能优化建议
### 9.1 缓存策略
- 热搜词列表缓存5分钟减少数据库查询
- 用户搜索历史缓存10分钟
- 推荐结果缓存3分钟
### 9.2 数据量控制
- 搜索历史每个用户最多保留100条
- 浏览历史每个用户最多保留50条
- 热搜词统计只统计最近30天的数据
## 十、扩展方向
### 10.1 短期优化
1. **时间衰减因子**:近期行为权重更高
2. **购买行为加权**:已购买商品的相关商品权重提升
3. **收藏行为加权**:收藏商品的相关商品权重提升
### 10.2 长期规划
1. **协同过滤**:基于相似用户的行为推荐
2. **商品相似度**:基于商品属性计算相似度
3. **机器学习**:使用推荐算法模型
---
*文档版本1.0*
*最后更新2024年*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,9 @@ const addToCart = async (product: Product) => {
const loadFavorites = async () => {
const res = await supabaseService.getFavorites()
favorites.value = res.map((item: any): Product => {
const productList: Product[] = []
for (let i = 0; i < res.length; i++) {
const item = res[i]
let prod: any | null = null
let itemObj: UTSJSONObject | null = null
@@ -117,7 +119,7 @@ const loadFavorites = async () => {
}
}
return {
const product: Product = {
id: id,
name: name,
price: price,
@@ -126,7 +128,9 @@ const loadFavorites = async () => {
main_image_url: image,
sale_count: sales
} as Product
})
productList.push(product)
}
favorites.value = productList
}
const goShopping = () => {

View File

@@ -8,7 +8,7 @@
</view>
</view>
<scroll-view class="footprint-content" scroll-y @scrolltolower="loadMore">
<scroll-view class="footprint-content" :scroll-y="true" @scrolltolower="loadMore">
<view v-if="footprints.length === 0 && !isLoading" class="empty-footprints">
<text class="empty-icon">👣</text>
<text class="empty-text">暂无浏览记录</text>
@@ -501,6 +501,7 @@ onMounted(() => {
.footprint-content {
flex: 1;
height: 0px;
}
.empty-footprints {

File diff suppressed because it is too large Load Diff

View File

@@ -174,7 +174,7 @@
</view>
</view> -->
<!-- 热销药品专区 -->
<!-- 热销药品专区 -->
<view class="hot-products">
<view class="section-header">
<view class="title-section">
@@ -285,10 +285,12 @@ const hasMore = ref(true)
const activeSort = ref('recommend') // 默认展示智能推荐
const activeFilter = ref('recommend')
const currentPage = ref(1)
const priceAscending = ref(true) // 价格排序方向true=升序false=降序
// 数据源
const hotProducts = ref<Product[]>([])
const recommendedProducts = ref<Product[]>([])
const hotKeywords = ref<string[]>([])
// 屏幕尺寸检测
const isMobile = ref(false)
@@ -367,7 +369,10 @@ const loadCategories = async (): Promise<void> => {
// 获取二级分类数据
const loadSubCategories = async (parentId: string): Promise<void> => {
try {
console.log('[loadSubCategories] 开始加载二级分类, parentId:', parentId)
const subData = await supabaseService.getSubCategories(parentId)
console.log('[loadSubCategories] 获取到二级分类数量:', subData.length)
console.log('[loadSubCategories] 二级分类数据:', JSON.stringify(subData))
subCategories.value = subData
} catch (error) {
console.error('加载子分类数据失败:', error)
@@ -377,18 +382,31 @@ const loadSubCategories = async (parentId: string): Promise<void> => {
// 点击一级分类
const onParentCategoryClick = async (category: Category): Promise<void> => {
// 如果已经选中,则切换显示/隐藏二级分类
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
console.log('[onParentCategoryClick] 点击一级分类:', category.name, 'id:', category.id)
// 如果已经选中,则切换显示/隐藏二级分类
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
console.log('[onParentCategoryClick] 切换显示状态')
showSubCategories.value = !showSubCategories.value
return
}
}
// 选中新的分类
selectedParentCategory.value = category
showSubCategories.value = true
// 选中新的分类
selectedParentCategory.value = category
showSubCategories.value = true
console.log('[onParentCategoryClick] showSubCategories 设置为 true')
// 加载二级分类
await loadSubCategories(category.id)
// 加载二级分类
await loadSubCategories(category.id)
// 如果没有二级分类,直接跳转到分类页
if (subCategories.value.length == 0) {
console.log('[onParentCategoryClick] 没有二级分类,直接跳转到分类页')
uni.setStorageSync('selectedCategory', category.id)
uni.switchTab({
url: '/pages/mall/consumer/category'
})
}
}
// 点击二级分类
@@ -453,32 +471,28 @@ const doLoadHotProducts = async (targetLimit: number, resolve: (value: void) =>
switch (activeSort.value) {
case 'sales':
console.log('调用 getHotProducts')
products = await supabaseService.getHotProducts(limit)
console.log('调用 getProductsBySales')
products = await supabaseService.getProductsBySales(limit)
break
case 'price':
console.log('调用 getProductsByPrice')
// 按价格升序(从低到高)
products = await supabaseService.getProductsByPrice(limit, true)
console.log('调用 getProductsByPrice, 升序:', priceAscending.value)
products = await supabaseService.getProductsByPrice(limit, priceAscending.value)
break
case 'new':
console.log('调用 getProductsByNewest')
// 按创建时间,最新的在前
products = await supabaseService.getProductsByNewest(limit)
break
case 'recommend':
console.log('调用 getRecommendedProducts')
// 推荐商品带badge的商品
products = await supabaseService.getRecommendedProducts(limit)
console.log('调用 getSmartRecommendations')
products = await supabaseService.getSmartRecommendations(limit)
break
case 'discount':
console.log('调用 getDiscountProducts')
// 特价商品badge为'特价'
products = await supabaseService.getDiscountProducts(limit)
break
default:
console.log('调用默认 getHotProducts')
products = await supabaseService.getHotProducts(limit)
console.log('调用默认 getProductsBySales')
products = await supabaseService.getProductsBySales(limit)
}
console.log('加载到的商品数量:', products.length)
@@ -516,6 +530,25 @@ function loadRecommendedProducts(limit: number): Promise<void> {
})
}
// 加载热搜词
const loadHotKeywords = async (): Promise<void> => {
try {
const keywords = await supabaseService.getHotKeywords(10)
hotKeywords.value = keywords
console.log('加载热搜词:', keywords.length, '个')
} catch (error) {
console.error('加载热搜词失败:', error)
hotKeywords.value = []
}
}
// 点击热搜词进行搜索
const searchByKeyword = (keyword: string): void => {
uni.navigateTo({
url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(keyword)}`
})
}
// 初始化数据
const initData = async () => {
// 首先确保用户资料已加载
@@ -527,6 +560,7 @@ const initData = async () => {
}
await loadCategories()
await loadBrands()
await loadHotKeywords()
await loadHotProducts(defaultLoadLimit)
await loadRecommendedProducts(defaultLoadLimit)
}
@@ -719,7 +753,17 @@ const switchBrand = (brand: Brand) => {
// 切换排序
const switchSort = (sortId: string) => {
activeSort.value = sortId
// 如果点击的是价格排序,切换升序/降序
if (sortId === 'price' && activeSort.value === 'price') {
priceAscending.value = !priceAscending.value
console.log('切换价格排序方向,升序:', priceAscending.value)
} else {
// 切换到其他排序时,重置价格排序为升序
if (sortId !== 'price') {
priceAscending.value = true
}
activeSort.value = sortId
}
hasMore.value = true // 重置加载更多状态
// 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts(defaultLoadLimit)
@@ -776,26 +820,45 @@ const loadMore = async () => {
try {
// 获取当前热销商品的数量
const currentCount = hotProducts.value.length
const nextLimit = currentCount + 6
const nextPage = Math.floor(currentCount / 6) + 1
const additionalLimit = 6
console.log('开始加载更多,当前数量:', currentCount, '目标数量:', nextLimit)
console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage)
// 加载更多热销商品
await loadHotProducts(nextLimit)
// 加载更多商品
let newProducts: Product[] = []
switch (activeSort.value) {
case 'sales':
newProducts = await supabaseService.getProductsBySales(currentCount + additionalLimit)
break
case 'price':
newProducts = await supabaseService.getProductsByPrice(currentCount + additionalLimit, priceAscending.value)
break
case 'new':
newProducts = await supabaseService.getProductsByNewest(currentCount + additionalLimit)
break
case 'recommend':
newProducts = await supabaseService.getSmartRecommendations(currentCount + additionalLimit)
break
case 'discount':
newProducts = await supabaseService.getDiscountProducts(currentCount + additionalLimit)
break
default:
newProducts = await supabaseService.getProductsBySales(currentCount + additionalLimit)
}
console.log('加载到的新商品数量:', newProducts.length)
// 检查是否还有更多数据
if (hotProducts.value.length === currentCount) {
if (newProducts.length <= currentCount) {
hasMore.value = false
uni.showToast({
title: '没有更多了',
icon: 'none'
})
} else {
// 还有数据,或者是刚加载了一批
/* uni.showToast({
title: '加载完成',
icon: 'success'
}) */
// 更新商品列表
hotProducts.value = newProducts
}
} catch (error) {
console.error('加载更多失败:', error)
@@ -963,14 +1026,10 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
background-color: #4CAF50;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15); /* 调整为与分类页一致 */
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* will-change: transform, opacity; removed for uniapp-x support */
/* pointer-events: auto; */
/* backface-visibility: hidden; */
/* -webkit-backface-visibility: hidden; */
}
/* 导航栏搜索框容器内边距调整 */
@@ -1320,14 +1379,12 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.card-name {
font-size: 12px;
font-weight: 500;
font-weight: normal;
color: #333;
margin-bottom: 4px;
text-align: center;
lines: 1;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
overflow-wrap: break-word;
}
.card-desc {
@@ -1524,6 +1581,59 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
color: #666;
}
/* 热搜词区域 */
.hot-keywords-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
}
.keywords-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 15px;
}
.keyword-item {
display: flex;
align-items: center;
padding: 8px 16px;
background: #f5f5f5;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s ease;
}
.keyword-item:hover {
background: #fff0f0;
}
.keyword-rank {
width: 20px;
height: 20px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
color: #999;
background: #eee;
margin-right: 8px;
}
.keyword-rank.top-three {
background: #ff4757;
color: white;
}
.keyword-text {
font-size: 14px;
color: #333;
}
/* 热销药品 */
.hot-products {
background: white;
@@ -2076,45 +2186,43 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.category-grid {
/* grid-template-columns: repeat(5, 1fr); removed for uniapp-x support */
/* gap: 8px; removed */
margin: 0 -1%;
padding: 0 4px;
}
.category-grid .category-card {
width: 18%; /* 5 cols : 20 - 2 */
margin: 0 1% 8px 1%;
padding: 8px 0;
background: transparent; /* 移除卡片背景 */
box-shadow: none; /* 移除阴影 */
border: none; /* 移除边框 */
width: 18%;
margin: 0 1% 6px 1%;
padding: 4px 0;
background: transparent;
box-shadow: none;
border: none;
}
.category-card:hover {
transform: none; /* 移动端移除悬停效果 */
transform: none;
box-shadow: none;
}
.card-icon {
width: 44px; /* 减小图标尺寸 */
height: 44px;
border-radius: 22px;
margin-bottom: 6px;
width: 36px;
height: 36px;
border-radius: 18px;
margin-bottom: 4px;
}
.card-icon-text {
font-size: 20px;
font-size: 18px;
}
.card-name {
font-size: 11px; /* 减小文字大小 */
font-size: 10px;
font-weight: normal;
color: #333;
}
.card-desc {
display: none; /* 手机端隐藏描述文字,保持界面整洁 */
display: none;
}
.services-grid .service-card {

View File

@@ -13,8 +13,8 @@
</view>
</view>
<!-- 导航栏占位符 -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
<!-- 导航栏占位符 - 需要包含statusBarHeight + 导航栏高度44px -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 消息分类标签 - 固定在顶部,方便随时切换 -->
<view class="tabs-container">
@@ -350,9 +350,15 @@ const loadMessages = async () => {
promoMessages.length = 0
// 1. 获取通知 (系统、订单、优惠)
console.log('[loadMessages] 开始获取通知...')
const notes = await supabaseService.getUserNotifications()
console.log('[loadMessages] 获取到通知数量:', notes.length)
notes.forEach((note: Notification) => {
// 使用 for 循环替代 forEach
for (let i = 0; i < notes.length; i++) {
const note = notes[i]
console.log('[loadMessages] 通知类型:', note.type, '标题:', note.title)
const item: MessageItem = {
id: note.id,
title: note.title,
@@ -361,7 +367,7 @@ const loadMessages = async () => {
read: note.is_read,
type: note.type,
avatar: note.icon_url,
important: note.type === 'system',
important: note.type == 'system',
coupon: '点击查看',
expiry: '',
claimed: false,
@@ -378,15 +384,17 @@ const loadMessages = async () => {
active: false
}
if (note.type === 'system') {
if (note.type == 'system') {
systemMessages.push(item)
} else if (note.type === 'order') {
} else if (note.type == 'order') {
orderMessages.push(item)
} else if (note.type === 'promotion') {
} else if (note.type == 'promotion') {
item.type = 'promo'
promoMessages.push(item)
}
})
}
console.log('[loadMessages] 系统消息:', systemMessages.length, '订单消息:', orderMessages.length, '优惠消息:', promoMessages.length)
// 2. 获取客服消息 (Chat)
const rooms = await supabaseService.getChatRooms()
@@ -641,15 +649,13 @@ const onRefresh = () => {
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
background-color: #4CAF50;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
/* height: 50px; 移除固定高度,由内容决定 */
display: flex;
flex-direction: row; /* 显式设置行方向 */
align-items: center;
justify-content: center;
flex-shrink: 0; /* 防止被压缩 */
flex-direction: column;
justify-content: flex-start;
flex-shrink: 0;
}
.nav-container {

View File

@@ -279,21 +279,63 @@ const loadOrderDetail = async () => {
try {
const data = await supabaseService.getOrderDetail(orderId.value)
if (data != null) {
const dataObj = data as Record<string, any>
// 使用 JSON.parse(JSON.stringify()) 转换数据
const dataObj = JSON.parse(JSON.stringify(data)) as UTSJSONObject
order.value = data as OrderType
const items = dataObj['ml_order_items']
orderItems.value = items != null ? (items as OrderItemType[]) : []
deliveryAddress.value = dataObj['shipping_address'] as AddressType
// 解析订单商品
const itemsRaw = dataObj.get('ml_order_items')
if (itemsRaw != null && Array.isArray(itemsRaw)) {
const items = itemsRaw as any[]
orderItems.value = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const orderItem: OrderItemType = {
id: (itemObj.get('id') ?? '') as string,
product_id: (itemObj.get('product_id') ?? '') as string,
product_name: (itemObj.get('product_name') ?? '') as string,
price: (itemObj.get('price') ?? 0) as number,
quantity: (itemObj.get('quantity') ?? 1) as number,
image_url: (itemObj.get('image_url') ?? '') as string,
specifications: (itemObj.get('specifications') ?? '') as string
}
orderItems.value.push(orderItem)
}
}
// 解析收货地址
const addressRaw = dataObj.get('shipping_address')
if (addressRaw != null) {
const addressObj = JSON.parse(JSON.stringify(addressRaw)) as UTSJSONObject
const province = (addressObj.get('province') ?? '') as string
const city = (addressObj.get('city') ?? '') as string
const district = (addressObj.get('district') ?? '') as string
const detail = (addressObj.get('detail') ?? '') as string
deliveryAddress.value = {
name: (addressObj.get('name') ?? '') as string,
phone: (addressObj.get('phone') ?? '') as string,
province: province,
city: city,
district: district,
detail: detail,
address: province + city + district + detail
} as AddressType
}
// 获取店铺信息
const merchantId = dataObj['merchant_id'] as string
const merchantId = dataObj.get('merchant_id') as string
if (merchantId != null && merchantId != '') {
loadShopInfo(merchantId)
}
console.log('订单详情加载成功,商品数量:', orderItems.value.length)
} else {
uni.showToast({ title: '订单不存在', icon: 'none' })
}
} catch (e) {
console.error('加载订单详情失败:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()

View File

@@ -379,90 +379,53 @@ export default {
async loadProductDetail(productId: string, options: any = {}) {
uni.showLoading({ title: '加载中...' })
try {
const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: UTSJSONObject | null = null
if (Array.isArray(dbProductResponse)) {
const arr = dbProductResponse as any[]
if (arr.length > 0) {
dbProduct = arr[0] as UTSJSONObject
}
} else if (dbProductResponse != null) {
dbProduct = dbProductResponse as UTSJSONObject
}
const dbProduct = await supabaseService.getProductById(productId)
if (dbProduct != null) {
// Map DB product to local product
const dbObj = dbProduct as UTSJSONObject
// 使用 getProductById 返回的 Product 对象
this.product = {
id: dbProduct['id'] as string,
merchant_id: (dbProduct['merchant_id'] ?? dbProduct['shop_id'] ?? '') as string,
category_id: (dbProduct['category_id'] ?? '') as string,
name: dbProduct['name'] as string,
description: (dbProduct['description'] ?? '') as string,
images: [] as string[],
price: (dbProduct['base_price'] ?? dbProduct['price'] ?? 0) as number,
original_price: (dbProduct['market_price'] ?? dbProduct['original_price'] ?? 0) as number,
stock: (dbProduct['available_stock'] ?? dbProduct['total_stock'] ?? dbProduct['stock'] ?? 0) as number,
sales: (dbProduct['sale_count'] ?? dbProduct['sales'] ?? 0) as number,
status: dbProduct['status'] != null ? dbProduct['status'] as number : 1,
created_at: (dbProduct['created_at'] ?? new Date().toISOString()) as string,
// Attributes
specification: dbProduct['specification'] as string | null,
usage: dbProduct['usage'] as string | null,
side_effects: dbProduct['side_effects'] as string | null,
precautions: dbProduct['precautions'] as string | null,
expiry_date: dbProduct['expiry_date'] as string | null,
storage_conditions: dbProduct['storage_conditions'] as string | null,
approval_number: dbProduct['approval_number'] as string | null,
id: dbProduct.id,
merchant_id: dbProduct.merchant_id ?? '',
category_id: dbProduct.category_id ?? '',
name: dbProduct.name,
description: dbProduct.description ?? '',
images: dbProduct.images ?? [] as string[],
price: dbProduct.price ?? dbProduct.base_price ?? 0,
original_price: dbProduct.original_price ?? dbProduct.market_price ?? 0,
stock: dbProduct.stock ?? dbProduct.total_stock ?? 0,
sales: dbProduct.sale_count ?? 0,
status: dbProduct.status ?? 1,
created_at: dbProduct.created_at ?? new Date().toISOString(),
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: [] as string[]
} as ProductType
// Handle Images
if (dbProduct['image_urls'] != null) {
// 解析 tags
if (dbProduct.tags != null && dbProduct.tags != '') {
try {
const imageUrls = dbProduct['image_urls']
const parsed = typeof imageUrls === 'string' ? JSON.parse(imageUrls) : imageUrls
if (Array.isArray(parsed)) {
this.product.images = (parsed as any[]).map((i: any): string => i as string)
}
} catch (e) { console.error('Error parsing image_urls', e) }
}
// Fallback to main_image_url if no images found
if (this.product.images.length === 0 && dbProduct['main_image_url'] != null) {
this.product.images.push(dbProduct['main_image_url'] as string)
}
// Fallback to 'image' field (legacy)
if (this.product.images.length === 0 && dbProduct['image'] != null) {
this.product.images.push(dbProduct['image'] as string)
}
// Final fallback
if (this.product.images.length === 0) {
this.product.images.push('/static/default-product.png')
}
// Handle Tags
if (dbProduct['tags'] != null) {
try {
const tagsData = dbProduct['tags']
const parsedTags = typeof tagsData === 'string' ? JSON.parse(tagsData) : tagsData
const parsedTags = JSON.parse(dbProduct.tags)
if (Array.isArray(parsedTags)) {
this.product.tags = (parsedTags as any[]).map((t: any): string => t as string)
}
} catch (e) {}
} catch(e) {}
}
// Handle Images - 使用 main_image_url 作为后备
if (this.product.images.length == 0 && dbProduct.main_image_url != null && dbProduct.main_image_url != '') {
this.product.images.push(dbProduct.main_image_url)
}
// Final fallback
if (this.product.images.length == 0) {
this.product.images.push('/static/default-product.png')
}
// Handle JSON attributes if present
const attributes = dbProduct['attributes']
if (attributes != null && typeof attributes === 'string') {
try {
const attrs = JSON.parse(attributes) as UTSJSONObject | null
if (attrs != null) {
// Merge attributes into product if they match keys
if (attrs['specification'] != null) this.product.specification = attrs['specification'] as string
if (attrs['usage'] != null) this.product.usage = attrs['usage'] as string
}
} catch(e) {}
}
console.log('商品详情加载成功:', this.product.name, '库存:', this.product.stock, '销量:', this.product.sales)
} else {
throw new Error('No product found')
}
@@ -471,12 +434,21 @@ export default {
// Fallback to options if available
this.product.id = productId
const opts = options as UTSJSONObject
const nameOpt = opts['name'] as string | null
this.product.name = (nameOpt != null && nameOpt !== '') ? decodeURIComponent(nameOpt) ?? '未知商品' : '未知商品'
const priceOpt = opts['price'] as string | null
this.product.price = (priceOpt != null && priceOpt !== '') ? parseFloat(priceOpt) : 0
const imageOpt = opts['image'] as string | null
const decodedImage = (imageOpt != null && imageOpt !== '') ? decodeURIComponent(imageOpt) : null
const nameOpt = opts['name']
this.product.name = (nameOpt != null && nameOpt != '') ? decodeURIComponent(nameOpt as string) ?? '未知商品' : '未知商品'
// price 可能是 string 或 number 类型
const priceOpt = opts['price']
if (typeof priceOpt == 'number') {
this.product.price = priceOpt as number
} else if (typeof priceOpt == 'string') {
this.product.price = parseFloat(priceOpt as string)
} else {
this.product.price = 0
}
const imageOpt = opts['image']
const decodedImage = (imageOpt != null && imageOpt != '') ? decodeURIComponent(imageOpt as string) : null
this.product.images = decodedImage != null ? [decodedImage] : ['/static/default-product.png']
}
@@ -499,22 +471,23 @@ export default {
try {
const shopResponse = await supabaseService.getShopByMerchantId(merchantId)
if (shopResponse != null) {
const shop = shopResponse as UTSJSONObject
// 直接使用 Shop 对象的属性
this.merchant = {
id: shop['id'] as string,
user_id: shop['merchant_id'] as string,
shop_name: shop['shop_name'] as string,
shop_logo: (shop['shop_logo'] ?? '/static/default-shop.png') as string,
shop_banner: (shop['shop_banner'] ?? '/static/default-banner.png') as string,
shop_description: (shop['description'] ?? '') as string,
contact_name: (shop['contact_name'] ?? '店主') as string,
contact_phone: (shop['contact_phone'] ?? '') as string,
id: shopResponse.id,
user_id: shopResponse.merchant_id,
shop_name: shopResponse.shop_name,
shop_logo: shopResponse.shop_logo ?? '/static/default-shop.png',
shop_banner: shopResponse.shop_banner ?? '/static/default-banner.png',
shop_description: shopResponse.description ?? '',
contact_name: shopResponse.contact_name ?? '店主',
contact_phone: shopResponse.contact_phone ?? '',
shop_status: 1,
rating: (shop['rating_avg'] ?? 5.0) as number,
total_sales: (shop['total_sales'] ?? 0) as number,
created_at: (shop['created_at'] ?? new Date().toISOString()) as string
rating: shopResponse.rating_avg ?? 5.0,
total_sales: shopResponse.total_sales ?? 0,
created_at: shopResponse.created_at ?? new Date().toISOString()
} as MerchantType
realMerchantLoaded = true
console.log('店铺信息加载成功:', this.merchant.shop_name)
}
} catch (e) {
console.error('Load shop failed', e)
@@ -555,33 +528,30 @@ export default {
const skus = await supabaseService.getProductSkus(productId)
if (skus.length > 0) {
console.log('加载到商品SKU:', skus.length)
this.productSkus = skus.map((skuData): ProductSkuType => {
const sku = skuData as UTSJSONObject
this.productSkus = []
for (let i = 0; i < skus.length; i++) {
const skuData = skus[i]
// 解析 specifications JSON 字符串
let specs: UTSJSONObject = {}
const specsData = sku['specifications']
if (specsData != null) {
if (skuData.specifications != null && skuData.specifications != '') {
try {
if (typeof specsData === 'string') {
specs = JSON.parse(specsData) as UTSJSONObject
} else {
// 假设已经是对象
specs = specsData as UTSJSONObject
}
specs = JSON.parse(skuData.specifications) as UTSJSONObject
} catch(e) {
console.error('解析SKU规格失败', e)
console.error('解析SKU规格失败', e)
}
}
return {
id: sku['id'] as string,
product_id: sku['product_id'] as string,
sku_code: sku['sku_code'] as string,
specifications: specs,
price: sku['price'] as number,
stock: sku['stock'] != null ? sku['stock'] as number : 0,
image_url: sku['image_url'] != null ? sku['image_url'] as string : '',
status: sku['status'] != null ? sku['status'] as number : 1
} as ProductSkuType
})
}
const sku: ProductSkuType = {
id: skuData.id,
product_id: skuData.product_id,
sku_code: skuData.sku_code,
specifications: specs,
price: skuData.price,
stock: skuData.stock ?? 0,
image_url: skuData.image_url ?? '',
status: skuData.status ?? 1
}
this.productSkus.push(sku)
}
return
}
} catch (e) {
@@ -594,17 +564,54 @@ export default {
if (this.product.merchant_id == '') return
// Safety check for cached service definition
try {
// @ts-ignore
const couponData = await supabaseService.fetchShopCoupons(this.product.merchant_id)
this.coupons = couponData as Array<CouponTemplateType>
} catch (e) {
try {
// @ts-ignore
const couponData2 = await supabaseService.getAvailableCoupons(this.product.merchant_id)
this.coupons = couponData2 as Array<CouponTemplateType>
} catch (e2) {
console.warn('SupabaseService coupon methods not available:', e2)
// 解析优惠券数据
this.coupons = []
if (couponData != null && couponData.length > 0) {
for (let i = 0; i < couponData.length; i++) {
const item = couponData[i]
const couponObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const getSafeString = (key: string): string => {
const val = couponObj.get(key)
if (val == null) return ''
if (typeof val == 'string') return val
return ''
}
const getSafeNumber = (key: string): number => {
const val = couponObj.get(key)
if (val == null) return 0
if (typeof val == 'number') return val
return 0
}
const coupon: CouponTemplateType = {
id: getSafeString('id'),
name: getSafeString('name'),
description: getSafeString('description'),
coupon_type: getSafeNumber('coupon_type'),
discount_type: getSafeNumber('discount_type'),
discount_value: getSafeNumber('discount_value'),
min_order_amount: getSafeNumber('min_order_amount'),
max_discount_amount: getSafeNumber('max_discount_amount'),
total_quantity: getSafeNumber('total_quantity'),
per_user_limit: getSafeNumber('per_user_limit'),
usage_limit: getSafeNumber('usage_limit'),
merchant_id: getSafeString('merchant_id'),
category_ids: [] as string[],
product_ids: [] as string[],
user_type_limit: getSafeNumber('user_type_limit'),
start_time: getSafeString('start_time'),
end_time: getSafeString('end_time'),
status: getSafeNumber('status'),
created_at: getSafeString('created_at')
}
this.coupons.push(coupon)
}
}
} catch (e) {
console.warn('SupabaseService coupon methods not available:', e)
}
},
@@ -801,9 +808,16 @@ export default {
},
goToShop() {
if (this.merchant.user_id != null && this.merchant.user_id !== '') {
const merchantId = this.merchant.id ?? this.product.merchant_id ?? ''
if (merchantId != '') {
console.log('进店点击merchantId:', merchantId)
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
url: `/pages/mall/consumer/shop-detail?merchantId=${merchantId}`
})
} else {
uni.showToast({
title: '店铺信息加载中',
icon: 'none'
})
}
},

View File

@@ -40,9 +40,9 @@
</view>
</view>
<scroll-view class="profile-scroll-content" direction="vertical" style="flex:1; height: 0; width: 100%;">
<!-- 导航栏占位符 - 恢复 -->
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
<scroll-view class="profile-scroll-content" :scroll-y="true">
<!-- 导航栏占位符 - 需要包含statusBarHeight + 导航栏高度44px -->
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 我的服务 (移到订单上方) -->
<view class="my-services" style="margin-top: 10px;">
@@ -249,7 +249,7 @@
</template>
<script>
import { UserType, OrderType } from '@/types/mall-types.uts'
import { UserType } from '@/types/mall-types.uts'
import supabaseService from '@/utils/supabaseService.uts'
type UserStatsType = {
@@ -283,6 +283,15 @@ type StatsPeriodType = {
label: string
}
type OrderItemType = {
id: string
order_no: string
status: number
actual_amount: number
created_at: string
ml_order_items: any | null
}
export default {
data() {
return {
@@ -313,7 +322,7 @@ export default {
coupons: 0,
favorites: 0
} as ServiceCountsType,
recentOrders: [] as Array<OrderType>,
recentOrders: [] as Array<OrderItemType>,
statsPeriods: [
{ key: 'month', label: '本月' },
{ key: 'quarter', label: '本季度' },
@@ -328,8 +337,8 @@ export default {
save_amount: 0
} as ConsumptionStatsType,
statusBarHeight: 0,
currentOrderTab: 'all' as string, // 当前选中的订单Tab
allOrders: [] as Array<OrderType> // 存储所有订单数据
currentOrderTab: 'all' as string,
allOrders: [] as Array<OrderItemType>
}
},
onLoad() {
@@ -348,8 +357,8 @@ export default {
uni.$off('orderUpdated', this.handleOrderUpdated)
},
computed: {
filteredOrders(): Array<OrderType> {
const result: Array<OrderType> = []
filteredOrders(): Array<OrderItemType> {
const result: Array<OrderItemType> = []
if (this.currentOrderTab === 'all') {
for (let i: number = 0; i < this.allOrders.length; i++) {
result.push(this.allOrders[i])
@@ -377,29 +386,37 @@ export default {
}
},
methods: {
// 加载订单数据
async loadOrders() {
try {
const orders = await supabaseService.getOrders()
const mappedOrders: Array<OrderType> = []
const mappedOrders: Array<OrderItemType> = []
for (let i: number = 0; i < orders.length; i++) {
const o = orders[i] as UTSJSONObject
const orderObj = new UTSJSONObject()
const rawItem = orders[i]
const o = JSON.parse(JSON.stringify(rawItem)) as UTSJSONObject
const keys = UTSJSONObject.keys(o)
for (let j: number = 0; j < keys.length; j++) {
const key = keys[j]
orderObj.set(key, o.get(key))
let status = o.getNumber('status')
if (status == null) {
const orderStatus = o.getNumber('order_status')
status = orderStatus != null ? orderStatus : 0
}
if (o.getNumber('status') == null && o.getNumber('order_status') != null) {
orderObj.set('status', o.getNumber('order_status'))
let actualAmount = o.getNumber('actual_amount')
if (actualAmount == null) {
const totalAmount = o.getNumber('total_amount')
actualAmount = totalAmount != null ? totalAmount : 0
}
if (o.getNumber('actual_amount') == null && o.getNumber('total_amount') != null) {
orderObj.set('actual_amount', o.getNumber('total_amount'))
const orderItem: OrderItemType = {
id: o.getString('id') ?? '',
order_no: o.getString('order_no') ?? '',
status: status,
actual_amount: actualAmount,
created_at: o.getString('created_at') ?? '',
ml_order_items: o.get('ml_order_items')
}
mappedOrders.push(orderObj as OrderType)
mappedOrders.push(orderItem)
}
for (let i: number = 0; i < mappedOrders.length; i++) {
@@ -418,7 +435,7 @@ export default {
this.allOrders = mappedOrders
const recentList: Array<OrderType> = []
const recentList: Array<OrderItemType> = []
const limit = mappedOrders.length < 5 ? mappedOrders.length : 5
for (let i: number = 0; i < limit; i++) {
recentList.push(mappedOrders[i])
@@ -627,30 +644,28 @@ export default {
return 'error'
},
getOrderMainImage(order: any): string {
const orderObj = order as UTSJSONObject
const itemsRaw = orderObj.get('ml_order_items')
getOrderMainImage(order: OrderItemType): string {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return '/static/product1.jpg'
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0] as UTSJSONObject
const imgUrl = firstItem.getString('image_url') ?? ''
const prodImg = firstItem.getString('product_image') ?? ''
const img = imgUrl !== '' ? imgUrl : prodImg
if (img !== '') return img
const firstItem = items[0] as Record<string, any>
const imgUrl = firstItem['image_url'] as string
const prodImg = firstItem['product_image'] as string
const img = (imgUrl != null && imgUrl !== '') ? imgUrl : prodImg
if (img != null && img !== '') return img
}
return '/static/product1.jpg'
},
getOrderTitle(order: any): string {
const orderObj = order as UTSJSONObject
const itemsRaw = orderObj.get('ml_order_items')
getOrderTitle(order: OrderItemType): string {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return '精选商品'
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0] as UTSJSONObject
const pName = firstItem.getString('product_name') ?? ''
const name = pName !== '' ? pName : '商品'
const firstItem = items[0] as Record<string, any>
const pName = firstItem['product_name'] as string
const name = (pName != null && pName !== '') ? pName : '商品'
if (items.length > 1) {
return `${name} 等${items.length}件商品`
@@ -712,19 +727,19 @@ export default {
})
},
viewOrderDetail(order: OrderType) {
viewOrderDetail(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?orderId=${order.id}`
})
},
payOrder(order: OrderType) {
payOrder(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}`
})
},
confirmReceive(order: OrderType) {
confirmReceive(order: OrderItemType) {
uni.showModal({
title: '确认收货',
content: '确认已收到商品吗?',
@@ -740,7 +755,7 @@ export default {
})
},
reviewOrder(order: OrderType) {
reviewOrder(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}`
})
@@ -855,6 +870,12 @@ export default {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.profile-scroll-content {
flex: 1;
}
/* 智能顶部导航栏 */
.smart-navbar {
@@ -862,13 +883,12 @@ export default {
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
background-color: #4CAF50;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-direction: column;
justify-content: flex-start;
}
.nav-container {

View File

@@ -284,7 +284,7 @@ const handleLogin = async () => {
uni.showToast({ title: '管理员登录成功', icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/mall/consumer/index' })
uni.reLaunch({ url: '/pages/mall/consumer/category' })
}, 500)
return
}
@@ -353,7 +353,7 @@ const handleLogin = async () => {
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/mall/consumer/index' })
uni.reLaunch({ url: '/pages/mall/consumer/category' })
}, 500)
} catch (err) {
console.error('登录错误:', err)

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">🛒</text></svg>

After

Width:  |  Height:  |  Size: 156 B

1
static/tabbar/cart.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">🛒</text></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">🔠</text></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">🔠</text></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">🏠</text></svg>

After

Width:  |  Height:  |  Size: 156 B

1
static/tabbar/home.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">🏠</text></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">💬</text></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">💬</text></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">👤</text></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><text x="12" y="18" font-size="20" text-anchor="middle">👤</text></svg>

After

Width:  |  Height:  |  Size: 156 B

File diff suppressed because it is too large Load Diff