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

367 lines
8.3 KiB
Vue

<template>
<view class="page-blind">
<!-- 头部 -->
<view class="blind-header">
<view class="back-btn" @click="goBack">
<text> 返回</text>
</view>
<text class="blind-title">{{ categoryIcon }} {{ categoryName }}</text>
</view>
<!-- 价格区间选择 -->
<view class="price-section card">
<text class="section-label">选择价格区间</text>
<view class="price-options">
<view
class="price-option"
:class="{ active: selectedPrice === p.value }"
v-for="p in priceRanges"
:key="p.value"
@click="selectedPrice = p.value"
>
<text>{{ p.label }}</text>
</view>
</view>
</view>
<!-- 盲选卡片 -->
<view class="blind-card-container">
<view class="blind-card" :class="{ spinning: isSpinning, revealed: isRevealed }">
<view class="card-front" v-if="!isRevealed">
<text class="card-logo">🎲</text>
<text class="card-hint">点击按钮开始盲选</text>
</view>
<view class="card-reveal" v-if="isRevealed && selectedResult">
<text class="reveal-icon"></text>
<text class="reveal-name">{{ selectedResult.merchant_name }}</text>
<text class="reveal-desc">{{ selectedResult.description }}</text>
<text class="reveal-price">¥{{ selectedResult.price_range }}</text>
<view class="reveal-tags">
<text class="reveal-tag" v-for="tag in resultTags" :key="tag">{{ tag }}</text>
</view>
<text class="reveal-match">🎯 匹配度 {{ (selectedResult.match_score * 100).toFixed(0) }}%</text>
<view class="reveal-coupon" v-if="selectedResult.has_coupon">
<text class="coupon-icon">🎁</text>
<text class="coupon-text">{{ selectedResult.coupon_value || '有优惠券!' }}</text>
</view>
</view>
</view>
</view>
<!-- 盲选按钮 -->
<view class="action-area" v-if="!isRevealed">
<button class="blind-btn" :loading="isSpinning" @click="startBlind" :disabled="isSpinning">
<text class="blind-btn-text">{{ isSpinning ? '正在抽取...' : '🎲 开始盲选' }}</text>
</button>
</view>
<!-- 结果操作 -->
<view class="result-actions" v-if="isRevealed && selectedResult">
<button class="accept-btn" @click="acceptResult">
<text> 接受 / 前往</text>
</button>
<button class="skip-btn" @click="declineResult">
<text>🔄 换一个</text>
</button>
<button class="star-btn" @click="goToReview">
<text> 已去过 · 打分</text>
</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute } from '@dcloudio/uni-app'
import { blindApi } from '@/api/index.js'
const route = useRoute()
const categoryName = ref(route.query.categoryName || '盲选')
const categoryIcon = ref(route.query.categoryIcon || '🍽️')
const packageId = ref(route.query.packageId || 0)
const categoryId = ref(parseInt(route.query.categoryId) || 1)
const selectedPrice = ref('100-300')
const isSpinning = ref(false)
const isRevealed = ref(false)
const selectedResult = ref(null)
const priceRanges = [
{ label: '¥100以内', value: '0-100' },
{ label: '¥100-300', value: '100-300' },
{ label: '¥300-500', value: '300-500' },
{ label: '¥500+', value: '500-9999' },
]
const resultTags = ['推荐', '好评', '特色']
async function startBlind() {
isSpinning.value = true
// 1秒动画延迟
await new Promise(r => setTimeout(r, 1000))
try {
// 调用盲选API
const priceMap = { '100-300': '100-300', '0-100': '0-100', '300-500': '300-500', '500-9999': '500-1000' }
const data = await blindApi.choose({
category_id: categoryId.value,
price_range: priceMap[selectedPrice.value] || '100-300',
})
// 显示结果
selectedResult.value = data.result
isRevealed.value = true
} catch (e) {
uni.showToast({ title: '盲选失败,请重试', icon: 'none' })
} finally {
isSpinning.value = false
}
}
function acceptResult() {
uni.showToast({ title: '已前往!', icon: 'success' })
goBack()
}
function declineResult() {
// 换一个
isRevealed.value = false
selectedResult.value = null
startBlind()
}
function goToReview() {
uni.navigateTo({
url: `/pages/blind/review?sessionId=${selectedResult.value?.session_id || 1}&packageId=${packageId.value || 1}`
})
}
function goBack() {
uni.navigateBack()
}
</script>
<style lang="scss" scoped>
.page-blind {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 40rpx;
}
.blind-header {
background: linear-gradient(135deg, #FF6B35, #FF8C42);
padding: 40rpx 30rpx;
color: #fff;
.back-btn {
font-size: 26rpx;
opacity: 0.8;
margin-bottom: 10rpx;
}
.blind-title {
font-size: 36rpx;
font-weight: 700;
display: block;
}
}
.card {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin: 20rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.section-label {
font-size: 28rpx;
font-weight: 600;
display: block;
margin-bottom: 16rpx;
}
.price-options {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.price-option {
padding: 12rpx 24rpx;
border: 2rpx solid #eee;
border-radius: 30rpx;
font-size: 24rpx;
color: #666;
&.active {
border-color: #FF6B35;
color: #FF6B35;
background: #FFF3E0;
}
}
.blind-card-container {
display: flex;
justify-content: center;
padding: 40rpx 0;
}
.blind-card {
width: 560rpx;
height: 400rpx;
border-radius: 30rpx;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
box-shadow: 0 20rpx 60rpx rgba(102,126,234,0.3);
transition: transform 0.6s, opacity 0.6s;
&.spinning {
animation: spin 1s ease-in-out;
}
&.revealed {
transform: scale(1);
}
}
@keyframes spin {
0% { transform: rotateY(0deg) scale(1); }
50% { transform: rotateY(90deg) scale(0.9); }
100% { transform: rotateY(360deg) scale(1); }
}
.card-front {
text-align: center;
.card-logo {
font-size: 100rpx;
display: block;
margin-bottom: 20rpx;
}
.card-hint {
font-size: 28rpx;
opacity: 0.8;
}
}
.card-reveal {
text-align: center;
padding: 30rpx;
.reveal-icon {
font-size: 48rpx;
display: block;
margin-bottom: 10rpx;
}
.reveal-name {
font-size: 36rpx;
font-weight: 700;
display: block;
margin-bottom: 12rpx;
}
.reveal-desc {
font-size: 24rpx;
opacity: 0.9;
display: block;
margin-bottom: 16rpx;
line-height: 1.5;
}
.reveal-price {
font-size: 32rpx;
font-weight: 600;
background: rgba(255,255,255,0.2);
padding: 8rpx 24rpx;
border-radius: 20rpx;
display: inline-block;
margin-bottom: 16rpx;
}
.reveal-match {
font-size: 22rpx;
opacity: 0.8;
display: block;
margin-bottom: 12rpx;
}
}
.reveal-tags {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8rpx;
margin-bottom: 12rpx;
}
.reveal-tag {
background: rgba(255,255,255,0.2);
padding: 4rpx 14rpx;
border-radius: 14rpx;
font-size: 20rpx;
}
.reveal-coupon {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
background: rgba(255,215,0,0.3);
padding: 10rpx 20rpx;
border-radius: 12rpx;
margin-top: 12rpx;
.coupon-icon { font-size: 24rpx; }
.coupon-text { font-size: 22rpx; color: #FFD700; }
}
.action-area {
text-align: center;
padding: 20rpx;
}
.blind-btn {
background: linear-gradient(135deg, #FF6B35, #FF8C42);
color: #fff;
border: none;
border-radius: 50rpx;
padding: 24rpx 60rpx;
font-size: 36rpx;
font-weight: 700;
box-shadow: 0 8rpx 30rpx rgba(255,107,53,0.4);
}
.blind-btn:disabled {
opacity: 0.7;
}
.result-actions {
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 0 40rpx;
}
.accept-btn {
background: linear-gradient(135deg, #4CAF50, #66BB6A);
color: #fff;
border: none;
border-radius: 30rpx;
font-size: 30rpx;
font-weight: 600;
}
.skip-btn {
background: #fff;
color: #666;
border: 2rpx solid #ddd;
border-radius: 30rpx;
font-size: 28rpx;
}
.star-btn {
background: transparent;
color: #FFD700;
border: 2rpx solid #FFD700;
border-radius: 30rpx;
font-size: 28rpx;
}
</style>