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>
This commit is contained in:
jacobxu666
2026-06-25 01:07:21 +08:00
co-authored by Claude
parent c062dee3e6
commit 5f1ab737e9
25 changed files with 2110 additions and 390 deletions
+204 -21
View File
@@ -1,9 +1,19 @@
<template>
<view class="page">
<!-- 搜索栏 -->
<view class="search-bar">
<uni-search-bar v-model="keyword" placeholder="搜索年卡" @confirm="handleSearch" @clear="handleSearch" />
<!-- 顶部风景头图 + 标题 -->
<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
@@ -16,11 +26,24 @@
<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"><text>加载中...</text></view>
<view v-else-if="cardList.length === 0">
<empty-state icon="🎫" text="暂无符合条件的年卡" />
<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" />
@@ -34,7 +57,6 @@ import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getCardList } from '../../../../api/card'
import CardItem from '../../../../components/card-item.vue'
import EmptyState from '../../../../components/empty-state.vue'
import type { CardInfo } from '../../../../api/types'
const cardList = ref<CardInfo[]>([])
@@ -42,7 +64,6 @@ const loading = ref(false)
const keyword = ref('')
const currentCategory = ref('')
/** 分类选项 */
const categories = [
{ label: '全部', value: '' },
{ label: '旅游年卡', value: 'travel' },
@@ -50,7 +71,6 @@ const categories = [
{ label: '情侣年卡', value: 'couple' }
]
/** 获取年卡列表 */
async function fetchCardList(params?: { keyword?: string; category?: string }) {
loading.value = true
try {
@@ -60,34 +80,197 @@ async function fetchCardList(params?: { keyword?: string; category?: string }) {
}
}
/** 页面加载时获取年卡列表 */
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 scoped>
.page { min-height: 100vh; background: #f5f5f5; }
.search-bar { background: #fff; padding: 12rpx 20rpx; }
.filter-bar { display: flex; gap: 16rpx; padding: 16rpx 20rpx; background: #fff; border-bottom: 1rpx solid #f0f0f0; }
.filter-tag { padding: 10rpx 24rpx; border-radius: 30rpx; font-size: 24rpx; color: #666; background: #f5f5f5; }
.filter-tag.active { background: #4A90D9; color: #fff; }
.card-list { padding: 20rpx; }
.loading { text-align: center; padding: 60rpx; color: #999; font-size: 28rpx; }
<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>