文件
shanghaipeiwan/pwuniapp/pages/expert/detail.vue

711 行
17 KiB
Vue

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

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

<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>