mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-11-06 17:09:01 +08:00
feat: 分类页支持后台配置的四种导航模板
This commit is contained in:
93
components/category/category-scroller.vue
Normal file
93
components/category/category-scroller.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<scroll-view scroll-x scroll-with-animation class="cate-scroller" :scroll-into-view="intoView">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:id="'cs-' + item.id"
|
||||
class="cs-item"
|
||||
:class="{ active: item.id === currentId, 'cs-item--text': mode === 'text' }"
|
||||
@click="$emit('change', item)"
|
||||
>
|
||||
<template v-if="mode === 'icon'">
|
||||
<view class="cs-icon">
|
||||
<u-image width="88rpx" height="88rpx" shape="circle" :src="item.image">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<text class="cs-name">{{ item.name }}</text>
|
||||
</template>
|
||||
<text v-else class="cs-text-name">{{ item.name }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list?: any[]
|
||||
currentId?: string | number | null
|
||||
mode?: 'icon' | 'text'
|
||||
}>(), {
|
||||
list: () => [],
|
||||
currentId: null,
|
||||
mode: 'icon'
|
||||
})
|
||||
|
||||
defineEmits(['change'])
|
||||
|
||||
const intoView = computed(() =>
|
||||
props.currentId != null ? `cs-${props.currentId}` : ''
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cate-scroller {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
.cs-item {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 120rpx;
|
||||
padding: 16rpx 0 10rpx;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cs-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
.cs-item.active .cs-icon {
|
||||
border-color: $light-color;
|
||||
}
|
||||
.cs-name {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #333;
|
||||
max-width: 110rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cs-item.active .cs-name {
|
||||
color: $light-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
.cs-item--text {
|
||||
width: auto;
|
||||
padding: 18rpx 24rpx;
|
||||
}
|
||||
.cs-text-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
.cs-item--text.active .cs-text-name {
|
||||
color: $light-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
51
components/category/category-side-list.vue
Normal file
51
components/category/category-side-list.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<scroll-view scroll-y scroll-with-animation class="side-list">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="side-item"
|
||||
:class="{ active: item.id === currentId }"
|
||||
@click="$emit('change', item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
list?: any[]
|
||||
currentId?: string | number | null
|
||||
}>(), {
|
||||
list: () => [],
|
||||
currentId: null
|
||||
})
|
||||
|
||||
defineEmits(['change'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.side-list {
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
.side-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 90rpx;
|
||||
padding: 10rpx 8rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
color: $light-color;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
components/category/sort-bar.vue
Normal file
83
components/category/sort-bar.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<view class="sort-bar">
|
||||
<view
|
||||
class="sort-item"
|
||||
:class="{ active: sort === 'releaseTime' }"
|
||||
@click="onClick('releaseTime')"
|
||||
>
|
||||
综合
|
||||
</view>
|
||||
<view
|
||||
class="sort-item"
|
||||
:class="{ active: sort === 'buyCount' }"
|
||||
@click="onClick('buyCount')"
|
||||
>
|
||||
销量
|
||||
</view>
|
||||
<view
|
||||
class="sort-item"
|
||||
:class="{ active: sort === 'price' }"
|
||||
@click="onClick('price')"
|
||||
>
|
||||
<text>价格</text>
|
||||
<view class="arrows">
|
||||
<u-icon
|
||||
name="arrow-up"
|
||||
size="10"
|
||||
:color="sort === 'price' && order === 'asc' ? '#ff6700' : '#c5c5c5'"
|
||||
></u-icon>
|
||||
<u-icon
|
||||
name="arrow-down"
|
||||
size="10"
|
||||
:color="sort === 'price' && order === 'desc' ? '#ff6700' : '#c5c5c5'"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
sort?: string
|
||||
order?: string
|
||||
}>(), {
|
||||
sort: 'releaseTime',
|
||||
order: 'desc'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
function onClick(type: string) {
|
||||
let nextOrder = 'desc'
|
||||
if (type === 'price') {
|
||||
nextOrder = props.sort === 'price' && props.order === 'desc' ? 'asc' : 'desc'
|
||||
}
|
||||
emit('change', { sort: type, order: nextOrder })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sort-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
background: #fff;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
.sort-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
&.active {
|
||||
color: $light-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
</style>
|
||||
55
components/category/sub-tag-bar.vue
Normal file
55
components/category/sub-tag-bar.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<scroll-view scroll-x scroll-with-animation class="tag-bar" :scroll-into-view="intoView">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:id="'tb-' + item.id"
|
||||
class="tag-item"
|
||||
:class="{ active: item.id === currentId }"
|
||||
@click="$emit('change', item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list?: any[]
|
||||
currentId?: string | number | null
|
||||
}>(), {
|
||||
list: () => [],
|
||||
currentId: null
|
||||
})
|
||||
|
||||
defineEmits(['change'])
|
||||
|
||||
const intoView = computed(() =>
|
||||
props.currentId != null ? `tb-${props.currentId}` : ''
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag-bar {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
.tag-item {
|
||||
display: inline-block;
|
||||
padding: 8rpx 24rpx;
|
||||
margin-right: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
background: #f5f5f5;
|
||||
border-radius: 24rpx;
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: $light-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
260
components/category/tpl-category-goods.vue
Normal file
260
components/category/tpl-category-goods.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<view class="tpl-goods">
|
||||
<!-- 模板2/3:顶部一级分类图标横滑 -->
|
||||
<category-scroller
|
||||
v-if="template !== 4"
|
||||
:list="categoryList"
|
||||
:current-id="currentL1 && currentL1.id"
|
||||
mode="icon"
|
||||
@change="onL1Change"
|
||||
/>
|
||||
<!-- 模板2:三级分类标签横滑 -->
|
||||
<sub-tag-bar
|
||||
v-if="template === 2 && l3List.length"
|
||||
:list="l3List"
|
||||
:current-id="currentL3 && currentL3.id"
|
||||
@change="onL3Change"
|
||||
/>
|
||||
<view class="tpl-main">
|
||||
<!-- 左侧:模板2/3 为二级分类,模板4 为一级分类 -->
|
||||
<category-side-list
|
||||
:list="sideList"
|
||||
:current-id="sideCurrentId"
|
||||
@change="onSideChange"
|
||||
/>
|
||||
<view class="tpl-right">
|
||||
<!-- 模板4:右上二级分类文字横滑(起点为当前一级名称,作为"全部"入口) -->
|
||||
<category-scroller
|
||||
v-if="template === 4"
|
||||
:list="l2Items"
|
||||
:current-id="topCurrentId"
|
||||
mode="text"
|
||||
@change="onL2Change"
|
||||
/>
|
||||
<sort-bar :sort="sort" :order="order" @change="onSortChange" />
|
||||
<scroll-view scroll-y class="goods-scroll" @scrolltolower="loadMore">
|
||||
<m-goods-list
|
||||
v-if="goodsList.length"
|
||||
:res="goodsList"
|
||||
type="oneColumns"
|
||||
:show-fields="setting.showFields"
|
||||
:tab-bar-gap="false"
|
||||
@add-cart="onAddCart"
|
||||
/>
|
||||
<view v-else-if="!loading" class="empty">
|
||||
<text>{{ failed ? '加载失败,请点击重试' : '暂无商品' }}</text>
|
||||
<view v-if="failed" class="retry" @click="loadGoods(true)">重试</view>
|
||||
</view>
|
||||
<uni-load-more
|
||||
v-if="goodsList.length"
|
||||
:status="loading ? 'loading' : hasMore ? 'more' : 'noMore'"
|
||||
></uni-load-more>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { getGoodsList } from '@/api/goods.js'
|
||||
import CategoryScroller from './category-scroller.vue'
|
||||
import CategorySideList from './category-side-list.vue'
|
||||
import SubTagBar from './sub-tag-bar.vue'
|
||||
import SortBar from './sort-bar.vue'
|
||||
import MGoodsList from '@/components/m-goods-list/list.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
categoryList?: any[]
|
||||
setting?: any
|
||||
template?: number
|
||||
}>(), {
|
||||
categoryList: () => [],
|
||||
setting: () => ({ template: 1, showFields: {} }),
|
||||
template: 2
|
||||
})
|
||||
|
||||
const currentL1 = ref<any>(null)
|
||||
const currentL2 = ref<any>(null)
|
||||
const currentL3 = ref<any>(null)
|
||||
const sort = ref('releaseTime')
|
||||
const order = ref('desc')
|
||||
|
||||
const goodsList = ref<any[]>([])
|
||||
const pageNumber = ref(1)
|
||||
const hasMore = ref(true)
|
||||
const loading = ref(false)
|
||||
const failed = ref(false)
|
||||
|
||||
// 模板2/3:左侧为当前一级的二级分类;模板4:左侧为一级分类
|
||||
const sideList = computed(() =>
|
||||
props.template === 4
|
||||
? props.categoryList
|
||||
: (currentL1.value && currentL1.value.children) || []
|
||||
)
|
||||
const sideCurrentId = computed(() =>
|
||||
props.template === 4
|
||||
? currentL1.value && currentL1.value.id
|
||||
: currentL2.value && currentL2.value.id
|
||||
)
|
||||
// 模板2 的三级标签
|
||||
const l3List = computed(() =>
|
||||
(currentL2.value && currentL2.value.children) || []
|
||||
)
|
||||
// 模板4 顶部:当前一级名称(作为"全部") + 其二级分类
|
||||
const l2Items = computed(() => {
|
||||
if (!currentL1.value) return []
|
||||
return [currentL1.value, ...((currentL1.value.children) || [])]
|
||||
})
|
||||
const topCurrentId = computed(() =>
|
||||
(currentL2.value && currentL2.value.id) || (currentL1.value && currentL1.value.id)
|
||||
)
|
||||
|
||||
const queryCategoryId = computed(() =>
|
||||
(currentL3.value && currentL3.value.id) ||
|
||||
(currentL2.value && currentL2.value.id) ||
|
||||
(currentL1.value && currentL1.value.id)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.categoryList,
|
||||
(list) => {
|
||||
if (list && list.length && !currentL1.value) {
|
||||
currentL1.value = list[0]
|
||||
currentL2.value = props.template === 4 ? null : (list[0].children && list[0].children[0]) || null
|
||||
currentL3.value = (currentL2.value && currentL2.value.children && currentL2.value.children[0]) || null
|
||||
loadGoods(true)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function onL1Change(item: any) {
|
||||
if (currentL1.value && item.id === currentL1.value.id) return
|
||||
currentL1.value = item
|
||||
currentL2.value = props.template === 4 ? null : (item.children && item.children[0]) || null
|
||||
currentL3.value = (currentL2.value && currentL2.value.children && currentL2.value.children[0]) || null
|
||||
resetSort()
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onSideChange(item: any) {
|
||||
if (props.template === 4) {
|
||||
onL1Change(item)
|
||||
return
|
||||
}
|
||||
if (currentL2.value && item.id === currentL2.value.id) return
|
||||
currentL2.value = item
|
||||
currentL3.value = (item.children && item.children[0]) || null
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onL3Change(item: any) {
|
||||
if (currentL3.value && item.id === currentL3.value.id) return
|
||||
currentL3.value = item
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onL2Change(item: any) {
|
||||
// 选中一级名称项 = 全部(L2 置空)
|
||||
if (currentL1.value && item.id === currentL1.value.id) {
|
||||
currentL2.value = null
|
||||
} else {
|
||||
currentL2.value = item
|
||||
}
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onSortChange(payload: { sort: string; order: string }) {
|
||||
sort.value = payload.sort
|
||||
order.value = payload.order
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function resetSort() {
|
||||
sort.value = 'releaseTime'
|
||||
order.value = 'desc'
|
||||
}
|
||||
|
||||
async function loadGoods(reset = false) {
|
||||
if (loading.value || !queryCategoryId.value) return
|
||||
if (reset) {
|
||||
pageNumber.value = 1
|
||||
goodsList.value = []
|
||||
hasMore.value = true
|
||||
}
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
const res = await getGoodsList({
|
||||
categoryId: queryCategoryId.value,
|
||||
sort: sort.value,
|
||||
order: order.value,
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize: 10,
|
||||
})
|
||||
const records = (res.data && res.data.result && res.data.result.records) || []
|
||||
goodsList.value.push(...records)
|
||||
hasMore.value = records.length >= 10
|
||||
} catch (e) {
|
||||
failed.value = true
|
||||
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (!hasMore.value || loading.value) return
|
||||
pageNumber.value += 1
|
||||
loadGoods(false)
|
||||
}
|
||||
|
||||
function onAddCart(item: any) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-goods {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
.tpl-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
.tpl-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.goods-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.retry {
|
||||
margin-top: 20rpx;
|
||||
padding: 10rpx 48rpx;
|
||||
border: 1rpx solid $light-color;
|
||||
color: $light-color;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,7 @@
|
||||
</view>
|
||||
</template>
|
||||
</u-navbar>
|
||||
<view class="content">
|
||||
<view class="content" v-if="pageTemplate === 1">
|
||||
<scroll-view scroll-y scroll-with-animation class="left-aside">
|
||||
<view v-for="(item, index) in tabList" :key="item.id" class="f-item b-b" :class="{ active: item.id === currentId }" @click="tabtap(item, index)">
|
||||
{{ item.name }}
|
||||
@@ -53,6 +53,12 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<tpl-category-goods
|
||||
v-else
|
||||
:category-list="tabList"
|
||||
:setting="categoryPageSetting"
|
||||
:template="pageTemplate"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -61,10 +67,17 @@ import { ref, computed } from 'vue'
|
||||
import { onLoad, onReady } from '@dcloudio/uni-app'
|
||||
import { getCategoryList } from '@/api/goods.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { DEFAULT_CATEGORY_PAGE_SETTING } from '@/utils/categoryPageSetting'
|
||||
import { useStore } from '@/store'
|
||||
import TplCategoryGoods from '@/components/category/tpl-category-goods.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const categoryPageSetting = computed(
|
||||
() => store.state.categoryPageSetting || DEFAULT_CATEGORY_PAGE_SETTING
|
||||
)
|
||||
const pageTemplate = computed(() => categoryPageSetting.value.template || 1)
|
||||
|
||||
const currentId = ref(0)
|
||||
const tabList = ref<any[]>([]) // 左侧标题列表
|
||||
const categoryList = ref<any[]>([]) // 右侧分类数据列表
|
||||
|
||||
Reference in New Issue
Block a user