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

1175 行
30 KiB
Vue

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

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

<template>
<view class="expert-detail-container">
<!-- 大神主页骨架屏 (首屏加载中流光占位,杜绝视觉空白) -->
<skeleton-screen v-if="isLoading" type="expert" />
<!-- 真实内容 (数据加载完成平滑展示) -->
<view v-else class="expert-body-wrapper">
<!-- 顶部封面与基本信息 -->
<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 class="expert-reply-bubble" v-if="rev.replyContent">
<view class="reply-badge-row">
<text class="reply-badge">大神回复</text>
<text class="reply-time" v-if="rev.replyTime">{{ rev.replyTime }}</text>
</view>
<text class="reply-text">{{ rev.replyContent }}</text>
</view>
<!-- 快捷回复感谢老板入口 -->
<view class="reply-action-row" v-if="!rev.replyContent">
<button class="btn-quick-reply btn-active" @click="handleReplyReview(rev)">回复感谢老板</button>
</view>
</view>
</view>
</view>
<view v-else class="empty-reviews">
<text>近期新增服务,暂无评价记录</text>
</view>
</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="icon-action btn-active" @click="toggleFavorite">
<image class="action-svg" :src="isFav ? ICONS.starFilled : ICONS.starOutline" mode="aspectFit" />
<text class="action-text" :class="{ 'fav-active': isFav }">{{ isFav ? '已收藏' : '收藏' }}</text>
</view>
<view class="icon-action btn-active" @click="openReportModal">
<image class="action-svg" :src="ICONS.shield" 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 class="review-modal-mask" v-if="reportModalVisible" @click.self="closeReportModal">
<view class="review-modal-content">
<view class="modal-header">
<view class="modal-header-left">
<text class="modal-title">举报违规行为</text>
<text class="modal-subtitle">
举报对象:{{ expert.nickname }}
</text>
</view>
<view class="modal-close" @click="closeReportModal">
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<!-- 违规类型单选 -->
<view class="tags-section">
<text class="tags-label">选择违规类型</text>
<view class="tags-wrap">
<view
v-for="item in presetReportReasons"
:key="item"
class="review-tag"
:class="{ active: reportForm.reason === item }"
@click="reportForm.reason = item"
>
{{ item }}
</view>
</view>
</view>
<!-- 举报描述 -->
<view class="textarea-section">
<text class="textarea-label">详细违规线索说明</text>
<textarea
class="review-textarea"
v-model="reportForm.description"
placeholder="请填写具体违规线索(如涉黄言论、私下诱导转账、脱离平台等),平台将严肃核处..."
placeholder-class="textarea-placeholder"
maxlength="200"
/>
<text class="char-count">{{ reportForm.description.length }}/200</text>
</view>
<!-- 操作按钮 -->
<view class="modal-actions">
<button class="modal-btn cancel-btn btn-active" @click="closeReportModal">取消</button>
<button class="modal-btn submit-btn btn-active danger" :disabled="submittingReport" @click="submitReportTicket">
{{ submittingReport ? '正在提交...' : '确认提交举报' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import SkeletonScreen from '@/components/SkeletonScreen.vue';
import { getCompanionDetail, getExpertReviews, replyOrderReview, submitReport as submitReportApi, checkFavorite, addFavorite as addFavApi, removeFavorite as removeFavApi } from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
const isLoading = ref(true);
const isFav = ref(false);
const expert = ref({
id: '',
nickname: '同城陪伴师',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
gender: 'female',
levelTag: '平台认证',
game: '同城陪伴',
rankTag: '专业级',
city: '北京市 · 朝阳/海淀',
rating: 5.0,
orderCount: 0,
bio: '专业同城生活陪伴,健康阳光公开。',
voiceDuration: 15,
price: 68,
services: [],
photos: [],
reviews: []
});
const selectedServiceId = ref(null);
const isPlayingVoice = ref(false);
const toggleFavorite = async () => {
try {
const targetId = Number(expert.value.id || 101);
if (isFav.value) {
await removeFavApi(targetId);
isFav.value = false;
uni.showToast({ title: '已取消收藏', icon: 'none' });
} else {
await addFavApi(targetId);
isFav.value = true;
uni.showToast({ title: '已收藏该陪玩', icon: 'success' });
}
} catch (err) {
uni.showToast({ title: err.message || '操作失败', icon: 'none' });
}
};
const handleReplyReview = (rev) => {
const defaultReplies = [
'感谢老板的支持与五星好评!开黑配合非常默契,期待下次一起上大分!',
'谢谢老板认可!非常准时礼貌的高素质玩家老板,下次继续开黑走起~',
'多谢老板认可!服务过程非常愉快,有空随时来约局呀!'
];
uni.showActionSheet({
itemList: defaultReplies,
success: async (res) => {
const selectedReply = defaultReplies[res.tapIndex] || defaultReplies[0];
try {
uni.showLoading({ title: '正在提交回复...' });
await replyOrderReview(rev.id, selectedReply);
rev.replyContent = selectedReply;
rev.replyTime = '刚刚';
uni.hideLoading();
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
} catch (err) {
uni.hideLoading();
rev.replyContent = selectedReply;
rev.replyTime = '刚刚';
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
}
}
});
};
onLoad(async (query) => {
const targetId = (query && query.id) ? query.id : '1';
try {
const liveData = await getCompanionDetail(targetId);
if (liveData) {
expert.value = liveData;
if (liveData.services && liveData.services.length > 0) {
selectedServiceId.value = liveData.services[0].id;
}
}
try {
isFav.value = await checkFavorite(targetId);
} catch (_) {}
try {
const liveReviews = await getExpertReviews(targetId);
if (Array.isArray(liveReviews) && liveReviews.length > 0) {
expert.value.reviews = liveReviews.map(r => ({
id: r.id,
userNickname: r.userNickname || '买家玩家',
userAvatar: r.userAvatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100',
rating: r.rating || 5,
tags: r.tags || [],
content: r.content || '好评!',
time: r.createdAt ? '近期' : '刚刚',
replyContent: r.replyContent || '',
replyTime: r.replyTime || ''
}));
}
} catch (_) {}
} catch (err) {
console.warn('[ExpertDetail] 获取线上大神详情异常:', err);
} finally {
setTimeout(() => {
isLoading.value = false;
}, 200);
}
});
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.showActionSheet({
itemList: [
`与【${expert.value.nickname}】开启即时连麦`,
`向【${expert.value.nickname}】发送打招呼私信`,
`复制大神游戏开黑专属房号`
],
success: (res) => {
if (res.tapIndex === 0) {
uni.showToast({ title: '已建立加密连麦通道', icon: 'success' });
} else if (res.tapIndex === 1) {
uni.showToast({ title: '打招呼私信已送达', icon: 'success' });
} else {
uni.setClipboardData({
data: `PW-${expert.value.id}-${Math.floor(1000 + Math.random() * 9000)}`,
success: () => {
uni.showToast({ title: '专属房号已复制', icon: 'success' });
}
});
}
}
});
};
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}`
});
};
// 举报违规行为状态与提交
const reportModalVisible = ref(false);
const submittingReport = ref(false);
const presetReportReasons = [
'涉黄低俗言论',
'私下诱导转账/欺诈',
'态度恶劣/人身攻击',
'开挂/作弊/战绩造假',
'线下违规/安全隐患',
'其他违规行为'
];
const reportForm = ref({
reason: '涉黄低俗言论',
description: ''
});
const openReportModal = () => {
reportForm.value = {
reason: '涉黄低俗言论',
description: ''
};
reportModalVisible.value = true;
};
const closeReportModal = () => {
reportModalVisible.value = false;
};
const submitReportTicket = async () => {
submittingReport.value = true;
uni.showLoading({ title: '正在提交举报...' });
try {
await submitReportApi({
reporterId: 1001,
targetType: 'EXPERT',
targetId: String(expert.value.id || '101'),
targetName: expert.value.nickname || '同城陪伴师',
reason: reportForm.value.reason,
description: reportForm.value.description.trim()
});
uni.hideLoading();
closeReportModal();
uni.showToast({ title: '举报已受理,平台将在24小时内核实处置', icon: 'success' });
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '举报提交失败', icon: 'none' });
} finally {
submittingReport.value = false;
}
};
</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;
}
.expert-reply-bubble {
margin-top: 12rpx;
background: rgba(16, 185, 129, 0.08);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 12rpx;
padding: 12rpx 16rpx;
.reply-badge-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6rpx;
.reply-badge {
font-size: 18rpx;
color: #10B981;
font-weight: 600;
background: rgba(16, 185, 129, 0.15);
padding: 2rpx 8rpx;
border-radius: 6rpx;
}
.reply-time {
font-size: 18rpx;
color: #64748B;
}
}
.reply-text {
font-size: 20rpx;
color: #34D399;
line-height: 1.4;
}
}
.reply-action-row {
margin-top: 10rpx;
display: flex;
justify-content: flex-end;
.btn-quick-reply {
font-size: 18rpx;
color: #10B981;
background: rgba(16, 185, 129, 0.12);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 20rpx;
height: 38rpx;
line-height: 36rpx;
padding: 0 16rpx;
margin: 0;
}
}
}
}
}
.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;
&.fav-active {
color: #F59E0B;
font-weight: 600;
}
}
}
.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;
}
}
.review-modal-mask {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.review-modal-content {
width: 100%;
max-width: 450px;
background: #151D30;
border-top-left-radius: 36rpx;
border-top-right-radius: 36rpx;
padding: 36rpx 36rpx calc(36rpx + env(safe-area-inset-bottom));
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.5);
animation: slideUp 0.25s ease-out;
.modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32rpx;
.modal-header-left {
display: flex;
flex-direction: column;
gap: 6rpx;
.modal-title {
font-size: 34rpx;
font-weight: 700;
color: #F8FAFC;
}
.modal-subtitle {
font-size: 24rpx;
color: #94A3B8;
}
}
.modal-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 36rpx;
height: 36rpx;
}
}
}
.tags-section {
margin-bottom: 24rpx;
.tags-label {
font-size: 24rpx;
color: #94A3B8;
margin-bottom: 16rpx;
display: block;
}
.tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.review-tag {
padding: 12rpx 24rpx;
font-size: 24rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 30rpx;
transition: all 0.2s;
&.active {
color: #FFFFFF;
background: rgba(239, 68, 68, 0.25);
border-color: #EF4444;
font-weight: 600;
}
}
}
}
.textarea-section {
margin-bottom: 32rpx;
position: relative;
.textarea-label {
font-size: 24rpx;
color: #94A3B8;
margin-bottom: 12rpx;
display: block;
}
.review-textarea {
width: 100%;
height: 160rpx;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
color: #F8FAFC;
}
.char-count {
position: absolute;
right: 20rpx;
bottom: 16rpx;
font-size: 20rpx;
color: #64748B;
}
}
.modal-actions {
display: flex;
gap: 20rpx;
.modal-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
font-weight: 600;
margin: 0;
&.cancel-btn {
background: rgba(255, 255, 255, 0.08);
color: #94A3B8;
}
&.submit-btn {
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
color: #FFFFFF;
box-shadow: 0 4rpx 16rpx rgba(239, 68, 68, 0.4);
}
}
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
</style>