Initial commit

This commit is contained in:
sjk
2025-11-17 13:32:54 +08:00
commit e788eab6eb
1659 changed files with 171560 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
# Sidebar 侧边导航
### 引入
全局引入在miniprogram根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。
```json
// app.json 或 index.json
"usingComponents": {
"wr-sidebar": "path/to/components/goods-category/wr-sidebar/index",
"wr-sidebar-item": "path/to/component/goods-category/wr-sidebar/wr-sidebar-item/index"
}
```
## 代码演示
### 基础用法
通过在`wr-sidebar`上设置`activeKey`属性来控制选中项
```html
<wr-sidebar active-key="{{ activeKey }}" bind:change="onChange">
<wr-sidebar-item title="标签名称" />
<wr-sidebar-item title="标签名称" />
<wr-sidebar-item title="标签名称" />
</wr-sidebar>
```
``` javascript
Page({
data: {
activeKey: 0
},
onChange(event) {
wx.showToast({
icon: 'none',
title: `切换至第${event.detail}项`
});
}
});
```
### 提示气泡(暂未实现)
设置`dot`属性后,会在右上角展示一个小红点。设置`info`属性后,会在右上角展示相应的徽标
```html
<wr-sidebar active-key="{{ activeKey }}">
<wr-sidebar-item title="标签名称" dot />
<wr-sidebar-item title="标签名称" info="5" />
<wr-sidebar-item title="标签名称" info="99+" />
</wr-sidebar>
```
## API
### Sidebar Props
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|-----------|-----------|-----------|-------------|-------------|
| activeKey | 选中项的索引 | *string \| number* | `0` | - |
### Sidebar Event
| 事件名 | 说明 | 参数 |
|------|------|------|
| change | 切换选项时触发 | 当前选中选项的索引 |
### Sidebar 外部样式类
| 类名 | 说明 |
|-----------|-----------|
| custom-class | 根节点样式类 |
### SidebarItem Props
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|-----------|-----------|-----------|-------------|-------------|
| title | 内容 | *string* | `''` | - |
| disabled | 是否禁用 | | *boolean* | `false` | - |
| dot | 是否显示右上角小红点 | *boolean* | `false` | - |
| info | 提示消息 | *string \| number* | `''` | - |
### SidebarItem Event
| 事件名 | 说明 | 参数 |
|------|------|------|
| click | 点击徽章时触发 | 当前徽章的索引 |
### SidebarItem 外部样式类
| 类名 | 说明 |
|-----------|-----------|
| custom-class | 根节点样式类 |

View File

@@ -0,0 +1,51 @@
Component({
relations: {
'../../c-sidebar/index': {
type: 'ancestor',
linked(target) {
this.parent = target;
},
},
},
externalClasses: ['custom-class'],
properties: {
title: String,
disabled: Boolean,
},
data: {
topRightRadius: false,
bottomRightRadius: false,
},
methods: {
setActive(selected) {
return this.setData({ selected });
},
onClick() {
const { parent } = this;
if (!parent || this.properties.disabled) {
return;
}
const index = parent.children.indexOf(this);
parent.setActive(index).then(() => {
this.triggerEvent('click', index);
parent.triggerEvent('change', { index });
});
},
setTopRightRadius(val) {
return this.setData({
topRightRadius: val,
});
},
setBottomRightRadius(val) {
return this.setData({
bottomRightRadius: val,
});
},
},
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,10 @@
<view class="c-sidebar-item-container">
<view
class="c-sidebar-item {{ selected ? 'active' : '' }} {{ disabled ? 'disabled' : '' }} {{topRightRadius ? 'top-right-radius' : ''}} {{bottomRightRadius ? 'bottom-right-radius' : ''}} custom-class"
hover-class="c-sidebar-item--hover"
hover-stay-time="70"
bind:tap="onClick"
>
<view class="c-sidebar-item__text text-overflow"> {{ title }} </view>
</view>
</view>

View File

@@ -0,0 +1,60 @@
.c-sidebar-item {
display: flex;
justify-content: center;
text-align: center;
background-color: #f5f5f5;
color: #222427;
padding: 20rpx 0;
font-size: 26rpx;
}
.c-sidebar-item.active {
position: relative;
background: white;
}
.c-sidebar-item.active::before {
content: '';
position: absolute;
width: 6rpx;
height: 48rpx;
background-color: #fa4126;
left: 0;
top: 50%;
transform: translate(0, -50%);
border-radius: 64rpx;
}
.c-sidebar-item__text {
width: 136rpx;
height: 36rpx;
padding: 8rpx 0;
line-height: 36rpx;
text-align: center;
font-size: 28rpx;
color: #666666;
}
.c-sidebar-item.active .c-sidebar-item__text {
background-color: white;
border-radius: 36rpx;
color: #fa4126;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-right-radius {
border-top-right-radius: 16rpx;
}
.bottom-right-radius {
border-bottom-right-radius: 16rpx;
}
.c-sidebar-item-container {
background-color: white;
}

View File

@@ -0,0 +1,108 @@
Component({
relations: {
'./c-sidebar-item/index': {
type: 'descendant',
linked(target) {
this.children.push(target);
this.setActive(this.properties.activeKey, true);
},
unlinked(target) {
this.children = this.children.filter((item) => item !== target);
this.setActive(this.properties.activeKey, true);
},
},
},
externalClasses: ['custom-class'],
properties: {
activeKey: {
type: Number,
value: 0,
},
},
observers: {
activeKey(newVal) {
this.setActive(newVal);
},
},
created() {
this.children = [];
this.currentActive = -1;
this.topRightRadiusItemIndexs = [];
this.bottomRightRadiusItemIndexs = [];
},
methods: {
setActive(activeKey, isChildrenChange) {
const {
children,
currentActive,
topRightRadiusItemIndexs: preTopRightRadiusItemIndexs,
bottomRightRadiusItemIndexs: preBottomRightRadiusItemIndexs,
} = this;
if (!children.length) {
return Promise.resolve();
}
if (activeKey === currentActive && !isChildrenChange) {
return Promise.resolve();
}
this.currentActive = activeKey;
this.topRightRadiusItemIndexs = this.getTopRightRadiusItemIndexs(activeKey, children);
this.bottomRightRadiusItemIndexs = this.getBottomRightRadiusItemIndexs(activeKey, children);
const stack = []; // 任务列表存放调用子组件的setActive后返回的一堆promise
// 将旧的选中项改为false
if (currentActive !== activeKey && currentActive >= 0 && currentActive < children.length && children[currentActive]) {
stack.push(children[currentActive].setActive(false));
}
// 将新的选中项改为true
if (activeKey >= 0 && activeKey < children.length && children[activeKey]) {
stack.push(children[activeKey].setActive(true));
}
preTopRightRadiusItemIndexs.forEach((item) => {
if (item >= 0 && item < children.length && children[item]) {
stack.push(children[item].setTopRightRadius(false));
}
});
preBottomRightRadiusItemIndexs.forEach((item) => {
if (item >= 0 && item < children.length && children[item]) {
stack.push(children[item].setBottomRightRadius(false));
}
});
this.topRightRadiusItemIndexs.forEach((item) => {
if (item >= 0 && item < children.length && children[item]) {
stack.push(children[item].setTopRightRadius(true));
}
});
this.bottomRightRadiusItemIndexs.forEach((item) => {
if (item >= 0 && item < children.length && children[item]) {
stack.push(children[item].setBottomRightRadius(true));
}
});
return Promise.all(stack);
},
getTopRightRadiusItemIndexs(activeKey, children) {
const { length } = children;
if (activeKey !== 0 && activeKey < length - 1) return [0, activeKey + 1];
if (activeKey !== 0) return [0];
if (activeKey < length - 1) return [activeKey + 1];
return [];
},
getBottomRightRadiusItemIndexs(activeKey) {
if (activeKey !== 0) return [activeKey - 1];
return [];
},
},
});

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,3 @@
<scroll-view class="c-sidebar custom-class" scroll-y>
<slot />
</scroll-view>

View File

@@ -0,0 +1,9 @@
.c-sidebar {
width: 176rpx;
height: 100%;
}
.c-sidebar::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}

View File

@@ -0,0 +1,37 @@
Component({
externalClasses: ['custom-class'],
properties: {
tabList: Array,
},
data: {
unfolded: false,
boardMaxHeight: null,
},
attached() {
wx.createSelectorQuery()
.in(this)
.select('.c-tabbar-more')
.boundingClientRect((rect) => {
this.setData({ boardMaxHeight: rect.height });
})
.exec();
},
methods: {
changeFold() {
this.setData({
unfolded: !this.data.unfolded,
});
const { unfolded } = this.data;
this.triggerEvent('change', { unfolded });
},
onSelect(event) {
const activeKey = event.currentTarget.dataset.index;
this.triggerEvent('select', activeKey);
this.changeFold();
},
},
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,25 @@
<view class="c-tabbar-more">
<view class="c-tabbar-more__btn" bind:tap="changeFold">
<view class="wr {{unfolded ? 'wr-arrow-up':'wr-arrow-down'}}"></view>
</view>
<view class="t-tabbar-more__boardwrapper" wx:if="{{ unfolded }}">
<view class="t-tabbar-more__mask"></view>
<scroll-view
class="c-tabbar-more__board"
style="{{ boardMaxHeight ? 'height:' + boardMaxHeight + 'px;' : '' }}"
scroll-y
>
<view class="c-tabbar-more__boardinner">
<view
class="c-tabbar-more__item text-overflow"
wx:for="{{ tabList }}"
wx:key="index"
data-index="{{ index }}"
bind:tap="onSelect"
>
{{ item.name }}
</view>
</view>
</scroll-view>
</view>
</view>

View File

@@ -0,0 +1,63 @@
.c-tabbar-more {
width: 100%;
height: calc(100% - var(--tabbar-height, 100rpx));
position: absolute;
top: var(--tabbar-height, 100rpx);
}
.c-tabbar-more__btn {
position: absolute;
top: calc(0rpx - var(--tabbar-height, 100rpx));
right: 0;
width: 80rpx;
height: var(--tabbar-height, 100rpx);
line-height: var(--tabbar-height, 100rpx);
background-color: var(--tabbar-background-color, white);
box-shadow: -20rpx 0 20rpx -10rpx var(--tabbar-background-color, white);
text-align: center;
}
.c-tabbar-more__btn .market {
font-size: 20rpx;
}
.t-tabbar-more__boardwrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.t-tabbar-more__mask {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.c-tabbar-more__board {
position: absolute;
top: 0;
left: 0;
width: 100%;
max-height: 100%;
}
.c-tabbar-more__boardinner {
padding: 20rpx 0 20rpx 20rpx;
background-color: var(--tabbar-background-color, white);
display: flex;
flex-flow: row wrap;
}
.c-tabbar-more__item {
margin: 0 20rpx 20rpx 0;
flex: 0 0 calc((100% - 60rpx) / 3);
box-sizing: border-box;
padding: 0 10rpx;
border-radius: 30rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 22rpx;
color: #5d5d5d;
background-color: #eee;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -0,0 +1,67 @@
Component({
externalClasses: ['custom-class'],
properties: {
activeKey: {
type: Number,
value: 0,
},
tabList: {
type: Array,
value: [],
},
showMore: Boolean, // 是否需要下拉功能
},
observers: {
activeKey(newVal) {
if (this.properties.tabList && newVal) {
this.setActive(newVal).catch((e) => {
console.error(e);
});
}
},
},
data: {
currentActive: -1,
},
attached() {
this.setActive(this.properties.activeKey).catch((e) => {
console.error(e);
});
},
methods: {
setActive(activeKey) {
if (!this.properties.tabList || !Array.isArray(this.properties.tabList) ||
activeKey < 0 || activeKey >= this.properties.tabList.length ||
!this.properties.tabList[activeKey] || this.properties.tabList[activeKey].disabled) {
return Promise.reject('数据异常或不可操作');
}
return new Promise((resolve) => {
this.setData(
{
currentActive: activeKey,
},
() => resolve(),
);
});
},
onClick(event) {
let activeKey;
if (event.type === 'select') {
activeKey = event.detail;
} else {
activeKey = event.currentTarget.dataset.index;
}
this.setActive(activeKey)
.then(() => {
const { currentActive } = this.data;
this.triggerEvent('change', { index: currentActive });
})
.catch((e) => {
console.error(e);
});
},
},
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"c-tabbar-more": "./c-tabbar-more/index"
}
}

View File

@@ -0,0 +1,23 @@
<view class="c-tabbar custom-class">
<scroll-view
wx:if="{{ tabList.length > 0 }}"
class="c-tabbar__scroll"
scroll-x="true"
scroll-into-view="{{ 'id-' + currentActive }}"
>
<view class="c-tabbar__inner {{showMore && tabList.length > 4 ? 'c-tabbar__inner_more' : ''}}">
<view
wx:for="{{ tabList }}"
wx:key="index"
id="{{ 'id-' + index }}"
class="c-tabbar-item {{ currentActive === index ? 'active' : '' }} {{ item.disabled ? 'disabled' : '' }}"
bind:tap="onClick"
data-index="{{index}}"
>
<view class="c-tabbar-item__text"> {{ item.name }} </view>
</view>
</view>
</scroll-view>
<c-tabbar-more wx:if="{{ showMore && tabList.length > 4 }}" tabList="{{tabList}}" bindselect="onClick" />
<slot />
</view>

View File

@@ -0,0 +1,53 @@
.c-tabbar {
width: 100%;
height: 100%;
position: relative;
--tabbar-height: 100rpx;
--tabbar-fontsize: 28rpx;
--tabbar-background-color: white;
}
.c-tabbar__inner {
display: flex;
flex-flow: row nowrap;
}
.c-tabbar__scroll {
position: relative;
}
.c-tabbar__scroll::after {
content: '';
display: block;
position: absolute;
width: 100%;
left: 0;
bottom: -1px;
height: 1px;
background-color: #eee;
z-index: 1;
}
.c-tabbar__inner.c-tabbar__inner_more::after {
content: '';
display: block;
width: 100rpx;
height: 100rpx;
flex: none;
}
.c-tabbar-item {
flex: none;
height: 100rpx;
color: #282828;
font-size: 28rpx;
padding: 0 20rpx;
}
.c-tabbar-item.active:not(.disabled) {
color: #0071ce;
position: relative;
}
.c-tabbar-item.disabled {
color: #ccc;
}
.c-tabbar-item__text {
width: 100%;
text-align: center;
height: 100rpx;
line-height: 100rpx;
}

View File

@@ -0,0 +1,109 @@
Component({
options: {
multipleSlots: true,
},
externalClasses: ['custom-class'],
properties: {
category: {
type: Array,
observer(newVal) {
// 分类数据到达后,若为二级分类并首次进入,则默认触发“全部”子项
if (newVal && Array.isArray(newVal) && newVal.length > 0) {
const activeKey = this.data.activeKey || 0;
const subActiveKey = this.data.subActiveKey || 0;
const parent = newVal[activeKey] || null;
const children = (parent && parent.children) ? parent.children : [];
if (!this._hasDefaultEmitted && this.properties.level === 2 && children.length > 0) {
this._hasDefaultEmitted = true;
const item = children[subActiveKey] || children[0];
this.triggerEvent('changeCategory', { item, source: 'parent' });
}
}
},
},
initActive: {
type: Array,
value: [],
observer(newVal, oldVal) {
if (newVal && oldVal && newVal.length > 0 && oldVal.length > 0 && newVal[0] !== oldVal[0]) {
this.setActiveKey(newVal[0], newVal[1] || 0);
}
},
},
isSlotRight: {
type: Boolean,
value: false,
},
level: {
type: Number,
value: 3,
},
},
data: {
activeKey: 0,
subActiveKey: 0,
},
attached() {
if (this.properties.initActive && Array.isArray(this.properties.initActive) && this.properties.initActive.length > 0) {
this.setData({
activeKey: this.properties.initActive[0],
subActiveKey: this.properties.initActive.length > 1 ? this.properties.initActive[1] : 0,
});
}
// 默认触发守卫标记
this._hasDefaultEmitted = false;
},
methods: {
onParentChange(event) {
this.setActiveKey(event.detail.index, 0).then(() => {
const { category } = this.properties;
const parent = category && category[this.data.activeKey];
const children = (parent && parent.children) ? parent.children : [];
const item = children[0] || null;
if (item) {
this.triggerEvent('changeCategory', { item, source: 'parent' });
}
this.triggerEvent('change', [this.data.activeKey, this.data.subActiveKey]);
});
},
onChildChange(event) {
this.setActiveKey(this.data.activeKey, event.detail.index).then(() => {
const { category } = this.properties;
const parent = category && category[this.data.activeKey];
const children = (parent && parent.children) ? parent.children : [];
const item = children[this.data.subActiveKey] || null;
if (item) {
this.triggerEvent('changeCategory', { item, source: 'child' });
}
this.triggerEvent('change', [this.data.activeKey, this.data.subActiveKey]);
});
},
changCategory(event) {
const { item, index } = event.currentTarget.dataset;
const nextIndex = typeof index === 'number' ? index : 0;
// 更新当前子分类选中态
this.setActiveKey(this.data.activeKey, nextIndex).then(() => {
// 触发分类变更事件传递完整的item数据
this.triggerEvent('changeCategory', { item, source: 'child' });
this.triggerEvent('change', [this.data.activeKey, this.data.subActiveKey]);
});
},
setActiveKey(key, subKey) {
return new Promise((resolve) => {
this.setData(
{
activeKey: key,
subActiveKey: subKey,
},
() => {
resolve();
},
);
});
},
onRightScrollToLower() {
this.triggerEvent('reachbottom');
},
},
});

View File

@@ -0,0 +1,9 @@
{
"component": true,
"usingComponents": {
"c-tabbar": "./components/c-tabbar/index",
"c-sidebar": "./components/c-sidebar/index",
"c-sidebar-item": "./components/c-sidebar/c-sidebar-item/index",
"t-image": "/components/webp-image/index"
}
}

View File

@@ -0,0 +1,65 @@
<view class="goods-category custom-class">
<c-sidebar custom-class="custom-sidebar" bindchange="onParentChange" activeKey="{{activeKey}}">
<c-sidebar-item wx:for="{{ category }}" wx:key="index" title="{{ item.name }}" disabled="{{ item.disabled }}" />
</c-sidebar>
<view class="goods-category__right">
<view class="goods-category__right-header">
<c-tabbar wx:if="{{isSlotRight}}" activeKey="{{subActiveKey}}" bindchange="onChildChange" showMore>
<slot />
</c-tabbar>
<view wx:if="{{!isSlotRight}}" class="goods-category-normal">
<view
class="goods-category-normal-item"
wx:if="{{category && category[activeKey] && category[activeKey].children && category[activeKey].children.length > 0}}"
>
<block
wx:for="{{category && category[activeKey] && category[activeKey].children || []}}"
wx:key="index"
wx:if="{{level === 3 && item.children && item.children.length > 0}}"
>
<view class="flex goods-category-normal-item-title"> {{item.name}} </view>
<view class="goods-category-normal-item-container">
<view
class="goods-category-normal-item-container-item category-button"
wx:for="{{item.children}}"
wx:for-index="subIndex"
wx:key="subIndex"
wx:for-item="subItem"
bindtap="changCategory"
data-item="{{subItem}}"
>
<view class="category-button-text">{{subItem.name}}</view>
</view>
</view>
</block>
<scroll-view class="goods-category-second-scroll" scroll-x="true" show-scrollbar="false" wx:if="{{level === 2}}">
<view class="goods-category-normal-item-second-container">
<block wx:for="{{category && category[activeKey] && category[activeKey].children || []}}" wx:key="index">
<view
class="goods-category-normal-item-second-container-item category-button {{ index === subActiveKey ? 'active' : '' }}"
bindtap="changCategory"
data-item="{{item}}"
data-index="{{index}}"
data-is-all="{{item.isAll || false}}"
>
<view class="category-button-text">{{item.name}}</view>
</view>
</block>
</view>
</scroll-view>
</view>
</view>
</view>
<scroll-view class="goods-category__right-scroll" scroll-y show-scrollbar="false" bindscrolltolower="onRightScrollToLower" style="height: 100%;">
<slot name="goodsList" />
<view
class="goods-category-empty"
wx:if="{{category && category[activeKey] && (!category[activeKey].children || category[activeKey].children.length === 0)}}"
>
<view class="goods-category-empty-icon">📦</view>
<view class="goods-category-empty-text">暂无分类</view>
<view class="goods-category-empty-desc">该分类下暂时没有子分类</view>
</view>
</scroll-view>
</view>
</view>

View File

@@ -0,0 +1,185 @@
.goods-category {
display: flex;
}
.custom-sidebar {
height: 100%;
}
.goods-category__right {
height: 100%;
flex: auto;
width: 0;
position: relative;
overflow: hidden;
-webkit-overflow-scrolling: touch;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.flex {
display: flex;
}
.goods-category-normal {
margin: 0;
background-color: transparent;
min-height: auto;
padding: 16rpx 24rpx 24rpx;
}
.goods-category-normal-item-title {
font-size: 28rpx;
font-weight: 500;
}
.goods-category-normal-item-container {
background-color: #fff;
border-radius: 8rpx;
padding: 20rpx;
margin-top: 0;
margin-bottom: 30rpx;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.goods-category-normal-item-container-item {
text-align: center;
}
.goods-category-normal-item-container-item .image {
width: 144rpx;
height: 144rpx;
}
.goods-category-normal-item-container-item-title {
justify-content: center;
font-size: 24rpx;
color: #666666;
margin-top: 20rpx;
}
.goods-category .custom-sidebar {
background-color: #f5f5f5;
}
.custom-sidebar {
width: 180rpx;
height: 100%;
}
.custom-sidebar::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.goods-category-normal-item-second-container {
background-color: #fff;
border-radius: 16rpx;
margin-top: 0;
margin-bottom: 16rpx;
display: inline-flex;
flex-wrap: nowrap;
padding: 24rpx 24rpx;
gap: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #f1f3f4;
white-space: nowrap;
width: auto;
}
.goods-category-normal-item-second-container-item {
text-align: center;
}
.goods-category-normal-item-second-container-item .image {
width: 144rpx;
height: 144rpx;
}
.goods-category-normal-item-second-container-item-title {
justify-content: center;
font-size: 24rpx;
color: #222427;
}
/* 二级分类横向滚动容器,保持区域吸顶 */
.goods-category-second-scroll {
position: sticky;
top: 0;
z-index: 5;
background-color: transparent;
overflow: hidden;
}
/* 无分类提示样式 */
.goods-category-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
text-align: center;
}
.goods-category-empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.goods-category-empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 12rpx;
font-weight: 500;
}
.goods-category-empty-desc {
font-size: 24rpx;
color: #999;
line-height: 1.5;
}
/* 右侧内容滚动区域占满剩余空间并可滚动 */
.goods-category__right-scroll {
flex: 1 1 auto;
height: 100%;
min-height: 0;
}
/* 优化后的按钮样式 */
.category-button {
background: #fff;
border: 1rpx solid #e6e8ea;
border-radius: 24rpx;
padding: 16rpx 24rpx;
margin: 0;
min-width: 120rpx;
box-shadow: none;
transition: background-color 0.2s ease, border-color 0.2s ease;
flex: 0 0 auto;
}
.category-button.active {
border-color: #fa4126;
}
.category-button.active .category-button-text {
color: #fa4126;
font-weight: 600;
}
/* 全部按钮使用柔和的主色描边 */
/* 取消“全部”按钮的特殊样式,统一使用选中态样式 */
.category-button-text {
color: #495057;
font-size: 26rpx;
font-weight: 500;
text-align: center;
white-space: nowrap;
line-height: 1.2;
}

View File

@@ -0,0 +1,142 @@
import { getCategoryList } from '../../services/good/fetchCategoryList';
import { fetchGoodsList } from '../../services/good/fetchGoodsList';
Page({
data: {
list: [],
goodsList: [],
hasLoaded: false,
loadMoreStatus: 0,
categoryId: '',
categoryName: '',
},
pageNum: 1,
pageSize: 30,
total: 0,
async init() {
try {
const result = await getCategoryList();
this.setData({
list: result,
});
} catch (error) {
console.error('err:', error);
}
},
onShow() {
this.getTabBar().init();
},
onChange(e) {
const { item, source } = e.detail;
let categoryId = item.groupId || item.categoryId || '';
const categoryName = item.name || '';
// 兼容“全部”按钮的 groupId: `${id}_all`
if (categoryId && categoryId.toString().endsWith('_all')) {
categoryId = categoryId.toString().replace('_all', '');
}
// 二级分类:跳转到商品列表页;一级分类:当前页加载数据
if (source === 'child') {
const url = `/pages/goods/list/index?categoryId=${encodeURIComponent(categoryId)}&categoryName=${encodeURIComponent(categoryName)}`;
wx.navigateTo({ url });
return;
}
// 一级分类只在当前页加载商品
this.pageNum = 1;
this.setData({ categoryId, categoryName, loadMoreStatus: 0 });
this.loadCategoryGoods(true);
},
// 组装查询参数
buildQueryParams(reset = true) {
const params = {
pageNum: reset ? 1 : this.pageNum + 1,
pageSize: this.pageSize,
category_id: this.data.categoryId || undefined,
};
return params;
},
async loadCategoryGoods(reset = true) {
const { loadMoreStatus, goodsList = [] } = this.data;
if (loadMoreStatus !== 0) return;
const params = this.buildQueryParams(reset);
this.setData({ loadMoreStatus: 1 });
try {
const result = await fetchGoodsList(params);
if (!result || !result.spuList) {
this.setData({ hasLoaded: true, loadMoreStatus: 0 });
return;
}
const { spuList, totalCount = 0 } = result;
const merged = reset ? spuList : goodsList.concat(spuList);
const status = merged.length === totalCount ? 2 : 0;
this.pageNum = params.pageNum || 1;
this.total = totalCount;
this.setData({
goodsList: merged,
loadMoreStatus: status,
hasLoaded: true,
});
} catch (error) {
console.error('获取分类商品失败:', error);
this.setData({ loadMoreStatus: 0, hasLoaded: true });
}
},
onReachBottom() {
const { goodsList } = this.data;
const total = this.total || 0;
if (goodsList.length >= total && total > 0) {
this.setData({ loadMoreStatus: 2 });
return;
}
this.loadCategoryGoods(false);
},
handleAddCart() {
wx.showToast({ title: '点击加购', icon: 'none' });
},
onLoad() {
this.init(true);
},
// 分享功能
onShareAppMessage() {
return {
title: '商品分类 - 浏览更多精选商品',
path: '/pages/category/index'
};
},
// 分享到朋友圈
onShareTimeline() {
return {
title: '商品分类 - 浏览更多精选商品'
};
},
handleClickGoods(e) {
const { index } = e.detail || {};
const { goodsList = [] } = this.data;
const item = goodsList[index];
const spuId = item && (item.spuId || item.id || item.skuId);
if (!spuId) {
wx.showToast({ title: '无法识别商品ID', icon: 'none' });
return;
}
wx.navigateTo({
url: `/pages/goods/details/index?spuId=${encodeURIComponent(spuId)}`
});
},
});

View File

@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "分类",
"usingComponents": {
"goods-category": "./components/goods-category/index",
"goods-list": "/components/goods-list/index",
"load-more": "/components/load-more/index",
"t-empty": "tdesign-miniprogram/empty/empty"
}
}

View File

@@ -0,0 +1,28 @@
<view class="wrap">
<goods-category
level="{{2}}"
custom-class="goods-category-class"
category="{{list}}"
initActive="{{[0,0]}}"
bind:changeCategory="onChange"
bind:reachbottom="onReachBottom"
>
<!-- 当前分类商品列表:插入到二级分类组件下方 -->
<view slot="goodsList" class="category-goods-container">
<view class="empty-wrap" wx:if="{{goodsList.length === 0 && hasLoaded}}">
<t-empty t-class="empty-tips" size="240rpx" description="暂无相关商品" />
</view>
<view class="category-goods-list" wx:if="{{goodsList.length}}">
<goods-list
wr-class="wr-goods-list"
goodsList="{{goodsList}}"
show-cart="{{false}}"
bind:click="handleClickGoods"
bind:addcart="handleAddCart"
/>
</view>
<load-more wx:if="{{goodsList.length > 0}}" status="{{loadMoreStatus}}" no-more-text="没有更多了" />
</view>
</goods-category>
</view>

View File

@@ -0,0 +1,43 @@
.tabbar-position {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
}
.wrap {
padding-bottom: 180rpx;
}
.goods-category-class {
background-color: #f6f6f6 !important;
height: calc(100vh - 180rpx);
}
.goods-category-class .goods-category-normal-item-container-item {
margin-top: 20rpx;
}
page {
background-color: #f5f5f5;
}
/* 商品列表区域样式 */
.category-goods-container {
background-color: transparent;
padding: 0 24rpx 24rpx 24rpx;
}
.category-goods-list {
background-color: transparent;
}
.wr-goods-list {
background: transparent !important;
}
.empty-wrap {
margin-top: 40rpx;
margin-bottom: 40rpx;
height: 300rpx;
}