Files
DJKB/my-vue-app/src/views/person/components/SalesTimelineWithTaskList.vue
lbw_9527443 544a66b8fa feat(导航): 添加双击导航功能并优化数据展示
- 在GroupComparison组件中添加双击部门跳转到经理页面的功能
- 在secondTop组件中添加双击成员跳转到销售页面的功能
- 优化topOne组件中客户迫切问题排行榜的数据格式转换
- 在RankingList组件中增加展示条目并添加排序功能
- 在SalesTimelineWithTaskList组件中替换alert弹窗为自定义模态框
- 优化secondTop组件路由跳转逻辑,避免重复请求
2025-08-21 10:47:41 +08:00

1155 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="sales-timeline-with-task-list">
<div class="timeline-title-section" style="display: flex; align-items: center; gap: 10px;">
<h2 style="font-size: 18px;margin: 0;">销售时间线</h2>
<span style="font-size: 14px;">客户转化全流程跟踪</span>
</div>
<div class="sales-timeline">
<div class="timeline-container">
<div class="timeline-line"></div>
<div
v-for="(stage, index) in stages"
:key="stage.id"
class="timeline-stage"
:class="{ 'active': stage.count > 0, 'selected': selectedStage === stage.name }"
@click="selectStage(stage.name)"
>
<div class="stage-marker">
<div class="marker-circle">
<span class="stage-number">{{ index + 1 }}</span>
</div>
<div class="marker-line" v-if="index < stages.length - 1"></div>
</div>
<div class="stage-content">
<h3 class="stage-title">{{ stage.displayName || stage.name }}</h3>
<div class="stage-stats">
<span class="stage-count">{{ stage.name === '全部' ? customersCount : stage.count }}</span>
<span class="stage-label">位客户</span>
</div>
<div v-if="stage.name !== '全部'" class="stage-percentage">{{ getPercentage(stage.count) }}%</div>
</div>
</div>
</div>
</div>
<div class="task-body">
<div class="actionable-list">
<div class="items-grid">
<div
v-for="item in contacts"
:key="item.id"
:id="`contact-item-${item.id}`"
class="action-item"
:class="[getHealthIndicator(item.health).class, { 'active': item.id === selectedContactId }]"
@click="selectContact(item.id)">
<div class="item-content">
<!-- 头像区域 -->
<div class="avatar-section">
<div class="avatar">
<img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" />
</div>
</div>
<!-- 信息区域 -->
<div class="info-section">
<div class="item-header">
<div class="item-name-group">
<span class="item-name" :title="item.name">{{ item.name }}</span>
</div>
<span class="item-time">{{ item.time }}</span>
</div>
<div class="item-footer">
<div class="profession-education">
<span class="profession">{{ item.profession||'未知' }}</span>
<span class="education">{{ item.education||'未知' }}</span>
<span class="course-attendance">
{{ getAttendedLessons(item.class_situation,item.class_num) }}
</span>
<span class="course-type">{{ item.type }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 到课详情区域 -->
<div v-if="selectedContactId && selectedContactDetails && selectedStage === '课1-4'" class="course-details">
<div class="course-details-header">
<h4>{{ selectedContactDetails.name }} - 到课详情</h4>
</div>
<div class="course-details-content">
<div v-if="!selectedContactDetails.class_situation || Object.keys(selectedContactDetails.class_situation).length === 0" class="no-data">
未到课
</div>
<div v-else class="course-lessons">
<div
v-for="(lessonData, lessonNum) in selectedContactDetails.class_situation"
:key="lessonNum"
class="lesson-item"
>
<div class="lesson-header">
<span class="lesson-number">{{ lessonNum }}节课</span>
</div>
<div class="lesson-details">
<div class="detail-item">
<span class="detail-label">听课时长:</span>
<span class="detail-value">{{ Math.round((lessonData.live_maximum_length_time || 0) / 60) }}分钟</span>
</div>
<div class="detail-item">
<span class="detail-label">是否看回放:</span>
<span class="detail-value">{{ lessonData.playback_maximum_length_time ? '是' : '否' }}</span>
</div>
<div class="detail-item" v-if="lessonData.playback_maximum_length_time">
<span class="detail-label">回放时长:</span>
<span class="detail-value">{{ Math.round((lessonData.playback_maximum_length_time || 0) / 60) }}分钟</span>
</div>
<div class="detail-item" v-if="lessonData.speak_message">
<span class="detail-label">直播发言:</span>
<span class="detail-value clickable" @click="showSpeakMessages(lessonData.speak_message)">{{ lessonData.speak_message.length || 0 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 发言内容弹窗 -->
<div v-if="showModal" class="modal-overlay" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>直播发言内容</h3>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-body">
<div v-if="!modalMessages || modalMessages.length === 0" class="no-messages">
暂无发言内容
</div>
<div v-else class="messages-list">
<div v-for="(message, index) in modalMessages" :key="index" class="message-item">
<span class="message-number">{{ index + 1 }}.</span>
<span class="message-content">{{ message }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
// 弹窗相关的响应式数据
const showModal = ref(false);
const modalMessages = ref([]);
// 定义props
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
selectedStage: {
type: String,
default: 'all'
},
contacts: {
type: Array,
required: true
},
selectedContactId: {
type: Number,
default: null
},
courseCustomers: {
type: Object,
default: () => ({})
},
customersList: {
type: Array,
default: () => []
},
customersCount: {
type: Number,
default: 0
},
payMoneyCustomersList: {
type: Array,
default: () => []
},
payMoneyCustomersCount: {
type: Number,
default: 0
}
});
const emit = defineEmits(['stage-select', 'select-contact']);
const totalCustomers = computed(() => {
// 全部阶段的数量就是前6个阶段的数量
if (props.customersList?.length > 0) return props.customersList.length;
// 如果没有实际数据使用props.data中的数据作为后备
if (props.data['全部']) return props.data['全部'];
const baseStages = [
props.data['未加微'] || 0,
props.data['已加微'] || 0,
props.data['已入群'] || 0,
props.data['已填表单'] || 0,
props.data['课1-4'] || 0,
props.data['点击未支付'] || 0,
props.data['付定金'] || 0,
props.data['定价转化'] || 0,
props.data['成交'] || 0
];
return Math.max(...baseStages, 1);
});
const getStageCount = (stageType) => {
// 成交阶段从payMoneyCustomersList获取数量
if (stageType === '成交') {
return props.payMoneyCustomersCount || (props.payMoneyCustomersList?.length || 0);
}
// 课1-4阶段从courseCustomers获取数量
if (stageType === '课1-4' && props.courseCustomers?.['课1-4']) {
return props.courseCustomers['课1-4'].length;
}
// 后3个阶段从courseCustomers中筛选
if (['点击未支付', '付定金', '定金转化'].includes(stageType)) {
if (props.courseCustomers?.['课1-4']) {
return props.courseCustomers['课1-4'].filter(customer => customer.type === stageType).length;
}
return 0;
}
// 前6个阶段从customersList中筛选
if (props.customersList?.length) {
return props.customersList.filter(customer => customer.type === stageType).length;
}
// 如果没有数据使用props.data中的数据
return props.data[stageType] || 0;
};
const stages = computed(() => {
// 全部阶段的数量就是前6个阶段的数量
const totalCount = props.customersList?.length || 0;
const stageList = [
{ id: 0, name: '全部', displayName: '全部', count: totalCount, color: '#f3f4f6' },
{ id: 1, name: '待加微', displayName: '待加微', count: getStageCount('待加微'), color: '#e3f2fd' },
{ id: 2, name: '待填表单', displayName: '待填表单', count: getStageCount('待填表单'), color: '#90caf9' },
{ id: 3, name: '待入群', displayName: '待入群', count: getStageCount('待入群'), color: '#bbdefb' },
{ id: 4, name: '待联系', displayName: '待联系', count: getStageCount('待联系'), color: '#bbdefb' },
{ id: 5, name: '待到课', displayName: '待到课', count: getStageCount('待到课'), color: '#bbdefb' },
{ id: 6, name: '课1-4', displayName: '课1-4', count: getStageCount('课1-4'), color: '#64b5f6' },
{ id: 7, name: '点击未支付', displayName: '点击未支付', count: getStageCount('点击未支付'), color: '#42a5f5' },
{ id: 8, name: '付定金', displayName: '付定金', count: getStageCount('付定金'), color: '#2196f3' },
{ id: 9, name: '定金转化', displayName: '定金转化', count: getStageCount('定金转化'), color: '#1e88e5' },
{ id: 10, name: '成交', displayName: '成交', count: getStageCount('成交'), color: '#1976d2' }
];
return stageList;
});
const getPercentage = (count) => {
if (totalCustomers.value === 0) return 0;
return Math.round((count / totalCustomers.value) * 100);
};
const selectStage = (stageName) => {
let filteredCustomers = [];
if (stageName === '全部') {
// 全部阶段只显示前6个阶段的客户数据
filteredCustomers = props.customersList || [];
} else if (stageName === '课1-4' && props.courseCustomers?.['课1-4']) {
emit('stage-select', stageName, {
isCourseStage: true,
courseData: props.courseCustomers['课1-4'],
filteredCustomers: props.courseCustomers['课1-4']
});
return;
} else if (stageName === '成交') {
// 成交阶段使用payMoneyCustomersList数据
if (props.payMoneyCustomersList && props.payMoneyCustomersList.length > 0) {
filteredCustomers = props.payMoneyCustomersList.map(customer => ({
customer_name: customer.customer_name,
phone: customer.phone,
customer_occupation: customer.customer_occupation,
customer_child_education: customer.customer_child_education,
latest_message_time: customer.latest_message_time,
customer_avatar_url: customer.customer_avatar_url,
type: '成交'
}));
}
} else if (['点击未支付', '付定金', '定金转化'].includes(stageName)) {
// 后3个阶段从courseCustomers中筛选
if (props.courseCustomers?.['课1-4']) {
filteredCustomers = props.courseCustomers['课1-4'].filter(customer => customer.type === stageName);
}
} else {
// 前6个阶段从customersList中筛选
filteredCustomers = props.customersList.filter(customer => customer.type === stageName);
}
emit('stage-select', stageName, {
filteredCustomers,
stageType: stageName,
customerCount: filteredCustomers.length
});
};
const selectedContactDetails = computed(() => {
if (!props.selectedContactId) return null;
return props.contacts.find(contact => contact.id === props.selectedContactId);
});
const selectContact = (id) => {
emit('select-contact', id);
};
const getHealthIndicator = (score) => {
if (score > 80) return { class: 'health-good', text: '健康', textColor: 'text-green' };
if (score > 50) return { class: 'health-ok', text: '一般', textColor: 'text-amber' };
return { class: 'health-risk', text: '高风险', textColor: 'text-red' };
};
// 显示发言内容弹框
const showSpeakMessages = (speakMessages) => {
modalMessages.value = speakMessages || [];
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
modalMessages.value = [];
};
const getAttendedLessons = (classSituation, classNum) => {
// 优先使用 class_num 字段
if (classNum && Array.isArray(classNum) && classNum.length > 0) {
return classNum.sort((a, b) => a - b).join(' ');
}
// 如果没有 class_num则使用 class_situation
if (!classSituation) return '未到课';
if (Array.isArray(classSituation)) {
return classSituation.join(' ');
}
if (typeof classSituation === 'object') {
const lessonNumbers = Object.keys(classSituation)
.map(key => parseInt(key))
.filter(num => !isNaN(num))
.sort((a, b) => a - b);
return lessonNumbers.length > 0 ? lessonNumbers.join(' ') : '未到课';
}
return '未到课';
};
</script>
<style lang="scss" scoped>
// Color Variables
$slate-50: #f8fafc;
$slate-100: #f1f5f9;
$slate-200: #e2e8f0;
$slate-300: #cbd5e1;
$slate-400: #94a3b8;
$slate-500: #64748b;
$slate-600: #475569;
$slate-700: #334155;
$slate-800: #1e293b;
$white: #ffffff;
$primary: #3b82f6;
$success: #22c55e;
$warning: #f59e0b;
$danger: #ef4444;
$indigo: #4f46e5;
// 到课详情区域样式
.course-details {
background: $white;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-top: 1rem;
overflow: hidden;
.course-details-header {
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
padding: 0.75rem 1rem;
border-bottom: 1px solid $slate-200;
h4 {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
color: $slate-700;
}
}
.course-details-content {
padding: 1rem;
max-height: 200px;
overflow-y: auto;
.no-data {
text-align: center;
color: $slate-500;
font-size: 0.875rem;
padding: 1rem 0;
}
.course-lessons {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 0.75rem;
.lesson-item {
background: $slate-50;
border-radius: 0.5rem;
padding: 0.75rem;
border-left: 3px solid $primary;
.lesson-header {
margin-bottom: 0.5rem;
.lesson-number {
font-size: 0.875rem;
font-weight: 600;
color: $slate-700;
}
}
.lesson-details {
display: flex;
flex-wrap: wrap;
gap: 1rem;
.detail-item {
display: flex;
align-items: center;
gap: 0.25rem;
.detail-label {
font-size: 0.75rem;
color: $slate-500;
font-weight: 500;
}
.detail-value {
font-size: 0.875rem;
color: $slate-700;
font-weight: 600;
}
}
}
}
}
}
@media (max-width: 768px) {
margin-top: 0.75rem;
.course-details-header {
padding: 0.625rem 0.75rem;
h4 {
font-size: 0.8125rem;
}
}
.course-details-content {
padding: 0.75rem;
max-height: 150px;
.course-lessons {
gap: 0.5rem;
.lesson-item {
padding: 0.625rem;
.lesson-details {
gap: 0.75rem;
.detail-item {
.detail-label {
font-size: 0.6875rem;
}
.detail-value {
font-size: 0.8125rem;
}
}
}
}
}
}
}
}
.sales-timeline-with-task-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
// Sales Timeline Styles
.sales-timeline {
padding: 1.5rem;
background: $white;
border-radius: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
@media (max-width: 768px) {
padding: 1rem;
}
}
.timeline-container {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
overflow-x: auto;
padding: 2rem 1rem 1rem 1rem;
@media (max-width: 768px) {
padding: 1.5rem 0.5rem 0.5rem 0.5rem;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
@media (max-width: 480px) {
padding: 1rem 0.25rem 0.25rem 0.25rem;
}
}
.timeline-line {
position: absolute;
top: 4rem;
left: 2rem;
right: 2rem;
height: 3px;
background: linear-gradient(to right, $primary, $success);
border-radius: 2px;
@media (max-width: 768px) {
top: 3rem;
left: 1.5rem;
right: 1.5rem;
}
}
.timeline-stage {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 0;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 8px;
padding: 0.5rem 8px;
@media (max-width: 768px) {
gap: 0.75rem;
min-width: 120px;
}
@media (max-width: 480px) {
gap: 0.5rem;
min-width: 100px;
padding: 0.25rem 4px;
}
&:hover {
background-color: rgba(59, 130, 246, 0.05);
transform: translateY(-2px);
.marker-circle {
border-color: $primary;
transform: scale(1.1);
}
}
&.selected {
background-color: rgba(59, 130, 246, 0.1);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
.marker-circle {
border-color: $primary;
background: $primary;
transform: scale(1.2);
.stage-number {
color: $white;
}
}
}
&.active {
.marker-circle {
background: linear-gradient(135deg, $primary, $success);
color: $white;
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
}
.stage-content {
.stage-title {
color: $slate-800;
font-weight: 600;
}
.stage-count {
color: $primary;
font-weight: 700;
}
}
}
}
.stage-marker {
position: relative;
z-index: 3;
.marker-circle {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: $slate-200;
border: 3px solid $white;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 3;
@media (max-width: 768px) {
width: 3rem;
height: 3rem;
}
.stage-number {
font-size: 1.125rem;
font-weight: 700;
color: $slate-600;
@media (max-width: 768px) {
font-size: 1rem;
}
}
}
}
.stage-content {
text-align: center;
width: 100%;
.stage-title {
font-size: 1.25rem;
font-weight: 500;
color: $slate-700;
margin: 0 0 0.75rem 0;
@media (max-width: 768px) {
font-size: 1.125rem;
}
}
.stage-stats {
display: flex;
align-items: baseline;
justify-content: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
.stage-count {
font-size: 2rem;
font-weight: 700;
color: $slate-600;
line-height: 1;
@media (max-width: 768px) {
font-size: 1.75rem;
}
}
.stage-label {
font-size: 0.875rem;
color: $slate-500;
font-weight: 500;
}
}
.stage-percentage {
font-size: 0.875rem;
color: $slate-400;
font-weight: 500;
background: $slate-100;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
display: inline-block;
}
}
// Task List Styles
.task-body {
padding: 1rem;
min-height: 15vh;
max-height: 50vh;
background: $white;
border-radius: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow-y: auto;
@media (max-width: 768px) {
padding: 0 1rem;
}
@media (max-width: 480px) {
padding: 0 0.75rem;
}
}
.actionable-list {
display: flex;
flex-direction: column;
}
.items-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.45rem;
@media (max-width: 768px) {
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
@media (max-width: 480px) {
grid-template-columns: repeat(2, 1fr);
gap: 0.375rem;
}
}
.action-item {
background-color: $white;
padding: 0.25rem;
border-radius: 0.5rem;
border: 1px solid $slate-200;
border-left-width: 4px;
cursor: pointer;
transition: all 0.2s ease-in-out;
min-height: 50px;
display: flex;
flex-direction: column;
justify-content: space-between;
@media (max-width: 768px) {
padding: 0.75rem;
min-height: 90px;
}
@media (max-width: 480px) {
padding: 0.625rem;
min-height: 50px;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
@media (max-width: 768px) {
transform: none;
}
}
@media (max-width: 768px) {
&:active {
transform: scale(0.98);
background-color: $slate-50;
}
}
&.active {
background-color: #eef2ff;
border-color: $indigo;
}
.item-content {
display: flex;
align-items: flex-start;
gap: 0.75rem;
width: 100%;
@media (max-width: 768px) {
gap: 0.5rem;
}
@media (max-width: 480px) {
gap: 0.375rem;
}
}
.avatar-section {
flex-shrink: 0;
.avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
overflow: hidden;
background-color: $slate-200;
display: flex;
align-items: center;
justify-content: center;
@media (max-width: 768px) {
width: 2.25rem;
height: 2.25rem;
}
@media (max-width: 480px) {
width: 2rem;
height: 2rem;
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
}
}
.info-section {
flex: 1;
min-width: 0; // 允许flex项目收缩
display: flex;
flex-direction: column;
gap: 0.5rem;
@media (max-width: 768px) {
gap: 0.375rem;
}
@media (max-width: 480px) {
gap: 0.25rem;
}
}
.item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.5rem;
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: flex-end;
flex-wrap: wrap;
gap: 0.5rem;
@media (max-width: 768px) {
gap: 0.375rem;
}
@media (max-width: 480px) {
gap: 0.25rem;
}
}
.item-name-group {
display: flex;
align-items: center;
flex: 1;
min-width: 0; // 允许收缩
.icon {
margin-right: 0.5rem;
flex-shrink: 0;
}
.item-name {
font-weight: 600;
font-size: 0.9rem;
line-height: 1.3;
color: $slate-800;
// 处理名字过长的情况
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
@media (max-width: 768px) {
font-size: 0.875rem;
}
@media (max-width: 480px) {
font-size: 0.8125rem;
}
}
}
.item-time {
font-size: 0.75rem;
color: $slate-500;
flex-shrink: 0;
white-space: nowrap;
@media (max-width: 768px) {
font-size: 0.6875rem;
}
@media (max-width: 480px) {
font-size: 0.625rem;
}
}
.health-status {
font-size: 0.75rem;
font-weight: bold;
@media (max-width: 768px) {
font-size: 0.6875rem;
}
@media (max-width: 480px) {
font-size: 0.625rem;
}
}
}
// Profession and Education
.profession-education {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
@media (max-width: 480px) {
gap: 0.5rem;
}
.profession, .education {
font-size: 0.75rem;
border-radius: 0.375rem;
font-weight: 500;
@media (max-width: 768px) {
font-size: 0.6875rem;
padding: 0.1875rem 0.375rem;
}
@media (max-width: 480px) {
font-size: 0.625rem;
padding: 0.125rem 0.25rem;
}
}
.profession {
background-color: #e0f2fe;
color: #0277bd;
}
.education {
background-color: #f3e5f5;
color: #7b1fa2;
}
.course-type {
background-color: #e8f5e8;
color: #2e7d32;
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-weight: 500;
display: inline-block;
margin-left: 0.5rem;
@media (max-width: 768px) {
font-size: 0.6875rem;
padding: 0.1875rem 0.375rem;
}
@media (max-width: 480px) {
font-size: 0.625rem;
padding: 0.125rem 0.25rem;
margin-left: 0.375rem;
}
}
.course-attendance {
display: inline-block;
// margin-top: 0.25rem;
padding: 0.125rem 0.5rem;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border-radius: 0.375rem;
font-size: 0.75rem;
color: #1565c0;
font-weight: 500;
border: 1px solid #90caf9;
@media (max-width: 768px) {
font-size: 0.6875rem;
padding: 0.1875rem 0.375rem;
}
@media (max-width: 480px) {
font-size: 0.625rem;
padding: 0.125rem 0.25rem;
}
}
}
// Health Status Colors
.health-good { border-color: $success; }
.health-ok { border-color: $warning; }
.health-risk { border-color: $danger; }
// Clickable styles
.clickable {
cursor: pointer;
color: #1976d2;
text-decoration: underline;
transition: color 0.2s ease;
&:hover {
color: #1565c0;
text-decoration: none;
}
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e5e7eb;
background-color: #f9fafb;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #374151;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #6b7280;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.close-btn:hover {
background-color: #e5e7eb;
color: #374151;
}
.modal-body {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
}
.no-messages {
text-align: center;
color: #6b7280;
font-size: 16px;
padding: 40px 20px;
}
.messages-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.message-item {
display: flex;
gap: 8px;
padding: 12px;
background-color: #f9fafb;
border-radius: 6px;
border-left: 3px solid #3b82f6;
}
.message-number {
font-weight: 600;
color: #3b82f6;
min-width: 20px;
}
.message-content {
flex: 1;
line-height: 1.5;
color: #374151;
}
</style>