Files
pingxiangcard/src/packages/user/pages/card/list.vue
T
jacobxu666andClaude 5f1ab737e9 feat: UI redesign with uv-ui color system and landscape imagery
- Replace all colors with uv-ui design tokens (primary #3c9cff, warning #f9ae3d, etc.)
- Hero section: Wugong Mountain landscape photo with dark overlay
- Super Player card: photo background, large typography, warning-color price/CTA, value comparison (¥198 vs ¥3800+)
- Spots section: landscape thumbnails instead of CSS gradients, warning-color prices
- Profile page: landscape hero header, CSS menu icons, floating card layout
- Card list page: landscape hero header, filter pills, empty state design
- TabBar: rename '入园码' to '旅游卡', Lucide-style PNG icons (home/card/profile)
- Typography: corrected font weights (semibold for prices, medium for labels)
- Remove broken font-loader (lxgw-wen-kai CDN 404)
- Skip wx.getPrivacySetting in devtools to avoid framework timeout
- Replace all inline SVG with CSS-only icons for mini-program compatibility
- Add design system files (styles/, components/, animations)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 01:07:21 +08:00

277 lines
5.7 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 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>