同步首页分类栏ui

This commit is contained in:
2026-05-19 17:45:27 +08:00
parent 00a859c551
commit b5ab00ab12
13 changed files with 2956 additions and 266 deletions

View File

@@ -9,17 +9,41 @@
:active-module="activeTopModule"
:search-keyword="searchKeyword"
:placeholder="headerSearchPlaceholder"
:categories="headerCategories"
:active-category="activeCategory"
@changeModule="handleTopModuleChange"
@change-module="handleTopModuleChange"
@update:searchKeyword="handleSearchKeywordUpdate"
@search="handleHeaderSearch"
@changeCategory="handleHeaderCategoryChange"
@focusSearch="handleSearchFocus"
></JdLikeHomeHeader>
<view v-if="activeTopModule == 'home'" class="category-wrapper">
<view :class="['category-bar-wrap', { 'category-bar-wrap-hidden': showCategoryPanel }]">
<scroll-view
class="category-scroll"
direction="horizontal"
:show-scrollbar="false"
:scroll-with-animation="true"
:scroll-into-view="categoryScrollIntoView"
>
<view
v-for="(item, index) in categoryList"
:key="buildListItemKey('top-category', item.id, index)"
:id="'cat-' + item.id"
:class="['category-item', { 'category-item-active': currentCategory === item.id }]"
@tap="handleCategoryTabClick(item)"
>
<text :class="['category-item-text', { 'category-item-text-active': currentCategory === item.id, 'category-item-text-accent': shouldHighlightCategory(item.name) && currentCategory !== item.id }]">{{ getCategoryTabDisplayName(item.name) }}</text>
<view v-if="currentCategory === item.id" class="category-active-line"></view>
</view>
</scroll-view>
<view class="category-expand-btn" @tap="toggleCategoryPanel">
<text class="category-expand-icon">{{ showCategoryPanel ? '∧' : '' }}</text>
</view>
</view>
</view>
</view>
<view class="jd-header-placeholder" :style="{ height: headerPlaceholderHeight + 'px' }"></view>
<view class="jd-header-placeholder" :style="{ height: (activeTopModule == 'home' ? headerPlaceholderHeight : navbarTotalHeight) + 'px' }"></view>
<scroll-view
direction="vertical"
@@ -54,6 +78,36 @@
<view class="safe-area" :style="{ height: bottomSafeArea + 88 + 'px' }"></view>
</scroll-view>
<view
v-if="activeTopModule == 'home' && showCategoryPanel"
class="category-panel-mask"
:style="{ top: navbarTotalHeight + 'px' }"
@tap="toggleCategoryPanel"
></view>
<view
v-if="activeTopModule == 'home' && showCategoryPanel"
class="category-panel"
:style="{ top: (navbarTotalHeight - 5) + 'px' }"
>
<view class="category-panel-header">
<text class="category-panel-title">全部分类</text>
<view class="category-panel-close-btn" @tap="toggleCategoryPanel">
<text class="category-panel-close-text">收起</text>
<text class="category-panel-close-arrow">∧</text>
</view>
</view>
<view class="category-panel-grid">
<view
v-for="(item, index) in categoryList"
:key="buildListItemKey('panel-category', item.id, index)"
:class="['category-panel-item', { 'category-panel-item-active': currentCategory === item.id }]"
@tap="selectCategoryFromPanel(item)"
>
<text :class="['category-panel-item-text', { 'category-panel-item-text-active': currentCategory === item.id }]">{{ item.name }}</text>
</view>
</view>
</view>
</view>
</template>
@@ -105,7 +159,6 @@ const headerPlaceholderHeight = ref(128)
const headerStyle = ref('')
const searchRowStyle = ref('')
const activeTopModule = ref('home')
const activeCategory = ref('recommend')
const searchKeyword = ref('')
type HeaderModuleItem = {
@@ -118,10 +171,6 @@ const topModules: Array<HeaderModuleItem> = [
{ key: 'service', label: '服务' }
]
const headerCategories = computed((): Array<CategoryItem> => {
return categoryList.value
})
const headerSearchPlaceholder = computed((): string => {
if (activeTopModule.value == 'service') {
return '居家护理 / 康复照护 / 血压计 / 助餐服务'
@@ -293,21 +342,7 @@ function handleHeaderSearch(keyword: string) {
function handleTopModuleChange(moduleKey: string) {
activeTopModule.value = moduleKey
if (moduleKey == 'home') {
activeCategory.value = currentCategory.value
} else {
activeCategory.value = 'all'
}
}
function handleHeaderCategoryChange(categoryId: string) {
activeCategory.value = categoryId
if (activeTopModule.value == 'home') {
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id == categoryId)
if (matchedItem != null) {
handleCategoryTabClick(matchedItem)
}
}
showCategoryPanel.value = false
}
function handleMainScrollToLower() {
@@ -363,6 +398,9 @@ const handleKeywordChange = (e: any) => {
}
const toggleCategoryPanel = (): void => {
if (activeTopModule.value != 'home') {
return
}
showCategoryPanel.value = !showCategoryPanel.value
}
@@ -442,29 +480,7 @@ function buildSimpleChannelCoverImages(startIndex: number): string[] {
}
function buildSimpleCategoryChannels(categoryId: string): SimpleCategoryChannel[] {
if (categoryId === 'recommend') {
return []
}
return [
{
id: 'rank',
title: '排行榜',
subtitle: '每日更新',
routeType: 'rank',
icon: '榜',
coverImages: buildSimpleChannelCoverImages(0),
categoryId
},
{
id: 'quality',
title: '品质优选',
subtitle: '精选好货',
routeType: 'quality',
icon: '选',
coverImages: buildSimpleChannelCoverImages(2),
categoryId
}
]
return []
}
function applyChannelDisplay(categoryId: string): void {
@@ -771,25 +787,14 @@ const onParentCategoryClick = async (category: Category): Promise<void> => {
// 如果没有二级分类,直接跳转到分类页
if (subCategories.value.length == 0) {
console.log('[onParentCategoryClick] 没有二级分类,直接跳转到分类页')
uni.setStorageSync('selectedCategory', category.id)
uni.switchTab({
url: '/pages/main/category'
})
console.log('[onParentCategoryClick] 没有二级分类,切换首页分类商品流')
void refreshHomeCategory({ id: category.id, name: category.name })
}
}
// 点击二级分类
const onSubCategoryClick = (category: Category): void => {
// 跳转到分类页面
uni.setStorageSync('selectedCategory', category.id)
const timestamp = Date.now()
const randomParam = Math.random().toString(36).substring(2, 8)
const url = `/pages/main/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}&timestamp=${timestamp}&random=${randomParam}`
uni.switchTab({
url: '/pages/main/category'
})
void refreshHomeCategory({ id: category.id, name: category.name })
}
// 获取品牌数据
@@ -876,6 +881,31 @@ function getCategoryLabelById(categoryId: string): string {
return '精选'
}
async function consumeSelectedCategoryFromStorage(): Promise<boolean> {
if (activeTopModule.value != 'home' || categoryList.value.length == 0) {
return false
}
const savedCategoryId = uni.getStorageSync('selectedCategory')
if (savedCategoryId == null) {
return false
}
const nextCategoryId = String(savedCategoryId)
if (nextCategoryId === '') {
return false
}
uni.removeStorageSync('selectedCategory')
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id == nextCategoryId)
if (matchedItem != null) {
await refreshHomeCategory(matchedItem)
return true
}
await refreshHomeCategory({
id: nextCategoryId,
name: getCategoryLabelById(nextCategoryId)
})
return true
}
function formatChannelPrice(price: number): string {
const rounded = Math.round(price)
if (Math.abs(price - rounded) < 0.001) {
@@ -1033,104 +1063,65 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
}
}
// 分类标签栏交互
function handleCategoryTabClick(item: CategoryItem): void {
async function refreshHomeCategory(item: CategoryItem): Promise<void> {
currentCategory.value = item.id
currentFeedCategoryId.value = item.id
currentPage.value = 1
hotProducts.value = []
hasMore.value = true
selectedSubCategoryId.value = ''
showCategoryPanel.value = false
categoryScrollIntoView.value = 'cat-' + item.id
void (async (): Promise<void> => {
currentFeedCategoryId.value = item.id
currentPage.value = 1
hasMore.value = true
selectedSubCategoryId.value = ''
if (item.id === 'recommend') {
subCategories.value = []
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
const result = await supabaseService.getSmartRecommendations(1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
hasMore.value = false
}
return
}
try {
const subData = await supabaseService.getSubCategories(item.id)
subCategories.value = subData
} catch (error) {
console.error('加载子分类数据失败:', error)
subCategories.value = []
}
failedProductImageIds.value = []
loading.value = true
if (item.id === 'recommend') {
subCategories.value = []
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
loading.value = true
const result = await supabaseService.getProductsByCategory(item.id, 1, defaultLoadLimit)
failedProductImageIds.value = []
const result = await supabaseService.getSmartRecommendations(1, defaultLoadLimit)
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
console.error('加载推荐商品失败:', error)
hotProducts.value = []
hasMore.value = false
} finally {
loading.value = false
}
})()
return
}
try {
const subData = await supabaseService.getSubCategories(item.id)
subCategories.value = subData
} catch (error) {
console.error('加载子分类数据失败:', error)
subCategories.value = []
}
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
const result = await supabaseService.getProductsByCategory(item.id, 1, defaultLoadLimit)
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
hotProducts.value = []
hasMore.value = false
} finally {
loading.value = false
}
}
// 分类标签栏交互
function handleCategoryTabClick(item: CategoryItem): void {
void refreshHomeCategory(item)
}
function selectCategoryFromPanel(item: CategoryItem): void {
currentCategory.value = item.id
showCategoryPanel.value = false
categoryScrollIntoView.value = 'cat-' + item.id
void (async (): Promise<void> => {
currentFeedCategoryId.value = item.id
currentPage.value = 1
hasMore.value = true
selectedSubCategoryId.value = ''
if (item.id === 'recommend') {
subCategories.value = []
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
const result = await supabaseService.getSmartRecommendations(1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
hasMore.value = false
}
return
}
try {
const subData = await supabaseService.getSubCategories(item.id)
subCategories.value = subData
} catch (error) {
console.error('加载子分类数据失败:', error)
subCategories.value = []
}
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
loading.value = true
const result = await supabaseService.getProductsByCategory(item.id, 1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
hotProducts.value = []
hasMore.value = false
} finally {
loading.value = false
}
})()
void refreshHomeCategory(item)
}
const loadRecommendedProducts = async (limit: number): Promise<void> => {
@@ -1170,6 +1161,10 @@ const initData = async () => {
await loadCategories()
await loadBrands()
await loadHotKeywords()
if (await consumeSelectedCategoryFromStorage()) {
await loadRecommendedProducts(defaultLoadLimit)
return
}
await loadCategoryGoods(currentCategory.value)
await loadRecommendedProducts(defaultLoadLimit)
}
@@ -1233,7 +1228,6 @@ const initPage = () => {
statusBarHeight.value = systemInfo.statusBarHeight != null ? systemInfo.statusBarHeight : 20
const searchContentHeight = Math.round(68 * systemInfo.screenWidth / 750)
const searchTopGap = Math.round(14 * systemInfo.screenWidth / 750)
const categoryTopGap = Math.round(12 * systemInfo.screenWidth / 750)
const headerBottomPadding = Math.round(10 * systemInfo.screenWidth / 750)
const moduleRowHeight = Math.round(62 * systemInfo.screenWidth / 750)
const categoryRowHeight = Math.round(56 * systemInfo.screenWidth / 750)
@@ -1251,24 +1245,23 @@ const initPage = () => {
if (menuInfo != null && menuInfo.top > 0) {
const navHeight = (menuInfo.top - statusBarHeight.value) * 2 + menuInfo.height
navBarHeight.value = navHeight
// 计算右边距避让
const rightReserve = systemInfo.screenWidth - menuInfo.left + 8
navBarRight.value = rightReserve
const searchRowTotalH = statusBarHeight.value + Math.max(navHeight, moduleRowHeight) + searchTopGap + searchContentHeight + categoryTopGap + categoryRowHeight + headerBottomPadding
searchRowStyle.value = `padding-top:${statusBarHeight.value}px;height:${searchRowTotalH}px;padding-right:${rightReserve}px;`
const searchRowTotalH = statusBarHeight.value + Math.max(navHeight, moduleRowHeight) + searchTopGap + searchContentHeight + headerBottomPadding
searchRowStyle.value = `padding-right:${rightReserve}px;`
navbarTotalHeight.value = searchRowTotalH
} else {
navBarHeight.value = 44
navBarRight.value = 0
const searchRowTotalH = statusBarHeight.value + Math.max(44, moduleRowHeight) + searchTopGap + searchContentHeight + categoryTopGap + categoryRowHeight + headerBottomPadding
searchRowStyle.value = `padding-top:${statusBarHeight.value}px;height:${searchRowTotalH}px;`
const searchRowTotalH = statusBarHeight.value + Math.max(44, moduleRowHeight) + searchTopGap + searchContentHeight + headerBottomPadding
searchRowStyle.value = ``
navbarTotalHeight.value = searchRowTotalH
}
headerStyle.value = ``
categoryBarHeightPx.value = categoryRowHeight
headerPlaceholderHeight.value = navbarTotalHeight.value
headerPlaceholderHeight.value = navbarTotalHeight.value + categoryBarHeightPx.value
const safeBottom = systemInfo.safeArea != null ? systemInfo.screenHeight - systemInfo.safeArea.bottom : 20
bottomSafeArea.value = safeBottom > 0 ? safeBottom : 20
@@ -1288,6 +1281,9 @@ onShow(() => {
console.log('=== index页面onShow被调用 ===')
console.log('主页重新显示,重置页面状态')
startPlaceholderScroll()
if (categoryList.value.length > 0) {
void consumeSelectedCategoryFromStorage()
}
// 重置导航栏显示状态
showNavbar.value = true
@@ -1297,9 +1293,8 @@ onShow(() => {
// 注意这里不能直接操作scroll-view的滚动位置
// 但可以重置一些页面状态
// 注意:这里不再清除selectedCategory
// 让分类页面在成功读取后自行清除
// 这样可以确保分类页面能正确读取到传递的数据
// 兼容旧分类桥接页:如果首页是从旧入口返回的,前面已经同步读取并清理 selectedCategory
// 这里不再额外处理,避免重复触发分类刷新
// 每次页面显示时尝试更新用户资料
if (!isFirstShow.value) {
@@ -1384,24 +1379,18 @@ const switchCategory = (category: any) => {
const categoryName = catObj.getString('name') ?? ''
console.log('分类ID:', categoryId, '分类名称:', categoryName)
// 使用Storage传递参数确保switchTab后能被读取
uni.setStorageSync('selectedCategory', categoryId)
// 生成唯一的时间戳和随机参数,确保每次跳转都是新的页面
const timestamp = Date.now()
const randomParam = Math.random().toString(36).substring(2, 8)
// 构建带参数的URL直接通过URL传递分类信息
const url = `/pages/main/category?categoryId=${categoryId}&name=${encodeURIComponent(categoryName)}&timestamp=${timestamp}&random=${randomParam}`
uni.switchTab({
url: '/pages/main/category',
success: () => {
// 通过 Storage 传递参数已在上面设置
console.log('跳转分类页面成功categoryId:', categoryId)
}
})
if (categoryId === '') {
return
}
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id == categoryId)
if (matchedItem != null) {
void refreshHomeCategory(matchedItem)
return
}
void refreshHomeCategory({
id: categoryId,
name: categoryName !== '' ? categoryName : getCategoryLabelById(categoryId)
})
}
const switchBrand = (brand: Brand) => {
@@ -1968,7 +1957,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
color: #999999;
line-height: 22rpx;
height: 22rpx;
transform: translateY(-5rpx);
transform: translateY(-10rpx);
}
.category-panel-mask {
@@ -2001,7 +1990,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 72rpx;
height: 64rpx;
padding: 0 0 0 20rpx;
box-sizing: border-box;
border-bottom-width: 1rpx;
@@ -2015,11 +2004,12 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
font-weight: 700;
color: #2f2f2f;
line-height: 1;
transform: translateY(-6rpx);
}
.category-panel-close-btn {
min-width: 96rpx;
height: 72rpx;
height: 64rpx;
display: flex;
flex-direction: row;
align-items: center;
@@ -2036,6 +2026,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
color: #666666;
font-weight: 600;
line-height: 1;
transform: translateY(-6rpx);
}
.category-panel-close-arrow {
@@ -2043,6 +2034,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
color: #666666;
margin-left: 6rpx;
line-height: 1;
transform: translateY(-6rpx);
}
.category-panel-grid {

View File

@@ -5,9 +5,11 @@
<script>
export default {
onLoad(options) {
const qs = options ? Object.keys(options).map(k => `${k}=${encodeURIComponent(options[k])}`).join('&') : '';
const url = '/pages/main/category' + (qs ? `?${qs}` : '');
uni.navigateTo({ url });
const categoryId = options && options.categoryId ? String(options.categoryId) : '';
if (categoryId !== '') {
uni.setStorageSync('selectedCategory', categoryId);
}
uni.switchTab({ url: '/pages/main/index' });
}
}
</script>