diff --git a/App.uvue b/App.uvue
index f93b1651..d2dd69f3 100644
--- a/App.uvue
+++ b/App.uvue
@@ -1,13 +1,88 @@
diff --git a/pages.json b/pages.json
index f1cc6064..fa41ce4d 100644
--- a/pages.json
+++ b/pages.json
@@ -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"
}
]
},
diff --git a/pages/mall/consumer/address-list.uvue b/pages/mall/consumer/address-list.uvue
index 4fcbd06c..11417c28 100644
--- a/pages/mall/consumer/address-list.uvue
+++ b/pages/mall/consumer/address-list.uvue
@@ -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
diff --git a/pages/mall/consumer/cart.uvue b/pages/mall/consumer/cart.uvue
index 17f4e715..7c86d44d 100644
--- a/pages/mall/consumer/cart.uvue
+++ b/pages/mall/consumer/cart.uvue
@@ -14,11 +14,17 @@
-
-
+
+
-
+
🛒
@@ -726,28 +732,28 @@ const goToCheckout = () => {
diff --git a/pages/mall/consumer/category copy.uvue b/pages/mall/consumer/category copy.uvue
new file mode 100644
index 00000000..0aa14b02
--- /dev/null
+++ b/pages/mall/consumer/category copy.uvue
@@ -0,0 +1,1246 @@
+
+
+
+
+
+
+
+ 请输入药品名称、症状或品牌
+
+
+
+ 🔳
+
+
+
+
+ 📷
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.icon }}
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ product.name }}
+
+ ¥{{ product.base_price ?? product.price ?? 0 }}
+
+ +
+
+
+
+
+
+
+
+ 💊
+ 暂无相关药品
+ 该分类下暂无商品,敬请期待
+
+
+
+
+ 上拉加载更多
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/consumer/category.uvue b/pages/mall/consumer/category.uvue
index c03ab4a1..1f964416 100644
--- a/pages/mall/consumer/category.uvue
+++ b/pages/mall/consumer/category.uvue
@@ -25,19 +25,24 @@
-
-
+
+
-
+
{{ item.icon }}
{{ item.name }}
@@ -46,7 +51,7 @@
{{ currentCategoryDesc }}
+
+
+
+
+ {{ sub.icon }}
+ {{ sub.name }}
+
+
+
+
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([])
+const subCategories = ref([]) // 二级分类列表
const productList = ref([])
const activePrimary = ref('')
+const activeSubCategory = ref('') // 当前选中的二级分类
+const selectedParentId = ref('') // 当前选中的一级分类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 {
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 {
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 {
+ 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 {
+ 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 {
+ 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 {
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 {
- 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 {
@@ -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%;
}
diff --git a/pages/mall/consumer/chat.uvue b/pages/mall/consumer/chat.uvue
index cff3cc15..6b551cc5 100644
--- a/pages/mall/consumer/chat.uvue
+++ b/pages/mall/consumer/chat.uvue
@@ -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 {
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({
diff --git a/pages/mall/consumer/coupons.uvue b/pages/mall/consumer/coupons.uvue
index 8cd67c72..c08112d7 100644
--- a/pages/mall/consumer/coupons.uvue
+++ b/pages/mall/consumer/coupons.uvue
@@ -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 = []
diff --git a/pages/mall/consumer/doc/SMART_RECOMMENDATION.md b/pages/mall/consumer/doc/SMART_RECOMMENDATION.md
new file mode 100644
index 00000000..3bb2d713
--- /dev/null
+++ b/pages/mall/consumer/doc/SMART_RECOMMENDATION.md
@@ -0,0 +1,235 @@
+# 智能推荐系统文档
+
+## 一、系统概述
+
+智能推荐系统基于用户行为数据,为用户提供个性化的商品推荐。系统综合分析用户的搜索历史、浏览历史,结合全站热销商品数据,生成智能推荐列表。
+
+## 二、推荐逻辑架构
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 智能推荐系统 │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ 用户搜索历史 │ │ 用户浏览历史 │ │ 全站热销商品 │ │
+│ │ (权重高) │ │ (权重中) │ │ (权重低) │ │
+│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
+│ │ │ │ │
+│ ▼ ▼ ▼ │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ 关键词匹配 │ │ 分类匹配 │ │ 销量排序 │ │
+│ │ 相关商品 │ │ 相似商品 │ │ 热门商品 │ │
+│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
+│ │ │ │ │
+│ └────────────────┼────────────────┘ │
+│ ▼ │
+│ ┌─────────────────────┐ │
+│ │ 去重 & 合并结果 │ │
+│ └──────────┬──────────┘ │
+│ ▼ │
+│ ┌─────────────────────┐ │
+│ │ 返回推荐商品列表 │ │
+│ └─────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## 三、权重分配策略
+
+### 3.1 数据源权重
+
+| 数据源 | 权重 | 说明 |
+|--------|------|------|
+| 用户搜索历史 | 50% | 用户主动搜索的关键词最能反映购买意向 |
+| 用户浏览历史 | 30% | 用户浏览过的商品分类反映兴趣偏好 |
+| 热销商品 | 20% | 全站热销商品作为兜底和补充 |
+
+### 3.2 推荐优先级
+
+```
+优先级 1: 用户搜索历史匹配的商品(最多占推荐列表的50%)
+优先级 2: 用户浏览过的分类下的商品(最多占推荐列表的30%)
+优先级 3: 全站热销商品(填充剩余位置)
+```
+
+## 四、核心算法详解
+
+### 4.1 智能推荐主流程
+
+```typescript
+async getSmartRecommendations(limit: number): Promise {
+ const products: Product[] = []
+ const addedIds = new Set() // 用于去重
+
+ // 步骤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 {
+ // 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 {
+ // 1. 获取用户浏览过的商品分类ID列表
+ // 2. 查询这些分类下的商品
+ // 3. 按销量排序返回
+}
+```
+
+## 五、热搜词系统
+
+### 5.1 热搜词计算
+
+```typescript
+async getHotKeywords(limit: number): Promise {
+ // 1. 获取最近100条搜索记录
+ // 2. 统计每个关键词的出现频率
+ // 3. 按频率降序排序
+ // 4. 返回前N个高频关键词
+}
+```
+
+### 5.2 热搜词展示规则
+
+| 排名 | 样式 | 说明 |
+|------|------|------|
+| 1-3名 | 红色背景 | 热度最高,突出显示 |
+| 4-10名 | 灰色背景 | 普通热度 |
+
+## 六、用户行为记录
+
+### 6.1 搜索行为记录
+
+```typescript
+// 在用户执行搜索时调用
+async recordSearch(keyword: string, resultCount: number): Promise {
+ // 记录字段:
+ // - user_id: 用户ID(可选,支持匿名)
+ // - keyword: 搜索关键词
+ // - result_count: 搜索结果数量
+ // - created_at: 搜索时间
+}
+```
+
+### 6.2 浏览行为记录
+
+```typescript
+// 在用户查看商品详情时调用
+async recordBrowse(productId: string, duration: number): Promise {
+ // 记录字段:
+ // - 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年*
diff --git a/pages/mall/consumer/doc/UTS_ANDROID_GUIDE.md b/pages/mall/consumer/doc/UTS_ANDROID_GUIDE.md
new file mode 100644
index 00000000..00feca29
--- /dev/null
+++ b/pages/mall/consumer/doc/UTS_ANDROID_GUIDE.md
@@ -0,0 +1,3364 @@
+# UTS Android 兼容性开发规范指南
+
+> 以下为 uni-app x (UTS) Android 端开发常见注意事项与踩坑点,建议所有开发成员遵循:
+
+================================================================================
+一、基础语法规范
+================================================================================
+
+1. 变量声明
+ - 只能使用 let 和 const,不能使用 var
+ - 变量声明必须有显式类型或初始化值
+ - 不支持 undefined 类型,变量未赋值就是 null
+ - 不支持 undefined 关键字,判断是否存在要用 != null
+
+2. 类型定义
+ - 只适合用 type,不适合使用 interface(interface 在 kotlin/swift 中另有不同)
+ - 不支持 Intersection Type(交叉类型)
+ - 不支持 Index Signature(索引签名)
+ - 类型推断严格,必要时用 as Type 明确类型
+ - 不支持内联对象类型(Object Literal Type),需要单独定义 type
+
+3. 函数定义
+ - 函数必须在使用前定义(不支持函数提升)
+ - 在 setup 模式下,调用的函数必须在调用之前定义
+ - 依赖关系需要明确:被调用的函数必须先定义
+ - 这与 JavaScript 的函数提升行为不同,UTS 更接近 C/Java 的编译方式
+
+4. 循环
+ - for 循环的 i 必须写明类型:let i: number = 0
+ - 不要用 forEach、map,数组遍历用 for 循环
+ - 嵌套的数组方法调用可能导致类型推断失败,应改用 for 循环
+
+================================================================================
+二、类型与对象访问
+================================================================================
+
+1. any 类型访问
+ - 不能直接访问 any 类型对象的属性
+ - 需要将对象转换为 UTSJSONObject 类型后使用 getString()、getNumber() 等方法访问属性
+ - any 类型属性访问需转换为 Record 后用索引访问
+ - 使用索引访问属性时,推荐使用方括号语法 obj['property'] 而非点语法 obj.property
+ - any 类型不支持索引访问 obj['key'],必须先转换为 UTSJSONObject
+
+2. UTSJSONObject 使用
+ - 用 utils/utis 下的 UTSJSONObject 做类型转换
+ - 不要用 safeget,只要 UTSJSONObject 就好
+ - 需要创建动态对象时,应使用 new UTSJSONObject() 然后调用 .set() 方法
+ - 对于 type 定义的对象类型,同样需要使用 UTSJSONObject
+ - 使用 getString()、getNumber() 方法获取属性值
+
+3. 数组类型
+ - 数组类型建议写成 Array,不要用 Type[] 简写
+ - 空数组需要明确指定类型,如 [] as string[]
+ - 数组元素需要明确的类型定义才能在模板中正确访问属性
+ - 对于 any[] 或 reactive 数组,访问元素属性时需要先转换为 Record 或 any[]
+
+4. 对象操作
+ - 不支持 Object.keys()、Object.values()、Object.entries()
+ - 不支持 Record 对象字面量语法
+ - 对象字面量 {...} 只能用于构造类型(class),不能用于接口(interface)
+ - reactive 对象在 UTS 中不支持索引器赋值操作
+
+================================================================================
+三、条件判断与逻辑运算
+================================================================================
+
+1. if 条件
+ - if 判断只能接 boolean 类型,不能是其他类型的值
+ - 判断空要用 !== null,不能用 !变量(uts android 不支持 !在变量前面的判断空方式)
+ - 模板中的 || 运算符左边必须是 boolean 类型
+ - 可空类型使用可选链 ?. 和空值合并 ??
+ - 字符串判断空要用:variable != null && variable !== ''
+
+2. 逻辑运算符
+ - || 表示逻辑或
+ - && 表示逻辑与
+ - ! 表示逻辑非(但 !变量 不支持用于判断空)
+ - ?? 表示空值合并运算符(当左侧为 null 时返回右侧值)
+ - ts 的为空则使用默认值的语法在 uts 中不能用 ||,要用 ?? 来代替
+
+================================================================================
+四、组件与模板
+================================================================================
+
+1. 表单与输入
+ - 表单优先用 form 组件
+ - 不支持 uni-easyinput,用 input 代替
+ - 时间选择用 uni_modules/lime-date-time-picker
+
+2. 选择器
+ - uts android 不支持 picker,用 picker-view 或 uni.showActionSheet
+ - 一维的优先用 uni.showActionSheet
+ - picker-view 的事件用 UniPickerViewChangeEvent
+
+3. 导航与布局
+ - 不支持 uni-nav-bar,先删除
+ - 不支持 uni-data-select,用 picker-view 代替
+ - 不支持 uni-datetime-picker,用 components/picker-date 或 components/picker-time 代替
+ - 不支持 uni-icons
+
+4. 模板注意事项
+ - 跟 template 交互的变量尽量用一维变量(不要嵌套对象)
+ - 模板中可空类型必须使用 ?. 安全访问
+ - 模板中访问可空类型属性前必须先判空 v-if="order != null"
+
+================================================================================
+五、CSS 样式限制
+================================================================================
+
+1. 布局方式
+ - 只支持 display: flex
+ - 不支持 display: grid
+ - 不支持 gap
+ - 不支持 table、grid、grid-template-columns
+
+2. 单位与计算
+ - 不支持 calc()
+ - 不支持的单位: vh
+ - property value `100%` is not supported for min-height (supported values are: number|pixel)
+ - property value `calc(33.33% - 10px)` is not supported for min-width
+
+3. 选择器
+ - [APP-ANDROID] 不支持伪类选择器
+ - [APP-IOS] 不支持伪类选择器
+ - ERROR: Selector `.login-button[disabled]` is not supported. uvue only support classname selector
+
+4. 其他样式
+ - WARNING: `backdrop-filter` is not a standard property name
+ - style property `white-space` is only supported on `|