Files
frontend-app/pages/sort/sort.vue
T
2026-08-17 11:32:40 +08:00

924 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="sort_warp" :class="platformType ? '' : 'iosplatformType'">
<!-- #ifndef MP-WEIXIN -->
<TopSafe bgColor="#F2EBFF"></TopSafe>
<!-- #endif -->
<!-- 顶部固定区域:搜索 + 分类区域 -->
<view class="top_fixed_header" :class="{ is_expanded: showAllCategories }" :style="mpHeaderStyle">
<!-- 搜索框 -->
<view class="search_bar" @click="jumpSearch">
<u-icon name="search" size="18" color="#999"></u-icon>
<text class="search_placeholder">搜索</text>
</view>
<!-- 1. 未展开时:顶部分类横向滚动条 -->
<view class="top_category_row" v-if="!showAllCategories">
<scroll-view class="top_category_scroll" scroll-x :show-scrollbar="false">
<view class="top_cat_list">
<view class="top_cat_item" v-for="(item, index) in categoryList" :key="item.id || index"
@click="selectCategory(index)">
<view class="top_cat_img_box" :class="{ active_img: currentCategoryIndex === index }">
<image class="top_cat_img" :src="item.icon || defaultIcon" mode="aspectFill" />
</view>
<view class="top_cat_name_badge" :class="{ active_badge: currentCategoryIndex === index }">
{{ item.name }}
</view>
</view>
</view>
</scroll-view>
<!-- 右侧全部按钮 -->
<view class="expand_all_btn" @click="toggleAllCategories">
<text class="expand_text">全部</text>
<u-icon name="arrow-down-fill" size="10" color="#666"></u-icon>
</view>
</view>
<!-- 2. 点开全部时:展开所有分类网格面板 -->
<view class="all_categories_panel" v-else>
<view class="panel_header">
<text class="panel_title">所有分类</text>
</view>
<scroll-view class="panel_grid_scroll" scroll-y :show-scrollbar="false">
<view class="panel_grid">
<view class="top_cat_item panel_cat_item" v-for="(item, index) in categoryList" :key="item.id || index"
@click="selectCategory(index, true)">
<view class="top_cat_img_box" :class="{ active_img: currentCategoryIndex === index }">
<image class="top_cat_img" :src="item.icon || defaultIcon" mode="aspectFill" />
</view>
<view class="top_cat_name_badge" :class="{ active_badge: currentCategoryIndex === index }">
{{ item.name }}
</view>
</view>
</view>
</scroll-view>
<view class="collapse_btn" @click="toggleAllCategories">
<text style="margin-right: 6rpx;">全部收起</text>
<u-icon name="arrow-up" size="12" color="#666"></u-icon>
</view>
</view>
</view>
<!-- 主体区域:左侧边栏 + 右侧商品列表 -->
<view class="main_container">
<!-- 展开所有分类时的半透明遮罩 -->
<view class="mask_overlay" v-if="showAllCategories" @click="showAllCategories = false"></view>
<!-- 左侧分类侧边栏 -->
<scroll-view class="left_sidebar_scroll" scroll-y>
<view class="sidebar_item" v-for="(item, index) in subCategoryList" :key="item.id || index"
:class="{ active: currentSubCategoryIndex === index }" @click="selectSubCategory(index)">
<text class="sidebar_name">{{ item.name }}</text>
</view>
</scroll-view>
<!-- 右侧商品列表区 -->
<view class="right_content_area">
<!-- 排序栏 -->
<view class="sort_option_bar">
<view></view>
<!-- <view class="cat_title_text">{{ currentCategory ? currentCategory.name : '' }}</view> -->
<view class="sort_actions">
<view class="sort_item" :class="{ active: sortBy === 'relevance' }" @click="setSort('relevance')">
<text>综合</text>
<view class="sort_icon_box">
<u-icon name="arrow-up-fill" size="10rpx"
:color="sortBy === 'relevance' && sortOrder === 'asc' ? '#7a35f6' : '#cccccc'"
class="icon_up"></u-icon>
<u-icon name="arrow-down-fill" size="10rpx"
:color="sortBy === 'relevance' && sortOrder === 'desc' ? '#7a35f6' : '#cccccc'"
class="icon_down"></u-icon>
</view>
</view>
<view class="sort_item" :class="{ active: sortBy === 'sales' }" @click="setSort('sales')">
<text>销量</text>
<view class="sort_icon_box">
<u-icon name="arrow-up-fill" size="10rpx"
:color="sortBy === 'sales' && sortOrder === 'asc' ? '#7a35f6' : '#cccccc'" class="icon_up"></u-icon>
<u-icon name="arrow-down-fill" size="10rpx"
:color="sortBy === 'sales' && sortOrder === 'desc' ? '#7a35f6' : '#cccccc'"
class="icon_down"></u-icon>
</view>
</view>
<view class="sort_item" :class="{ active: sortBy === 'price' }" @click="setSort('price')">
<text>价格</text>
<view class="sort_icon_box">
<u-icon name="arrow-up-fill" size="10rpx"
:color="sortBy === 'price' && sortOrder === 'asc' ? '#7a35f6' : '#cccccc'" class="icon_up"></u-icon>
<u-icon name="arrow-down-fill" size="10rpx"
:color="sortBy === 'price' && sortOrder === 'desc' ? '#7a35f6' : '#cccccc'"
class="icon_down"></u-icon>
</view>
</view>
</view>
</view>
<!-- 商品列表 -->
<scroll-view class="product_list_scroll" scroll-y @scrolltolower="onReachBottomGoods" :lower-threshold="50">
<view class="product_list_box" v-if="displayProducts && displayProducts.length > 0">
<view class="product_row_card" v-for="(item, index) in displayProducts" :key="item.id || index"
@click="jumpInfo(item)">
<view class="product_img_box">
<image class="product_img" :src="item.url || defaultProductImg" mode="aspectFill" lazy-load />
</view>
<view class="product_info_box">
<view class="product_title">{{ item.name || item.title }}</view>
<view class="product_tags_row">
<text class="tag_item tag_blue" v-if="item.tags && item.tags[0]">{{ item.tags[0] }}</text>
<text class="tag_item tag_green" v-if="item.tags && item.tags[1]">{{ item.tags[1] }}</text>
<text class="tag_item tag_blue" v-if="!item.tags">专业检测</text>
<text class="tag_item tag_green" v-if="!item.tags">生态有机</text>
</view>
<view class="product_price_row">
<view class="price_box">
<text class="price_symbol">¥</text>
<text class="price_val">{{ item.price }}</text>
</view>
<text class="sales_val">全网销量{{ limit999(item.sales) }}</text>
</view>
</view>
</view>
<!-- 加载更多与无数据状态提示 -->
<view class="load_more_tips" v-if="loadingMore">加载中...</view>
<view class="load_more_tips" v-else-if="noMore && displayProducts.length > 0">没有更多了</view>
</view>
<view class="empty_box" v-else-if="!loading">
<image src="https://static.tbmall.xin/static/not_shopping.png" class="empty_img" />
<text class="empty_text">暂无商品数据</text>
</view>
</scroll-view>
</view>
</view>
<!-- #ifndef H5 || MP-WEIXIN-->
<CustomTabBar :tabIndex="1" />
<!-- #endif -->
<qiaobao-assistant page-key="pages/sort/sort" title="商品分类" />
</view>
</template>
<script>
import { getCascadeCategory } from "@/api/common.js";
import { searchList } from "@/api/product.js";
import CustomTabBar from "@/components/custom-tab-bar.vue";
import TopSafe from "@/components/common/top-safe.nvue";
export default {
components: { CustomTabBar, TopSafe },
data() {
return {
platformType: true,
categoryList: [], // 一级分类列表 (parentId = 0)
currentCategoryIndex: 0, // 当前选中的一级分类索引
subCategoryList: [], // 二级分类列表 (parentId = 一级ID)
currentSubCategoryIndex: 0, // 当前选中的二级分类索引
displayProducts: [], // 三级商品列表 (goods/searchList)
showAllCategories: false,
sortBy: 'relevance', // 'relevance' | 'sales' | 'price'
sortOrder: 'asc', // 'asc' | 'desc'
capsuleTop: 0,
defaultIcon: 'https://static.tbmall.xin/static/new-home/xinren.png',
defaultProductImg: 'https://static.tbmall.xin/static/new-home/nangua.png',
isFromSortShop: false,
loading: false,
loadingMore: false,
noMore: false,
page: 1,
pageSize: 10
};
},
computed: {
mpHeaderStyle() {
/*#ifdef MP-WEIXIN*/
if (this.capsuleTop) {
return { paddingTop: this.capsuleTop + 'px' };
}
return { paddingTop: '80rpx' };
/*#endif*/
return {};
},
currentCategory() {
return this.categoryList[this.currentCategoryIndex] || null;
},
currentSubCategory() {
return this.subCategoryList[this.currentSubCategoryIndex] || null;
}
},
onShow() {
/*#ifdef APP-PLUS*/
uni.hideTabBar({ animation: false });
const appUpdate = uni.getStorageSync("platform");
this.platformType = appUpdate == "android" ? "ANDROID" : "IOS";
/*#endif*/
/*#ifdef MP-WEIXIN*/
try {
const menu = uni.getMenuButtonBoundingClientRect();
if (menu && menu.bottom) {
this.capsuleTop = menu.bottom + 6;
}
} catch (e) { }
/*#endif*/
const isBackFromSortShop = this.isFromSortShop;
this.isFromSortShop = false;
this.fetchCategories(isBackFromSortShop);
},
methods: {
// 异步查询级联分类接口 (common/getCascadeCategory)
async getCategoryApi(pId) {
try {
const resp = await getCascadeCategory({ parentId: pId });
if (resp && resp.bizcode === 100 && Array.isArray(resp.data)) {
return resp.data;
}
} catch (e) {
console.error("getCascadeCategory error:", e);
}
return [];
},
// 1. 刚进来获取一级分类 (parentId = 0)
async fetchCategories(isBack = false) {
try {
const topList = await this.getCategoryApi(0);
this.categoryList = topList || [];
} catch (e) {
console.error("获取一级分类失败:", e);
this.categoryList = [];
}
if (this.categoryList.length === 0) {
this.subCategoryList = [];
this.displayProducts = [];
return;
}
let targetIndex = 0;
if (isBack) {
const sortId = uni.getStorageSync("sortId");
if (sortId) {
let idx = this.categoryList.findIndex(item => item.id === sortId);
if (idx !== -1) targetIndex = idx;
else if (this.currentCategoryIndex >= 0 && this.currentCategoryIndex < this.categoryList.length) {
targetIndex = this.currentCategoryIndex;
}
}
} else {
uni.removeStorageSync("sortId");
uni.removeStorageSync("subSortId");
targetIndex = 0;
}
this.selectCategory(targetIndex, false, isBack);
},
// 2. 选择一级分类,根据其 id 获取二级分类 (parentId = 该一级id)
async selectCategory(index, closePopup = false, isBack = false) {
this.currentCategoryIndex = index;
if (closePopup) {
this.showAllCategories = false;
}
const category = this.categoryList[index];
if (!category || !category.id) {
this.subCategoryList = [];
this.displayProducts = [];
return;
}
uni.setStorageSync("sortId", category.id);
this.loading = true;
try {
const subList = await this.getCategoryApi(category.id);
this.subCategoryList = subList || [];
if (this.subCategoryList.length === 0) {
this.displayProducts = [];
this.loading = false;
return;
}
let targetSubIndex = 0;
if (isBack) {
const subSortId = uni.getStorageSync("subSortId");
if (subSortId) {
let sIdx = this.subCategoryList.findIndex(item => item.id === subSortId);
if (sIdx !== -1) targetSubIndex = sIdx;
}
}
this.selectSubCategory(targetSubIndex);
} catch (e) {
console.error("获取二级分类失败:", e);
this.subCategoryList = [];
this.displayProducts = [];
this.loading = false;
}
},
// 3. 选择二级分类,更新当前二级分类并调用 goods/searchList 获取商品列表
selectSubCategory(subIndex) {
this.currentSubCategoryIndex = subIndex;
const subCat = this.subCategoryList[subIndex];
if (subCat && subCat.id) {
uni.setStorageSync("subSortId", subCat.id);
}
this.fetchGoodsList(false);
},
// 4. 调用 /goods/searchList 接口获取商品列表 (支持分页与排序)
async fetchGoodsList(isLoadMore = false) {
const currentSubCat = this.subCategoryList[this.currentSubCategoryIndex];
if (!currentSubCat || !currentSubCat.id) {
this.displayProducts = [];
this.loading = false;
this.loadingMore = false;
return;
}
if (isLoadMore) {
if (this.loadingMore || this.noMore) return;
this.loadingMore = true;
this.page++;
} else {
this.loading = true;
this.page = 1;
this.noMore = false;
}
const params = {
categoryId: currentSubCat.id,
page: this.page,
pageSize: this.pageSize,
sortBy: this.sortBy,
sortOrder: this.sortOrder
};
try {
const resp = await searchList(params);
if (resp && resp.bizcode === 100) {
let list = [];
if (Array.isArray(resp.data)) {
list = resp.data;
} else if (resp.data && Array.isArray(resp.data.entitys)) {
list = resp.data.entitys;
} else if (resp.data && Array.isArray(resp.data.list)) {
list = resp.data.list;
}
if (isLoadMore) {
this.displayProducts = [...this.displayProducts, ...list];
} else {
this.displayProducts = list;
}
if (list.length < this.pageSize) {
this.noMore = true;
}
} else {
if (!isLoadMore) {
this.displayProducts = [];
}
this.noMore = true;
}
} catch (e) {
console.error("searchList error:", e);
if (!isLoadMore) {
this.displayProducts = [];
}
} finally {
this.loading = false;
this.loadingMore = false;
}
},
// 5. 右侧商品列表触底触发分页加载
onReachBottomGoods() {
if (!this.noMore && !this.loadingMore && !this.loading) {
this.fetchGoodsList(true);
}
},
toggleAllCategories() {
this.showAllCategories = !this.showAllCategories;
},
// 6. 排序作用于 goods/searchList 接口
setSort(type) {
if (this.sortBy === type) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortBy = type;
this.sortOrder = 'asc';
}
this.fetchGoodsList(false);
},
limit999(n) {
const num = Number(n);
if (isNaN(num) || num < 10000) {
return n != null && n !== '' ? String(n) : '0';
}
return `${Math.floor(num / 10000)}万+`;
},
jumpSearch() {
uni.navigateTo({
url: "/pages/rwa_package/insights/search",
});
},
jumpInfo(item) {
this.isFromSortShop = true;
uni.navigateTo({
url: `/pages/rwa_package/shop/sort-shop?id=${item.id}&name=${encodeURIComponent(item.name || item.title || '')}`,
});
}
}
};
</script>
<style lang="scss" scoped>
page {
height: 100%;
overflow: hidden;
}
.sort_warp {
height: 100vh;
/* #ifdef H5 */
height: calc(100vh - var(--window-top, 0px) - var(--window-bottom, 0px));
/* #endif */
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
background-color: #ffffff;
position: relative;
}
/* 顶部固定区域 */
.top_fixed_header {
position: relative;
z-index: 100;
background: linear-gradient(180deg, #F2EBFF 0%, #FFFFFF 100%);
border-bottom-left-radius: 36rpx;
border-bottom-right-radius: 36rpx;
overflow: hidden;
flex-shrink: 0;
box-shadow: none;
transition: all 0.2s;
&.is_expanded {
background: linear-gradient(180deg, #F2EBFF 0%, #FFFFFF 100%, #FFFFFF 100%) !important;
border-bottom-left-radius: 36rpx !important;
border-bottom-right-radius: 36rpx !important;
box-shadow: 0 12rpx 32rpx rgba(122, 53, 246, 0.12) !important;
overflow: hidden !important;
}
}
/* 搜索框 */
.search_bar {
margin: 16rpx 24rpx 12rpx;
height: 72rpx;
background-color: #ffffff;
border-radius: 36rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
.search_placeholder {
font-size: 28rpx;
color: #999999;
margin-left: 12rpx;
}
}
/* 顶部分类横划栏 */
.top_category_row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10rpx 0 16rpx 20rpx;
.top_category_scroll {
flex: 1;
overflow-x: auto;
white-space: nowrap;
min-width: 0;
}
.top_cat_list {
display: inline-flex;
align-items: flex-start;
}
.expand_all_btn {
width: 90rpx;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #666666;
padding: 0 12rpx;
.expand_text {
font-size: 22rpx;
color: #666666;
margin-bottom: 4rpx;
}
}
}
/* 分类 Icon 与 Badge 通用样式(同时作用于横滑栏与展开网格) */
.top_cat_item {
display: inline-flex;
flex-direction: column;
align-items: center;
margin-right: 32rpx;
box-sizing: border-box;
&.panel_cat_item {
width: 20% !important;
max-width: 20% !important;
margin-right: 0 !important;
margin-bottom: 24rpx !important;
}
.top_cat_img_box {
width: 96rpx !important;
height: 96rpx !important;
min-width: 96rpx !important;
max-width: 96rpx !important;
min-height: 96rpx !important;
max-height: 96rpx !important;
border-radius: 50% !important;
overflow: hidden !important;
background: #f7f7f7;
display: flex !important;
align-items: center !important;
justify-content: center !important;
box-sizing: border-box !important;
border: 4rpx solid transparent;
transition: all 0.2s;
flex-shrink: 0 !important;
&.active_img {
border-color: #7a35f6 !important;
}
}
.top_cat_img {
width: 88rpx !important;
height: 88rpx !important;
min-width: 88rpx !important;
max-width: 88rpx !important;
min-height: 88rpx !important;
max-height: 88rpx !important;
border-radius: 50% !important;
display: block !important;
flex-shrink: 0 !important;
}
.top_cat_name_badge {
font-size: 22rpx !important;
color: #666666 !important;
margin-top: 10rpx !important;
padding: 2rpx 12rpx !important;
border-radius: 16rpx !important;
line-height: 32rpx !important;
white-space: nowrap !important;
transition: all 0.2s;
&.active_badge {
background-color: #7a35f6 !important;
color: #ffffff !important;
font-weight: bold !important;
}
}
}
/* 点开全部时的网格面板 */
.all_categories_panel {
background: transparent;
padding: 16rpx 24rpx 0;
border-bottom-left-radius: 36rpx !important;
border-bottom-right-radius: 36rpx !important;
overflow: hidden !important;
.panel_header {
margin-bottom: 20rpx;
padding-left: 8rpx;
.panel_title {
font-size: 30rpx;
font-weight: bold;
color: #1a1a1a;
}
}
.panel_grid_scroll {
max-height: 55vh;
overflow-y: auto;
}
.panel_grid {
display: flex !important;
flex-wrap: wrap !important;
width: 100% !important;
}
.collapse_btn {
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 24rpx !important;
color: #666666 !important;
height: 80rpx !important;
background-color: #ffffff !important;
border-top: 1rpx solid #eeeeee !important;
margin-top: 16rpx !important;
margin-left: -24rpx !important;
margin-right: -24rpx !important;
border-bottom-left-radius: 36rpx !important;
border-bottom-right-radius: 36rpx !important;
overflow: hidden !important;
}
}
/* 主内容区 */
.main_container {
position: relative;
flex: 1;
display: flex;
flex-direction: row;
min-height: 0;
width: 100%;
background-color: #f7f7f7;
overflow: hidden;
/* 展开所有分类时的半透明遮罩 */
.mask_overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
background-color: rgba(0, 0, 0, 0.45) !important;
z-index: 90 !important;
}
}
/* 左侧边栏 */
.left_sidebar_scroll {
width: 180rpx;
height: 100%;
flex-shrink: 0;
background-color: #f7f7f7;
overflow-y: auto;
/* #ifdef APP-PLUS */
padding-bottom: calc(110rpx + env(safe-area-inset-bottom));
/* #endif */
.sidebar_item {
height: 104rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #555555;
padding: 0 12rpx;
text-align: center;
position: relative;
box-sizing: border-box;
&.active {
background-color: #ffffff;
color: #7a35f6;
font-weight: bold;
font-size: 28rpx;
&::before {
content: "";
position: absolute;
left: 0;
top: 34rpx;
width: 6rpx;
height: 36rpx;
background-color: #7a35f6;
border-radius: 0 4rpx 4rpx 0;
}
}
}
}
/* 右侧内容区域 */
.right_content_area {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
background-color: #ffffff;
min-width: 0;
}
/* 右侧排序栏 */
.sort_option_bar {
height: 80rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: none;
flex-shrink: 0;
.cat_title_text {
font-size: 30rpx;
font-weight: bold;
color: #7a35f6;
}
.sort_actions {
display: flex;
align-items: center;
gap: 32rpx;
.sort_item {
font-size: 26rpx;
color: #999999;
display: flex;
align-items: center;
cursor: pointer;
&.active {
color: #7a35f6;
font-weight: bold;
}
.sort_icon_box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-left: 6rpx;
line-height: 1;
.icon_up {
margin-bottom: 2rpx;
}
}
}
}
}
/* 商品列表 */
.product_list_scroll {
flex: 1;
height: 100%;
overflow-y: auto;
/* #ifdef APP-PLUS */
padding-bottom: calc(110rpx + env(safe-area-inset-bottom));
/* #endif */
.product_list_box {
padding: 0 24rpx;
}
.product_row_card {
display: flex;
flex-direction: row;
padding: 24rpx 0;
border-bottom: 1rpx solid #f7f7f7;
background: #ffffff;
.product_img_box {
width: 200rpx;
height: 200rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
margin-right: 20rpx;
background: #f9f9f9;
.product_img {
width: 100%;
height: 100%;
}
}
.product_info_box {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
.product_title {
font-size: 28rpx;
font-weight: bold;
color: #1a1a1a;
line-height: 38rpx;
max-height: 76rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-all;
}
.product_tags_row {
display: flex;
gap: 12rpx;
margin-top: 8rpx;
.tag_item {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 6rpx;
white-space: nowrap;
&.tag_blue {
color: #2979ff;
border: 1rpx solid #2979ff;
background-color: rgba(41, 121, 255, 0.05);
}
&.tag_green {
color: #19be6b;
border: 1rpx solid #19be6b;
background-color: rgba(25, 190, 107, 0.05);
}
}
}
.product_price_row {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-top: 12rpx;
.price_box {
color: #ff2a2a;
font-weight: bold;
.price_symbol {
font-size: 24rpx;
}
.price_val {
font-size: 38rpx;
}
}
.sales_val {
font-size: 24rpx;
color: #999999;
}
}
}
}
.empty_box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 120rpx;
.empty_img {
width: 160rpx;
height: 160rpx;
}
.empty_text {
font-size: 26rpx;
color: #999999;
margin-top: 20rpx;
}
}
.load_more_tips {
text-align: center;
font-size: 24rpx;
color: #999999;
padding: 24rpx 0;
}
}
</style>