文件
shanghaipeiwan/pwuniapp/pages/index/index.vue

743 行
20 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="index-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 顶部搜索栏 (矢量SVG搜索图标,交互聚焦微动效) -->
<view class="header-search">
<view class="search-box">
<image class="search-icon-img" :src="ICONS.search" mode="aspectFit" />
<input class="search-input" placeholder="搜索同城陪吃、陪喝、KTV陪唱或达人" placeholder-class="placeholder-text" />
</view>
</view>
<!-- 游戏分类滑动栏 (SVG分类图标,单行横滑,抗挤压) -->
<scroll-view scroll-x class="category-scroll" :show-scrollbar="false">
<view class="category-list">
<view
v-for="item in games"
:key="item.id"
class="category-item btn-active"
:class="{ active: currentCategory === item.id }"
@click="selectCategory(item.id)"
>
<image class="cat-icon-svg" :src="item.icon" mode="aspectFit" />
<text class="cat-name">{{ item.name }}</text>
<view v-if="item.hot" class="cat-badge">
<text class="badge-text">HOT</text>
</view>
</view>
</view>
</scroll-view>
<!-- 精彩活动横幅轮播 (流光投影) -->
<swiper class="banner-swiper" circular autoplay interval="4000" indicator-dots indicator-color="rgba(255,255,255,0.2)" indicator-active-color="#6366F1">
<swiper-item v-for="banner in banners" :key="banner.id" class="banner-item" @click="goToActivity">
<view class="banner-card" :style="{ background: banner.bg }">
<view class="banner-tag">{{ banner.tag }}</view>
<view class="banner-title">{{ banner.title }}</view>
<view class="banner-sub">点击立即参与</view>
</view>
</swiper-item>
</swiper>
<!-- 快捷功能金刚区 (纯矢量SVG图标,88rpx触控热区) -->
<view class="quick-grid">
<view class="quick-item btn-active" @click="navTo('/pages/activity/list')">
<view class="quick-icon-bg bg-purple">
<image class="quick-svg" :src="ICONS.teamUp" mode="aspectFit" />
</view>
<text class="quick-title">组队开黑</text>
<text class="quick-sub">AA制五排</text>
</view>
<view class="quick-item btn-active" @click="navTo('/pages/activity/my-ticket')">
<view class="quick-icon-bg bg-green">
<image class="quick-svg" :src="ICONS.ticket" mode="aspectFit" />
</view>
<text class="quick-title">我的门票</text>
<text class="quick-sub">4位核销码</text>
</view>
<view class="quick-item btn-active" @click="navTo('/pages/order/list')">
<view class="quick-icon-bg bg-blue">
<image class="quick-svg" :src="ICONS.orderList" mode="aspectFit" />
</view>
<text class="quick-title">服务订单</text>
<text class="quick-sub">状态实时看</text>
</view>
<view class="quick-item btn-active" @click="navTo('/pages/expert/workspace')">
<view class="quick-icon-bg bg-orange">
<image class="quick-svg" :src="ICONS.expertBadge" mode="aspectFit" />
</view>
<text class="quick-title">大神中心</text>
<text class="quick-sub">接单工作台</text>
</view>
</view>
<!-- 陪玩大神瀑布流标题 -->
<view class="section-title-wrap">
<view class="section-title">
<view class="title-line"></view>
<text>精选在线大神</text>
</view>
<view class="section-more">
<view class="pulse-point"></view>
<text>实时在线</text>
</view>
</view>
<!-- 陪玩大神卡片网格 -->
<view class="expert-grid">
<view
v-for="expert in filteredExperts"
:key="expert.id"
class="expert-card card-active"
@click="goToExpert(expert.id)"
>
<!-- 封面图构图优化:高保真黄金比例与底部流光渐变 -->
<view class="card-cover-wrap">
<image class="card-cover" :src="expert.avatar" mode="aspectFill" />
<view class="cover-gradient"></view>
<view class="status-tag" :class="expert.isOnline ? 'online' : 'offline'">
<view class="pulse-dot" v-if="expert.isOnline"></view>
<text>{{ expert.isOnline ? '在线' : '离线' }}</text>
</view>
<view class="game-badge">{{ expert.game }}</view>
</view>
<!-- 信息区 -->
<view class="card-body">
<view class="row-nickname">
<text class="nickname">{{ expert.nickname }}</text>
<view class="gender-tag" :class="expert.gender">
<image class="gender-svg" :src="expert.gender === 'female' ? ICONS.female : ICONS.male" mode="aspectFit" />
</view>
</view>
<view class="city-badge" v-if="expert.city">
<image class="city-pin-svg" :src="ICONS.mapPin" mode="aspectFit" />
<text class="city-text">{{ expert.city }}</text>
</view>
<view class="rank-badge" v-else>{{ expert.rankTag }}</view>
<!-- 语音/歌声试听条 (矢量SVG播放图标,带播放高亮光晕) -->
<view class="voice-bar btn-active" :class="{ playing: playingId === expert.id, 'song-bar': expert.voiceType === 'song' }" @click.stop="toggleVoice(expert.id)">
<image class="voice-icon-svg" :src="playingId === expert.id ? ICONS.pause : (expert.voiceType === 'song' ? ICONS.micVocal : ICONS.play)" mode="aspectFit" />
<text class="voice-text">{{ playingId === expert.id ? '播放中...' : (expert.voiceTitle || expert.voiceDuration + 's 原声试听') }}</text>
<view class="wave-lines" v-if="playingId === expert.id">
<view class="wave-item w1"></view>
<view class="wave-item w2"></view>
<view class="wave-item w3"></view>
</view>
</view>
<view class="card-footer">
<view class="price-wrap tabular-num">
<text class="price-symbol">¥</text>
<text class="price-val">{{ expert.price }}</text>
<text class="price-unit">/时</text>
</view>
<text class="order-count tabular-num">{{ expert.orderCount }}单</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { GAMES_LIST, BANNERS, EXPERTS_LIST } from '@/common/mock.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey } from '@/common/theme.js';
const games = ref(GAMES_LIST);
const banners = ref(BANNERS);
const experts = ref(EXPERTS_LIST);
const currentCategory = ref('all');
const playingId = ref(null);
const currentThemeKey = ref(getCurrentThemeKey());
const onThemeChanged = (key) => {
currentThemeKey.value = key;
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
uni.$on('theme-change', onThemeChanged);
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});
const filteredExperts = computed(() => {
if (currentCategory.value === 'all') return experts.value;
const gameMap = {
offline_meal: '美食陪吃',
offline_drink: '下午茶陪喝',
offline_sing: '同城陪唱',
offline_play: '同城陪玩',
wzry: '王者荣耀',
hpjy: '和平精英',
lol: '英雄联盟',
val: '无畏契约'
};
const target = gameMap[currentCategory.value];
return target ? experts.value.filter(e => e.game === target) : experts.value;
});
const selectCategory = (id) => {
currentCategory.value = id;
};
const toggleVoice = (id) => {
if (playingId.value === id) {
playingId.value = null;
} else {
playingId.value = id;
uni.showToast({ title: '正在试听打招呼语音', icon: 'none' });
setTimeout(() => {
if (playingId.value === id) playingId.value = null;
}, 4000);
}
};
const goToExpert = (id) => {
uni.navigateTo({
url: `/pages/expert/detail?id=${id}`
});
};
const goToActivity = () => {
uni.switchTab({
url: '/pages/activity/list'
});
};
const navTo = (url) => {
if (url === '/pages/activity/list' || url === '/pages/order/list' || url === '/pages/user/index') {
uni.switchTab({ url });
} else {
uni.navigateTo({ url });
}
};
</script>
<style lang="scss" scoped>
.index-container {
min-height: 100vh;
background-color: #0A0F1D;
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
}
.header-search {
padding: 20rpx 32rpx;
background-color: #0A0F1D;
position: sticky;
top: 0;
z-index: 50;
.search-box {
display: flex;
align-items: center;
background-color: #151D30;
height: 80rpx;
border-radius: 40rpx;
padding: 0 28rpx;
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.2s ease;
&:focus-within {
border-color: #6366F1;
box-shadow: 0 0 16rpx rgba(99, 102, 241, 0.35);
}
.search-icon-img {
width: 32rpx;
height: 32rpx;
margin-right: 16rpx;
opacity: 0.8;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #F8FAFC;
}
.placeholder-text {
color: #64748B;
}
}
}
.category-scroll {
white-space: nowrap;
padding: 0 32rpx 10rpx;
width: 100%;
box-sizing: border-box;
.category-list {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
padding: 16rpx 0 10rpx;
}
.category-item {
display: inline-flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
white-space: nowrap;
padding: 14rpx 28rpx;
background-color: #151D30;
border-radius: 32rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
position: relative;
transition: all 0.15s ease-out;
.cat-icon-svg {
width: 28rpx;
height: 28rpx;
margin-right: 10rpx;
}
.cat-name {
font-size: 24rpx;
color: #94A3B8;
font-weight: 500;
}
.cat-badge {
position: absolute;
top: -10rpx;
right: -8rpx;
height: 26rpx;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FF3B30 0%, #FF2D55 100%);
border: 2rpx solid #0A0F1D;
padding: 0 10rpx;
border-radius: 14rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 45, 85, 0.5);
z-index: 3;
pointer-events: none;
.badge-text {
color: #FFFFFF;
font-size: 16rpx;
font-weight: 800;
font-style: italic;
line-height: 1;
letter-spacing: 1rpx;
}
}
&.active {
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
border-color: #818CF8;
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.45);
.cat-name {
color: #FFFFFF;
font-weight: 600;
}
}
}
}
.banner-swiper {
height: 200rpx;
margin: 10rpx 32rpx 24rpx;
.banner-card {
height: 100%;
border-radius: 20rpx;
padding: 28rpx 32rpx;
display: flex;
flex-direction: column;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.45);
.banner-tag {
align-self: flex-start;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #FFF;
margin-bottom: 8rpx;
}
.banner-title {
font-size: 30rpx;
font-weight: 700;
color: #FFF;
margin-bottom: 6rpx;
}
.banner-sub {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.85);
}
}
}
.quick-grid {
display: flex;
justify-content: space-between;
margin: 0 32rpx 32rpx;
background-color: #151D30;
border-radius: 24rpx;
padding: 24rpx 16rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
.quick-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.quick-icon-bg {
width: 80rpx;
height: 80rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
transition: transform 0.15s ease;
.quick-svg {
width: 40rpx;
height: 40rpx;
}
&.bg-purple { background: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.3); }
&.bg-green { background: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.3); }
&.bg-blue { background: rgba(99, 102, 241, 0.15); border: 1px solid rgba(99, 102, 241, 0.3); }
&.bg-orange { background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.3); }
}
.quick-title {
font-size: 24rpx;
color: #F8FAFC;
font-weight: 600;
margin-bottom: 4rpx;
}
.quick-sub {
font-size: 18rpx;
color: #64748B;
}
}
}
.section-title-wrap {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 32rpx 20rpx;
.section-title {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 700;
color: #F8FAFC;
.title-line {
width: 8rpx;
height: 28rpx;
background: linear-gradient(180deg, #6366F1, #8B5CF6);
border-radius: 4rpx;
margin-right: 14rpx;
}
}
.section-more {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 22rpx;
color: #10B981;
background: rgba(16, 185, 129, 0.12);
padding: 6rpx 16rpx;
border-radius: 16rpx;
.pulse-point {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #10B981;
box-shadow: 0 0 8rpx #10B981;
}
}
}
.expert-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 0 32rpx;
.expert-card {
background-color: #151D30;
border-radius: 20rpx;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.06);
.card-cover-wrap {
position: relative;
height: 260rpx;
overflow: hidden;
background-color: #1E2942;
.card-cover {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
}
.cover-gradient {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(21, 29, 48, 0.85) 100%);
}
.status-tag {
position: absolute;
top: 14rpx;
left: 14rpx;
display: flex;
align-items: center;
gap: 6rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-size: 18rpx;
font-weight: 600;
&.online {
background: rgba(16, 185, 129, 0.9);
color: #FFF;
box-shadow: 0 0 10rpx rgba(16, 185, 129, 0.5);
}
&.offline {
background: rgba(100, 116, 139, 0.8);
color: #FFF;
}
.pulse-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: #FFF;
animation: pulse 1.5s infinite;
}
}
.game-badge {
position: absolute;
bottom: 14rpx;
right: 14rpx;
background: rgba(10, 15, 29, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(6px);
font-size: 18rpx;
color: #E2E8F0;
padding: 4rpx 10rpx;
border-radius: 8rpx;
}
}
.card-body {
padding: 18rpx;
.row-nickname {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
.nickname {
font-size: 26rpx;
font-weight: 600;
color: #F8FAFC;
max-width: 200rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gender-tag {
width: 32rpx;
height: 32rpx;
border-radius: 8rpx;
display: inline-flex;
align-items: center;
justify-content: center;
&.female {
background: rgba(236, 72, 153, 0.2);
}
&.male {
background: rgba(59, 130, 246, 0.2);
}
.gender-svg {
width: 20rpx;
height: 20rpx;
}
}
}
.city-badge {
font-size: 20rpx;
color: #38BDF8;
background: rgba(56, 189, 248, 0.12);
padding: 4rpx 10rpx;
border-radius: 6rpx;
display: inline-flex;
align-items: center;
margin-bottom: 12rpx;
border: 1px solid rgba(56, 189, 248, 0.2);
.city-pin-svg {
width: 20rpx;
height: 20rpx;
margin-right: 6rpx;
}
}
.rank-badge {
font-size: 20rpx;
color: #F59E0B;
background: rgba(245, 158, 11, 0.12);
padding: 4rpx 10rpx;
border-radius: 6rpx;
display: inline-block;
margin-bottom: 12rpx;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.voice-bar {
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.3);
height: 48rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
padding: 0 16rpx;
margin-bottom: 16rpx;
transition: all 0.2s ease;
&.playing {
background: rgba(99, 102, 241, 0.3);
border-color: #818CF8;
box-shadow: 0 0 14rpx rgba(99, 102, 241, 0.4);
}
&.song-bar {
background: rgba(244, 63, 94, 0.15);
border-color: rgba(244, 63, 94, 0.35);
.voice-text {
color: #FB7185;
}
&.playing {
background: rgba(244, 63, 94, 0.3);
border-color: #F43F5E;
box-shadow: 0 0 14rpx rgba(244, 63, 94, 0.4);
}
.wave-lines .wave-item {
background: #FB7185;
}
}
.voice-icon-svg {
width: 20rpx;
height: 20rpx;
margin-right: 8rpx;
}
.voice-text {
font-size: 20rpx;
color: #818CF8;
font-weight: 600;
}
.wave-lines {
display: flex;
align-items: flex-end;
gap: 4rpx;
margin-left: 8rpx;
.wave-item {
width: 4rpx;
background: #818CF8;
border-radius: 2rpx;
&.w1 { height: 14rpx; animation: bounce 0.6s infinite ease-in-out; }
&.w2 { height: 22rpx; animation: bounce 0.6s infinite ease-in-out 0.2s; }
&.w3 { height: 10rpx; animation: bounce 0.6s infinite ease-in-out 0.4s; }
}
}
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: baseline;
.price-wrap {
color: #10B981;
.price-symbol { font-size: 22rpx; font-weight: 600; }
.price-val { font-size: 32rpx; font-weight: 700; margin: 0 2rpx; }
.price-unit { font-size: 20rpx; color: #64748B; }
}
.order-count {
font-size: 20rpx;
color: #64748B;
}
}
}
}
}
.index-container.theme-warm {
background-color: #140E0A;
.header-search {
background-color: #140E0A;
.search-box {
background-color: #221711;
border-color: rgba(249, 115, 22, 0.15);
&:focus-within {
border-color: #F97316;
box-shadow: 0 0 16rpx rgba(249, 115, 22, 0.35);
}
}
}
.category-scroll .category-item.active {
background: linear-gradient(135deg, rgba(249, 115, 22, 0.25), rgba(245, 158, 11, 0.1));
border-color: #F97316;
.cat-name {
color: #FB923C;
}
}
.expert-grid .expert-card {
background-color: #221711;
border-color: rgba(249, 115, 22, 0.12);
}
}
@keyframes pulse {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 8rpx rgba(255, 255, 255, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
}
@keyframes bounce {
0%, 100% { transform: scaleY(0.4); }
50% { transform: scaleY(1); }
}
</style>