- 起因:上一批移除"系统自动生成演示服务包"后,未配置服务包的大神返回空列表,下单页模板直接取 selectedService.title 触发空指针 - 下单页新增 serviceView 安全视图(computed 带兜底对象),模板统一改用 serviceView.title/price/unit,杜绝解引用 null - 无服务包时按大神基础时薪兜底生成"<品类>基础陪伴服务",保证仍可下单(价格仍由服务端档案价二次校验) - 详情页取 services[0] 前先判空,并新增"暂未配置服务项目"空态提示 - 新增回归护栏 null-service-guard.spec.ts(98 套件/677 用例全绿)
1662 行
42 KiB
Vue
1662 行
42 KiB
Vue
<template>
|
||
<view class="expert-detail-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
|
||
<!-- 大神主页骨架屏 (首屏加载中流光占位,杜绝视觉空白) -->
|
||
<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" v-if="isCompanion">
|
||
<view class="sec-title">服务项目</view>
|
||
<view class="service-list">
|
||
<view
|
||
v-if="!expert.services || expert.services.length === 0"
|
||
class="service-empty"
|
||
>
|
||
<text class="service-empty-text">该大神暂未配置服务项目,可直接发起基础陪伴预约</text>
|
||
</view>
|
||
<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>
|
||
<template v-if="isCompanion">
|
||
<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>
|
||
</template>
|
||
<view v-else class="price-summary">
|
||
<text class="summary-label">该用户尚未认证为大神,暂不可预约</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 举报不良行为弹窗 -->
|
||
<view class="review-modal-mask" v-if="reportModalVisible" @click="closeReportModal">
|
||
<view class="review-modal-content" @click.stop="stopPropagation">
|
||
<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, onMounted, onUnmounted } from 'vue';
|
||
import { onLoad, onShow } 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';
|
||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||
import { ensureLoginOrRedirect } from '@/common/role.js';
|
||
|
||
const currentThemeKey = ref(getCurrentThemeKey());
|
||
|
||
const onThemeChanged = (key) => {
|
||
currentThemeKey.value = key;
|
||
applyThemeToNativeUI(key);
|
||
};
|
||
|
||
onMounted(() => {
|
||
currentThemeKey.value = getCurrentThemeKey();
|
||
applyThemeToNativeUI(currentThemeKey.value);
|
||
uni.$on('theme-change', onThemeChanged);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
uni.$off('theme-change', onThemeChanged);
|
||
});
|
||
|
||
const stopPropagation = (e) => {
|
||
if (e && typeof e.stopPropagation === 'function') {
|
||
e.stopPropagation();
|
||
}
|
||
};
|
||
|
||
const isLoading = ref(true);
|
||
const isFav = ref(false);
|
||
const buildDefaultServices = (gameTitle = '同城陪伴', basePrice = 68) => [
|
||
{
|
||
id: 'srv_1',
|
||
name: `${gameTitle} · 基础陪伴 (2小时)`,
|
||
title: `${gameTitle} · 基础陪伴 (2小时)`,
|
||
price: basePrice,
|
||
unit: '小时',
|
||
desc: '正规公开商圈同城陪伴,AA制或协商消费'
|
||
},
|
||
{
|
||
id: 'srv_2',
|
||
name: `${gameTitle} · 半日畅享 (3小时)`,
|
||
title: `${gameTitle} · 半日畅享 (3小时)`,
|
||
price: Math.round(basePrice * 2.8),
|
||
unit: '次',
|
||
desc: '深度互动与陪伴交流,赠送专属点歌或语音祝福'
|
||
}
|
||
];
|
||
|
||
const expert = ref({
|
||
id: '101',
|
||
nickname: '同城陪伴师',
|
||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
|
||
gender: 'female',
|
||
levelTag: '',
|
||
game: '同城陪伴',
|
||
rankTag: '',
|
||
city: '',
|
||
rating: 0,
|
||
orderCount: 0,
|
||
isOnline: false,
|
||
bio: '',
|
||
voiceDuration: 0,
|
||
price: 0,
|
||
services: [],
|
||
photos: [
|
||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400',
|
||
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400'
|
||
],
|
||
reviews: []
|
||
});
|
||
// 是否为真实大神档案(非合成的普通用户资料)
|
||
const isCompanion = computed(() => expert.value.isCompanion !== false);
|
||
const selectedServiceId = ref('srv_1');
|
||
const isPlayingVoice = ref(false);
|
||
let currentExpertTargetId = null;
|
||
|
||
const refreshFavoriteState = async () => {
|
||
if (!currentExpertTargetId) return;
|
||
try {
|
||
const favStatus = await checkFavorite(currentExpertTargetId);
|
||
isFav.value = Boolean(favStatus);
|
||
} catch (_) {}
|
||
};
|
||
|
||
onShow(() => {
|
||
currentThemeKey.value = getCurrentThemeKey();
|
||
applyThemeToNativeUI(currentThemeKey.value);
|
||
refreshFavoriteState();
|
||
});
|
||
|
||
const toggleFavorite = async () => {
|
||
const targetId = Number(expert.value.id || currentExpertTargetId || 101);
|
||
if (!ensureLoginOrRedirect(`/pages/expert/detail?id=${targetId}`)) {
|
||
return;
|
||
}
|
||
try {
|
||
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 targetId = Number(expert.value.id || currentExpertTargetId || 101);
|
||
if (!ensureLoginOrRedirect(`/pages/expert/detail?id=${targetId}`)) {
|
||
return;
|
||
}
|
||
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' });
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
const safeDecode = (val) => {
|
||
if (!val) return '';
|
||
try {
|
||
return decodeURIComponent(val);
|
||
} catch (_) {
|
||
return val;
|
||
}
|
||
};
|
||
|
||
onLoad(async (query) => {
|
||
const targetId = (query && query.id) ? safeDecode(query.id) : '1';
|
||
const queryName = query && query.name ? safeDecode(query.name) : '';
|
||
const queryAvatar = query && query.avatar ? safeDecode(query.avatar) : '';
|
||
const queryTag = query && query.tag ? safeDecode(query.tag) : '';
|
||
const queryCity = query && query.city ? safeDecode(query.city) : '';
|
||
currentExpertTargetId = targetId;
|
||
|
||
const cachedAuthor = uni.getStorageSync('ACTIVE_AUTHOR_PROFILE');
|
||
const matchedSnapshot = (cachedAuthor && (String(cachedAuthor.id) === String(targetId) || String(cachedAuthor.userId) === String(targetId) || (queryName && cachedAuthor.nickname === queryName)))
|
||
? cachedAuthor
|
||
: null;
|
||
|
||
if (matchedSnapshot || queryName) {
|
||
const snapName = queryName || (matchedSnapshot && matchedSnapshot.nickname) || expert.value.nickname;
|
||
const snapAvatar = queryAvatar || (matchedSnapshot && matchedSnapshot.avatar) || expert.value.avatar;
|
||
const snapTag = queryTag || (matchedSnapshot && matchedSnapshot.rankTag) || expert.value.rankTag;
|
||
const snapCity = queryCity || (matchedSnapshot && matchedSnapshot.city) || expert.value.city;
|
||
const snapGame = (matchedSnapshot && matchedSnapshot.game) || '同城搭子';
|
||
const snapBio = (matchedSnapshot && matchedSnapshot.bio) || expert.value.bio;
|
||
const snapPhotos = (matchedSnapshot && Array.isArray(matchedSnapshot.photos) && matchedSnapshot.photos.length > 0)
|
||
? matchedSnapshot.photos
|
||
: [snapAvatar];
|
||
const snapPrice = snapGame === '车队开黑' ? 88 : 128;
|
||
|
||
expert.value = {
|
||
...expert.value,
|
||
id: String(targetId),
|
||
nickname: snapName,
|
||
avatar: snapAvatar,
|
||
coverImage: snapPhotos[0] || snapAvatar,
|
||
rankTag: snapTag,
|
||
levelTag: '实名认证',
|
||
game: snapGame,
|
||
city: snapCity,
|
||
bio: snapBio,
|
||
price: snapPrice,
|
||
photos: snapPhotos,
|
||
services: buildDefaultServices(snapGame, snapPrice)
|
||
};
|
||
if (expert.value.services && expert.value.services.length > 0) {
|
||
selectedServiceId.value = expert.value.services[0].id;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const liveData = await getCompanionDetail(targetId);
|
||
if (liveData) {
|
||
if (queryName || matchedSnapshot) {
|
||
const finalName = queryName || (matchedSnapshot && matchedSnapshot.nickname) || liveData.nickname;
|
||
const finalAvatar = queryAvatar || (matchedSnapshot && matchedSnapshot.avatar) || liveData.avatar;
|
||
const finalTag = queryTag || (matchedSnapshot && matchedSnapshot.rankTag) || liveData.rankTag;
|
||
const finalCity = queryCity || (matchedSnapshot && matchedSnapshot.city) || liveData.city;
|
||
const finalBio = (matchedSnapshot && matchedSnapshot.bio) || liveData.bio;
|
||
const finalPhotos = (matchedSnapshot && Array.isArray(matchedSnapshot.photos) && matchedSnapshot.photos.length > 0)
|
||
? matchedSnapshot.photos
|
||
: (liveData.photos || [finalAvatar]);
|
||
|
||
expert.value = {
|
||
...liveData,
|
||
nickname: finalName,
|
||
avatar: finalAvatar,
|
||
coverImage: finalPhotos[0] || finalAvatar,
|
||
rankTag: finalTag,
|
||
city: finalCity,
|
||
bio: finalBio,
|
||
photos: finalPhotos
|
||
};
|
||
} else {
|
||
expert.value = liveData;
|
||
}
|
||
if (expert.value.services && expert.value.services.length > 0) {
|
||
selectedServiceId.value = expert.value.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 && expert.value.services[0]);
|
||
const srvId = srv ? srv.id : 'srv_1';
|
||
const targetUrl = `/pages/order/create?expertId=${expert.value.id || currentExpertTargetId || 101}&serviceId=${srvId}`;
|
||
if (!ensureLoginOrRedirect(targetUrl)) {
|
||
return;
|
||
}
|
||
uni.navigateTo({
|
||
url: targetUrl
|
||
});
|
||
};
|
||
|
||
// 举报违规行为状态与提交
|
||
const reportModalVisible = ref(false);
|
||
const submittingReport = ref(false);
|
||
const presetReportReasons = [
|
||
'涉黄低俗言论',
|
||
'私下诱导转账/欺诈',
|
||
'态度恶劣/人身攻击',
|
||
'开挂/作弊/战绩造假',
|
||
'线下违规/安全隐患',
|
||
'其他违规行为'
|
||
];
|
||
const reportForm = ref({
|
||
reason: '涉黄低俗言论',
|
||
description: ''
|
||
});
|
||
|
||
const openReportModal = () => {
|
||
const targetId = Number(expert.value.id || currentExpertTargetId || 101);
|
||
if (!ensureLoginOrRedirect(`/pages/expert/detail?id=${targetId}`)) {
|
||
return;
|
||
}
|
||
reportForm.value = {
|
||
reason: '涉黄低俗言论',
|
||
description: ''
|
||
};
|
||
reportModalVisible.value = true;
|
||
};
|
||
|
||
const closeReportModal = () => {
|
||
reportModalVisible.value = false;
|
||
};
|
||
|
||
const submitReportTicket = async () => {
|
||
submittingReport.value = true;
|
||
uni.showLoading({ title: '正在提交举报...' });
|
||
try {
|
||
await submitReportApi({
|
||
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: '举报提交成功', icon: 'success' });
|
||
setTimeout(() => {
|
||
uni.showModal({
|
||
title: '举报受理通知',
|
||
content: '感谢您的监督!平台安全团队将在 24 小时内核查聊天记录与履约凭据,违规属实将从严处罚。',
|
||
showCancel: false,
|
||
confirmText: '我知道了'
|
||
});
|
||
}, 400);
|
||
} catch (err) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: err.message || '举报提交失败', icon: 'none' });
|
||
} finally {
|
||
submittingReport.value = false;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.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-empty {
|
||
padding: 28rpx 24rpx;
|
||
border-radius: 16rpx;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px dashed rgba(255, 255, 255, 0.12);
|
||
text-align: center;
|
||
|
||
.service-empty-text {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.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);
|
||
}
|
||
}
|
||
|
||
/* 暖阳晨曦浅色主题适配 (theme-warm) */
|
||
.expert-detail-container.theme-warm {
|
||
background-color: #FDFBF7;
|
||
|
||
.hero-header {
|
||
.cover-mask {
|
||
background: linear-gradient(180deg, rgba(253, 251, 247, 0.45) 0%, #FDFBF7 100%);
|
||
}
|
||
|
||
.profile-card {
|
||
.avatar-wrap {
|
||
.avatar-img {
|
||
border-color: #EA580C;
|
||
}
|
||
}
|
||
|
||
.info-wrap {
|
||
.name-row {
|
||
.nickname {
|
||
color: #1C1917;
|
||
}
|
||
}
|
||
|
||
.rank-row {
|
||
.game-tag {
|
||
background: #F5F5F4;
|
||
color: #57534E;
|
||
}
|
||
.rank-tag {
|
||
background: #FFF7ED;
|
||
color: #EA580C;
|
||
}
|
||
}
|
||
|
||
.city-row {
|
||
.city-text {
|
||
color: #EA580C;
|
||
}
|
||
}
|
||
|
||
.data-row {
|
||
color: #78716C;
|
||
|
||
.rating-box {
|
||
.rating-val {
|
||
color: #D97706;
|
||
}
|
||
}
|
||
.order-val {
|
||
color: #78716C;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.voice-card {
|
||
background: #FFFFFF;
|
||
border-color: #FED7AA;
|
||
box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.08);
|
||
|
||
.voice-icon-wrap {
|
||
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
|
||
box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.3);
|
||
}
|
||
|
||
.voice-info {
|
||
.voice-title {
|
||
color: #1C1917;
|
||
}
|
||
.voice-dur {
|
||
color: #78716C;
|
||
}
|
||
.voice-wave .bar {
|
||
background: #EA580C;
|
||
}
|
||
}
|
||
|
||
.listen-tag {
|
||
background: #FFF7ED;
|
||
color: #EA580C;
|
||
border: 1px solid rgba(234, 88, 12, 0.3);
|
||
}
|
||
}
|
||
|
||
.section-card {
|
||
background: #FFFFFF;
|
||
border: 1px solid #F0ECE4;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
||
|
||
.sec-title {
|
||
color: #1C1917;
|
||
}
|
||
|
||
.bio-text {
|
||
color: #57534E;
|
||
}
|
||
|
||
.rate-percent {
|
||
color: #EA580C;
|
||
}
|
||
|
||
.empty-reviews {
|
||
color: #A8A29E;
|
||
}
|
||
}
|
||
|
||
.service-list {
|
||
.service-item {
|
||
background: #FDFBF7;
|
||
border-color: #E7E5E4;
|
||
|
||
.srv-left {
|
||
.srv-title {
|
||
color: #1C1917;
|
||
}
|
||
.srv-desc {
|
||
color: #78716C;
|
||
}
|
||
}
|
||
|
||
.srv-right {
|
||
.price-sym, .price-num {
|
||
color: #EA580C;
|
||
}
|
||
.price-unit {
|
||
color: #A8A29E;
|
||
}
|
||
}
|
||
|
||
&.selected {
|
||
border-color: #EA580C;
|
||
background: #FFF7ED;
|
||
box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.12);
|
||
}
|
||
}
|
||
}
|
||
|
||
.offline-safety-card {
|
||
background: #FFFBEB;
|
||
border-color: #FDE68A;
|
||
|
||
.safety-content {
|
||
.safety-title {
|
||
color: #B45309;
|
||
}
|
||
.safety-desc {
|
||
color: #78716C;
|
||
}
|
||
}
|
||
}
|
||
|
||
.gallery-list {
|
||
.gallery-item {
|
||
border-color: #E7E5E4;
|
||
}
|
||
}
|
||
|
||
.review-list {
|
||
.review-item {
|
||
border-bottom-color: #F5F5F4;
|
||
|
||
.rev-body {
|
||
.rev-top {
|
||
.rev-name {
|
||
color: #1C1917;
|
||
}
|
||
.rev-time {
|
||
color: #A8A29E;
|
||
}
|
||
}
|
||
|
||
.tag-row {
|
||
.rev-tag {
|
||
background: #F5F5F4;
|
||
color: #57534E;
|
||
}
|
||
}
|
||
|
||
.rev-content {
|
||
color: #44403C;
|
||
}
|
||
|
||
.expert-reply-bubble {
|
||
background: #FDFBF7;
|
||
border-left-color: #EA580C;
|
||
|
||
.reply-badge-row {
|
||
.reply-badge {
|
||
background: #FFF7ED;
|
||
color: #EA580C;
|
||
}
|
||
.reply-time {
|
||
color: #A8A29E;
|
||
}
|
||
}
|
||
|
||
.reply-text {
|
||
color: #57534E;
|
||
}
|
||
}
|
||
|
||
.reply-action-row {
|
||
.btn-quick-reply {
|
||
background: #FFF7ED;
|
||
color: #EA580C;
|
||
border-color: rgba(234, 88, 12, 0.3);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.bottom-bar {
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border-top-color: #F0ECE4;
|
||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||
|
||
.icon-action {
|
||
.action-text {
|
||
color: #57534E;
|
||
|
||
&.fav-active {
|
||
color: #EA580C;
|
||
}
|
||
}
|
||
}
|
||
|
||
.price-summary {
|
||
.summary-label {
|
||
color: #78716C;
|
||
}
|
||
.summary-price {
|
||
color: #EA580C;
|
||
}
|
||
}
|
||
|
||
.btn-book {
|
||
background: linear-gradient(135deg, #EA580C 0%, #C2410C 100%);
|
||
box-shadow: 0 4rpx 20rpx rgba(234, 88, 12, 0.35);
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
|
||
.review-modal-content {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.modal-header {
|
||
.modal-title {
|
||
color: #1C1917;
|
||
}
|
||
.modal-subtitle {
|
||
color: #78716C;
|
||
}
|
||
}
|
||
|
||
.tags-section {
|
||
.tags-label {
|
||
color: #1C1917;
|
||
}
|
||
.tags-wrap {
|
||
.review-tag {
|
||
background: #F5F5F4;
|
||
color: #57534E;
|
||
border-color: #E7E5E4;
|
||
|
||
&.active {
|
||
color: #EF4444;
|
||
background: #FEF2F2;
|
||
border-color: #EF4444;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.textarea-section {
|
||
.textarea-label {
|
||
color: #1C1917;
|
||
}
|
||
.review-textarea {
|
||
background: #FDFBF7;
|
||
color: #1C1917;
|
||
border-color: #E7E5E4;
|
||
}
|
||
.char-count {
|
||
color: #A8A29E;
|
||
}
|
||
}
|
||
|
||
.modal-actions {
|
||
.modal-btn.cancel-btn {
|
||
background: #F5F5F4;
|
||
color: #57534E;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|