| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- <template>
- <view class="page-container">
- <!-- 导航栏 -->
- <u-navbar :autoBack="true" :placeholder="true" :border-bottom="false">
- <view class="navbar-title">团餐商品列表</view>
- <view slot="right" class="navbar-right">
- <text class="nav-btn" @click="addGoods">添加商品</text>
- <text class="nav-btn manage-btn" @click="toggleManageMode">{{ isManaging ? '退出管理' : '批量管理' }}</text>
- </view>
- </u-navbar>
- <!-- 搜索框 -->
- <view class="search-bar">
- <u-search bg-color="#F7F8FC" placeholder="请输入商品名称" search-icon-color="#999"
- :showAction="true" actionText="搜索"
- :actionStyle="{
- width: 'auto',
- color: '#333',
- padding: '0 20rpx',
- borderLeft: '1rpx solid #DDDDDD'
- }"
- ></u-search>
- </view>
- <!-- 一级分类Tabs -->
- <u-tabs :list="statusTabs"
- :current="currentStatusTab"
- inactive-color="#666666"
- :bar-style="{
- bottom: '20rpx',
- background: 'linear-gradient(90deg, #82BCFF 0%, rgba(130,188,255,0) 100%)'
- }"
- :active-item-style="{
- fontWeight: '500',
- fontSize: '32rpx',
- color: '#000000',
- }"
- @change="onTabChange"
- ></u-tabs>
-
- <!-- 主内容区 -->
- <view class="main-content">
- <!-- 左侧二级分类 -->
- <view class="category-list">
- <view v-for="(category, index) in categoryTabs" :key="index">
- <view
- :class="['category-item', currentCategoryTab === index ? 'active' : '']"
- @click="onCategoryTabChange(index)"
- >
- {{ category.name }}
- </view>
- </view>
- </view>
- <!-- 右侧商品列表 -->
- <scroll-view scroll-y class="goods-scroll-view"
- :style="{ paddingBottom: isManaging ? '120rpx' : '0' }"
- :scroll-into-view="scrollIntoViewId"
- :scroll-with-animation="true"
- @scroll="onScroll"
- >
- <view class="goods-category-section" :id="category.scrollId" v-for="category in filteredGoods" :key="category.id">
- <view class="category-title">{{ category.name }}</view>
- <u-checkbox-group v-model="selectedGoods" placement="column">
- <view class="goods-card" v-for="goods in category.items" :key="goods.id">
- <!-- <image class="goods-image" :src="goods.image" mode="aspectFill"></image> -->
- <u-image width="120rpx" height="120rpx" border-radius="12rpx" :src="goods.image" mode="aspectFill"></u-image>
- <view class="goods-info">
- <view class="goods-name">{{ goods.name }}</view>
- <view class="goods-desc">{{ goods.description }}</view>
- <view class="goods-sales">销量:{{ goods.sales }}</view>
- <view class="goods-price-line">
- <text class="price"><text class="unit">¥</text>{{ goods.price }}</text>
- <text class="original-price">¥{{ goods.originalPrice }}</text>
- </view>
- </view>
- <!-- 根据模式和状态显示不同操作 -->
- <view class="goods-actions">
- <u-button
- v-if="!isManaging && currentStatusTab === 2"
- type="primary" plain
- @click="editGoods(goods)"
- >编辑</u-button>
- <u-checkbox v-if="isManaging" :name="goods.id" shape="circle" v-model="goods.checked"></u-checkbox>
- </view>
- </view>
- </u-checkbox-group>
- </view>
- </scroll-view>
- </view>
- <!-- 批量管理底部操作栏 -->
- <view class="footer-bar" v-if="isManaging">
- <u-checkbox-group v-model="isAllSelected" placement="row">
- <u-checkbox name="all" shape="circle" v-model="isAllSelected" @change="selectAll">全选</u-checkbox>
- </u-checkbox-group>
-
- <view class="action-buttons">
- <template v-if="currentStatusTab === 0 || currentStatusTab === 1">
- <u-button type="primary" @click="handleBatchAction('unpublish')">下架</u-button>
- </template>
- <template v-if="currentStatusTab === 2">
- <u-button type="primary" @click="handleBatchAction('publish')">上架</u-button>
- <u-button type="error" @click="handleBatchAction('delete')">删除</u-button>
- </template>
- </view>
- </view>
- </view>
- </template>
- <script>
- export default {
- data() {
- return {
- isManaging: false, // 是否处于批量管理模式
- // 一级分类
- statusTabs: [{ name: '售卖中' }, { name: '审核中' }, { name: '未上架' }],
- currentStatusTab: 0,
- // 二级分类
- categoryTabs: [{ name: '早餐' }, { name: '午餐' }, { name: '晚餐' }],
- currentCategoryTab: 0,
- // 所有商品数据 (模拟)
- allGoods: [
- { id: 1, categoryId: 0, status: 0, name: '套餐名称套餐名称套餐名称套餐名称', sales: 1255, price: 10.8, originalPrice: 12, description: '米饭, 鱼香肉丝, 宫保鸡丁', image: '/static/demo/goods1.png' },
- { id: 2, categoryId: 0, status: 0, name: '套餐名称套餐名称', sales: 1255, price: 14.8, originalPrice: 16, description: '米饭, 红烧排骨', image: '/static/demo/goods2.png' },
- { id: 3, categoryId: 1, status: 0, name: '套餐名称套餐名称', sales: 1255, price: 13.1, originalPrice: 15.0, description: '米饭, 糖醋里脊', image: '/static/demo/goods3.png' },
- { id: 4, categoryId: 0, status: 1, name: '审核中套餐', sales: 0, price: 20, originalPrice: 25, description: '审核中商品描述', image: '/static/demo/goods1.png' },
- { id: 5, categoryId: 0, status: 2, name: '未上架早餐', sales: 0, price: 15, originalPrice: 18, description: '未上架商品描述', image: '/static/demo/goods2.png' },
- { id: 6, categoryId: 1, status: 2, name: '未上架午餐', sales: 0, price: 25, originalPrice: 30, description: '未上架商品描述', image: '/static/demo/goods3.png' },
- ],
- selectedGoods: [], // 已选中的商品ID
- isAllSelected: false, // 用于全选checkbox的状态绑定
- // 联动功能相关数据
- scrollIntoViewId: '', // 滚动到指定视图的 ID
- categoryOffsetTops: [], // 每个分类区域到 scroll-view 顶部的距离
- isScrolling: false, // 标记是否正在进行点击滚动
- };
- },
- computed: {
- // 根据一、二级分类筛选商品
- filteredGoods() {
- const filteredByCategory = this.allGoods.filter(goods => goods.status === this.currentStatusTab);
- const grouped = [];
- this.categoryTabs.forEach((category, index) => {
- const items = filteredByCategory.filter(goods => goods.categoryId === index);
- if (items.length > 0) {
- grouped.push({
- id: index,
- name: category.name,
- items: items,
- scrollId: `category_${index}` // 滚动目标 ID
- });
- }
- });
- // 每次 filteredGoods 数据变化后,重新计算 offset
- this.$nextTick(() => {
- // 确保在数据渲染完成后再计算
- this.calculateCategoryOffsets();
- });
- return grouped;
- }
- },
- methods: {
- addGoods() {
- uni.navigateTo({
- url: '/pages/groupMeal/goods/form-step1'
- });
- },
- toggleManageMode() {
- this.isManaging = !this.isManaging;
- this.selectedGoods = []; // 切换模式时清空选择
- this.isAllSelected = false;
- },
- onTabChange(e) {
- this.currentStatusTab = e;
- this.selectedGoods = []; // 切换Tab时清空选择
- this.isAllSelected = false;
- // 切换 Tab 后,重置左侧分类高亮到第一个
- // this.currentCategoryTab = 0;
- // this.$nextTick(() => {
- // // 确保在新数据渲染后,滚动到第一个分类
- // this.scrollIntoViewId = this.filteredGoods[0]?.scrollId || '';
- // });
- },
- onCategoryTabChange(index) {
- this.currentCategoryTab = index;
- // 这里可以添加滚动到对应锚点的逻辑
- },
- editGoods(goods) {
- console.log('编辑商品:', goods);
- uni.navigateTo({
- url: '/pages/groupMeal/goods/form-step1?id=' + goods.id
- });
- },
- selectAll(e) {
- if (e.value) {
- // 全选
- this.selectedGoods = this.filteredGoods.flatMap(category => category.items.map(item => item.id));
- } else {
- // 取消全选
- this.selectedGoods = [];
- }
- this.filteredGoods.flatMap(category => category.items.forEach(item => {
- item.checked = e.value;
- }));
- },
- handleBatchAction(action) {
- if (this.selectedGoods.length === 0) {
- uni.showToast({ title: '请先选择商品', icon: 'none' });
- return;
- }
- console.log(`执行批量操作: ${action}`, this.selectedGoods);
- uni.showToast({ title: '操作成功', icon: 'success' });
- },
- // 核心功能 1: 点击左侧二级分类,右侧商品列表滚动到对应位置
- onCategoryTabChange(index) {
- this.currentCategoryTab = index;
-
- // 1. 设置标记,在滚动动画期间忽略 onGoodsScroll,防止快速切换闪烁
- this.isScrolling = true;
-
- // 2. 找到对应的 scrollId 并设置给 scroll-view
- const targetCategory = this.filteredGoods.find(c => c.id === index);
- if (targetCategory) {
- this.scrollIntoViewId = targetCategory.scrollId;
- }
-
- // 3. 延迟清除标记,等待滚动动画结束 (通常 300ms)
- setTimeout(() => {
- this.isScrolling = false;
- }, 300);
- },
- // 核心功能 2: 滚动商品列表时,左侧二级分类高亮联动
- onGoodsScroll(e) {
- // 如果是点击左侧分类触发的滚动,则忽略联动
- if (this.isScrolling) {
- return;
- }
-
- const scrollTop = e.detail.scrollTop;
- const threshold = 10; // 切换阈值,避免刚好在边缘时闪烁
- // 遍历所有分类的 offset,找到当前位于视图顶部的分类
- for (let i = 0; i < this.categoryOffsetTops.length; i++) {
- const current = this.categoryOffsetTops[i];
- const next = this.categoryOffsetTops[i + 1];
- // 检查条件:
- // 1. 当前分类的 top 已经小于或等于 scrollTop (即分类标题已经经过视图顶部)
- // 2. 并且 (如果存在下一个分类) 下一个分类的 top 还没有经过视图顶部
- if (current.top <= scrollTop + threshold && (!next || next.top > scrollTop + threshold)) {
- // 确保左侧分类的索引是正确的
- if (this.currentCategoryTab !== current.id) {
- this.currentCategoryTab = current.id;
- }
- break;
- }
- }
- },
- // 核心功能 3: 计算所有分类区块的顶部偏移量
- calculateCategoryOffsets() {
- if (this.filteredGoods.length === 0) {
- this.categoryOffsetTops = [];
- return;
- }
-
- // 必须使用 $nextTick 确保 DOM 已经渲染
- this.$nextTick(() => {
- const query = uni.createSelectorQuery().in(this);
- let scrollRectTop = 0;
-
- // 1. 获取 scroll-view 顶部到页面的距离 (作为基准点)
- query.select('.goods-scroll-view').boundingClientRect(rect => {
- scrollRectTop = rect.top;
- }).exec();
-
- // 2. 获取所有分类 section 的位置信息
- query.selectAll('.goods-category-section').boundingClientRect(data => {
- if (!data.length) return;
- this.categoryOffsetTops = data.map(item => ({
- id: parseInt(item.dataset.categoryIndex), // 从 data-category-index 获取 categoryId
- top: item.top - scrollRectTop // 计算相对于 scroll-view 顶部的距离 (即 scrollTop 的值)
- }));
- }).exec();
- });
- },
- }
- };
- </script>
- <style lang="scss" scoped>
- .page-container {
- height: 100vh;
- display: flex;
- flex-direction: column;
- .navbar-title {
- font-weight: 500;
- font-size: 36rpx;
- color: #333333;
- }
- .navbar-right {
- padding: 0 24rpx;
- display: flex;
- align-items: center;
- .nav-btn {
- font-weight: 500;
- font-size: 28rpx;
- color: #333333;
- }
- .manage-btn {
- margin-left: 24rpx;
- }
- }
- .search-bar {
- padding: 12rpx 32rpx;
- background-color: #fff;
- .u-search {
- border-radius: 8rpx;
- background-color: #F7F8FC;
- }
- }
- }
- .main-content {
- flex: 1;
- display: flex;
- overflow: hidden;
- .category-list {
- width: 160rpx;
- height: 100%;
- background-color: #F7F8FC;
- .category-item {
- font-weight: 400;
- font-size: 24rpx;
- color: #333333;
- text-align: left;
- padding: 24rpx 32rpx;
- &.active {
- background-color: #fff;
- font-weight: 500;
- color: #2983EC;
- }
- }
- }
- .goods-scroll-view {
- flex: 1;
- height: 100%;
- background-color: #fff;
- padding: 20rpx 24rpx;
- .category-title {
- font-weight: 500;
- font-size: 26rpx;
- color: #000000;
- }
- .u-checkbox-group {
- width: 100%;
- }
- }
- .goods-card {
- width: 100%;
- padding: 20rpx 0;
- border-bottom: 1rpx solid #f0f0f0;
- display: flex;
- align-items: flex-start;
- position: relative;
- .goods-image {
- width: 120rpx;
- height: 120rpx;
- border-radius: 12rpx;
- flex-shrink: 0;
- background-color: #eee;
- }
- .goods-info {
- flex: 1;
- margin-left: 20rpx;
- .goods-name {
- font-size: 28rpx;
- font-weight: 500;
- color: #333333;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- .goods-sales, .goods-desc {
- font-size: 22rpx;
- font-weight: 400;
- color: #8A91A8;
- margin-top: 4rpx;
- display: -webkit-box;
- -webkit-line-clamp: 1;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- .goods-price-line {
- margin-top: 8rpx;
- .price {
- font-size: 32rpx;
- font-weight: 500;
- color: #FD4912;
- .unit {
- font-size: 22rpx;
- margin-right: 2rpx;
- }
- }
- .original-price {
- font-size: 22rpx;
- font-weight: 400;
- color: #999999;
- text-decoration: line-through;
- margin-left: 10rpx;
- }
- }
- }
- .goods-actions {
- position: absolute;
- right: 0;
- bottom: 20rpx;
- ::v-deep .u-btn {
- height: 46rpx;
- font-weight: 400;
- font-size: 26rpx;
- color: #2D88F4;
- padding: 0 24rpx;
- // background: rgba(45,136,244,0.05);
- border-radius: 61rpx;
- border: 1rpx solid #2D88F4;
- &::after {
- display: none;
- }
- }
- }
- }
- }
- .footer-bar {
- .action-buttons {
- display: flex;
- gap: 20rpx;
- ::v-deep .u-btn {
- height: 69rpx;
- border-radius: 49rpx;
- }
- }
- }
- </style>
|