Files
lucky_shop/components-diy/diy-category.vue

1803 lines
48 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 data-component-name="diy-category" :class="['category-page-wrap', 'category-template-' + value.template]"
:style="{ height: 'calc(100vh - var(--window-top, 0px) - ' + tabBarHeight + ')' }">
<!-- 高度说明: 100vh - H5顶部导航栏(--window-top, 小程序为0) - 底部导航高度(tabBarHeight),
保证底部购物车结算栏始终落在可视区内, 点击加购后无需向下翻即可看到 -->
<!-- 模板4: 顶部搜索框(图片样式) -->
<view class="search-box search-box-template-4" v-if="value.template == 4 && value.search"
@tap.stop="onClickSearch()">
<view class="search-content">
<input type="text" class="uni-input" maxlength="50" :placeholder="$lang('components-diy.diy-search.searchBtn')"
confirm-type="search" @tap.stop="onClickSearch()" disabled="true" />
<text class="iconfont icon-sousuo3"></text>
</view>
<!-- 视图切换: 网格/列表, 模板4也要可切换 -->
<view class="iconfont" :class="{ 'icon-apps': !isList, 'icon-list': isList }"
@tap.stop.prevent="changeListStyle()"></view>
</view>
<!-- #ifdef MP-WEIXIN -->
<!-- <block v-if="value.template == 4">
<view class="search-box" v-if="value.search" @tap.stop="$util.redirectTo('/pages_tool/goods/search')" :style="navbarInnerStyle">
<view class="search-content">
<input type="text" class="uni-input font-size-tag" maxlength="50" :placeholder="$lang('components-diy.diy-search.searchBtn')" confirm-type="search" @tap.stop="onClickSearch()" disabled="true" />
<text class="iconfont icon-sousuo3"></text>
</view>
</view>
<view :style="navbarInnerStyle" v-if="!value.search">商品分类</view>
</block> -->
<block v-if="value.template != 4">
<!-- <view :style="navbarInnerStyle">商品分类</view> -->
<view class="search-box" v-if="value.search" @tap.stop="onClickSearch()"
:style="wxSearchHeight">
<view class="search-content">
<input type="text" class="uni-input" maxlength="50" :placeholder="$lang('components-diy.diy-search.searchBtn')"
confirm-type="search" @tap.stop="onClickSearch()"
disabled="true" />
<text class="iconfont icon-sousuo3"></text>
</view>
<view class="iconfont" :class="{ 'icon-apps': !isList, 'icon-list': isList }"
@tap.stop.prevent="changeListStyle()"></view>
</view>
</block>
<!-- #endif -->
<!-- #ifdef H5 || MP-HARMONY -->
<view class="search-box" v-if="value.search && value.template != 4" @tap.stop="onClickSearch()">
<view class="search-content">
<input type="text" class="uni-input" maxlength="50" :placeholder="$lang('components-diy.diy-search.searchBtn')" confirm-type="search"
@tap.stop="onClickSearch()" disabled="true" />
<text class="iconfont icon-sousuo3"></text>
</view>
<view class="iconfont" :class="{ 'icon-apps': !isList, 'icon-list': isList }"
@tap.stop.prevent="changeListStyle()"></view>
</view>
<!-- #endif -->
<view class="template-four wx" v-if="value.template == 4">
<scroll-view scroll-x="true" class="template-four-wrap" :scroll-with-animation="true"
:scroll-into-view="'category-one-' + oneCategorySelect" enable-flex="true">
<view class="category-item" :id="'category-one-' + index" v-for="(item, index) in templateFourData"
:key="index" :class="{ select: oneCategorySelect == index }" @tap.stop="templateFourOneFn(index)">
<view class="image-warp" :class="[{ 'color-base-border': oneCategorySelect == index }]">
<image :src="$util.img(item.image)" mode="aspectFill" show-menu-by-longpress/>
</view>
<view :class="['text', { 'color-base-bg': oneCategorySelect == index }]">{{ item.category_name }}
</view>
</view>
</scroll-view>
<view class="category-item-all" @tap.stop="$refs.templateFourPopup.open()">
<view class="category-item-all-wrap">
<text class="text">{{ $lang('components-diy.diy-category.expand') }}</text>
<image class="img" :src="$util.img('/public/uniapp/category/unfold.png')" mode="aspectFill" show-menu-by-longpress></image>
</view>
</view>
<uni-popup type="top" ref="templateFourPopup" :top="uniPopupTop">
<view class="template-four-popup">
<scroll-view scroll-y="true" class="template-four-scroll" enable-flex="true">
<view class="item" :class="{ selected: oneCategorySelect == index }"
@tap.stop="templateFourOneFn(index)" v-for="(item, index) in templateFourData" :key="index">
<view class="image-warp" :class="[{ 'color-base-border': oneCategorySelect == index }]">
<image :src="$util.img(item.image)" mode="aspectFill" show-menu-by-longpress></image>
</view>
<view :class="['text', { 'color-base-bg': oneCategorySelect == index }]">{{
item.category_name }}</view>
</view>
</scroll-view>
<view class="pack-up" @tap.stop="$refs.templateFourPopup.close()">
<text>{{ $lang('components-diy.diy-category.clickToCollapse') }}</text>
<text class="iconfont icon-iconangledown-copy"></text>
</view>
</view>
</uni-popup>
</view>
<!-- 全屏骨架占位 (只显示: 左分类树 + 右商品网格 + 底部空隙, 不画 navbar/TabBar,
避免与小程序/H5 顶部 navbar / diy-bottom-nav 错位)。
通过 v-if 拿到 categoryTreeLoading 时按真实布局骨架填充。
search-box / 商品 nav-bar 由外层 category.vue 渲染, 不在本组件里动它 -->
<view class="category-page-skeleton" v-if="categoryTreeLoading">
<view class="skeleton-content">
<view class="skeleton-tree">
<view class="skeleton-tree-item" v-for="n in 8" :key="n"></view>
</view>
<view class="skeleton-grid">
<view class="skeleton-card" v-for="n in 6" :key="n"></view>
</view>
</view>
</view>
<view class="content-box" v-if="categoryTree">
<block v-if="categoryTree.length">
<scroll-view scroll-y="true" class="tree-wrap">
<view class="category-item-wrap">
<!-- 模板4: 左侧显示一级分类(顶部轮播中的分类) -->
<view v-if="value.template == 4">
<view class="category-item" v-for="(item, index) in templateFourData" :key="index" :class="[
{ select: oneCategorySelect == index },
{ 'border-bottom': oneCategorySelect + 1 === index },
{ 'border-top': oneCategorySelect - 1 === index }
]" @tap.stop="templateFourOneFn(index)">
<view class="">{{ item.category_name }}</view>
</view>
</view>
<!-- 其他模板: 左侧显示二级分类 -->
<view v-else>
<view class="category-item" v-for="(item, index) in categoryTree" :key="index" :class="[
{ select: select == index },
{ 'border-bottom': value.template == 4 && select + 1 === index },
{ 'border-top': value.template == 4 && select - 1 === index },
{ 'first-category-item': index == 0 && value.template == 99 }
]" @tap.stop="switchOneCategory(index)">
<view class="">{{ item.category_name }}</view>
</view>
</view>
</view>
</scroll-view>
<view class="right-flex-wrap">
<!-- 模板99 + 二级分类: 全局二级分类按钮(去重, 一直显示) + 商品列表 -->
<scroll-view scroll-y="true" class="content-wrap" v-if="value.template == 99 && template99Category"
:refresher-enabled="true" refresher-default-style="none"
:refresher-triggered="triggered" @refresherrefresh="onRefresh" @refresherrestore="onRestore">
<diy-category-item :category="template99Category" :value="value" ref="categoryItem" :index="0" :select="0"
:oneCategorySelect="oneCategorySelect" :isList="isList"
:currentOneCategory="categoryTree && categoryTree[select] ? categoryTree[select] : null"
:lastSecondCategoryName="lastSecondCategoryName"
@tologin="toLogin" @selectsku="selectSku($event, 0)" @addCart="addCartPoint"
@saveSecondCategory="saveSecondCategoryName"></diy-category-item>
</scroll-view>
<!-- 模板1: 仿京东/淘宝商品分类 - 按 template 2 的 part-load 逻辑, 切换一级分类时只渲染选中分类的子分类网格 -->
<scroll-view scroll-y="true" class="content-wrap" v-else-if="value.template == 1"
:refresher-enabled="true" refresher-default-style="none"
:refresher-triggered="triggered" @refresherrefresh="onRefresh" @refresherrestore="onRestore">
<view class="child-category-wrap" v-for="(item, index) in categoryTree" :key="index"
:id="'category-' + index" :style="{ display: select == index ? 'block' : 'none' }">
<diy-category-item :category="item" :value="value" ref="categoryItem" :index="index"
:select="select" :oneCategorySelect="oneCategorySelect" :isList="isList"
@tologin="toLogin" @selectsku="selectSku($event, index)" @addCart="addCartPoint"
@saveSecondCategory="saveSecondCategoryName"></diy-category-item>
</view>
</scroll-view>
<scroll-view scroll-y="true" class="content-wrap" v-else-if="value.template == 99 || loadType == 'all'"
ref="contentWrap" :scroll-into-view="categoryId" :scroll-with-animation="true"
@scroll="listenScroll" @touchstart="touchStart" :refresher-enabled="true"
refresher-default-style="none" :refresher-triggered="triggered" @refresherrefresh="onRefresh"
@refresherrestore="onRestore">
<view class="child-category" v-for="(item, index) in categoryTree" :key="index"
:id="'category-' + index" :style="{ display: value.template == 99 && select != index ? 'none' : 'block' }">
<diy-category-item :category="item" :value="value" ref="categoryItem" :index="index"
:select="select" :oneCategorySelect="oneCategorySelect" :isList="isList"
:lastSecondCategoryName="value.template == 99 ? lastSecondCategoryName : ''"
:currentOneCategory="categoryTree && categoryTree[select] ? categoryTree[select] : null"
@tologin="toLogin" @selectsku="selectSku($event, index)" @addCart="addCartPoint"
@loadfinish="getHeightArea" @saveSecondCategory="saveSecondCategoryName"></diy-category-item>
</view>
<view class="end-tips" ref="endTips" :style="{ opacity: endTips }">{{ $lang('components-diy.diy-category.reachedBottom') }}</view>
</scroll-view>
<view class="content-wrap"
v-if="(value.template == 2 || value.template == 3 || value.template == 4) && loadType == 'part'">
<view class="child-category-wrap" v-for="(item, index) in categoryTree" :key="index"
:id="'category-' + index" :style="{ display: select == index ? 'block' : 'none' }">
<diy-category-item :category="item" :value="value" ref="categoryItem" :index="index"
:last="index == categoryTree.length - 1 ? true : false" :select="select"
:oneCategorySelect="oneCategorySelect" :isList="isList" @tologin="toLogin"
:currentOneCategory="value.template == 4 && templateFourData[oneCategorySelect] ? templateFourData[oneCategorySelect] : null"
@selectsku="selectSku($event, index)" @addCart="addCartPoint"
@switch="switchOneCategory"></diy-category-item>
</view>
</view>
</view>
</block>
<view class="category-empty" v-else>
<image :src="$util.img('public/uniapp/category/empty.png')" mode="widthFix" show-menu-by-longpress></image>
<view class="tips">{{ $lang('components-diy.diy-category.noCategory') }}</view>
</view>
</view>
<!-- 风格2/模板4: 底部购物车栏
1) 不用 position:fixed: 小程序/H5/鸿蒙下 fixed 受页面避让与父级 transform 影响,
会"跟在内容后面"只有滚到底才看得见。
.category-page-wrap 是固定高度(calc(100vh - tabBarHeight))的纵向 flex 容器,
content-box 为 flex:1 height:0, 把结算栏作为普通子项排在末尾,
无论右侧商品列表怎么滚动, 结算栏始终固定显示在底部
2) cartNumber > 0: 只在购物车里有商品时才出现(点加购即出现, 减到 0 即消失),
商品加购后会立即 dispatch cartCalculate 同步 cartNumber, 不依赖接口回调 -->
<view class="cart-box cart-box-template-4"
v-if="(value.template == 2 || value.template == 4) && showCartSettlement && storeToken && cartNumber > 0 && categoryTree && categoryTree.length">
<view class="left-wrap">
<view class="cart-icon" ref="cartIcon" :animation="cartAnimation"
@tap.stop="$util.redirectTo('/pages_goods/cart')">
<text class="iconfont icon-ziyuan1"></text>
<view class="num" v-if="cartNumber">{{ cartNumber < 99 ? cartNumber : '99+' }}</view>
</view>
<view class="price">
<text class="title">总计:</text>
<text class="unit font-size-tag price-font">¥</text>
<text class="money font-size-toolbar price-font">{{ cartTotalMoney[0] }}</text>
<text class="unit font-size-tag price-font">.{{ cartTotalMoney[1] ? cartTotalMoney[1] : '00' }}</text>
</view>
</view>
<view class="right-wrap"><button type="primary" class="settlement-btn"
@tap.stop="settlement">去结算</button>
</view>
</view>
<view class="cart-point" v-for="(item, index) in carIconList" :key="index"
:style="{ left: item.left + 'px', top: item.top + 'px' }"></view>
<ns-goods-sku-category ref="skuSelect" @refresh="refreshData" @addCart="addCartPoint"></ns-goods-sku-category>
</view>
</template>
<script>
// 获取系统状态栏的高度
let systemInfo = uni.getSystemInfoSync();
let menuButtonInfo = {};
// 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容)
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
menuButtonInfo = uni.getMenuButtonBoundingClientRect();
// #endif
import DiyMinx from './minx.js'
var contentWrapHeight, query, cartPosition;
// 商品分类
export default {
name: 'diy-category',
props: {
value: {
type: Object,
default: () => {
return {};
}
}
},
mixins: [DiyMinx],
data() {
return {
oneCategorySelect: 0,
select: 0,
categoryId: 'category-0',
categoryTree: null,
scrollLock: false,
triggered: true,
heightArea: [],
isSub: false,
carIconList: {},
endTips: 0,
cartAnimation: {},
loadType: '',
templateFourData: [],
isList: false,
lang: uni.getStorageSync("lang"),
lastSecondCategoryName: '',
template99Category: null,
// 修复体验: 进入页面初期显示 "暂未分类" 然后又变成有数据的体验很差
// 用 categoryTreeLoading 区分"还在请求"和"真的没分类", 请求未完成时
// 显示骨架占位而非空态文案
categoryTreeLoading: true,
};
},
created() {
this.getCategoryTree();
this.loadType = this.value.goodsLevel == 1 && this.value.loadType == 'all' ? 'all' : 'part';
// 模板4: 默认单列列表布局(图片样式)
// 模板2/4 的商品卡默认为"左图右文"横卡片, 默认单列保持现有观感, 点击切换图标可切双列
if (this.value.template == 2 || this.value.template == 4) {
this.isList = true;
}
},
mounted() {
query = uni.createSelectorQuery().in(this);
query
.select('.content-wrap')
.boundingClientRect(data => {
if (data) contentWrapHeight = data.height;
})
.exec();
setTimeout(() => {
query
.select('.end-tips')
.boundingClientRect(data => {
if (data && data.top > contentWrapHeight) this.endTips = 1;
})
.exec();
query
.select('.cart-icon')
.boundingClientRect(data => {
if (data) cartPosition = data;
})
.exec();
if (this.value.template == 1) this.getHeightArea(-1);
}, 500);
},
watch: {
// 组件刷新监听
componentRefresh: function (nval) { },
categoryTreeLoading: function (nval, oval) {
if (nval === false && oval === true) {
this.$nextTick(() => {
if (!this.heightArea.length) this.getHeightArea(-1);
});
}
}
},
computed: {
cartTotalMoney() {
let money = parseFloat(this.cartMoney).toFixed(2);
return money.split('.');
},
// 后台新增开关:快捷购买(显示购物车及结算按钮) / 加购按钮
// 后台开启哪个就显示哪个, 关闭哪个就不显示哪个; 未下发新字段时兼容旧字段 quickBuy
showCartSettlement() {
const t = this.getDiyToggle(['showCartSettlement', 'show_cart_settlement']);
if (t !== undefined) return t;
const legacy = this.getDiyToggle(['quickBuy', 'quick_buy']);
return legacy !== undefined ? legacy : false;
},
showAddCart() {
const t = this.getDiyToggle(['showAddCart', 'show_add_cart']);
if (t !== undefined) return t;
const legacy = this.getDiyToggle(['quickBuy', 'quick_buy']);
return legacy !== undefined ? legacy : false;
},
// 转换字符数值为真正的数值
navbarHeight() {
// #ifdef APP-PLUS || H5
return 44;
// #endif
// #ifdef MP
// 小程序特别处理,让导航栏高度 = 胶囊高度 + 两倍胶囊顶部与状态栏底部的距离之差(相当于同时获得了导航栏底部与胶囊底部的距离)
// 此方法有缺陷,暂不用(会导致少了几个px),采用直接固定值的方式
// return menuButtonInfo.height + (menuButtonInfo.top - this.navbarHeight) * 2;//导航高度
// let height = systemInfo.platform == 'ios' ? 44 : 48;
let height = menuButtonInfo.top;
// height += systemInfo.navbarHeight;
return height;
// #endif
},
// 导航栏内部盒子的样式
navbarInnerStyle() {
let style = '';
// 导航栏宽度,如果在小程序下,导航栏宽度为胶囊的左边到屏幕左边的距离
// style += 'height:' + this.navbarHeight + 'px;';
// // 如果是各家小程序,导航栏内部的宽度需要减少右边胶囊的宽度
// #ifdef MP
if (this.value.template == 4 && this.value.search) {
style += 'height:' + menuButtonInfo.height + 'px;';
let rightButtonWidth = menuButtonInfo.width ? menuButtonInfo.width * 2 + 'rpx' : '70rpx';
style += 'padding-right:calc(' + rightButtonWidth + ' + 30rpx);';
style += 'padding-top:' + this.navbarHeight + 'px;';
}
// #endif
if (this.value.template != 4 || (this.value.template == 4 && !this.value.search)) {
style += 'height:' + menuButtonInfo.height * 2 + 'rpx;';
style += 'padding-top:' + this.navbarHeight + 'px;';
style += 'text-align: center;';
style += 'line-height:' + menuButtonInfo.height * 2 + 'rpx;';
style += 'font-size: 14px;';
style += 'padding-bottom: 10rpx;';
}
return style;
},
wxSearchHeight() {
let style = '';
// #ifdef MP
style += 'height: 64rpx;';
// #endif
return style;
},
uniPopupTop() {
let top = 0;
// #ifdef MP
top = this.navbarHeight + menuButtonInfo.height + 'px';
// #endif
// #ifdef H5
top = '100rpx';
// #endif
return top;
}
},
methods: {
/**
* 页面显示
*/
pageShow() {
this.$store.dispatch('getCartNumber');
if (!this.heightArea.length) this.getHeightArea(-1);
},
/**
* 获取高度区间
*
* 鸿蒙 JSVM 在 mounted() 之前触发 onShow,导致 query 还是 undefined
* 加防御性检查,避免上线后第一次进入崩溃
*/
getHeightArea(index) {
// 节流: 500ms 内的重复请求复用上次结果, 避免每次 onShow 都跑 selectAll 触发频闪
const now = Date.now();
if (this._lastHeightAreaTime && now - this._lastHeightAreaTime < 500 && this.heightArea && this.heightArea.length) {
if (index != -1 && index < this.categoryTree.length - 1 && Array.isArray(this.$refs.categoryItem) && this.$refs.categoryItem[index + 1]) this.$refs.categoryItem[index + 1].getGoodsList();
return;
}
this._lastHeightAreaTime = now;
let heightArea = [];
if (!query || typeof query.selectAll !== 'function') {
query = uni.createSelectorQuery && uni.createSelectorQuery().in(this);
}
if (!query || typeof query.selectAll !== 'function') {
// 极端兜底:鸿蒙下若 createSelectorQuery 不存在,跳过计算
this.heightArea = heightArea;
return;
}
query
.selectAll('.content-wrap .child-category')
.boundingClientRect(data => {
if (data && data.length) {
data.forEach((item, index) => {
if (index == 0) heightArea.push([0, item.height]);
else heightArea.push([heightArea[index - 1][1], heightArea[index - 1][1] +
item.height
]);
});
}
})
.exec();
this.heightArea = heightArea;
if (index != -1 && index < this.categoryTree.length - 1 && Array.isArray(this.$refs.categoryItem) && this.$refs.categoryItem[index + 1]) this.$refs.categoryItem[index + 1].getGoodsList();
},
/**
* 获取全部分类
*/
getCategoryTree() {
this.categoryTreeLoading = true;
this.$api.sendRequest({
url: '/api/goodscategory/tree',
data: {
level: 3
},
success: res => {
if (res.code == 0) {
this.categoryTree = res.data;
if (this.value.template == 4) {
this.templateFourData = JSON.parse(JSON.stringify(this.categoryTree));
this.categoryTree = this.templateFourData[0].child_list;
}
//切换英文标题
if (this.lang == 'en-us') {
this.categoryTree.map(item => {
item?.child_list?.map(items => {
items.category_name = items.en_category_name;
return items;
})
item.category_name = item.en_category_name;
return item;
});
}
// 模板99 + 二级分类: 全局查询所有有商品的二级分类(去重), 按钮一直显示
if (this.value.template == 99) {
this.loadGlobalSecondCategoryBtns();
}
}
},
fail: () => {
// 失败也保证 loading 关闭, 让骨架替换为"暂无分类"空态
this.categoryTree = this.categoryTree || [];
},
complete: () => {
// 无论成功失败都关闭 loading 骨架
this.categoryTreeLoading = false;
}
});
},
/**
* 全局查询所有有商品的二级分类, 按名称去重, 汇总成虚拟分类供 template99 渲染按钮
* 只要任何商品属于该二级分类, 按钮就显示; 切换一级分类时按钮不变(一直显示)
* 新增的二级分类只要有商品也会自动包含
*/
loadGlobalSecondCategoryBtns() {
if (!this.categoryTree || !this.categoryTree.length) {
this.template99Category = { category_name: '', child_list: [] };
return;
}
// 收集所有二级分类(保留原始顺序, 不去重, 查询所有同名后再去重)
const allSecond = [];
this.categoryTree.forEach(one => {
if (one.child_list && one.child_list.length) {
one.child_list.forEach(two => {
allSecond.push(Object.assign({ _origIndex: allSecond.length }, two));
});
}
});
if (!allSecond.length) {
this.template99Category = { category_name: '', child_list: [] };
return;
}
let pending = allSecond.length;
const results = [];
const finishOne = () => {
pending--;
if (pending <= 0) {
results.sort((a, b) => a._origIndex - b._origIndex);
// 按名称去重, 同名只保留第一个有商品的(只要任一同名有商品就显示)
const deduped = [];
const seenNames = {};
results.forEach(item => {
if (!seenNames[item.category_name]) {
seenNames[item.category_name] = true;
deduped.push(item);
}
});
this.template99Category = {
category_name: '',
child_list: deduped
};
}
};
allSecond.forEach(two => {
this.$api.sendRequest({
url: '/api/goodssku/pageByCategory',
data: {
category_id: two.category_id,
page: 1,
page_size: 1
},
success: res => {
if (res && res.code == 0 && res.data && res.data.list && res.data.list.length > 0) {
results.push(two);
}
},
complete: () => {
finishOne();
}
});
});
},
/**
* 切换一级分类
* @param {Object} index
*/
switchOneCategory(index) {
if (index >= this.categoryTree.length) return;
this.select = index;
this.categoryId = 'category-' + index;
this.scrollLock = true;
},
saveSecondCategoryName(name) {
this.lastSecondCategoryName = name;
},
touchStart() {
this.scrollLock = false;
},
/**
* 监听滚动
* @param {Object} event
*/
listenScroll(event) {
if (this.scrollLock) return;
let scrollTop = event.detail.scrollTop;
if (this.heightArea.length) {
for (let i = 0; i < this.heightArea.length; i++) {
if (scrollTop >= this.heightArea[i][0] && scrollTop <= this.heightArea[i][1]) {
this.select = i;
break;
}
}
if (this.value.template != 1 && this.value.loadType == 'all' && this.heightArea[this.select][1] -
scrollTop - contentWrapHeight < 300) {
if (Array.isArray(this.$refs.categoryItem) && this.$refs.categoryItem[this.select]) this.$refs.categoryItem[this.select].getGoodsList();
}
}
},
onRefresh() {
this.triggered = false;
},
onRestore() {
this.triggered = 'restore'; // 需要重置
},
toLogin() {
this.$emit('tologin');
},
/**
* sku选择
* @param {Object} data
* @param {Object} index
*/
selectSku(data, index) {
this.$api.sendRequest({
url: '/api/goodssku/getInfoForCategory',
data: {
sku_id: data.sku_id
},
success: res => {
if (res.code >= 0) {
let item = res.data;
item.unit = item.unit || this.$lang('components-diy.diy-category.pieceUnit');
if (item.sku_images) item.sku_images = item.sku_images.split(',');
else item.sku_images = [];
// 多规格时合并主图
if (item.goods_spec_format && item.goods_image) {
item.goods_image = item.goods_image.split(',');
item.sku_images = item.goods_image.concat(item.sku_images);
}
// 当前商品SKU规格
if (item.sku_spec_format) item.sku_spec_format = JSON.parse(item.sku_spec_format);
// 商品SKU格式
if (item.goods_spec_format) item.goods_spec_format = JSON.parse(item.goods_spec_format);
// 限时折扣
if (item.promotion_type == 1) {
item.discountTimeMachine = this.$util.countDown(item.end_time - res.timestamp);
}
if (item.promotion_type == 1 && item.discountTimeMachine) {
if (item.member_price > 0 && Number(item.member_price) <= Number(item.discount_price)) {
item.show_price = item.member_price;
} else {
item.show_price = item.discount_price;
}
} else if (item.member_price > 0) {
item.show_price = item.member_price;
} else {
item.show_price = item.price;
}
this.$refs.skuSelect.show(item);
}
}
});
},
settlement() {
if (!this.cartIds.length || this.isSub) return;
this.isSub = true;
uni.removeStorageSync('delivery');
uni.setStorage({
key: 'orderCreateData',
data: {
cart_ids: this.cartIds.toString()
},
success: () => {
this.$util.redirectTo('/pages_order/payment');
this.isSub = false;
}
});
},
/**
* 添加点
* @param {Object} left
* @param {Object} top
*/
addCartPoint(left, top) {
if (this.value.template != 2 && this.value.template != 4) return;
if (!this.showCartSettlement) return;
let key = new Date().getTime();
this.$set(this.carIconList, key, {
left: left,
top: top,
index: 0,
bezierPos: this.$util.bezier([{
x: left,
y: top
}, {
x: left - 200,
y: left - 120
},
{
x: cartPosition.left + 10,
y: cartPosition.top
}
], 6).bezier_points,
timer: null
});
this.startAnimation(key);
},
/**
* 执行动画
* @param {Object} key
*/
startAnimation(key) {
let bezierPos = this.carIconList[key].bezierPos,
index = this.carIconList[key].index;
this.carIconList[key].timer = setInterval(() => {
if (index < 6) {
this.carIconList[key].left = bezierPos[index].x;
this.carIconList[key].top = bezierPos[index].y;
index++;
} else {
clearInterval(this.carIconList[key].timer);
delete this.carIconList[key];
this.$forceUpdate();
// 购物车图标
setTimeout(() => {
this.$store.commit('setCartChange');
}, 100);
let animation = uni.createAnimation({
duration: 200,
timingFunction: 'ease'
});
animation.scale(1.2).step();
this.cartAnimation = animation.export();
setTimeout(() => {
animation.scale(1).step();
this.cartAnimation = animation.export();
}, 300);
}
}, 50);
},
// 风格四头部切换
templateFourOneFn(index) {
this.categoryTree = this.templateFourData[index].child_list || [];
this.oneCategorySelect = index;
this.select = 0;
},
// 操作多规格商品弹框后,刷新商品数据
refreshData() {
if (Array.isArray(this.$refs.categoryItem) && this.$refs.categoryItem[this.select]) this.$refs.categoryItem[this.select].loadGoodsCartNum(true);
},
changeListStyle() {
this.isList = !this.isList;
},
onClickSearch() {
this.$util.redirectTo('/pages_tool/goods/search');
}
}
};
</script>
<style lang="scss" scoped>
.category-page-wrap {
width: 100vw;
// height: calc(100vh - var(--tab-bar-height, 0));
display: flex;
flex-direction: column;
background-color: #fff;
}
.content-box {
flex: 1;
height: 0;
display: flex;
align-items: flex-start;
.tree-wrap {
width: 170rpx;
height: 100%;
background-color: #f5f5f5;
}
.right-flex-wrap {
flex: 1;
width: 0;
height: 100%;
background: #f4f6fa;
display: flex;
flex-direction: column;
transform: translateX(0px);
.content-wrap {
display: flex;
flex: 1;
height: 0;
width: 100%;
}
.child-category-wrap {
width: 100%;
height: 100%;
}
}
}
.tree-wrap .category-item-wrap {
height: auto;
background-color: #fff;
}
.tree-wrap .category-item {
line-height: 1.5;
padding: 26rpx 28rpx;
box-sizing: border-box;
position: relative;
background-color: var(--diy-category-tree-normal-bg-color, #f5f5f5);
view {
color: var(--diy-category-tree-normal-text-color, #222222);
width: 100%;
white-space: break-word;
overflow: hidden;
text-align: center;
font-size: var(--diy-category-tree-normal-size, 20rpx);
}
&.border-top {
border-bottom-right-radius: 12rpx;
}
&.border-bottom {
border-top-right-radius: 12rpx;
}
&.select {
background: var(--diy-category-tree-select-bg-color, #fff);
view {
color: var(--diy-category-tree-select-text-color, #333);
font-weight: bold;
font-size: var(--diy-category-tree-select-size, 28rpx);
}
&::before {
content: ' ';
width: 8rpx;
height: 34rpx;
background: var(--base-color);
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
.search-box {
position: relative;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
background: #fff;
.search-content {
position: relative;
/* #ifndef MP-WEIXIN */
height: 64rpx;
/* #endif */
/* #ifdef MP-WEIXIN */
height: 100%;
/* #endif */
border-radius: 40rpx;
flex: 1;
background-color: #f5f5f5;
input {
box-sizing: border-box;
display: block;
height: 100%;
width: 100%;
padding: 0 20rpx 0 40rpx;
background: #f5f5f5;
color: #333;
border-radius: 40rpx;
}
.iconfont {
position: absolute;
top: 50%;
right: 10rpx;
transform: translateY(-50%);
font-size: $font-size-toolbar;
z-index: 10;
color: #89899a;
width: 80rpx;
text-align: center;
}
}
// 切换列表图标
.iconfont {
font-size: 36rpx;
margin: 0 0rpx 0 20rpx;
}
}
.cart-box {
height: 100rpx;
width: 100%;
background: #fff;
border-top: 1px solid #f5f5f5;
box-sizing: border-box;
padding: 0 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
.left-wrap {
display: flex;
align-items: center;
}
.cart-icon {
width: 70rpx;
height: 70rpx;
position: relative;
.iconfont {
color: var(--btn-text-color);
width: inherit;
height: inherit;
background-color: $base-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.num {
position: absolute;
top: 0;
right: 0;
transform: translate(60%, 0);
display: inline-block;
box-sizing: border-box;
color: #fff;
line-height: 1.2;
text-align: center;
font-size: 24rpx;
padding: 0 6rpx;
min-width: 30rpx;
border-radius: 16rpx;
background-color: var(--price-color);
border: 2rpx solid #fff;
}
}
.price {
margin-left: 30rpx;
.title {
color: #333;
}
.money {
font-weight: bold;
color: var(--price-color);
}
.unit {
font-weight: bold;
color: var(--price-color);
}
}
.settlement-btn {
margin: 0 0 0 20rpx;
width: 200rpx;
font-weight: bold;
border-radius: 50rpx;
height: 70rpx;
line-height: 70rpx;
}
}
.cart-point {
width: 26rpx;
height: 26rpx;
position: fixed;
z-index: 1000;
background: #f00;
border-radius: 50%;
transition: all 0.05s;
}
.category-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
image {
width: 380rpx;
}
.tips {
font-size: 26rpx;
font-weight: 500;
color: #999;
margin-top: 50rpx;
}
}
.end-tips {
text-align: center;
color: #999;
font-size: 24rpx;
padding: 20rpx 0;
opacity: 0;
}
// 全屏骨架占位 — 不画 navbar/TabBar (由外层 category.vue 渲染), 避免错位
// 兼容鸿蒙元服务 (JSVM 对 gap 支持不一): 用 margin 兜底替代 gap
.category-page-skeleton {
flex: 1;
height: 0; // 与 .content-box 一致, 让 flex 子项撑开父容器剩余高度
display: flex;
background-color: #f4f6fa;
.skeleton-content {
flex: 1;
display: flex;
min-height: 0;
.skeleton-tree {
width: 170rpx;
background-color: #f5f5f5;
padding: 20rpx 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
// #ifndef MP-HARMONY
gap: 18rpx;
// #endif
flex-shrink: 0;
.skeleton-tree-item {
height: 40rpx;
// #ifndef MP-HARMONY
margin: 0 28rpx;
// #endif
// #ifdef MP-HARMONY
margin: 9rpx 28rpx; // 18rpx 间距 上下各 9rpx, 替代 gap:18rpx
// #endif
border-radius: 12rpx;
background: linear-gradient(90deg, #ececec 25%, #f5f5f5 37%, #ececec 63%);
background-size: 400% 100%;
animation: category-skeleton-shimmer 1.4s ease infinite;
}
}
.skeleton-grid {
flex: 1;
padding: 24rpx;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
// #ifndef MP-HARMONY
gap: 16rpx;
// #endif
background-color: #f4f6fa;
min-width: 0;
.skeleton-card {
width: calc((100% - 32rpx) / 3);
height: 220rpx;
// #ifdef MP-HARMONY
margin: 8rpx;
// #endif
border-radius: 16rpx;
background: linear-gradient(90deg, #ececec 25%, #f5f5f5 37%, #ececec 63%);
background-size: 400% 100%;
animation: category-skeleton-shimmer 1.4s ease infinite;
}
}
}
}
@keyframes category-skeleton-shimmer {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
// 风格四
.category-template-4 {
// 模板4 顶部搜索框(图片样式)
.search-box-template-4 {
padding: 20rpx 30rpx 16rpx;
background: #fff;
.search-content {
height: 64rpx;
background-color: #f5f5f5;
border-radius: 40rpx;
input {
height: 64rpx;
line-height: 64rpx;
padding: 0 30rpx 0 30rpx;
background: #f5f5f5;
border-radius: 40rpx;
font-size: 26rpx;
color: #333;
}
.iconfont {
font-size: 32rpx;
color: #89899a;
width: 70rpx;
text-align: center;
right: 10rpx;
}
}
}
// 模板4 底部购物车栏(图片样式)
.cart-box-template-4 {
height: 110rpx;
width: 100%;
flex-shrink: 0;
background: #fff;
border-top: 1rpx solid #f0f0f0;
position: relative;
z-index: 10;
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.04);
.left-wrap {
display: flex;
align-items: center;
}
.cart-icon {
width: 90rpx;
height: 90rpx;
position: relative;
margin-top: -10rpx;
.iconfont {
color: var(--btn-text-color, #fff);
width: 80rpx;
height: 80rpx;
background-color: $base-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.35);
}
.num {
position: absolute;
top: 0;
right: 0;
transform: translate(20%, 0);
display: inline-block;
box-sizing: border-box;
color: #fff;
line-height: 1.2;
text-align: center;
font-size: 24rpx;
padding: 0 8rpx;
min-width: 32rpx;
border-radius: 18rpx;
background-color: var(--price-color, #FF6B35);
border: 2rpx solid #fff;
}
}
.price {
margin-left: 16rpx;
display: flex;
align-items: baseline;
line-height: 1;
.title {
color: #333;
font-size: 28rpx;
}
.unit,
.money {
color: var(--price-color, #FF6B35);
font-weight: bold;
}
.unit {
font-size: 24rpx;
}
.money {
font-size: 36rpx;
}
}
.right-wrap {
display: flex;
align-items: center;
}
.settlement-btn {
margin: 0;
width: 220rpx;
font-weight: bold;
border-radius: 50rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
background: $base-color;
color: var(--btn-text-color, #fff);
&::after {
border: none;
}
}
}
.search-box {
.search-content input {
background-color: #f1f1f1;
}
}
::v-deep .template-four {
position: relative;
z-index: 1;
.template-four-wrap {
position: relative;
z-index: 1;
padding-left: 20rpx;
padding-right: 80rpx;
padding-bottom: 10rpx;
display: flex;
height: 172rpx;
align-items: baseline;
box-sizing: border-box;
box-shadow: 0 4rpx 4rpx rgba(123, 123, 123, 0.1);
}
.template-four-popup {
display: flex;
flex-direction: column;
overflow: hidden;
.title {
line-height: 1;
margin-bottom: 20rpx;
font-weight: bold;
}
.template-four-scroll {
display: flex;
flex-wrap: wrap;
align-items: baseline;
align-content: baseline;
padding: 20rpx;
white-space: nowrap;
height: 380rpx;
box-sizing: border-box;
.uni-scroll-view-content {
flex-wrap: wrap;
align-items: baseline;
align-content: baseline;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
padding: 4rpx 0;
color: #666;
margin-right: 16rpx;
border-radius: 40rpx;
margin-bottom: 10rpx;
width: calc((100% - 64rpx) / 5);
&:nth-child(5n + 5) {
margin-right: 0;
}
.image-warp {
margin-bottom: 6rpx;
padding: 4rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 42rpx;
border: 4rpx solid transparent;
}
image {
width: 84rpx;
height: 84rpx;
border-radius: 32rpx;
}
.text {
padding: 2rpx 16rpx;
border-radius: 40rpx;
font-size: $font-size-tag;
}
&.selected {
.text {
background-color: $base-color;
color: var(--btn-text-color);
}
}
}
}
.pack-up {
font-size: $font-size-tag;
color: #888888;
height: 74rpx;
display: flex;
align-items: center;
justify-content: center;
border-top: 2rpx solid #f2f2f2;
.iconfont {
font-size: 40rpx;
margin-left: -4rpx;
}
}
}
.category-item-all {
position: absolute;
bottom: 0;
z-index: 1;
right: 0;
top: 0;
width: 72rpx;
line-height: 1;
background-color: #fff;
.category-item-all-wrap {
position: absolute;
bottom: 0;
right: 0;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 2;
}
.text {
writing-mode: tb-rl;
margin-bottom: 6rpx;
letter-spacing: 4rpx;
font-size: $font-size-tag;
font-weight: bold;
}
.img {
width: 20rpx;
height: 20rpx;
}
&::after {
content: '';
// box-shadow: -4rpx 10rpx 20rpx rgba(0, 0, 0, 0.1);
position: absolute;
left: 0;
width: 10rpx;
// height: 100rpx;
top: 20%;
bottom: 20%;
// transform: translateY(-50%);
// background-color: #F0F5FF;
}
}
.uni-scroll-view-content {
display: flex;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 130rpx;
flex-shrink: 0;
margin-right: 20rpx;
padding: 4rpx 0;
&:last-of-type {
margin-right: 0;
}
.image-warp {
margin-bottom: 6rpx;
padding: 4rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 42rpx;
border: 4rpx solid transparent;
}
image {
width: 84rpx;
height: 84rpx;
border-radius: 32rpx;
}
.text {
font-size: $font-size-tag;
}
}
.select {
.text {
padding: 8rpx 16rpx;
border-radius: 40rpx;
color: #fff;
font-size: $font-size-tag;
line-height: 1;
}
}
}
// 风格2 复用模板4 的底部购物车栏样式
.category-template-2 {
// 模板4 底部购物车栏(图片样式)
.cart-box-template-4 {
height: 110rpx;
width: 100%;
flex-shrink: 0;
background: #fff;
border-top: 1rpx solid #f0f0f0;
position: relative;
z-index: 10;
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.04);
.left-wrap {
display: flex;
align-items: center;
}
.cart-icon {
width: 90rpx;
height: 90rpx;
position: relative;
margin-top: -10rpx;
.iconfont {
color: var(--btn-text-color, #fff);
width: 80rpx;
height: 80rpx;
background-color: $base-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.35);
}
.num {
position: absolute;
top: 0;
right: 0;
transform: translate(20%, 0);
display: inline-block;
box-sizing: border-box;
color: #fff;
line-height: 1.2;
text-align: center;
font-size: 24rpx;
padding: 0 8rpx;
min-width: 32rpx;
border-radius: 18rpx;
background-color: var(--price-color, #FF6B35);
border: 2rpx solid #fff;
}
}
.price {
margin-left: 16rpx;
display: flex;
align-items: baseline;
line-height: 1;
.title {
color: #333;
font-size: 28rpx;
}
.unit,
.money {
color: var(--price-color, #FF6B35);
font-weight: bold;
}
.unit {
font-size: 24rpx;
}
.money {
font-size: 36rpx;
}
}
.right-wrap {
display: flex;
align-items: center;
}
.settlement-btn {
margin: 0;
width: 220rpx;
font-weight: bold;
border-radius: 50rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
background: $base-color;
color: var(--btn-text-color, #fff);
&::after {
border: none;
}
}
}
}
.content-wrap .categoty-goods-wrap .goods-list {
margin-top: 30rpx;
}
.tree-wrap .category-item.select::before {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
}
// ===================== 模板1 仿京东/淘宝商品分类样式 =====================
.category-template-1 {
.search-box {
padding: 20rpx 24rpx;
background: #fff;
.search-content {
background-color: #f5f5f5;
border-radius: 40rpx;
input {
background: #f5f5f5;
border-radius: 40rpx;
padding: 0 30rpx;
font-size: 26rpx;
color: #333;
}
.iconfont {
right: 16rpx;
color: #89899a;
font-size: 32rpx;
}
}
}
.content-box {
background: #fff;
.tree-wrap {
width: 170rpx;
background-color: #fff;
}
.right-flex-wrap {
background: #fff;
.content-wrap {
background: #fff;
}
}
}
// 穿透到子组件 diy-category-item: 调整 banner 横向内边距 + 分类网格容器为透明
::v-deep .category-adv {
padding: 0 20rpx;
margin-top: 0;
}
::v-deep .category-list {
margin: 0 20rpx;
border-radius: 0;
background: transparent;
padding: 0;
}
::v-deep .category-title-with-lines {
padding: 30rpx 0 20rpx;
}
// 穿透修复: 图片不需要灰色背景框,与目标图保持一致
::v-deep .img-box {
background: transparent !important;
border-radius: 0 !important;
width: 70% !important;
padding-bottom: 70% !important;
margin-bottom: 16rpx !important;
}
::v-deep .img-box image {
width: 100% !important;
height: 100% !important;
left: 0 !important;
top: 0 !important;
transform: none !important;
}
// 左侧分类栏样式
.tree-wrap .category-item {
padding: 28rpx 16rpx;
background-color: var(--diy-category-tree-normal-bg-color, #f5f5f5);
border-radius: 0;
view {
color: var(--diy-category-tree-normal-text-color, #222222);
font-size: 26rpx;
line-height: 1.3;
}
&.border-top,
&.border-bottom {
border-radius: 0;
}
&.select {
background: var(--diy-category-tree-select-bg-color, #fff);
view {
color: var(--diy-category-tree-select-text-color, #333);
font-weight: bold;
font-size: 28rpx;
}
&::before {
content: ' ';
width: 6rpx;
height: 32rpx;
background: var(--base-color, #FF6B35);
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
border-radius: 0 6rpx 6rpx 0;
}
}
}
}
// ===================== 模板99 商品分类样式 =====================
.category-template-99 {
.search-box {
padding: 20rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
.search-content {
height: 64rpx;
background-color: #f5f5f5;
border-radius: 40rpx;
input {
height: 64rpx;
line-height: 64rpx;
padding: 0 80rpx 0 30rpx;
background: #f5f5f5;
border-radius: 40rpx;
font-size: 26rpx;
color: #333;
}
.iconfont {
font-size: 32rpx;
color: #89899a;
width: 70rpx;
text-align: center;
}
}
> .iconfont {
font-size: 40rpx;
color: #666;
margin-left: 20rpx;
}
}
.content-box {
.tree-wrap {
width: 200rpx;
background-color: #f5f5f5;
}
.right-flex-wrap {
flex: 1;
width: 0;
background: #f5f5f5;
}
}
.tree-wrap .category-item {
padding: 30rpx 16rpx;
background-color: var(--diy-category-tree-normal-bg-color, #f5f5f5);
white-space: normal;
word-break: break-all;
view {
color: var(--diy-category-tree-normal-text-color, #222222);
font-size: 26rpx;
line-height: 1.5;
}
&.select {
background: var(--diy-category-tree-select-bg-color, #fff);
border-bottom: 1rpx solid #f0f0f0;
view {
color: var(--diy-category-tree-select-text-color, #333);
font-weight: bold;
font-size: 28rpx;
}
&::before {
content: ' ';
width: 6rpx;
height: 36rpx;
background: #FF4646;
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
border-radius: 0 6rpx 6rpx 0;
}
}
}
// 模板99: 第一个分类项固定高度,与右侧二级分类按钮区域对齐
.tree-wrap .category-item.first-category-item {
height: 100rpx;
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
view {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.content-wrap {
background: #f5f5f5;
padding: 0;
// #ifdef H5
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
background: transparent;
}
// #endif
}
.child-category {
width: 100%;
box-sizing: border-box;
}
}
</style>