395 lines
9.4 KiB
Vue
395 lines
9.4 KiB
Vue
<template>
|
||
<view
|
||
class="container"
|
||
:class="['skin--' + pageSkin, 'density--' + pageDensity, 'list--' + packageList]"
|
||
:style="themePageStyle"
|
||
>
|
||
<ThemeNavbar title="套餐" :show-back="false" />
|
||
<view class="search-section" :style="{ background: themeSurface }">
|
||
<ThemeSearch
|
||
v-model="keyword"
|
||
placeholder="搜索套餐名称"
|
||
:variant="packageSearch"
|
||
@search="handleSearch"
|
||
/>
|
||
</view>
|
||
|
||
<view class="content-body">
|
||
<view class="page-head">
|
||
<text class="page-en" :style="{ color: themePrimary }">SETS</text>
|
||
<text class="page-cn" :style="{ color: themeText }">本期套餐</text>
|
||
<view class="page-rule" :style="{ background: themePrimary }"></view>
|
||
</view>
|
||
|
||
<view v-if="loading && items.length === 0" class="loading-container">
|
||
<ThemeSkeleton page="catalog" />
|
||
</view>
|
||
|
||
<view v-else-if="items.length === 0" class="empty-container">
|
||
<text class="empty-en" :style="{ color: themePrimary }">SETS</text>
|
||
<text class="empty-text" :style="{ color: themeTextSoft }">
|
||
{{ keyword ? '未找到相关套餐' : '还没有上架套餐' }}
|
||
</text>
|
||
</view>
|
||
|
||
<ThemeEnter v-else>
|
||
<view class="pkg-list" :class="'pkg-list--' + packageList">
|
||
<!-- pkg-card 放在本页节点上,避免 scoped 打不到 ThemeCardFrame 根节点 -->
|
||
<view
|
||
v-for="(item, index) in items"
|
||
:key="item.id"
|
||
class="pkg-card"
|
||
>
|
||
<ThemeCardFrame>
|
||
<view class="pkg-inner" @tap="goDetail(item.id)">
|
||
<image :src="item.cover" mode="aspectFill" class="pkg-cover" />
|
||
<view class="pkg-veil"></view>
|
||
<view class="pkg-meta">
|
||
<text class="pkg-vol" :style="{ color: themePrimary }">SET {{ padNo(index + 1) }}</text>
|
||
<text class="pkg-name">{{ item.name }}</text>
|
||
<text v-if="item.subtitle" class="pkg-sub">{{ item.subtitle }}</text>
|
||
<text class="pkg-count">{{ item.item_count || 0 }} 件搭配</text>
|
||
<view v-if="item.is_show_price" class="pkg-price">
|
||
<text class="pkg-origin">¥{{ item.original_amount_text }}</text>
|
||
<text class="pkg-quote" :style="{ color: themePrice }">¥{{ item.package_amount_text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</ThemeCardFrame>
|
||
</view>
|
||
</view>
|
||
<view v-if="loadingMore" class="loading-more">
|
||
<ThemeSpinner text="加载更多" />
|
||
</view>
|
||
<view v-if="!hasMore && items.length > 0" class="no-more">
|
||
<text class="line" :style="{ background: themePrimary }"></text>
|
||
<text :style="{ color: themeTextSoft }">THE END</text>
|
||
<text class="line" :style="{ background: themePrimary }"></text>
|
||
</view>
|
||
</ThemeEnter>
|
||
</view>
|
||
|
||
<ThemeTabBar id="themeTabBar" ref="themeTabBar" />
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* 套餐 tab:图录式封面卡,有价权时划线原价 + 套餐价。
|
||
* 应用没开套餐时回首页,避免从微信 tab 列表点进空白页。
|
||
*/
|
||
import { getPackageListApi } from '@/api/page/package'
|
||
import { getThemeFeatures } from '@/utils/theme'
|
||
import themeMixin from '@/mixins/theme'
|
||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||
import ThemeSearch from '@/components/theme/ThemeSearch.vue'
|
||
import ThemeSkeleton from '@/components/theme/ThemeSkeleton.vue'
|
||
import ThemeSpinner from '@/components/theme/ThemeSpinner.vue'
|
||
import ThemeEnter from '@/components/theme/ThemeEnter.vue'
|
||
import ThemeCardFrame from '@/components/theme/ThemeCardFrame.vue'
|
||
import ThemeTabBar from '@/components/theme/ThemeTabBar.vue'
|
||
|
||
export default {
|
||
components: {
|
||
ThemeNavbar,
|
||
ThemeSearch,
|
||
ThemeSkeleton,
|
||
ThemeSpinner,
|
||
ThemeEnter,
|
||
ThemeCardFrame,
|
||
ThemeTabBar,
|
||
},
|
||
mixins: [themeMixin],
|
||
data() {
|
||
return {
|
||
currentThemePage: 'package',
|
||
keyword: '',
|
||
items: [],
|
||
page: 1,
|
||
hasMore: true,
|
||
loading: false,
|
||
loadingMore: false,
|
||
}
|
||
},
|
||
async onShow() {
|
||
await this.applyThemeOnShow()
|
||
if (!getThemeFeatures().package_enabled) {
|
||
uni.switchTab({ url: '/pages/goods/index' })
|
||
return
|
||
}
|
||
this.reload()
|
||
},
|
||
onPullDownRefresh() {
|
||
this.reload().finally(() => {
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
},
|
||
onReachBottom() {
|
||
this.loadMore()
|
||
},
|
||
methods: {
|
||
/** 检索套餐名/副标题,从第一页重拉 */
|
||
handleSearch() {
|
||
this.reload()
|
||
},
|
||
reload() {
|
||
this.page = 1
|
||
this.hasMore = true
|
||
this.items = []
|
||
return this.fetchList()
|
||
},
|
||
/**
|
||
* 拉套餐列表。拦截器已经拆过 result,这里只认 items / has_more。
|
||
*/
|
||
fetchList() {
|
||
if (this.loading || this.loadingMore || !this.hasMore) return Promise.resolve()
|
||
if (this.page === 1) {
|
||
this.loading = true
|
||
} else {
|
||
this.loadingMore = true
|
||
}
|
||
return getPackageListApi({ page: this.page, pageSize: 20, keyword: this.keyword })
|
||
.then((res) => {
|
||
const rows = (res && res.items) || []
|
||
this.items = this.page === 1 ? rows : this.items.concat(rows)
|
||
this.hasMore = !!(res && res.has_more)
|
||
this.page += 1
|
||
})
|
||
.catch(() => {
|
||
this.hasMore = false
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
this.loadingMore = false
|
||
})
|
||
},
|
||
loadMore() {
|
||
this.fetchList()
|
||
},
|
||
goDetail(id) {
|
||
uni.navigateTo({ url: `/pages/package/detail?id=${id}` })
|
||
},
|
||
/** 图录编号:01 / 02,跟首页 VOL 同一套语气 */
|
||
padNo(n) {
|
||
return n < 10 ? '0' + n : String(n)
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.container {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.search-section {
|
||
padding: 20rpx 30rpx 8rpx;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
.content-body {
|
||
flex: 1;
|
||
}
|
||
.skin--magazine .page-en {
|
||
letter-spacing: 12rpx;
|
||
}
|
||
.skin--magazine .page-cn {
|
||
font-weight: 400;
|
||
letter-spacing: 8rpx;
|
||
}
|
||
.density--compact .pkg-list {
|
||
gap: 16rpx;
|
||
}
|
||
.density--airy .pkg-list {
|
||
gap: 40rpx;
|
||
}
|
||
.page-head {
|
||
padding: 12rpx 32rpx 8rpx;
|
||
}
|
||
.page-en {
|
||
display: block;
|
||
font-size: 20rpx;
|
||
letter-spacing: 8rpx;
|
||
}
|
||
.page-cn {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 34rpx;
|
||
font-weight: 500;
|
||
letter-spacing: 4rpx;
|
||
}
|
||
.page-rule {
|
||
width: 48rpx;
|
||
height: 2rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
.loading-container,
|
||
.empty-container {
|
||
padding: 80rpx 0 160rpx;
|
||
}
|
||
.empty-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.empty-en {
|
||
font-size: 22rpx;
|
||
letter-spacing: 10rpx;
|
||
}
|
||
.empty-text {
|
||
margin-top: 16rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
.pkg-list {
|
||
padding: 16rpx 24rpx 8rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 28rpx;
|
||
}
|
||
.pkg-card {
|
||
display: block;
|
||
}
|
||
.pkg-inner {
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.pkg-cover {
|
||
width: 100%;
|
||
height: 420rpx;
|
||
display: block;
|
||
}
|
||
.pkg-veil {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
height: 56%;
|
||
background: linear-gradient(transparent, rgba(28, 25, 23, 0.62));
|
||
pointer-events: none;
|
||
}
|
||
.pkg-meta {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
padding: 28rpx 28rpx 32rpx;
|
||
}
|
||
.pkg-vol {
|
||
display: block;
|
||
font-size: 18rpx;
|
||
letter-spacing: 6rpx;
|
||
}
|
||
.pkg-name,
|
||
.pkg-sub,
|
||
.pkg-count,
|
||
.pkg-origin {
|
||
color: #fff;
|
||
}
|
||
.pkg-name {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 36rpx;
|
||
font-weight: 500;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
.pkg-sub,
|
||
.pkg-count {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
letter-spacing: 1rpx;
|
||
opacity: 0.86;
|
||
}
|
||
.pkg-price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 14rpx;
|
||
margin-top: 14rpx;
|
||
}
|
||
.pkg-origin {
|
||
font-size: 22rpx;
|
||
text-decoration: line-through;
|
||
opacity: 0.7;
|
||
}
|
||
.pkg-quote {
|
||
font-size: 34rpx;
|
||
font-weight: 500;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
/* 首图:第一张拉高,其余略矮,和装修「首图」对得上 */
|
||
.pkg-list--featured .pkg-card:first-child .pkg-cover {
|
||
height: 560rpx;
|
||
}
|
||
.pkg-list--featured .pkg-card:not(:first-child) .pkg-cover {
|
||
height: 320rpx;
|
||
}
|
||
/* 刊名条:左图右文,取消压字,换主题立刻能看出差别 */
|
||
.pkg-list--magazine .pkg-inner {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
}
|
||
.pkg-list--magazine .pkg-cover {
|
||
width: 280rpx;
|
||
height: 220rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.pkg-list--magazine .pkg-veil {
|
||
display: none;
|
||
}
|
||
.pkg-list--magazine .pkg-meta {
|
||
position: relative;
|
||
padding: 24rpx 24rpx 24rpx 28rpx;
|
||
}
|
||
.pkg-list--magazine .pkg-name,
|
||
.pkg-list--magazine .pkg-sub,
|
||
.pkg-list--magazine .pkg-count,
|
||
.pkg-list--magazine .pkg-origin {
|
||
color: inherit;
|
||
}
|
||
/* 单列:更紧凑的左图右文 */
|
||
.pkg-list--list .pkg-inner {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
.pkg-list--list .pkg-cover {
|
||
width: 220rpx;
|
||
height: 168rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.pkg-list--list .pkg-veil {
|
||
display: none;
|
||
}
|
||
.pkg-list--list .pkg-meta {
|
||
position: relative;
|
||
padding: 20rpx 24rpx;
|
||
}
|
||
.pkg-list--list .pkg-name,
|
||
.pkg-list--list .pkg-sub,
|
||
.pkg-list--list .pkg-count,
|
||
.pkg-list--list .pkg-origin {
|
||
color: inherit;
|
||
}
|
||
.pkg-list--list .pkg-name {
|
||
font-size: 30rpx;
|
||
}
|
||
.loading-more,
|
||
.no-more {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16rpx;
|
||
padding: 28rpx 0 48rpx;
|
||
font-size: 20rpx;
|
||
letter-spacing: 6rpx;
|
||
}
|
||
.no-more .line {
|
||
width: 48rpx;
|
||
height: 1rpx;
|
||
opacity: 0.45;
|
||
}
|
||
</style>
|