Files
blind-select/frontend-app/pages/index/index.vue
T
admin 06488f0237 Initial commit: 帮我选盲选应用
功能:
- Go后端 (Gin + GORM + PostgreSQL)
- UniApp用户端 (iOS/Android/小程序)
- DaisyUI5后台管理
- JWT认证 + 微信登录
- 盲选加权算法
- 会员系统 + 优惠券
- 打分评价 + 偏好学习
2026-06-08 20:18:31 +00:00

348 lines
8.6 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-home">
<!-- 顶部欢迎区 -->
<view class="header" :style="{ background: headerGradient }">
<view class="header-content">
<view class="greeting">
<text class="greeting-label">你好, {{ userName }} 👋</text>
<text class="greeting-message">{{ welcomeMessage }}</text>
</view>
<view class="member-badge" v-if="hasMember">
<text>👑 VIP</text>
</view>
<view class="member-badge free" @click="goToMember">
<text>升级VIP</text>
</view>
</view>
<view class="daily-usage">
<text>今日盲选: {{ usedToday }}/10</text>
<view class="usage-bar">
<view class="usage-fill" :style="{ width: usagePercent + '%' }"></view>
</view>
</view>
</view>
<!-- AI推荐 -->
<view class="ai-recommend card" v-if="aiRecommend">
<text class="ai-label"> AI推荐</text>
<text class="ai-text">{{ aiRecommend }}</text>
</view>
<!-- 盲选分类卡片 -->
<view class="categories">
<text class="section-title">选择类别</text>
<scroll-view scroll-x class="category-scroll" show-scrollbar="false">
<view
class="category-card"
v-for="cat in categories"
:key="cat.id"
@click="selectCategory(cat)"
>
<text class="category-icon">{{ cat.icon }}</text>
<text class="category-name">{{ cat.name }}</text>
</view>
</scroll-view>
</view>
<!-- 热门套餐推荐模糊展示 -->
<view class="pools">
<text class="section-title">附近精选</text>
<view class="pool-grid">
<view class="pool-card card" v-for="pkg in poolList" :key="pkg.id" @click="goToBlind(pkg)">
<view class="pool-header">
<text class="pool-merchant">{{ pkg.merchant }}</text>
<text class="pool-rating"> {{ pkg.rating.toFixed(1) }}</text>
</view>
<text class="pool-name">{{ pkg.name }}</text>
<text class="pool-cat">{{ pkg.cat_name }}</text>
<text class="pool-price">{{ pkg.price_range }}</text>
</view>
</view>
</view>
<!-- 底部TabBar -->
<view class="tabbar">
<view class="tab-item" @click="goPage('pages/index/index')">
<text class="tab-icon">🏠</text>
<text :class="['tab-label', currentTab === 0 ? 'active' : '']">首页</text>
</view>
<view class="tab-item" @click="goPage('pages/coupon/list')">
<text class="tab-icon">🎫</text>
<text :class="['tab-label', currentTab === 1 ? 'active' : '']">优惠券</text>
</view>
<view class="tab-item" @click="goPage('pages/member/member')">
<text class="tab-icon">👑</text>
<text :class="['tab-label', currentTab === 2 ? 'active' : '']">会员</text>
</view>
<view class="tab-item" @click="goPage('pages/user/profile')">
<text class="tab-icon">👤</text>
<text :class="['tab-label', currentTab === 3 ? 'active' : '']">我的</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { blindApi, memberApi } from '@/api/index.js'
import { useUserStore } from '@/store/user.js'
const userStore = useUserStore()
const categories = ref([])
const poolList = ref([])
const aiRecommend = ref('')
const usedToday = ref(0)
const userName = computed(() => userStore.userInfo.nickname || '朋友')
const hasMember = computed(() => userStore.hasMember)
const usagePercent = computed(() => Math.min((usedToday.value / 10) * 100, 100))
const headerGradient = computed(() =>
hasMember.value ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF8C42 100%)'
)
const welcomeMessages = [
'今天想带你吃点惊喜的~',
'来一场未知的味蕾冒险?',
'准备好遇见新味道了吗?',
'你的盲选管家已上线 ✨',
]
const welcomeMessage = ref(welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)])
const aiTexts = [
'看你最近爱吃日料,今天推荐一家隐藏好评店?',
'好久没吃辣了,要不要来场火锅盲选?',
'根据你的口味,今天适合一场甜品之旅 🍰',
'探索型用户!今天试试没去过的类型?',
]
aiRecommend.value = aiTexts[Math.floor(Math.random() * aiTexts.length)]
onMounted(async () => {
// Load categories
try {
categories.value = await blindApi.getCategories()
} catch(e) {}
// Load pool
try {
poolList.value = await blindApi.getPool({ limit: 6 })
} catch(e) {}
// Check member status
try {
const status = await memberApi.getStatus()
userStore.setHasMember(status.active)
usedToday.value = status.used_today || 0
} catch(e) {}
})
function selectCategory(cat) {
uni.navigateTo({
url: `/pages/blind/blind?categoryId=${cat.id}&categoryName=${cat.name}&categoryIcon=${cat.icon}`
})
}
function goToBlind(pkg) {
uni.navigateTo({
url: `/pages/blind/blind?packageId=${pkg.id}&merchant=${pkg.merchant}`
})
}
function goPage(path) {
if (path === 'pages/index/index') {
uni.switchTab({ url: '/pages/index/index' })
} else {
uni.switchTab({ url: path })
}
}
function goToMember() {
uni.navigateTo({ url: '/pages/member/member' })
}
</script>
<style lang="scss" scoped>
.page-home {
min-height: 100vh;
padding-bottom: 120rpx;
background: #f5f5f5;
}
.header {
padding: 60rpx 30rpx 40rpx;
color: #fff;
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
.greeting {
.greeting-label {
font-size: 32rpx;
font-weight: 600;
display: block;
margin-bottom: 8rpx;
}
.greeting-message {
font-size: 26rpx;
opacity: 0.9;
}
}
.member-badge {
background: rgba(255,255,255,0.25);
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 22rpx;
backdrop-filter: blur(10rpx);
&.free {
background: rgba(255,255,255,0.15);
}
}
}
.daily-usage {
margin-top: 24rpx;
font-size: 22rpx;
opacity: 0.8;
.usage-bar {
height: 6rpx;
background: rgba(255,255,255,0.2);
border-radius: 3rpx;
margin-top: 8rpx;
overflow: hidden;
.usage-fill {
height: 100%;
background: #fff;
border-radius: 3rpx;
transition: width 0.3s;
}
}
}
}
.card {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin: 20rpx 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.ai-recommend {
.ai-label {
font-size: 22rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.ai-text {
font-size: 28rpx;
font-weight: 500;
line-height: 1.6;
}
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
padding: 20rpx 30rpx 10rpx;
display: block;
}
.category-scroll {
white-space: nowrap;
padding: 10rpx 30rpx;
}
.category-card {
display: inline-flex;
flex-direction: column;
align-items: center;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 30rpx;
margin-right: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.06);
.category-icon {
font-size: 48rpx;
margin-bottom: 8rpx;
}
.category-name {
font-size: 24rpx;
color: #666;
}
}
.pool-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
padding: 10rpx 20rpx;
}
.pool-card {
background: #fff;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 0;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
.pool-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.pool-merchant {
font-size: 22rpx;
color: #999;
}
.pool-rating {
font-size: 20rpx;
}
.pool-name {
font-size: 28rpx;
font-weight: 600;
margin-top: 8rpx;
display: block;
}
.pool-cat {
font-size: 20rpx;
color: #FF6B35;
margin-top: 4rpx;
display: inline-block;
background: #FFF3E0;
padding: 2rpx 10rpx;
border-radius: 8rpx;
}
.pool-price {
font-size: 24rpx;
color: #FF6B35;
font-weight: 600;
margin-top: 12rpx;
display: block;
}
}
.tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: #fff;
padding: 10rpx 0 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06);
z-index: 100;
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.tab-icon { font-size: 36rpx; }
.tab-label {
font-size: 20rpx;
color: #999;
margin-top: 4rpx;
&.active { color: #FF6B35; }
}
}
}
</style>