- 详情页:地图按钮改为复制地址,电话点击直接拨号 - 详情页:移除免X次/年徽章,增加信息卡片与卡种卡片间距 - 详情页:运营状态实时判断(根据营业时间自动显示营业中/已闭园) - 详情页:轮播圆点仅多图时显示,加大尺寸+投影,z-index置顶 - 详情页:景区电话箭头靠右对齐 - 首页:热门景点两列布局改用padding方案,修复小屏手机单列显示 - 代码质量:card/order API移入分包,消除主包未使用JS文件 - 代码质量:manifest启用minified和lazyCodeLoading - 构建:vite配置静默Sass弃用警告 Co-Authored-By: Claude <noreply@anthropic.com>
277 lines
5.7 KiB
Vue
277 lines
5.7 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- 顶部风景头图 + 标题 -->
|
||
<view class="list-hero">
|
||
<image
|
||
class="hero-bg"
|
||
src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?fm=jpg&q=80&w=750&h=300&fit=crop"
|
||
mode="aspectFill"
|
||
/>
|
||
<view class="hero-overlay" />
|
||
<view class="hero-inner">
|
||
<text class="hero-title">旅游卡</text>
|
||
<text class="hero-sub">畅游赣湘,一卡在手</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分类筛选 -->
|
||
<view class="filter-bar">
|
||
<view
|
||
v-for="cat in categories"
|
||
:key="cat.value"
|
||
class="filter-tag"
|
||
:class="{ active: currentCategory === cat.value }"
|
||
@click="selectCategory(cat.value)"
|
||
>
|
||
<text>{{ cat.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 搜索栏 -->
|
||
<view class="search-bar">
|
||
<uni-search-bar v-model="keyword" placeholder="搜索景区、年卡" @confirm="handleSearch" @clear="handleSearch" />
|
||
</view>
|
||
|
||
<!-- 年卡列表 -->
|
||
<view class="card-list">
|
||
<view v-if="loading" class="loading">
|
||
<view class="loading-dot" />
|
||
<text>加载中...</text>
|
||
</view>
|
||
<view v-else-if="cardList.length === 0" class="empty-state">
|
||
<view class="empty-icon">
|
||
<view class="icon-ticket" />
|
||
</view>
|
||
<text class="empty-text">暂无符合条件的年卡</text>
|
||
<text class="empty-hint">换个关键词试试</text>
|
||
</view>
|
||
<view v-else>
|
||
<card-item v-for="card in cardList" :key="card.id" :card="card" @click="goDetail" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { getCardList } from '../../api/card'
|
||
import CardItem from '../../../../components/card-item.vue'
|
||
import type { CardInfo } from '../../../../api/types'
|
||
|
||
const cardList = ref<CardInfo[]>([])
|
||
const loading = ref(false)
|
||
const keyword = ref('')
|
||
const currentCategory = ref('')
|
||
|
||
const categories = [
|
||
{ label: '全部', value: '' },
|
||
{ label: '旅游年卡', value: 'travel' },
|
||
{ label: '亲子年卡', value: 'family' },
|
||
{ label: '情侣年卡', value: 'couple' }
|
||
]
|
||
|
||
async function fetchCardList(params?: { keyword?: string; category?: string }) {
|
||
loading.value = true
|
||
try {
|
||
cardList.value = await getCardList(params)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onLoad(() => {
|
||
fetchCardList()
|
||
})
|
||
|
||
function handleSearch() {
|
||
fetchCardList({ keyword: keyword.value, category: currentCategory.value })
|
||
}
|
||
|
||
function selectCategory(value: string) {
|
||
currentCategory.value = value
|
||
handleSearch()
|
||
}
|
||
|
||
function goDetail(card: CardInfo) {
|
||
uni.navigateTo({ url: `/packages/user/pages/card/detail?id=${card.id}` })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background: $uv-bg-color;
|
||
}
|
||
|
||
/* Hero header */
|
||
.list-hero {
|
||
position: relative;
|
||
height: 280rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.hero-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(
|
||
180deg,
|
||
rgba(48, 49, 51, 0.3) 0%,
|
||
rgba(48, 49, 51, 0.6) 100%
|
||
);
|
||
}
|
||
|
||
.hero-inner {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: $spacing-12 $spacing-4 $spacing-4;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.hero-title {
|
||
font-family: $font-family-display;
|
||
font-size: $font-size-2xl;
|
||
font-weight: $font-weight-bold;
|
||
color: #fff;
|
||
line-height: $line-height-tight;
|
||
margin-bottom: $spacing-1;
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.hero-sub {
|
||
font-size: $font-size-sm;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
/* Filter bar */
|
||
.filter-bar {
|
||
display: flex;
|
||
gap: $spacing-2;
|
||
padding: $spacing-3 $spacing-3 $spacing-1;
|
||
background: $uv-bg-color;
|
||
}
|
||
|
||
.filter-tag {
|
||
padding: $spacing-1 $spacing-3;
|
||
border-radius: $radius-full;
|
||
font-size: $font-size-sm;
|
||
color: $uv-content-color;
|
||
background: $color-card;
|
||
border: $border-width solid $uv-border-color;
|
||
font-weight: $font-weight-medium;
|
||
transition: all $transition-fast;
|
||
|
||
&.active {
|
||
background: $uv-primary;
|
||
color: #fff;
|
||
border-color: $uv-primary;
|
||
font-weight: $font-weight-semibold;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.96);
|
||
}
|
||
}
|
||
|
||
/* Search bar */
|
||
.search-bar {
|
||
padding: $spacing-1 $spacing-3 $spacing-2;
|
||
background: $uv-bg-color;
|
||
}
|
||
|
||
/* Card list */
|
||
.card-list {
|
||
padding: 0 $spacing-3 $spacing-3;
|
||
}
|
||
|
||
/* Loading */
|
||
.loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: $spacing-10 0;
|
||
gap: $spacing-2;
|
||
|
||
text {
|
||
font-size: $font-size-sm;
|
||
color: $uv-tips-color;
|
||
}
|
||
}
|
||
|
||
.loading-dot {
|
||
width: 16rpx;
|
||
height: 16rpx;
|
||
border-radius: 50%;
|
||
background: $uv-primary;
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
||
50% { opacity: 1; transform: scale(1.2); }
|
||
}
|
||
|
||
/* Empty state */
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: $spacing-10 0;
|
||
}
|
||
|
||
.empty-icon {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
border-radius: 50%;
|
||
background: $uv-primary-light;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: $spacing-3;
|
||
}
|
||
|
||
.icon-ticket {
|
||
width: 40rpx;
|
||
height: 28rpx;
|
||
border: 3rpx solid $uv-primary;
|
||
border-radius: 6rpx;
|
||
position: relative;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: -3rpx;
|
||
right: -3rpx;
|
||
height: 3rpx;
|
||
background: $uv-primary;
|
||
transform: translateY(-50%);
|
||
}
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: $font-size-base;
|
||
color: $uv-main-color;
|
||
font-weight: $font-weight-medium;
|
||
margin-bottom: $spacing-1;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: $font-size-sm;
|
||
color: $uv-tips-color;
|
||
}
|
||
</style>
|