Files
frontend-app/pages/sort/sort.vue
T
2026-08-06 18:50:25 +08:00

858 lines
23 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></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 categoryList" :key="item.id || index"
:class="{ active: currentCategoryIndex === index }" @click="selectCategory(index)">
<text class="sidebar_name">{{ item.name }}</text>
</view>
</scroll-view>
<!-- 右侧商品列表区 -->
<view class="right_content_area">
<!-- 排序栏 -->
<view class="sort_option_bar">
<view class="cat_title_text">{{ currentCategory ? currentCategory.name : '' }}</view>
<view class="sort_actions">
<view class="sort_item" :class="{ active: sortType === 'default' }" @click="setSort('default')">
综合 <text class="sort_arrow">⇅</text>
</view>
<view class="sort_item" :class="{ active: sortType === 'sales' }" @click="setSort('sales')">
销量 <text class="sort_arrow">⇅</text>
</view>
<view class="sort_item" :class="{ active: sortType === 'price' }" @click="setSort('price')">
价格 <text class="sort_arrow">⇅</text>
</view>
</view>
</view>
<!-- 商品列表 -->
<scroll-view class="product_list_scroll" scroll-y>
<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.image || item.icon || 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>
<view class="empty_box" v-else>
<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 -->
</view>
</template>
<script>
import { getCascadeCategory } from "@/api/common.js";
import CustomTabBar from "@/components/custom-tab-bar.vue";
import TopSafe from "@/components/common/top-safe.nvue";
// 假数据列表:当后端接口未就绪时自动提供高保真模拟数据
const MOCK_CATEGORIES = [
{
id: 101,
name: '有机食品',
icon: 'https://static.tbmall.xin/static/new-home/xinren.png',
foods: [
{
id: 1001,
name: '鲜8有机贝贝南瓜2500g粉糯板栗味自然生长不催熟',
title: '鲜8有机贝贝南瓜2500g粉糯板栗味自然生长不催熟',
price: '39.00',
sales: 888,
tags: ['专业检测', '生态有机'],
image: 'https://static.tbmall.xin/static/new-home/nangua.png'
},
{
id: 1002,
name: '采缇国产车厘子5kg4j大连美早樱桃生鲜次日达樱桃',
title: '采缇国产车厘子5kg4j大连美早樱桃生鲜次日达樱桃',
price: '26.99',
sales: 888,
tags: ['专业检测', '生态有机'],
image: 'https://static.tbmall.xin/static/new-home/baokuan.png'
},
{
id: 1003,
name: '江苏白凤水蜜桃 应季水果 &新鲜礼盒装',
title: '江苏白凤水蜜桃 应季水果 &新鲜礼盒装',
price: '34.88',
sales: 888,
tags: ['专业检测', '生态有机'],
image: 'https://static.tbmall.xin/static/new-home/cart.png'
},
{
id: 1004,
name: '季新鲜 &进口山竹时令 &老树麻竹整箱送礼',
title: '季新鲜 &进口山竹时令 &老树麻竹整箱送礼',
price: '52.00',
sales: 888,
tags: ['专业检测', '生态有机'],
image: 'https://static.tbmall.xin/static/new-home/huiyuan.png'
}
]
},
{
id: 102,
name: '生鲜食材',
icon: 'https://static.tbmall.xin/static/new-home/baokuan.png',
foods: [
{
id: 1005,
name: '精选新鲜三文鱼中段切片 200g刺身级',
title: '精选新鲜三文鱼中段切片 200g刺身级',
price: '59.90',
sales: 1200,
tags: ['冷链速达', '产地直供'],
image: 'https://static.tbmall.xin/static/new-home/baokuan.png'
},
{
id: 1006,
name: '澳洲进口谷饲安格斯原切雪花牛肉卷 500g',
title: '澳洲进口谷饲安格斯原切雪花牛肉卷 500g',
price: '89.00',
sales: 650,
tags: ['原切牛肉', '品质严选'],
image: 'https://static.tbmall.xin/static/new-home/cart.png'
}
]
},
{
id: 103,
name: '米面粮油',
icon: 'https://static.tbmall.xin/static/new-home/cart.png',
foods: [
{
id: 1007,
name: '五常大米 优质长粒香米 5kg真空包装',
title: '五常大米 优质长粒香米 5kg真空包装',
price: '49.90',
sales: 3400,
tags: ['核心产区', '自然醇香'],
image: 'https://static.tbmall.xin/static/new-home/xinren.png'
}
]
},
{
id: 104,
name: '零食饮料',
icon: 'https://static.tbmall.xin/static/new-home/huiyuan.png',
foods: [
{
id: 1008,
name: '花生造黑松露黑麦苏打饼干 590g*2盒',
title: '花生造黑松露黑麦苏打饼干 590g*2盒',
price: '29.90',
sales: 2300,
tags: ['零反式脂肪酸', '拒绝油炸'],
image: 'https://static.tbmall.xin/static/new-home/huiyuan.png'
}
]
},
{
id: 105,
name: '滋补养生',
icon: 'https://static.tbmall.xin/static/new-home/xinren.png',
foods: [
{
id: 1009,
name: '青海野生黑枸杞干果 250g特级罐装',
title: '青海野生黑枸杞干果 250g特级罐装',
price: '78.00',
sales: 510,
tags: ['正宗原产', '花青素丰富'],
image: 'https://static.tbmall.xin/static/new-home/nangua.png'
}
]
},
{
id: 106,
name: '非遗传承',
icon: 'https://static.tbmall.xin/static/new-home/baokuan.png',
foods: []
},
{
id: 107,
name: '家居百货',
icon: 'https://static.tbmall.xin/static/new-home/cart.png',
foods: []
},
{
id: 108,
name: '美妆护肤',
icon: 'https://static.tbmall.xin/static/new-home/huiyuan.png',
foods: []
},
{
id: 109,
name: '其他特色',
icon: 'https://static.tbmall.xin/static/new-home/xinren.png',
foods: []
}
];
export default {
components: { CustomTabBar, TopSafe },
data() {
return {
platformType: true,
currentCategoryIndex: 0,
showAllCategories: false,
sortType: 'default', // default | sales | price
sortAsc: true,
categoryList: [],
capsuleTop: 0,
defaultIcon: 'https://static.tbmall.xin/static/new-home/xinren.png',
defaultProductImg: 'https://static.tbmall.xin/static/new-home/nangua.png'
};
},
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;
},
displayProducts() {
if (!this.currentCategory) return [];
let list = this.currentCategory.foods || this.currentCategory.productList || [];
if (!list || list.length === 0) return [];
list = [...list];
if (this.sortType === 'sales') {
list.sort((a, b) => (Number(b.sales) || 0) - (Number(a.sales) || 0));
} else if (this.sortType === 'price') {
if (this.sortAsc) {
list.sort((a, b) => (Number(a.price) || 0) - (Number(b.price) || 0));
} else {
list.sort((a, b) => (Number(b.price) || 0) - (Number(a.price) || 0));
}
}
return list;
}
},
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*/
this.fetchCategories();
},
methods: {
async fetchCategories() {
try {
const resp = await getCascadeCategory({ parentId: 0 });
if (resp && resp.bizcode === 100 && resp.data && resp.data.length > 0) {
this.categoryList = resp.data;
} else {
this.categoryList = MOCK_CATEGORIES;
}
} catch (e) {
console.log("使用模拟分类数据", e);
this.categoryList = MOCK_CATEGORIES;
}
},
selectCategory(index, closePopup = false) {
this.currentCategoryIndex = index;
if (closePopup) {
this.showAllCategories = false;
}
},
toggleAllCategories() {
this.showAllCategories = !this.showAllCategories;
},
setSort(type) {
if (this.sortType === type && type === 'price') {
this.sortAsc = !this.sortAsc;
} else {
this.sortType = type;
this.sortAsc = true;
}
},
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) {
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: #ffffff;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
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: 1rpx solid #f5f5f5;
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;
&.active {
color: #7a35f6;
font-weight: bold;
}
.sort_arrow {
font-size: 20rpx;
margin-left: 4rpx;
}
}
}
}
/* 商品列表 */
.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;
}
}
}
</style>