711 行
17 KiB
Vue
711 行
17 KiB
Vue
<template>
|
||
<view class="expert-detail-container">
|
||
<!-- 顶部封面与基本信息 -->
|
||
<view class="hero-header">
|
||
<image class="cover-bg" :src="expert.coverImage || expert.avatar" mode="aspectFill" />
|
||
<view class="cover-mask"></view>
|
||
|
||
<view class="profile-card">
|
||
<view class="avatar-wrap">
|
||
<image class="avatar-img" :src="expert.avatar" mode="aspectFill" />
|
||
<view class="status-badge" :class="{ online: expert.isOnline }">
|
||
<view class="pulse-dot" v-if="expert.isOnline"></view>
|
||
<text>{{ expert.isOnline ? '在线' : '离线' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="info-wrap">
|
||
<view class="name-row">
|
||
<text class="nickname">{{ expert.nickname }}</text>
|
||
<text class="level-tag">{{ expert.levelTag }}</text>
|
||
</view>
|
||
|
||
<view class="rank-row">
|
||
<text class="game-tag">{{ expert.game }}</text>
|
||
<text class="rank-tag">{{ expert.rankTag }}</text>
|
||
</view>
|
||
|
||
<view class="city-row" v-if="expert.city">
|
||
<image class="city-pin-svg" :src="ICONS.mapPin" mode="aspectFit" />
|
||
<text class="city-text">{{ expert.city }}</text>
|
||
</view>
|
||
|
||
<view class="data-row">
|
||
<view class="rating-box">
|
||
<image class="star-svg" :src="ICONS.star" mode="aspectFit" />
|
||
<text class="rating-val">{{ expert.rating }}</text>
|
||
</view>
|
||
<text class="order-val">已陪玩 {{ expert.orderCount }} 次</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 专属打招呼试听语音 (矢量SVG播放条) -->
|
||
<view class="voice-card btn-active" @click="toggleVoice">
|
||
<view class="voice-icon-wrap">
|
||
<image class="play-svg" :src="isPlayingVoice ? ICONS.pause : ICONS.play" mode="aspectFit" />
|
||
</view>
|
||
<view class="voice-info">
|
||
<text class="voice-title">{{ isPlayingVoice ? '正在连麦试听语音...' : '点击试听打招呼原声' }}</text>
|
||
<view class="voice-wave" v-if="isPlayingVoice">
|
||
<view class="bar b1"></view>
|
||
<view class="bar b2"></view>
|
||
<view class="bar b3"></view>
|
||
<view class="bar b4"></view>
|
||
</view>
|
||
<text class="voice-dur" v-else>{{ expert.voiceDuration }}s 真实原声</text>
|
||
</view>
|
||
<text class="listen-tag">试听</text>
|
||
</view>
|
||
|
||
<!-- 个人简介 -->
|
||
<view class="section-card">
|
||
<view class="sec-title">个人介绍</view>
|
||
<text class="bio-text">{{ expert.bio }}</text>
|
||
</view>
|
||
|
||
<!-- 服务项目选择 -->
|
||
<view class="section-card">
|
||
<view class="sec-title">服务项目</view>
|
||
<view class="service-list">
|
||
<view
|
||
v-for="srv in expert.services"
|
||
:key="srv.id"
|
||
class="service-item btn-active"
|
||
:class="{ selected: selectedServiceId === srv.id }"
|
||
@click="selectService(srv)"
|
||
>
|
||
<view class="srv-left">
|
||
<view class="srv-title">{{ srv.title }}</view>
|
||
<view class="srv-desc">{{ srv.desc }}</view>
|
||
</view>
|
||
<view class="srv-right">
|
||
<text class="price-sym">¥</text>
|
||
<text class="price-num">{{ srv.price }}</text>
|
||
<text class="price-unit">/{{ srv.unit }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 线下安全守则提示卡 -->
|
||
<view class="offline-safety-card" v-if="expert.city">
|
||
<image class="safety-icon-svg" :src="ICONS.shield" mode="aspectFit" />
|
||
<view class="safety-content">
|
||
<text class="safety-title">绿色合规陪伴服务承诺</text>
|
||
<text class="safety-desc">该大神已完成公安实名联网比对与健康认证。同城陪玩/陪吃/陪喝仅限在公开正规商业场所履约,到场出示4位核销码现场签到。</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 战绩与生活照片 -->
|
||
<view class="section-card" v-if="expert.photos && expert.photos.length > 0">
|
||
<view class="sec-title">战绩相册</view>
|
||
<scroll-view scroll-x class="gallery-scroll" :show-scrollbar="false">
|
||
<view class="gallery-list">
|
||
<image
|
||
v-for="(photo, index) in expert.photos"
|
||
:key="index"
|
||
class="gallery-item"
|
||
:src="photo"
|
||
mode="aspectFill"
|
||
@click="previewImage(index)"
|
||
/>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 真实买家评价 -->
|
||
<view class="section-card">
|
||
<view class="sec-header">
|
||
<text class="sec-title">服务好评 ({{ expert.reviews ? expert.reviews.length : 0 }})</text>
|
||
<text class="rate-percent">好评率 99.8%</text>
|
||
</view>
|
||
|
||
<view class="review-list" v-if="expert.reviews && expert.reviews.length > 0">
|
||
<view v-for="rev in expert.reviews" :key="rev.id" class="review-item">
|
||
<image class="rev-avatar" :src="rev.userAvatar" mode="aspectFill" />
|
||
<view class="rev-body">
|
||
<view class="rev-top">
|
||
<text class="rev-name">{{ rev.userNickname }}</text>
|
||
<text class="rev-time">{{ rev.time }}</text>
|
||
</view>
|
||
<view class="tag-row">
|
||
<text class="rev-tag" v-for="(t, i) in rev.tags" :key="i">{{ t }}</text>
|
||
</view>
|
||
<text class="rev-content">{{ rev.content }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="empty-reviews">
|
||
<text>近期新增服务,暂无评价记录</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部固定栏 (限制居中与安全区) -->
|
||
<view class="bottom-bar">
|
||
<view class="icon-action btn-active" @click="chatOnline">
|
||
<image class="action-svg" :src="ICONS.chat" mode="aspectFit" />
|
||
<text class="action-text">私聊</text>
|
||
</view>
|
||
<view class="price-summary">
|
||
<text class="summary-label">当前选定:</text>
|
||
<text class="summary-price">¥{{ currentSelectedService ? currentSelectedService.price : expert.price }}</text>
|
||
</view>
|
||
<button class="btn-book btn-active" @click="goToOrder">立即预约</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
import { EXPERTS_LIST } from '@/common/mock.js';
|
||
import { ICONS } from '@/common/icons.js';
|
||
|
||
const expert = ref(EXPERTS_LIST[0]);
|
||
const selectedServiceId = ref(EXPERTS_LIST[0].services[0] ? EXPERTS_LIST[0].services[0].id : null);
|
||
const isPlayingVoice = ref(false);
|
||
|
||
onLoad((query) => {
|
||
if (query && query.id) {
|
||
const found = EXPERTS_LIST.find(e => e.id === query.id);
|
||
if (found) {
|
||
expert.value = found;
|
||
if (found.services && found.services.length > 0) {
|
||
selectedServiceId.value = found.services[0].id;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const currentSelectedService = computed(() => {
|
||
if (!expert.value.services) return null;
|
||
return expert.value.services.find(s => s.id === selectedServiceId.value);
|
||
});
|
||
|
||
const selectService = (srv) => {
|
||
selectedServiceId.value = srv.id;
|
||
};
|
||
|
||
const toggleVoice = () => {
|
||
isPlayingVoice.value = !isPlayingVoice.value;
|
||
if (isPlayingVoice.value) {
|
||
uni.showToast({ title: '正在播放大神打招呼原声', icon: 'none' });
|
||
setTimeout(() => {
|
||
isPlayingVoice.value = false;
|
||
}, 4000);
|
||
}
|
||
};
|
||
|
||
const chatOnline = () => {
|
||
uni.showToast({ title: '已开启即时加密连麦通道', icon: 'none' });
|
||
};
|
||
|
||
const previewImage = (index) => {
|
||
uni.previewImage({
|
||
current: index,
|
||
urls: expert.value.photos
|
||
});
|
||
};
|
||
|
||
const goToOrder = () => {
|
||
const srv = currentSelectedService.value || expert.value.services[0];
|
||
uni.navigateTo({
|
||
url: `/pages/order/create?expertId=${expert.value.id}&serviceId=${srv.id}`
|
||
});
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.expert-detail-container {
|
||
min-height: 100vh;
|
||
background-color: #0A0F1D;
|
||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.hero-header {
|
||
position: relative;
|
||
height: 440rpx;
|
||
overflow: hidden;
|
||
|
||
.cover-bg {
|
||
width: 100%;
|
||
height: 100%;
|
||
filter: blur(8px);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.cover-mask {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(180deg, rgba(10, 15, 29, 0.4) 0%, #0A0F1D 100%);
|
||
}
|
||
|
||
.profile-card {
|
||
position: absolute;
|
||
bottom: 20rpx;
|
||
left: 32rpx;
|
||
right: 32rpx;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 24rpx;
|
||
|
||
.avatar-wrap {
|
||
position: relative;
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
flex-shrink: 0;
|
||
|
||
.avatar-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 70rpx;
|
||
border: 4rpx solid #6366F1;
|
||
}
|
||
|
||
.status-badge {
|
||
position: absolute;
|
||
bottom: 0;
|
||
right: 0;
|
||
background: #64748B;
|
||
color: #FFF;
|
||
font-size: 18rpx;
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
|
||
&.online {
|
||
background: #10B981;
|
||
box-shadow: 0 0 8rpx #10B981;
|
||
}
|
||
|
||
.pulse-dot {
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
background: #FFF;
|
||
border-radius: 50%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-wrap {
|
||
flex: 1;
|
||
|
||
.name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-bottom: 8rpx;
|
||
|
||
.nickname {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #FFF;
|
||
}
|
||
.level-tag {
|
||
background: linear-gradient(135deg, #F59E0B, #D97706);
|
||
color: #FFF;
|
||
font-size: 18rpx;
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 6rpx;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.rank-row {
|
||
display: flex;
|
||
gap: 10rpx;
|
||
margin-bottom: 8rpx;
|
||
|
||
.game-tag {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
color: #E2E8F0;
|
||
font-size: 20rpx;
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
.rank-tag {
|
||
background: rgba(99, 102, 241, 0.2);
|
||
color: #818CF8;
|
||
font-size: 20rpx;
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
}
|
||
|
||
.city-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
margin-bottom: 8rpx;
|
||
|
||
.city-pin-svg {
|
||
width: 22rpx;
|
||
height: 22rpx;
|
||
}
|
||
.city-text {
|
||
font-size: 20rpx;
|
||
color: #38BDF8;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.data-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
|
||
.rating-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
|
||
.star-svg {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
}
|
||
|
||
.rating-val {
|
||
color: #F59E0B;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.voice-card {
|
||
margin: 16rpx 32rpx 24rpx;
|
||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||
border: 1px solid rgba(99, 102, 241, 0.35);
|
||
border-radius: 20rpx;
|
||
padding: 20rpx 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.voice-icon-wrap {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 30rpx;
|
||
background: #6366F1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 18rpx;
|
||
|
||
.play-svg {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
}
|
||
}
|
||
|
||
.voice-info {
|
||
flex: 1;
|
||
|
||
.voice-title {
|
||
font-size: 24rpx;
|
||
color: #F8FAFC;
|
||
font-weight: 600;
|
||
display: block;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.voice-dur {
|
||
font-size: 20rpx;
|
||
color: #818CF8;
|
||
}
|
||
|
||
.voice-wave {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 6rpx;
|
||
height: 24rpx;
|
||
|
||
.bar {
|
||
width: 6rpx;
|
||
background-color: #818CF8;
|
||
border-radius: 3rpx;
|
||
|
||
&.b1 { height: 16rpx; }
|
||
&.b2 { height: 24rpx; }
|
||
&.b3 { height: 12rpx; }
|
||
&.b4 { height: 20rpx; }
|
||
}
|
||
}
|
||
}
|
||
|
||
.listen-tag {
|
||
font-size: 20rpx;
|
||
color: #818CF8;
|
||
border: 1px solid rgba(129, 140, 248, 0.4);
|
||
padding: 4rpx 14rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
}
|
||
|
||
.section-card {
|
||
margin: 0 32rpx 24rpx;
|
||
background-color: #151D30;
|
||
border-radius: 24rpx;
|
||
padding: 28rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
|
||
.sec-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
|
||
.rate-percent {
|
||
font-size: 22rpx;
|
||
color: #10B981;
|
||
}
|
||
}
|
||
|
||
.sec-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #F8FAFC;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.bio-text {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.service-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
|
||
.service-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 16rpx;
|
||
padding: 20rpx 24rpx;
|
||
|
||
&.selected {
|
||
background: rgba(99, 102, 241, 0.16);
|
||
border-color: #6366F1;
|
||
box-shadow: 0 0 16rpx rgba(99, 102, 241, 0.35);
|
||
}
|
||
|
||
.srv-left {
|
||
.srv-title {
|
||
font-size: 26rpx;
|
||
color: #F8FAFC;
|
||
font-weight: 600;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
.srv-desc {
|
||
font-size: 20rpx;
|
||
color: #64748B;
|
||
}
|
||
}
|
||
|
||
.srv-right {
|
||
color: #10B981;
|
||
font-variant-numeric: tabular-nums;
|
||
.price-sym { font-size: 22rpx; }
|
||
.price-num { font-size: 34rpx; font-weight: 700; }
|
||
.price-unit { font-size: 20rpx; color: #64748B; }
|
||
}
|
||
}
|
||
}
|
||
|
||
.offline-safety-card {
|
||
margin-top: 20rpx;
|
||
background: rgba(16, 185, 129, 0.08);
|
||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||
border-radius: 16rpx;
|
||
padding: 16rpx 20rpx;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12rpx;
|
||
|
||
.safety-icon-svg {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
flex-shrink: 0;
|
||
margin-top: 4rpx;
|
||
}
|
||
.safety-content {
|
||
flex: 1;
|
||
|
||
.safety-title {
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
color: #10B981;
|
||
display: block;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
.safety-desc {
|
||
font-size: 20rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.4;
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
|
||
.gallery-scroll {
|
||
white-space: nowrap;
|
||
|
||
.gallery-list {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.gallery-item {
|
||
width: 220rpx;
|
||
height: 160rpx;
|
||
border-radius: 16rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
}
|
||
|
||
.review-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
|
||
.review-item {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
|
||
.rev-avatar {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 32rpx;
|
||
}
|
||
|
||
.rev-body {
|
||
flex: 1;
|
||
|
||
.rev-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 6rpx;
|
||
|
||
.rev-name {
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
color: #E2E8F0;
|
||
}
|
||
.rev-time {
|
||
font-size: 20rpx;
|
||
color: #64748B;
|
||
}
|
||
}
|
||
|
||
.tag-row {
|
||
display: flex;
|
||
gap: 10rpx;
|
||
margin-bottom: 8rpx;
|
||
|
||
.rev-tag {
|
||
font-size: 18rpx;
|
||
color: #818CF8;
|
||
background: rgba(99, 102, 241, 0.15);
|
||
padding: 2rpx 8rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
}
|
||
|
||
.rev-content {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-reviews {
|
||
text-align: center;
|
||
color: #64748B;
|
||
font-size: 22rpx;
|
||
padding: 20rpx 0;
|
||
}
|
||
}
|
||
|
||
.bottom-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: calc(120rpx + env(safe-area-inset-bottom));
|
||
padding: 0 32rpx env(safe-area-inset-bottom);
|
||
box-sizing: border-box;
|
||
background-color: rgba(21, 29, 48, 0.95);
|
||
backdrop-filter: blur(12px);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
z-index: 100;
|
||
max-width: 450px;
|
||
margin: 0 auto;
|
||
|
||
.icon-action {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 80rpx;
|
||
|
||
.action-svg {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
}
|
||
.action-text {
|
||
font-size: 18rpx;
|
||
color: #94A3B8;
|
||
margin-top: 4rpx;
|
||
}
|
||
}
|
||
|
||
.price-summary {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: baseline;
|
||
|
||
.summary-label {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
}
|
||
.summary-price {
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
color: #10B981;
|
||
margin-left: 6rpx;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
}
|
||
|
||
.btn-book {
|
||
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
|
||
color: #FFF;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
border-radius: 44rpx;
|
||
height: 80rpx;
|
||
line-height: 80rpx;
|
||
padding: 0 48rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(99, 102, 241, 0.4);
|
||
margin: 0;
|
||
}
|
||
}
|
||
</style>
|