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
+17 -11
View File
@@ -4,15 +4,20 @@ import { useUserStore } from './store/user'
onLaunch(() => {
// #ifdef MP-WEIXIN
wx.getPrivacySetting({
success: (res: any) => {
if (res.needAuthorization) {
uni.setStorageSync('needPrivacy', true)
} else {
uni.removeStorageSync('needPrivacy')
}
}
})
// 仅真机检查隐私协议,模拟器中跳过(getPrivacySetting 在模拟器会触发框架级 timeout)
const sysInfo = uni.getSystemInfoSync()
if (sysInfo.platform !== 'devtools' && wx.getPrivacySetting) {
wx.getPrivacySetting({
success: (res: any) => {
if (res.needAuthorization) {
uni.setStorageSync('needPrivacy', true)
} else {
uni.removeStorageSync('needPrivacy')
}
},
fail: () => {},
})
}
// #endif
const userStore = useUserStore()
@@ -25,9 +30,10 @@ onLaunch(() => {
})
</script>
<style>
<style lang="scss">
@import './styles/main.scss';
page {
min-height: 100vh;
background-color: #f5f5f5;
}
</style>
+123
View File
@@ -0,0 +1,123 @@
<template>
<view
class="design-button"
:class="[
`design-button--${variant}`,
`design-button--${size}`,
{ 'design-button--block': block, 'design-button--disabled': disabled, 'design-button--loading': loading }
]"
@tap="handleTap"
>
<view v-if="loading" class="design-button__spinner" />
<slot v-else />
</view>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
variant?: 'primary' | 'secondary' | 'ghost' | 'outline'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
block?: boolean
}>(), {
variant: 'primary',
size: 'md',
disabled: false,
loading: false,
block: false,
})
const emit = defineEmits<{
(e: 'click', event: any): void
}>()
function handleTap(event: any) {
if (props.disabled || props.loading) return
emit('click', event)
}
</script>
<style lang="scss" scoped>
.design-button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: $radius-base;
font-family: $font-family-body;
font-weight: $font-weight-semibold;
transition: all $transition-fast;
position: relative;
overflow: hidden;
&:active:not(.design-button--disabled):not(.design-button--loading) {
transform: scale(0.98);
}
// --- Variants ---
&--primary {
background: $gradient-accent;
color: $color-accent-foreground;
box-shadow: $shadow-sm;
}
&--secondary {
background: $color-muted;
color: $color-foreground;
}
&--ghost {
background: transparent;
color: $color-accent;
}
&--outline {
background: transparent;
border: 2rpx solid $color-border;
color: $color-foreground;
}
// --- Sizes ---
&--sm {
height: 64rpx;
padding: 0 $spacing-4;
font-size: $font-size-sm;
border-radius: $radius-sm;
}
&--md {
height: 88rpx;
padding: 0 $spacing-6;
font-size: $font-size-base;
}
&--lg {
height: 104rpx;
padding: 0 $spacing-8;
font-size: $font-size-lg;
border-radius: $radius-lg;
}
// --- States ---
&--block {
width: 100%;
}
&--disabled {
opacity: 0.5;
}
&--loading {
opacity: 0.8;
}
&__spinner {
width: 32rpx;
height: 32rpx;
border: 3rpx solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: rotate 0.8s linear infinite;
}
}
</style>
+84
View File
@@ -0,0 +1,84 @@
<template>
<!-- featured variant 使用嵌套 div 实现渐变边框 -->
<view
v-if="variant === 'featured'"
class="design-card design-card--featured"
:class="[`design-card--padding-${padding}`]"
@tap="handleTap"
>
<view class="design-card__inner">
<slot />
</view>
</view>
<view
v-else
class="design-card"
:class="[
`design-card--${variant}`,
`design-card--padding-${padding}`
]"
@tap="handleTap"
>
<slot />
</view>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
variant?: 'default' | 'featured' | 'interactive'
padding?: 'none' | 'sm' | 'md' | 'lg'
}>(), {
variant: 'default',
padding: 'md',
})
const emit = defineEmits<{
(e: 'click', event: any): void
}>()
function handleTap(event: any) {
emit('click', event)
}
</script>
<style lang="scss" scoped>
.design-card {
background: $color-card;
border-radius: $radius-lg;
// --- Variants ---
&--default {
box-shadow: $shadow-base;
}
&--interactive {
box-shadow: $shadow-base;
transition: all $transition-fast;
&:active {
transform: scale(0.98);
box-shadow: $shadow-sm;
}
}
// Featured: 嵌套 div 渐变边框
&--featured {
background: $gradient-accent;
padding: 2rpx;
box-shadow: $shadow-lg;
.design-card__inner {
background: $color-card;
border-radius: calc(#{$radius-lg} - 2rpx);
height: 100%;
}
}
// --- Padding ---
&--padding-none { padding: 0; }
&--padding-sm { padding: $spacing-3; }
&--padding-md { padding: $spacing-5; }
&--padding-lg { padding: $spacing-8; }
}
</style>
+122
View File
@@ -0,0 +1,122 @@
<template>
<view
class="design-input"
:class="{ 'design-input--error': error, 'design-input--disabled': disabled }"
>
<text v-if="label" class="design-input__label">{{ label }}</text>
<textarea
v-if="type === 'textarea'"
class="design-input__field design-input__textarea"
:placeholder="placeholder"
:value="value"
:disabled="disabled"
:maxlength="maxlength"
placeholder-class="design-input__placeholder"
@input="onInput"
/>
<input
v-else
class="design-input__field"
:type="type"
:password="type === 'password'"
:placeholder="placeholder"
:value="value"
:disabled="disabled"
:maxlength="maxlength"
placeholder-class="design-input__placeholder"
@input="onInput"
/>
<text v-if="error && errorMessage" class="design-input__error-msg">
{{ errorMessage }}
</text>
</view>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
type?: 'text' | 'password' | 'number' | 'textarea'
label?: string
placeholder?: string
value?: string | number
disabled?: boolean
error?: boolean
errorMessage?: string
maxlength?: number
}>(), {
type: 'text',
placeholder: '',
value: '',
disabled: false,
error: false,
errorMessage: '',
maxlength: 140,
})
const emit = defineEmits<{
(e: 'update:value', value: string): void
}>()
function onInput(event: any) {
emit('update:value', event.detail.value)
}
</script>
<style lang="scss" scoped>
.design-input {
display: flex;
flex-direction: column;
gap: $spacing-1;
&__label {
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: $color-foreground;
margin-bottom: $spacing-1;
}
&__field {
width: 100%;
height: 88rpx;
padding: 0 $spacing-4;
background: $color-card;
border: 2rpx solid $color-border;
border-radius: $radius-base;
font-family: $font-family-body;
font-size: $font-size-base;
color: $color-foreground;
transition: border-color $transition-fast;
}
&__textarea {
height: 200rpx;
padding: $spacing-3 $spacing-4;
}
&__placeholder {
color: $color-muted-foreground;
}
&__error-msg {
font-size: $font-size-xs;
color: $color-error;
margin-top: $spacing-1;
}
// --- States ---
&--error {
.design-input__field {
border-color: $color-error;
}
}
&--disabled {
opacity: 0.5;
.design-input__field {
background: $color-muted;
}
}
}
</style>
+43
View File
@@ -0,0 +1,43 @@
<template>
<view class="section-label">
<view class="section-label__dot" :style="{ backgroundColor: dotColor }" />
<text class="section-label__text">{{ text }}</text>
</view>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
text: string
dotColor?: string
}>(), {
dotColor: '#0052FF',
})
</script>
<style lang="scss" scoped>
.section-label {
display: inline-flex;
align-items: center;
gap: $spacing-2;
background: rgba(0, 82, 255, 0.05);
border: 1rpx solid rgba(0, 82, 255, 0.15);
border-radius: $radius-full;
padding: $spacing-1 $spacing-3;
&__dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: $color-accent;
animation: pulse 2s ease-in-out infinite;
}
&__text {
font-family: $font-family-mono;
font-size: $font-size-xs;
font-weight: $font-weight-semibold;
color: $color-accent;
letter-spacing: 0.1em;
}
}
</style>
+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>
+5 -5
View File
@@ -78,11 +78,11 @@
"navigationBarTextStyle": "black",
"navigationBarTitleText": "爱上萍乡",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F5F5F5"
"backgroundColor": "#FAFAFA"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#4A90D9",
"selectedColor": "#3c9cff",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
@@ -94,9 +94,9 @@
},
{
"pagePath": "pages/qrcode/index",
"text": "入园码",
"iconPath": "static/tabbar/qrcode.png",
"selectedIconPath": "static/tabbar/qrcode-active.png"
"text": "旅游卡",
"iconPath": "static/tabbar/card.png",
"selectedIconPath": "static/tabbar/card-active.png"
},
{
"pagePath": "pages/profile/index",
+497 -232
View File
@@ -1,92 +1,98 @@
<template>
<view class="page">
<!-- 头部区域:背景图 + 渐变遮罩 + 标题 + 搜索栏 -->
<view class="hero">
<!-- Hero: mountain landscape photo -->
<view class="hero animate-fade-in-up">
<image
class="hero-bg"
src="https://images.pexels.com/photos/28594448/pexels-photo-28594448.jpeg?auto=compress&cs=tinysrgb&w=750"
mode="aspectFill"
/>
<view class="hero-overlay" />
<view class="hero-content">
<view class="hero-inner">
<text class="hero-title">爱上萍乡</text>
<text class="hero-subtitle">赣湘之间 · 发现旅途美好</text>
<view class="search-bar" @click="goSearch">
<view class="search-icon-wrap">
<view class="search-icon-circle" />
</view>
<text class="search-placeholder">搜索景区、活动...</text>
</view>
<text class="hero-sub">赣湘之间 · 发现旅途美好</text>
</view>
</view>
<scroll-view scroll-y class="scroll-content">
<scroll-view scroll-y class="main-scroll">
<!-- 公告栏 -->
<view class="notice-bar" v-if="announcements.length">
<view class="notice-label">
<view class="notice-dot" />
<text class="notice-text">公告</text>
<view v-if="announcements.length" class="notice animate-fade-in-up stagger-1">
<view class="notice-tag">
<view class="notice-pip animate-pulse" />
<text class="notice-label">公告</text>
</view>
<swiper class="notice-swiper" vertical autoplay circular :interval="3000" :duration="500">
<swiper-item v-for="item in announcements" :key="item.id">
<text class="notice-content">{{ item.content }}</text>
<text class="notice-text">{{ item.content }}</text>
</swiper-item>
</swiper>
</view>
<!-- 卡片销售宣传 -->
<view class="card-section">
<view class="promo-card promo-card-gx" @click="goCardDetail('gx')">
<view class="promo-card-badge">工会会员专属</view>
<text class="promo-card-name">赣湘全域旅游年卡</text>
<view class="promo-card-price-row">
<text class="promo-card-currency">¥</text>
<text class="promo-card-price">300</text>
<text class="promo-card-unit">/套卡(2张)</text>
</view>
<view class="promo-card-features">
<view class="promo-card-feature"><view class="feature-dot" /><text>22家景区免门票</text></view>
<view class="promo-card-feature"><view class="feature-dot" /><text>武功山·方特·君澜温泉</text></view>
<view class="promo-card-feature"><view class="feature-dot" /><text>覆盖萍乡/长沙/株洲/湘潭</text></view>
</view>
<view class="promo-card-btn">
<text class="promo-card-btn-text">立即抢购</text>
<view class="btn-arrow" />
</view>
</view>
<view class="promo-card promo-card-super" @click="goCardDetail('super')">
<view class="promo-card-badge">大学生·市民专属</view>
<text class="promo-card-name">超级玩家卡</text>
<view class="promo-card-price-row">
<text class="promo-card-currency">¥</text>
<text class="promo-card-price">198</text>
</view>
<view class="promo-card-features">
<view class="promo-card-feature"><view class="feature-dot" /><text>40家景区免门票</text></view>
<view class="promo-card-feature"><view class="feature-dot" /><text>武功山·方特·韶山·滴水洞</text></view>
<view class="promo-card-feature"><view class="feature-dot" /><text>含外骨骼登山·青年旅社等</text></view>
</view>
<view class="promo-card-btn">
<text class="promo-card-btn-text">立即抢购</text>
<view class="btn-arrow" />
<!-- 旅游卡 — 大字报风格 -->
<view class="promo-wrap animate-fade-in-up stagger-2">
<view class="promo" @click="goCardDetail()">
<image
class="promo-bg"
src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=750&h=400&fit=crop"
mode="aspectFill"
/>
<view class="promo-overlay" />
<view class="promo-content">
<view class="promo-badge">大学生·市民专属</view>
<text class="promo-name">超级玩家卡</text>
<view class="promo-price-row">
<view class="promo-price-block">
<text class="promo-currency">¥</text>
<text class="promo-price">198</text>
</view>
<view class="promo-value-block">
<text class="promo-value-label">景区总价值</text>
<text class="promo-value">¥3800+</text>
</view>
</view>
<text class="promo-desc">40家景区免门票 · 武功山·方特·韶山·滴水洞</text>
<view class="promo-cta">
<text class="cta-label">立即抢购</text>
<view class="cta-arrow" />
</view>
</view>
</view>
</view>
<!-- 热门景点列表 -->
<!-- 热门景点 -->
<view class="spots-section">
<view class="spots-header">
<view class="spots-head animate-fade-in-up stagger-3">
<text class="spots-title">热门景点</text>
<view class="spots-more" @click="goMore">
<text>查看全部</text>
<view class="chevron-right" />
<text class="spots-more-text">查看全部</text>
<view class="chevron" />
</view>
</view>
<view class="spot-card" v-for="spot in scenicSpots" :key="spot.id" @click="goSpot(spot)">
<view class="spot-image" :style="{ background: getGradient(spot.region) }">
<view class="spot-region-tag">{{ spot.region }}</view>
</view>
<view class="spot-info">
<text class="spot-name">{{ spot.name }}</text>
<view class="spot-meta">
<text class="spot-price">¥{{ spot.price }}</text>
<view class="spot-free-tag">持卡免门票</view>
<view class="spots-grid">
<view
v-for="(spot, idx) in scenicSpots.slice(0, 6)"
:key="spot.id"
class="spot animate-fade-in-up"
:class="`stagger-${Math.min(idx + 4, 10)}`"
@click="goSpot(spot)"
>
<view class="spot-cover">
<image class="spot-img" :src="getSpotImage(spot.id)" mode="aspectFill" />
<view class="spot-region">
<view class="pin-dot" />
<text>{{ spot.region }}</text>
</view>
</view>
<view class="spot-body">
<text class="spot-name">{{ spot.name }}</text>
<view class="spot-meta">
<text class="spot-price">¥{{ spot.price }}</text>
<view class="spot-free">
<view class="free-dot" />
<text>免门票</text>
</view>
</view>
</view>
</view>
</view>
@@ -107,202 +113,461 @@ const announcements = ref([
{ id: 3, content: '武功山金顶景区已开放,持卡可免费入园1次。' },
])
/** 区域对应的渐变色(景点图片占位) */
const regionGradients: Record<string, string> = {
'萍乡': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'长沙': 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
'株洲': 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
'湘潭': 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
/** 景点山水风景图 */
const spotImages: Record<number, string> = {
1: 'https://images.unsplash.com/photo-1513415564515-763d91423bdd?fm=jpg&q=80&w=400&h=250&fit=crop',
2: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=400&h=250&fit=crop',
3: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?fm=jpg&q=80&w=400&h=250&fit=crop',
4: 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?fm=jpg&q=80&w=400&h=250&fit=crop',
5: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?fm=jpg&q=80&w=400&h=250&fit=crop',
6: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?fm=jpg&q=80&w=400&h=250&fit=crop',
}
function getGradient(region: string): string {
return regionGradients[region] || regionGradients['萍乡']
function getSpotImage(id: number): string {
return spotImages[id] || `https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=400&h=250&fit=crop`
}
/** 跳转搜索(暂不实现) */
function goSearch() {
uni.showToast({ title: '搜索功能开发中', icon: 'none' })
}
/** 跳转卡片详情 */
function goCardDetail(type: 'gx' | 'super') {
function goCardDetail() {
uni.navigateTo({ url: '/packages/user/pages/card/list' })
}
/** 跳转景点详情 */
function goSpot(spot: ScenicSpotItem) {
uni.showToast({ title: `${spot.name} 详情开发中`, icon: 'none' })
}
/** 查看全部景点 */
function goMore() {
uni.showToast({ title: '景点列表开发中', icon: 'none' })
}
</script>
<style scoped>
.page { display: flex; flex-direction: column; min-height: 100vh; background: #f5f5f5; }
<style lang="scss" scoped>
/* ============================================
Page
============================================ */
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-background;
}
/* === 头部区域 === */
.hero { position: relative; height: 360rpx; overflow: hidden; }
.hero-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.6) 100%);
z-index: 1;
}
.hero::before {
content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(135deg, #1a237e 0%, #4a148c 50%, #e65100 100%);
}
.hero-content {
position: relative; z-index: 2;
padding: 80rpx 30rpx 30rpx;
display: flex; flex-direction: column;
}
.hero-title { font-size: 42rpx; font-weight: bold; color: #fff; margin-bottom: 8rpx; }
.hero-subtitle { font-size: 24rpx; color: rgba(255,255,255,0.8); margin-bottom: 24rpx; }
/* 搜索栏 */
.search-bar {
display: flex; align-items: center;
background: rgba(255,255,255,0.95); border-radius: 40rpx;
padding: 18rpx 24rpx;
min-height: 88rpx;
transition: opacity 0.2s;
}
.search-bar:active { opacity: 0.7; }
.search-icon-wrap {
width: 36rpx; height: 36rpx; margin-right: 12rpx;
display: flex; align-items: center; justify-content: center;
}
.search-icon-circle {
width: 24rpx; height: 24rpx;
border: 3rpx solid #999; border-radius: 50%;
/* ============================================
Hero — mountain landscape photo
============================================ */
.hero {
position: relative;
}
.search-icon-circle::after {
content: ''; position: absolute;
width: 3rpx; height: 10rpx;
background: #999; border-radius: 2rpx;
bottom: -10rpx; right: -4rpx;
transform: rotate(-45deg);
}
.search-placeholder { font-size: 26rpx; color: #999; }
/* === 公告栏 === */
.notice-bar {
display: flex; align-items: center;
background: #fff; padding: 20rpx 24rpx; margin: 0;
border-bottom: 1rpx solid #f0f0f0;
}
.notice-label { display: flex; align-items: center; margin-right: 16rpx; flex-shrink: 0; }
.notice-dot {
width: 14rpx; height: 14rpx; border-radius: 50%;
background: #E74C3C; margin-right: 8rpx;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.notice-text { font-size: 24rpx; color: #E74C3C; font-weight: 500; }
.notice-swiper { flex: 1; height: 40rpx; }
.notice-content { font-size: 24rpx; color: #666; line-height: 40rpx; }
/* === 卡片销售宣传 === */
.card-section { padding: 24rpx; display: flex; flex-direction: column; gap: 24rpx; }
.promo-card {
position: relative; border-radius: 24rpx; padding: 40rpx 32rpx;
height: 340rpx;
overflow: hidden;
transition: transform 0.15s ease;
}
.promo-card:active { transform: scale(0.98); }
.promo-card-gx {
background: linear-gradient(135deg, #1a237e 0%, #283593 40%, #ffd54f 100%);
.hero-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.promo-card-super {
background: linear-gradient(135deg, #e65100 0%, #bf360c 40%, #7b1fa2 100%);
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.45) 0%,
rgba(0, 0, 0, 0.3) 40%,
rgba(0, 0, 0, 0.65) 100%
);
}
.promo-card-badge {
.hero-inner {
position: relative;
z-index: 2;
padding: $spacing-16 $spacing-4 $spacing-4;
display: flex;
flex-direction: column;
}
.hero-title {
font-family: $font-family-display;
font-size: $font-size-3xl;
font-weight: $font-weight-bold;
color: #fff;
line-height: $line-height-tight;
margin-bottom: $spacing-1;
text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
}
.hero-sub {
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 4rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
/* ============================================
CSS-only icons
============================================ */
.notice-pip {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: $color-accent;
flex-shrink: 0;
}
.chevron {
width: 10rpx;
height: 10rpx;
border-top: 2rpx solid $color-muted-foreground;
border-right: 2rpx solid $color-muted-foreground;
transform: rotate(45deg);
flex-shrink: 0;
}
.pin-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: #fff;
flex-shrink: 0;
}
.free-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: $color-accent;
flex-shrink: 0;
}
/* ============================================
Notice bar
============================================ */
.notice {
display: flex;
align-items: center;
background: $color-card;
padding: $spacing-2 $spacing-3;
border-bottom: $border-width solid $color-border;
}
.notice-tag {
display: flex;
align-items: center;
margin-right: $spacing-2;
flex-shrink: 0;
gap: $spacing-1;
}
.notice-label {
font-size: $font-size-xs;
color: $color-accent;
font-weight: $font-weight-medium;
}
.notice-swiper {
flex: 1;
height: 40rpx;
}
.notice-text {
font-size: $font-size-sm;
color: $color-muted-foreground;
line-height: 40rpx;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ============================================
Promo card — landscape photo hero card
============================================ */
.promo-wrap {
padding: $spacing-3;
}
.promo {
position: relative;
border-radius: $radius-xl;
overflow: hidden;
min-height: 400rpx;
transition: transform $transition-fast;
&:active {
transform: scale(0.98);
}
}
.promo-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.promo-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
160deg,
rgba($color-foreground, 0.88) 0%,
rgba($color-foreground, 0.6) 55%,
rgba($color-foreground, 0.3) 100%
);
}
.promo-content {
position: relative;
z-index: 2;
padding: $spacing-5 $spacing-4;
display: flex;
flex-direction: column;
}
.promo-badge {
display: inline-block;
background: rgba(255,255,255,0.2); border-radius: 20rpx;
padding: 6rpx 16rpx; margin-bottom: 16rpx;
font-size: 22rpx; color: rgba(255,255,255,0.9);
}
.promo-card-name { font-size: 34rpx; font-weight: bold; color: #fff; display: block; margin-bottom: 12rpx; }
.promo-card-price-row { display: flex; align-items: baseline; margin-bottom: 20rpx; }
.promo-card-currency { font-size: 32rpx; color: #fff; font-weight: bold; }
.promo-card-price { font-size: 64rpx; color: #fff; font-weight: bold; line-height: 1; }
.promo-card-unit { font-size: 24rpx; color: rgba(255,255,255,0.8); margin-left: 8rpx; }
.promo-card-features { display: flex; flex-direction: column; gap: 10rpx; margin-bottom: 24rpx; }
.promo-card-feature {
display: flex; align-items: center; gap: 10rpx;
font-size: 24rpx; color: rgba(255,255,255,0.9);
}
.feature-dot {
width: 8rpx; height: 8rpx; border-radius: 50%;
background: rgba(255,255,255,0.7); flex-shrink: 0;
}
.promo-card-btn {
display: flex; align-items: center; justify-content: center; gap: 8rpx;
background: #fff; border-radius: 44rpx;
padding: 22rpx 0;
min-height: 88rpx;
transition: opacity 0.2s;
}
.promo-card-btn:active { opacity: 0.8; }
.promo-card-btn-text { font-size: 28rpx; font-weight: bold; color: #333; }
.btn-arrow {
width: 12rpx; height: 12rpx;
border-top: 3rpx solid #333;
border-right: 3rpx solid #333;
transform: rotate(45deg);
align-self: flex-start;
background: rgba($color-accent, 0.2);
border: $border-width solid rgba($color-accent, 0.4);
border-radius: $radius-full;
padding: 6rpx $spacing-3;
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.95);
font-weight: $font-weight-semibold;
margin-bottom: $spacing-3;
}
/* === 热门景点列表 === */
.spots-section { padding: 24rpx; }
.spots-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 20rpx;
.promo-name {
font-family: $font-family-display;
font-size: 56rpx;
font-weight: $font-weight-bold;
color: #fff;
display: block;
margin-bottom: $spacing-3;
text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
letter-spacing: 2rpx;
}
.spots-title { font-size: 34rpx; font-weight: bold; color: #333; }
.promo-price-row {
display: flex;
align-items: flex-end;
gap: $spacing-4;
margin-bottom: $spacing-3;
}
.promo-price-block {
display: flex;
align-items: baseline;
}
.promo-currency {
font-size: $font-size-2xl;
font-weight: $font-weight-bold;
color: $uv-warning;
}
.promo-price {
font-size: 96rpx;
font-weight: $font-weight-bold;
color: $uv-warning;
line-height: 0.85;
letter-spacing: -4rpx;
}
.promo-value-block {
display: flex;
flex-direction: column;
padding-bottom: $spacing-2;
}
.promo-value-label {
font-size: $font-size-xs;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 4rpx;
}
.promo-value {
font-size: $font-size-xl;
color: rgba(255, 255, 255, 0.45);
text-decoration: line-through;
font-weight: $font-weight-medium;
}
.promo-desc {
font-size: $font-size-base;
color: rgba(255, 255, 255, 0.8);
display: block;
margin-bottom: $spacing-4;
line-height: $line-height-relaxed;
}
.promo-cta {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-2;
align-self: flex-start;
padding: $spacing-2 $spacing-8;
border-radius: $radius-full;
background: $uv-warning;
transition: transform $transition-fast;
&:active {
transform: scale(0.96);
}
.cta-label {
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-main-color;
}
.cta-arrow {
width: 10rpx;
height: 10rpx;
border-top: 3rpx solid $uv-main-color;
border-right: 3rpx solid $uv-main-color;
transform: rotate(45deg);
}
}
/* ============================================
Spots section
============================================ */
.spots-section {
padding: $spacing-3;
}
.spots-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $spacing-3;
}
.spots-title {
font-family: $font-family-display;
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: $color-foreground;
}
.spots-more {
display: flex; align-items: center; gap: 4rpx;
font-size: 24rpx; color: #999;
}
.chevron-right {
width: 10rpx; height: 10rpx;
border-top: 2rpx solid #999;
border-right: 2rpx solid #999;
transform: rotate(45deg);
display: flex;
align-items: center;
gap: $spacing-1;
}
.spot-card {
background: #fff; border-radius: 20rpx; overflow: hidden;
margin-bottom: 20rpx;
transition: transform 0.15s ease;
}
.spot-card:active { transform: scale(0.98); }
.spot-image {
width: 100%; height: 320rpx; position: relative;
}
.spot-region-tag {
position: absolute; top: 16rpx; left: 16rpx;
background: rgba(0,0,0,0.5); border-radius: 16rpx;
padding: 6rpx 16rpx;
font-size: 22rpx; color: #fff;
}
.spot-info { padding: 20rpx 24rpx; }
.spot-name { font-size: 30rpx; font-weight: 600; color: #333; margin-bottom: 12rpx; display: block; }
.spot-meta { display: flex; justify-content: space-between; align-items: center; }
.spot-price { font-size: 32rpx; color: #E74C3C; font-weight: bold; }
.spot-free-tag {
background: #E8F5E9; border-radius: 16rpx;
padding: 6rpx 16rpx;
font-size: 22rpx; color: #2E7D32; font-weight: 500;
.spots-more-text {
font-size: $font-size-sm;
color: $color-muted-foreground;
}
/* === 滚动区域 === */
.scroll-content { flex: 1; }
/* Spots grid — 2 column */
.spots-grid {
display: flex;
flex-wrap: wrap;
gap: $spacing-2;
}
.spot {
width: calc(50% - #{$spacing-1});
background: $color-card;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-sm;
transition: transform $transition-fast, box-shadow $transition-base;
&:active {
transform: scale(0.97);
box-shadow: $shadow-base;
}
}
.spot-cover {
width: 100%;
height: 220rpx;
position: relative;
background: $color-muted;
}
.spot-img {
width: 100%;
height: 100%;
}
.spot-region {
position: absolute;
top: $spacing-2;
left: $spacing-2;
display: flex;
align-items: center;
gap: 4rpx;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8rpx);
border-radius: $radius-sm;
padding: 4rpx $spacing-2;
text {
font-size: $font-size-xs;
color: #fff;
}
}
.spot-body {
padding: $spacing-2;
}
.spot-name {
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
color: $color-foreground;
display: block;
margin-bottom: $spacing-1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spot-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.spot-price {
font-size: $font-size-base;
color: $uv-warning;
font-weight: $font-weight-semibold;
}
.spot-free {
display: flex;
align-items: center;
gap: 4rpx;
background: rgba($color-accent, 0.08);
border-radius: $radius-sm;
padding: 4rpx $spacing-1;
text {
font-size: $font-size-xs;
color: $color-accent;
font-weight: $font-weight-medium;
}
}
/* ============================================
Scroll area
============================================ */
.main-scroll {
flex: 1;
}
</style>
+296 -44
View File
@@ -1,34 +1,57 @@
<template>
<view class="page">
<view class="user-card">
<view class="avatar-wrap">
<image class="avatar" :src="userStore.userInfo?.avatarUrl || defaultAvatar" mode="aspectFill" />
</view>
<view class="user-info">
<text class="nickname">{{ userStore.userInfo?.nickname || '未登录' }}</text>
<text class="user-desc" v-if="userStore.isLogin">欢迎使用爱上萍乡</text>
<text class="user-desc" v-else @click="goLogin">点击登录</text>
<!-- 头部:风景图 + 用户信息 -->
<view class="profile-hero">
<image
class="hero-bg"
src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=750&h=400&fit=crop"
mode="aspectFill"
/>
<view class="hero-overlay" />
<view class="hero-content">
<view class="avatar-wrap">
<image class="avatar" :src="userStore.userInfo?.avatarUrl || defaultAvatar" mode="aspectFill" />
</view>
<view class="user-info">
<text class="nickname">{{ userStore.userInfo?.nickname || '未登录' }}</text>
<text class="user-desc" v-if="userStore.isLogin">欢迎使用爱上萍乡</text>
<text class="user-desc user-desc--link" v-else @click="goLogin">点击登录 →</text>
</view>
</view>
</view>
<view class="menu-card">
<view class="menu-item" @click="goTo('/packages/user/pages/card/bind')">
<text class="menu-icon">🎫</text>
<text class="menu-text">我的年卡</text>
<text class="menu-arrow">›</text>
</view>
<view class="menu-item" @click="showComingSoon">
<text class="menu-icon">📦</text>
<text class="menu-text">订单管理</text>
<text class="menu-arrow">›</text>
</view>
<view class="menu-item" @click="showComingSoon">
<text class="menu-icon">📋</text>
<text class="menu-text">使用记录</text>
<text class="menu-arrow">›</text>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-card">
<view class="menu-item" @click="goTo('/packages/user/pages/card/bind')">
<view class="menu-icon-wrap menu-icon-wrap--primary">
<view class="icon-card" />
</view>
<text class="menu-text">我的年卡</text>
<view class="menu-chevron" />
</view>
<view class="menu-item" @click="showComingSoon">
<view class="menu-icon-wrap menu-icon-wrap--warning">
<view class="icon-order" />
</view>
<text class="menu-text">订单管理</text>
<view class="menu-chevron" />
</view>
<view class="menu-item menu-item--last" @click="showComingSoon">
<view class="menu-icon-wrap menu-icon-wrap--success">
<view class="icon-history" />
</view>
<text class="menu-text">使用记录</text>
<view class="menu-chevron" />
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="btn-area" v-if="userStore.isLogin">
<button class="btn-logout" @click="handleLogout">退出登录</button>
<view class="btn-logout" @click="handleLogout">
<text>退出登录</text>
</view>
</view>
</view>
</template>
@@ -39,16 +62,13 @@ import { useUserStore } from '../../store/user'
const userStore = useUserStore()
const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
/** 跳转登录页 */
function goLogin() { uni.navigateTo({ url: '/pages/login/login' }) }
/** 需要登录的页面跳转 */
function goTo(url: string) {
if (!userStore.isLogin) { uni.navigateTo({ url: '/pages/login/login' }); return }
uni.navigateTo({ url })
}
/** 退出登录 */
function handleLogout() {
uni.showModal({
title: '提示', content: '确定要退出登录吗?',
@@ -62,24 +82,256 @@ function handleLogout() {
})
}
/** 即将上线提示 */
function showComingSoon() { uni.showToast({ title: '即将上线,敬请期待', icon: 'none' }) }
</script>
<style scoped>
.page { min-height: 100vh; background: #f5f5f5; }
.user-card { background: linear-gradient(135deg, #4A90D9, #357ABD); padding: 50rpx 30rpx 40rpx; display: flex; align-items: center; }
.avatar-wrap { margin-right: 24rpx; }
.avatar { width: 120rpx; height: 120rpx; border-radius: 50%; border: 4rpx solid rgba(255,255,255,0.5); background: #eee; }
.user-info { display: flex; flex-direction: column; }
.nickname { font-size: 34rpx; font-weight: bold; color: #fff; margin-bottom: 8rpx; }
.user-desc { font-size: 24rpx; color: rgba(255,255,255,0.8); }
.menu-card { background: #fff; border-radius: 16rpx; margin: 20rpx; overflow: hidden; }
.menu-item { display: flex; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
.menu-item:last-child { border-bottom: none; }
.menu-icon { font-size: 36rpx; margin-right: 20rpx; }
.menu-text { flex: 1; font-size: 28rpx; color: #333; }
.menu-arrow { font-size: 36rpx; color: #ccc; }
.btn-area { padding: 40rpx 20rpx; }
.btn-logout { width: 100%; height: 88rpx; line-height: 88rpx; background: #fff; color: #E74C3C; border-radius: 44rpx; font-size: 30rpx; border: 1rpx solid #f0f0f0; }
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: $uv-bg-color;
}
/* Hero header */
.profile-hero {
position: relative;
height: 360rpx;
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.5) 0%,
rgba(48, 49, 51, 0.7) 100%
);
}
.hero-content {
position: relative;
z-index: 2;
display: flex;
align-items: center;
padding: $spacing-12 $spacing-4 $spacing-4;
}
.avatar-wrap {
margin-right: $spacing-3;
flex-shrink: 0;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 4rpx solid rgba(255, 255, 255, 0.4);
background: $uv-info-light;
}
.user-info {
display: flex;
flex-direction: column;
}
.nickname {
font-family: $font-family-display;
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: #fff;
margin-bottom: $spacing-1;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.user-desc {
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.7);
&--link {
color: $uv-warning;
}
}
/* Menu section */
.menu-section {
padding: $spacing-3;
margin-top: -40rpx;
position: relative;
z-index: 3;
}
.menu-card {
background: $color-card;
border-radius: $radius-xl;
box-shadow: $shadow-base;
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
padding: $spacing-3 $spacing-4;
border-bottom: $border-width solid $uv-border-color;
transition: background $transition-fast;
&:active {
background: $uv-info-light;
}
&--last {
border-bottom: none;
}
}
.menu-icon-wrap {
width: 64rpx;
height: 64rpx;
border-radius: $radius-lg;
display: flex;
align-items: center;
justify-content: center;
margin-right: $spacing-3;
flex-shrink: 0;
&--primary {
background: $uv-primary-light;
}
&--warning {
background: $uv-warning-light;
}
&--success {
background: $uv-success-light;
}
}
/* CSS icons */
.icon-card {
width: 28rpx;
height: 20rpx;
border: 3rpx solid $uv-primary;
border-radius: 4rpx;
position: relative;
&::after {
content: '';
position: absolute;
top: 6rpx;
left: 0;
width: 100%;
height: 3rpx;
background: $uv-primary;
}
}
.icon-order {
width: 22rpx;
height: 26rpx;
border: 3rpx solid $uv-warning;
border-radius: 4rpx;
position: relative;
&::before {
content: '';
position: absolute;
top: 6rpx;
left: 4rpx;
width: 10rpx;
height: 3rpx;
background: $uv-warning;
}
&::after {
content: '';
position: absolute;
top: 14rpx;
left: 4rpx;
width: 8rpx;
height: 3rpx;
background: $uv-warning;
}
}
.icon-history {
width: 24rpx;
height: 24rpx;
border: 3rpx solid $uv-success;
border-radius: 50%;
position: relative;
&::before {
content: '';
position: absolute;
top: 4rpx;
left: 8rpx;
width: 3rpx;
height: 8rpx;
background: $uv-success;
}
&::after {
content: '';
position: absolute;
top: 9rpx;
left: 8rpx;
width: 6rpx;
height: 3rpx;
background: $uv-success;
}
}
.menu-text {
flex: 1;
font-size: $font-size-base;
color: $uv-main-color;
font-weight: $font-weight-medium;
}
.menu-chevron {
width: 10rpx;
height: 10rpx;
border-top: 2rpx solid $uv-light-color;
border-right: 2rpx solid $uv-light-color;
transform: rotate(45deg);
flex-shrink: 0;
}
/* Logout button */
.btn-area {
padding: $spacing-5 $spacing-3;
}
.btn-logout {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 88rpx;
background: $color-card;
border-radius: $radius-full;
box-shadow: $shadow-sm;
transition: transform $transition-fast;
&:active {
transform: scale(0.98);
}
text {
font-size: $font-size-base;
color: $uv-error;
font-weight: $font-weight-medium;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

+81
View File
@@ -0,0 +1,81 @@
// ============================================
// Keyframe Animations
// ============================================
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30rpx);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// === Animation utility classes ===
.animate-pulse {
animation: pulse 2s ease-in-out infinite;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-rotate {
animation: rotate 1s linear infinite;
}
.animate-fade-in-up {
animation: fadeInUp 0.4s ease-out forwards;
}
.animate-scale-in {
animation: scaleIn 0.3s ease-out forwards;
}
// Stagger delays for list entrance
@for $i from 1 through 10 {
.stagger-#{$i} {
animation-delay: #{$i * 0.08}s;
opacity: 0;
}
}
+66
View File
@@ -0,0 +1,66 @@
// ============================================
// SCSS Mixins
// ============================================
// === FLEX ===
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin flex-column {
display: flex;
flex-direction: column;
}
// === TYPOGRAPHY ===
@mixin text-display {
font-family: $font-family-display;
font-weight: $font-weight-bold;
line-height: $line-height-tight;
}
@mixin text-body {
font-family: $font-family-body;
font-weight: $font-weight-normal;
line-height: $line-height-normal;
}
@mixin text-mono {
font-family: $font-family-mono;
letter-spacing: 0.05em;
}
// === VISUAL ===
@mixin gradient-text {
background: $gradient-accent;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
@mixin card-base {
background: $color-card;
border-radius: $radius-lg;
box-shadow: $shadow-base;
}
// === INTERACTION ===
@mixin press-effect {
transition: transform $transition-fast, opacity $transition-fast;
&:active {
transform: scale(0.98);
opacity: 0.8;
}
}
+143
View File
@@ -0,0 +1,143 @@
// ============================================
// Design Tokens — Minimalist Modern
// ============================================
// === COLORS ===
// Brand accent — Electric Blue
$color-accent: #0052FF;
$color-accent-secondary: #4D7CFF;
$color-accent-foreground: #FFFFFF;
// Semantic
$color-success: #10B981;
$color-warning: #F59E0B;
$color-error: #EF4444;
// Neutrals
$color-background: #FAFAFA;
$color-foreground: #0F172A;
$color-muted: #F1F5F9;
$color-muted-foreground: #64748B;
$color-border: #E2E8F0;
$color-card: #FFFFFF;
$color-ring: #0052FF;
// Gradients
$gradient-accent: linear-gradient(135deg, #{$color-accent}, #{$color-accent-secondary});
// === TYPOGRAPHY ===
$font-family-display: 'Noto Serif SC', 'STSong', 'SimSun', serif;
$font-family-body: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'HarmonyOS Sans SC', 'Microsoft YaHei', sans-serif;
$font-family-mono: 'Menlo', 'Monaco', 'Courier New', monospace;
$font-size-xs: 20rpx;
$font-size-sm: 24rpx;
$font-size-base: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
$font-size-2xl: 44rpx;
$font-size-3xl: 56rpx;
$font-size-display: 72rpx;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
$line-height-tight: 1.2;
$line-height-normal: 1.5;
$line-height-relaxed: 1.75;
// === SPACING (rpx) ===
$spacing-0: 0;
$spacing-1: 8rpx;
$spacing-2: 16rpx;
$spacing-3: 24rpx;
$spacing-4: 32rpx;
$spacing-5: 40rpx;
$spacing-6: 48rpx;
$spacing-8: 64rpx;
$spacing-10: 80rpx;
$spacing-12: 96rpx;
$spacing-16: 128rpx;
$spacing-20: 160rpx;
// === BORDER RADIUS ===
$radius-sm: 8rpx;
$radius-base: 16rpx;
$radius-lg: 24rpx;
$radius-xl: 32rpx;
$radius-2xl: 48rpx;
$radius-full: 9999rpx;
// === SHADOWS ===
$shadow-sm: 0 2rpx 8rpx rgba(15, 23, 42, 0.04);
$shadow-base: 0 4rpx 16rpx rgba(15, 23, 42, 0.08);
$shadow-lg: 0 8rpx 32rpx rgba(15, 23, 42, 0.12);
$shadow-xl: 0 16rpx 48rpx rgba(15, 23, 42, 0.16);
$shadow-accent: 0 8rpx 24rpx rgba(0, 82, 255, 0.25);
// === TRANSITIONS ===
$transition-fast: 150ms ease;
$transition-base: 250ms ease;
$transition-slow: 400ms ease;
// === Z-INDEX ===
$z-base: 0;
$z-dropdown: 100;
$z-sticky: 200;
$z-modal: 300;
$z-toast: 400;
// === BORDER ===
$border-width: 1rpx;
// ============================================
// Legacy $uni-* compatibility (for uni-ui)
// ============================================
$uni-color-primary: $color-accent;
$uni-color-success: $color-success;
$uni-color-warning: $color-warning;
$uni-color-error: $color-error;
$uni-text-color: $color-foreground;
$uni-text-color-inverse: #FFFFFF;
$uni-text-color-grey: $color-muted-foreground;
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
$uni-bg-color: $color-background;
$uni-bg-color-grey: $color-muted;
$uni-bg-color-hover: #f1f1f1;
$uni-bg-color-mask: rgba(0, 0, 0, 0.4);
$uni-border-color: $color-border;
$uni-font-size-sm: $font-size-sm;
$uni-font-size-base: $font-size-base;
$uni-font-size-lg: $font-size-lg;
$uni-font-size-title: 40rpx;
$uni-font-size-subtitle: $font-size-xl;
$uni-font-size-paragraph: 30rpx;
$uni-img-size-sm: 40rpx;
$uni-img-size-base: 52rpx;
$uni-img-size-lg: 80rpx;
$uni-border-radius-sm: $radius-sm;
$uni-border-radius-base: $radius-base;
$uni-border-radius-lg: $radius-lg;
$uni-border-radius-circle: 50%;
$uni-spacing-row-sm: 10rpx;
$uni-spacing-row-base: 20rpx;
$uni-spacing-row-lg: 30rpx;
$uni-spacing-col-sm: 8rpx;
$uni-spacing-col-base: 16rpx;
$uni-spacing-col-lg: 24rpx;
$uni-opacity-disabled: 0.3;
$uni-color-title: $color-foreground;
$uni-color-subtitle: $color-muted-foreground;
$uni-color-paragraph: #3f536e;
+145
View File
@@ -0,0 +1,145 @@
// ============================================
// Utility Classes
// ============================================
// === TEXT SIZE ===
.text-xs { font-size: $font-size-xs; }
.text-sm { font-size: $font-size-sm; }
.text-base { font-size: $font-size-base; }
.text-lg { font-size: $font-size-lg; }
.text-xl { font-size: $font-size-xl; }
.text-2xl { font-size: $font-size-2xl; }
.text-3xl { font-size: $font-size-3xl; }
.text-display { font-size: $font-size-display; }
// === FONT WEIGHT ===
.font-normal { font-weight: $font-weight-normal; }
.font-medium { font-weight: $font-weight-medium; }
.font-semibold { font-weight: $font-weight-semibold; }
.font-bold { font-weight: $font-weight-bold; }
// === TEXT COLOR ===
.text-primary { color: $color-foreground; }
.text-secondary { color: $color-muted-foreground; }
.text-accent { color: $color-accent; }
.text-success { color: $color-success; }
.text-error { color: $color-error; }
.text-white { color: #FFFFFF; }
.text-gradient {
background: $gradient-accent;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
// === FONT FAMILY ===
.font-display { font-family: $font-family-display; }
.font-body { font-family: $font-family-body; }
.font-mono { font-family: $font-family-mono; }
// === SPACING ===
$spacing-map: (
'0': $spacing-0,
'1': $spacing-1,
'2': $spacing-2,
'3': $spacing-3,
'4': $spacing-4,
'5': $spacing-5,
'6': $spacing-6,
'8': $spacing-8,
'10': $spacing-10,
);
@each $key, $val in $spacing-map {
.m-#{$key} { margin: $val; }
.mt-#{$key} { margin-top: $val; }
.mb-#{$key} { margin-bottom: $val; }
.ml-#{$key} { margin-left: $val; }
.mr-#{$key} { margin-right: $val; }
.mx-#{$key} { margin-left: $val; margin-right: $val; }
.my-#{$key} { margin-top: $val; margin-bottom: $val; }
.p-#{$key} { padding: $val; }
.pt-#{$key} { padding-top: $val; }
.pb-#{$key} { padding-bottom: $val; }
.pl-#{$key} { padding-left: $val; }
.pr-#{$key} { padding-right: $val; }
.px-#{$key} { padding-left: $val; padding-right: $val; }
.py-#{$key} { padding-top: $val; padding-bottom: $val; }
}
// === FLEX ===
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1; }
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
.gap-1 { gap: $spacing-1; }
.gap-2 { gap: $spacing-2; }
.gap-3 { gap: $spacing-3; }
.gap-4 { gap: $spacing-4; }
.gap-5 { gap: $spacing-5; }
// === BACKGROUND ===
.bg-page { background-color: $color-background; }
.bg-card { background-color: $color-card; }
.bg-muted { background-color: $color-muted; }
.bg-accent { background-color: $color-accent; }
.bg-foreground { background-color: $color-foreground; }
.bg-gradient-accent { background: $gradient-accent; }
// === BORDER RADIUS ===
.rounded-sm { border-radius: $radius-sm; }
.rounded { border-radius: $radius-base; }
.rounded-lg { border-radius: $radius-lg; }
.rounded-xl { border-radius: $radius-xl; }
.rounded-2xl { border-radius: $radius-2xl; }
.rounded-full { border-radius: $radius-full; }
// === SHADOW ===
.shadow-sm { box-shadow: $shadow-sm; }
.shadow { box-shadow: $shadow-base; }
.shadow-lg { box-shadow: $shadow-lg; }
.shadow-xl { box-shadow: $shadow-xl; }
// === INTERACTION ===
.press-effect {
transition: transform $transition-fast, opacity $transition-fast;
&:active {
transform: scale(0.98);
opacity: 0.8;
}
}
// === DISPLAY ===
.w-full { width: 100%; }
.h-full { height: 100%; }
.overflow-hidden { overflow: hidden; }
.relative { position: relative; }
.absolute { position: absolute; }
+38
View File
@@ -0,0 +1,38 @@
// ============================================
// Design System — Main Entry
// ============================================
// Imported globally via App.vue <style lang="scss">
// Token variables are separately injected via uni.scss
@import 'tokens';
@import 'mixins';
@import 'animations';
@import 'utilities';
// === GLOBAL BASE ===
page {
background-color: $color-background;
color: $color-foreground;
font-family: $font-family-body;
font-size: $font-size-base;
line-height: $line-height-normal;
}
view, text, scroll-view {
box-sizing: border-box;
}
// === TYPOGRAPHY CLASSES ===
.heading-display {
@include text-display;
}
.body-text {
@include text-body;
}
.mono-text {
@include text-mono;
}
+181 -76
View File
@@ -1,76 +1,181 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333; // 基本色
$uni-text-color-inverse: #fff; // 反色
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #fff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16;
/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px;
$uni-color-subtitle: #555; // 二级标题颜色
$uni-font-size-subtitle: 18px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;
/**
* uni-app 全局 SCSS 变量
* uni-app 会自动将此文件内容注入到每个组件的 <style> 作用域
* 因此这里只放变量定义,不用 @import(路径在 node_modules 组件中无法解析)
*/
// === COLORS — uv-ui Design System ===
// https://www.uvui.cn/components/color.html
// Primary
$uv-primary: #3c9cff;
$uv-primary-dark: #398ade;
$uv-primary-disabled: #9acafc;
$uv-primary-light: #ecf5ff;
// Error
$uv-error: #f56c6c;
$uv-error-dark: #e45656;
$uv-error-disabled: #f7b2b2;
$uv-error-light: #fef0f0;
// Warning
$uv-warning: #f9ae3d;
$uv-warning-dark: #f1a532;
$uv-warning-disabled: #f9d39b;
$uv-warning-light: #fdf6ec;
// Success
$uv-success: #5ac725;
$uv-success-dark: #53c21d;
$uv-success-disabled: #a9e08f;
$uv-success-light: #f5fff0;
// Info
$uv-info: #909399;
$uv-info-dark: #767a82;
$uv-info-disabled: #c4c6c9;
$uv-info-light: #f4f4f5;
// Aliases (backward compat)
$color-accent: $uv-primary;
$color-accent-secondary: $uv-primary-dark;
$color-accent-foreground: #FFFFFF;
$color-success: $uv-success;
$color-warning: $uv-warning;
$color-error: $uv-error;
// Typography & Neutrals
$uv-main-color: #303133;
$uv-content-color: #606266;
$uv-tips-color: #909193;
$uv-light-color: #c0c4cc;
$uv-disabled-color: #c8c9cc;
$uv-bg-color: #f3f4f6;
$uv-border-color: #dadbde;
// Aliases (backward compat)
$color-background: $uv-bg-color;
$color-foreground: $uv-main-color;
$color-muted: $uv-info-light;
$color-muted-foreground: $uv-content-color;
$color-border: $uv-border-color;
$color-card: #FFFFFF;
$color-ring: $uv-primary;
// Gradients
$gradient-accent: linear-gradient(135deg, #{$uv-primary}, #{$uv-primary-dark});
// === TYPOGRAPHY ===
$font-family-display: 'Noto Serif SC', 'STSong', 'SimSun', serif;
$font-family-body: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'HarmonyOS Sans SC', 'Microsoft YaHei', sans-serif;
$font-family-mono: 'Menlo', 'Monaco', 'Courier New', monospace;
$font-size-xs: 20rpx;
$font-size-sm: 24rpx;
$font-size-base: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
$font-size-2xl: 44rpx;
$font-size-3xl: 56rpx;
$font-size-display: 72rpx;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
$line-height-tight: 1.2;
$line-height-normal: 1.5;
$line-height-relaxed: 1.75;
// === SPACING (rpx) ===
$spacing-0: 0;
$spacing-1: 8rpx;
$spacing-2: 16rpx;
$spacing-3: 24rpx;
$spacing-4: 32rpx;
$spacing-5: 40rpx;
$spacing-6: 48rpx;
$spacing-8: 64rpx;
$spacing-10: 80rpx;
$spacing-12: 96rpx;
$spacing-16: 128rpx;
$spacing-20: 160rpx;
// === BORDER RADIUS ===
$radius-sm: 8rpx;
$radius-base: 16rpx;
$radius-lg: 24rpx;
$radius-xl: 32rpx;
$radius-2xl: 48rpx;
$radius-full: 9999rpx;
// === SHADOWS ===
$shadow-sm: 0 2rpx 8rpx rgba(15, 23, 42, 0.04);
$shadow-base: 0 4rpx 16rpx rgba(15, 23, 42, 0.08);
$shadow-lg: 0 8rpx 32rpx rgba(15, 23, 42, 0.12);
$shadow-xl: 0 16rpx 48rpx rgba(15, 23, 42, 0.16);
$shadow-accent: 0 8rpx 24rpx rgba(60, 156, 255, 0.25);
// === TRANSITIONS ===
$transition-fast: 150ms ease;
$transition-base: 250ms ease;
$transition-slow: 400ms ease;
// === Z-INDEX ===
$z-base: 0;
$z-dropdown: 100;
$z-sticky: 200;
$z-modal: 300;
$z-toast: 400;
// === BORDER ===
$border-width: 1rpx;
// === Legacy $uni-* compatibility (for uni-ui) ===
$uni-color-primary: $color-accent;
$uni-color-success: $color-success;
$uni-color-warning: $color-warning;
$uni-color-error: $color-error;
$uni-text-color: $color-foreground;
$uni-text-color-inverse: #FFFFFF;
$uni-text-color-grey: $color-muted-foreground;
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
$uni-bg-color: $color-background;
$uni-bg-color-grey: $color-muted;
$uni-bg-color-hover: #f1f1f1;
$uni-bg-color-mask: rgba(0, 0, 0, 0.4);
$uni-border-color: $color-border;
$uni-font-size-sm: $font-size-sm;
$uni-font-size-base: $font-size-base;
$uni-font-size-lg: $font-size-lg;
$uni-font-size-title: 40rpx;
$uni-font-size-subtitle: $font-size-xl;
$uni-font-size-paragraph: 30rpx;
$uni-img-size-sm: 40rpx;
$uni-img-size-base: 52rpx;
$uni-img-size-lg: 80rpx;
$uni-border-radius-sm: $radius-sm;
$uni-border-radius-base: $radius-base;
$uni-border-radius-lg: $radius-lg;
$uni-border-radius-circle: 50%;
$uni-spacing-row-sm: 10rpx;
$uni-spacing-row-base: 20rpx;
$uni-spacing-row-lg: 30rpx;
$uni-spacing-col-sm: 8rpx;
$uni-spacing-col-base: 16rpx;
$uni-spacing-col-lg: 24rpx;
$uni-opacity-disabled: 0.3;
$uni-color-title: $color-foreground;
$uni-color-subtitle: $color-muted-foreground;
$uni-color-paragraph: #3f536e;