Files
DJKB/my-vue-app/src/views/person/sale.vue
lbw_9527443 c48f39fb5e feat(团队管理): 实现团队概览和统计指标数据绑定
- 增加团队概览组件数据绑定逻辑
- 实现统计指标组件数据获取与展示
- 更新API接口调用和数据处理逻辑
- 调整超时时间为30秒以适应网络环境
- 添加调试日志用于问题排查
2025-08-12 21:33:02 +08:00

1887 lines
44 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-dashboard">
<!-- 悬浮待办组件 -->
<FloatingTodo />
<!-- 顶部导航栏 -->
<!-- 数据分析区域 - 独立占据整行 -->
<section class="analytics-section-full">
<div class="section-header">
<h1 class="app-title">销售驾驶舱</h1>
<div
class="quick-stats"
style="display: flex; align-items: center; gap: 30px"
>
<div
v-for="(stat, key) in MOCK_DATA.performance"
:key="key"
class="quick-stat-item"
>
<div class="stat-label">
{{ stat.label + ":" + stat.value.toLocaleString() }}
</div>
</div>
</div>
<UserDropdown />
</div>
<div class="section-content">
<PersonalDashboard
:kpi-data="kpiData"
:funnel-data="funnelData"
:contact-time-data="contactTimeData"
:statistics-data="statisticsData"
:urgent-problem-data="urgentProblemData"
/>
</div>
</section>
<!-- 销售时间线区域 -->
<section class="timeline-section">
<div class="section-header">
<h2>销售时间线</h2>
<p class="section-subtitle">客户转化全流程跟踪</p>
</div>
<div class="section-content">
<SalesTimelineWithTaskList
:data="timelineData"
@stage-select="handleStageSelect"
:selected-stage="selectedStage"
:contacts="filteredContacts"
:selected-contact-id="selectedContactId"
:course-customers="courseCustomers"
:kpi-data="kpiData"
@select-contact="selectContact" />
</div>
</section>
<!-- 原始数据卡片区域 -->
<section class="raw-data-section">
<div class="section-header">
<h2>原始数据</h2>
<p class="section-subtitle">客户互动的原始记录和数据</p>
</div>
<div class="section-content">
<RawDataCards
:selected-contact="selectedContact"
@view-form-data="handleViewFormData"
@view-chat-data="handleViewChatData"
@view-call-data="handleViewCallData" />
</div>
</section>
<!-- 主要内容区域 -->
<div class="main-layout">
<!-- 主要工作区域 -->
<main class="main-content">
<!-- 客户详情区域 -->
<section class="detail-section">
<div class="section-header">
<h2>客户详情</h2>
</div>
<div class="section-content">
<CustomerDetail :selected-contact="selectedContact" />
</div>
</section>
</main>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, nextTick } from "vue";
import { useRouter } from "vue-router";
import { useUserStore } from "@/stores/user";
import http from "@/utils/https";
import CustomerDetail from "./components/CustomerDetail.vue";
import PersonalDashboard from "./components/PersonalDashboard.vue";
import SalesTimelineWithTaskList from "./components/SalesTimelineWithTaskList.vue";
import RawDataCards from "./components/RawDataCards.vue";
import FloatingTodo from "./components/FloatingTodo.vue";
import Header from "@/components/header.vue";
import UserDropdown from "@/components/UserDropdown.vue";
import {getCustomerAttendance,getTodayCall,getProblemDistribution,getTableFillingRate,getAverageResponseTime,getWeeklyActiveCommunicationRate,getTimeoutResponseRate,getCustomerCallInfo,getCustomerChatInfo,getCustomerFormInfo,getConversionRateAndAllocatedData} from "@/api/api.js"
// 路由实例
const router = useRouter();
// 用户store
const userStore = useUserStore();
// STATE
const selectedContactId = ref(null);
const contextPanelRef = ref(null);
const selectedStage = ref('全部'); // 选中的销售阶段
const problemDistributionData = ref(null); // 问题分布数据
const isLoadingProblemData = ref(false); // 加载状态
// KPI数据
const kpiDataState = reactive({
totalCalls: 85,
successRate: 28,
avgDuration: 125,
conversionRate: 12,
assignedData: 256,
wechatAddRate: 68
});
// 统计指标数据
const statisticsData = reactive({
customerCommunicationRate: 0,
averageResponseTime: 0,
timeoutResponseRate: 0,
severeTimeoutRate: 0,
formCompletionRate: 0
});
// 客户迫切解决的问题数据
const urgentProblemData = ref([]);
// 时间线数据
const timelineData = ref({});
// 客户列表数据
const customersList = ref({});
// 课程客户数据课1-4
const courseCustomers = ref({});
// MOCK DATA (Should ideally come from a store or API)
const MOCK_DATA = reactive({
contacts: [
// 今日必须联系 (high priority) - 12个任务
{
id: 1,
name: "王女士",
time: "今日 10:00",
}
],
performance: {
calls: { label: "呼叫量", value: 85, target: 100 },
talkTime: { label: "通话时长(分)", value: 125, target: 180 },
revenue: { label: "签单额(元)", value: 8500, target: 15000 },
},
personalKPIs: {
winRate: { label: "赢率", value: 28, unit: "%" },
avgDealSize: { label: "平均客单价", value: 12500, unit: "元" },
salesCycle: { label: "销售周期", value: 45, unit: "天" },
},
personalFunnel: [200, 150, 90, 40, 12],
commission: { confirmed: 4250, estimated: 7800, nextDealValue: 350 },
contactTimeAnalysis: {
labels: ["9-10点", "10-11点", "11-12点", "14-15点", "15-16点", "16-17点"],
data: [65, 85, 80, 92, 75, 60],
description: "基于历史通话数据统计的客户接听成功率",
},
});
// 核心Kpi
async function getCoreKpi() {
console.log('userStore.userInfo.user_level', userStore.userInfo)
const params = {
user_level: userStore.userInfo.user_level.toString(),
user_name: userStore.userInfo.username
}
// 今日通话
const res = await getTodayCall(params)
if (res.code === 200) {
kpiDataState.totalCalls = res.data.today_call
}
// 转化率、分配数据量、加微率
const conversionRes = await getConversionRateAndAllocatedData(params)
if (conversionRes.code === 200) {
kpiDataState.conversionRate = conversionRes.data.conversion_rate || 0
kpiDataState.assignedData = conversionRes.data.all_count || 0
kpiDataState.wechatAddRate = conversionRes.data.plus_v_conversion_rate || 0
}
}
// 获取统计数据
async function getStatisticsData() {
const params = {
user_level: userStore.userInfo.user_level.toString(),
user_name: userStore.userInfo.username
}
try {
// 获取表单填写率
const fillingRateRes = await getTableFillingRate(params)
if (fillingRateRes.code === 200) {
statisticsData.formCompletionRate = fillingRateRes.data.filling_rate
}
// 获取平均响应时间
const avgResponseRes = await getAverageResponseTime(params)
if (avgResponseRes.code === 200) {
statisticsData.averageResponseTime = avgResponseRes.data.average_minutes
}
// 获取客户沟通率
const communicationRes = await getWeeklyActiveCommunicationRate(params)
if (communicationRes.code === 200) {
statisticsData.customerCommunicationRate = communicationRes.data.communication_rate
}
// 获取超时响应率
const timeoutRes = await getTimeoutResponseRate(params)
if (timeoutRes.code === 200) {
statisticsData.timeoutResponseRate = timeoutRes.data.overtime_rate_600
statisticsData.severeTimeoutRate = timeoutRes.data.overtime_rate_800
}
} catch (error) {
console.error('获取统计数据失败:', error)
}
}
// 客户迫切解决的问题
async function getUrgentProblem() {
const params = {
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString(),
}
const res = await getProblemDistribution(params)
if(res.code === 200) {
// 将API返回的对象格式转换为数组格式
const problemDistribution = res.data.problem_distribution
urgentProblemData.value = Object.entries(problemDistribution).map(([name, percentage]) => ({
name: name,
value: parseInt(percentage.replace('%', '')) || 0
}))
}
}
// 时间线
async function getTimeline() {
const params = {
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString(),
}
const res = await getCustomerAttendance(params)
if(res.code === 200) {
timelineData.value = res.data.customers_count
// 处理客户列表数据
if (res.data.customers_list) {
customersList.value = res.data.customers_list
// 将客户数据转换为统一格式的数组
const processedCustomers = []
Object.keys(res.data.customers_list).forEach(stage => {
const customers = res.data.customers_list[stage]
// 检查是否是课程数据课1-4
if (stage === '课1-4') {
// 单独处理课程客户数据
if (Array.isArray(customers)) {
courseCustomers.value[stage] = customers.map((customer, index) => {
const stableId = `${stage}_${customer.scrm_user_main_code || customer.weChat_nickname || 'unknown'}_${index}`.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]/g, '_')
return {
id: Math.abs(stableId.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0)),
name: customer.weChat_nickname || customer.customer_name || '未知用户',
time: customer.records && customer.records.length > 0 ? customer.records[0].first_visit_time : '未知时间',
profession: customer.customer_occupation || '学员',
education: customer.customer_child_education || '未知',
avatar: customer.weChat_avatar ? customer.weChat_avatar.trim() : null,
salesStage: stage,
health: Math.floor(Math.random() * 100) + 1,
// 原始课程相关数据
customer_name: customer.customer_name,
customer_occupation: customer.customer_occupation,
customer_child_education: customer.customer_child_education,
scrm_user_main_code: customer.scrm_user_main_code,
weChat_avatar: customer.weChat_avatar,
class_situation: customer.class_situation,
records: customer.records
}
})
// 课程客户数据独立存储,不添加到总客户列表中
}
} else {
// 处理普通销售阶段数据
if (Array.isArray(customers)) {
customers.forEach((customer, index) => {
// 生成稳定的ID
const stableId = `${stage}_${customer.customer_name || 'unknown'}_${index}`.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]/g, '_')
processedCustomers.push({
id: Math.abs(stableId.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0)),
name: customer.customer_name || '未知',
time: customer.format_update_time || '未知时间',
profession: customer.customer_occupation || '未知职业',
education: customer.customer_child_education || '未知学历',
avatar: customer.customer_avatar_url ? customer.customer_avatar_url.trim() : null,
salesStage: stage,
health: Math.floor(Math.random() * 100) + 1
})
})
}
}
})
console.log('课程客户数据 (courseCustomers):', courseCustomers.value)
console.log('所有处理后的客户 (processedCustomers):', processedCustomers)
console.log('原始客户列表数据 (customersList):', customersList.value)
// 更新 MOCK_DATA.contacts 为实际数据
MOCK_DATA.contacts = processedCustomers
}
}
}
// 获取客户表单
async function getCustomerForm() {
if (!selectedContact.value || !selectedContact.value.name) {
console.warn('无法获取客户表单:客户信息不完整');
return;
}
const params = {
user_name: userStore.userInfo.username,
customer_name: selectedContact.value.name,
}
console.log('获取客户表单参数:', params);
console.log('选中的客户信息:', selectedContact.value);
try {
const res = await getCustomerFormInfo(params)
console.log('客户表单API响应:', res)
if(res.code === 200) {
MOCK_DATA.formFields = res.data
console.log('客户表单数据已更新:', MOCK_DATA.formFields);
} else {
console.error('获取客户表单失败:', res.message || '未知错误');
}
} catch (error) {
console.error('获取客户表单请求失败:', error);
}
}
// 为组件准备数据
const kpiData = computed(() => kpiDataState);
// COMPUTED PROPERTIES
const selectedContact = computed(() => {
return (
MOCK_DATA.contacts.find((c) => c.id === selectedContactId.value) || null
);
});
const funnelData = computed(() => ({
labels: ["线索", "沟通", "意向", "预约", "成交"],
data: MOCK_DATA.personalFunnel,
}));
const contactTimeData = computed(() => ({
labels: MOCK_DATA.contactTimeAnalysis.labels,
data: MOCK_DATA.contactTimeAnalysis.data,
}));
// 根据选中阶段筛选联系人
const filteredContacts = computed(() => {
// 默认显示"全部"阶段的客户
if (selectedStage.value === 'all' || selectedStage.value === '全部') {
return MOCK_DATA.contacts.filter(contact => contact.salesStage === '全部');
}
return MOCK_DATA.contacts.filter(contact => contact.salesStage === selectedStage.value);
});
// METHODS
const selectContact = (id) => {
selectedContactId.value = id;
// 当选中客户后,获取客户表单数据
nextTick(async () => {
if (selectedContact.value && selectedContact.value.name) {
await getCustomerForm();
}
contextPanelRef.value?.scrollIntoView({
behavior: "smooth",
block: "center",
});
});
};
// 处理时间线阶段选择
const handleStageSelect = (stage, extraData = null) => {
selectedStage.value = stage;
// 如果是课1-4阶段处理课程数据
if (extraData && extraData.isCourseStage) {
console.log('处理课1-4阶段选择课程数据:', extraData.courseData);
// 将课程数据转换为contacts格式并更新显示
const courseContacts = extraData.courseData.map(customer => ({
id: customer.id,
name: customer.name,
time: customer.time,
profession: customer.profession,
education: customer.education,
avatar: customer.avatar,
salesStage: '课1-4',
health: customer.health,
// 保留课程相关的原始数据
customer_name: customer.customer_name,
customer_occupation: customer.customer_occupation,
customer_child_education: customer.customer_child_education,
scrm_user_main_code: customer.scrm_user_main_code,
weChat_avatar: customer.weChat_avatar,
class_situation: customer.class_situation,
records: customer.records
}));
// 临时更新MOCK_DATA.contacts以显示课程客户
MOCK_DATA.contacts = [...MOCK_DATA.contacts.filter(c => c.salesStage !== '课1-4'), ...courseContacts];
console.log('课1-4客户数据已更新到contacts:', courseContacts);
}
};
// 处理查看表单数据
const handleViewFormData = (contact) => {
console.log('查看表单数据:', contact);
// 这里可以添加打开表单数据弹窗的逻辑
};
// 处理查看聊天记录
const handleViewChatData = (contact) => {
console.log('查看聊天记录:', contact);
// 这里可以添加打开聊天记录弹窗的逻辑
};
// 处理查看通话录音
const handleViewCallData = (contact) => {
console.log('查看通话录音:', contact);
// 这里可以添加打开通话录音弹窗的逻辑
};
// LIFECYCLE HOOKS
onMounted(async () => {
await getCoreKpi()
await getStatisticsData()
await getUrgentProblem()
await getTimeline()
// 默认选择第一个今日必须联系人
const highPriorityContacts = MOCK_DATA.contacts.filter(
(contact) => contact.priority === "high"
);
if (highPriorityContacts.length > 0) {
selectedContactId.value = highPriorityContacts[0].id;
}
});
</script>
<style lang="scss" scoped>
body {
margin: 0;
padding: 0;
}
// Color Palette
$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;
$blue: #3b82f6;
$green: #22c55e;
$amber: #f59e0b;
$red: #ef4444;
$indigo: #4f46e5;
$purple: #a855f7;
// 移动端全局优化
@media (max-width: 768px) {
// 优化触摸目标大小
* {
-webkit-tap-highlight-color: transparent;
}
// 优化滚动性能
.sidebar,
.detail-section,
.analytics-section-full {
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
// 防止横向滚动
body {
overflow-x: hidden;
}
}
// 超小屏幕优化
@media (max-width: 480px) {
// 减少动画以提升性能
* {
transition-duration: 0.1s !important;
}
// 优化字体渲染
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sales-dashboard {
padding: 0.25rem;
gap: 0.5rem;
}
.analytics-section-full {
.section-header {
padding: 0.5rem;
.app-title {
font-size: 1.125rem;
}
.quick-stats {
flex-direction: column;
gap: 0.25rem !important;
.quick-stat-item {
.stat-label {
font-size: 0.6875rem;
}
}
}
.header-ringht {
.avatar {
width: 25px !important;
height: 25px !important;
}
span {
font-size: 0.75rem;
}
}
}
}
.timeline-section {
.section-header {
padding: 0.5rem;
h2 {
font-size: 0.875rem;
}
.section-subtitle {
font-size: 0.6875rem;
}
}
}
.sidebar {
.sidebar-header {
padding: 0.5rem;
h2 {
font-size: 0.875rem;
}
.task-summary {
.task-count {
font-size: 1.25rem;
}
.task-label {
font-size: 0.75rem;
}
}
}
.sidebar-content {
padding: 0.5rem;
}
}
.detail-section {
.section-header {
padding: 0.5rem;
h2 {
font-size: 0.875rem;
}
.section-actions {
.action-btn {
padding: 0.25rem 0.5rem;
font-size: 0.6875rem;
}
}
}
}
}
/* 移动端优化 */
@media (max-width: 768px) {
.sales-dashboard {
padding: 0.5rem;
gap: 0.75rem;
}
.main-layout {
flex-direction: column;
gap: 1rem;
}
.sidebar {
width: 100%;
min-height: auto;
order: 2; /* 在移动端将侧边栏放到主内容下方 */
.sidebar-header {
padding: 0.75rem 1rem;
h2 {
font-size: 1rem;
}
}
.sidebar-content {
padding: 0.75rem;
}
}
.main-content {
width: 100%;
order: 1; /* 在移动端将主内容放到侧边栏上方 */
}
.detail-section {
.section-header {
padding: 0.75rem 1rem;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
h2 {
font-size: 1rem;
}
.section-actions {
width: 100%;
justify-content: flex-end;
.action-btn {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
}
}
}
}
.analytics-section-full {
.section-header {
padding: 0.75rem 1rem;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
.app-title {
font-size: 1.25rem;
}
.quick-stats {
width: 100%;
flex-wrap: wrap;
gap: 0.5rem !important;
.quick-stat-item {
.stat-label {
font-size: 0.75rem;
}
}
}
.header-ringht {
width: 100%;
justify-content: flex-end;
.avatar {
width: 30px !important;
height: 30px !important;
}
span {
font-size: 0.875rem;
}
}
}
}
}
// Base Styles
.sales-dashboard {
background: #ffffff;
min-height: 100vh;
color: $slate-800;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "Noto Sans", sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
box-sizing: border-box;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 16px;
line-height: 1.6;
padding: 0;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 15px;
line-height: 1.55;
padding: 0;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 14px;
line-height: 1.5;
padding: 0;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 13px;
line-height: 1.4;
padding: 0;
}
// 确保所有子元素都使用border-box
*, *::before, *::after {
box-sizing: border-box;
}
}
// 顶部导航栏
.top-navbar {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
position: sticky;
top: 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
width: 100%;
z-index: 1000;
// PC端保持一致布局
@media (min-width: 1024px) {
width: 100%;
margin: 0;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
width: 100%;
margin: 0;
}
// 移动端适配
@media (max-width: 768px) {
width: 100%;
margin: 0;
}
// 小屏移动端适配
@media (max-width: 480px) {
width: 100%;
margin: 0;
}
.navbar-content {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
height: 70px;
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 0 2.5rem;
height: 75px;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 0 2rem;
height: 70px;
}
// 移动端适配
@media (max-width: 768px) {
padding: 0 1rem;
height: 60px;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 0 0.5rem;
height: 55px;
}
}
.navbar-left {
display: flex;
align-items: center;
gap: 2rem;
@media (max-width: 768px) {
gap: 1rem;
}
@media (max-width: 480px) {
gap: 0.75rem;
}
.app-title {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-text-fill-color: transparent;
margin: 0;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 1.75rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 1.5rem;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 1.25rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 1.125rem;
}
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: $slate-500;
@media (max-width: 480px) {
font-size: 0.75rem;
gap: 0.25rem;
}
.separator {
color: $slate-400;
}
.current {
color: $slate-700;
font-weight: 500;
}
}
}
.navbar-right {
display: flex;
align-items: center;
gap: 2rem;
@media (max-width: 768px) {
gap: 1rem;
}
@media (max-width: 480px) {
gap: 0.75rem;
}
.quick-stats {
display: flex;
gap: 1.5rem;
// PC端保持一致布局
@media (min-width: 1024px) {
gap: 2rem;
flex-wrap: nowrap;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
gap: 1.5rem;
flex-wrap: nowrap;
}
// 移动端适配
@media (max-width: 768px) {
gap: 1rem;
flex-wrap: wrap;
}
// 小屏移动端适配
@media (max-width: 480px) {
gap: 0.5rem;
flex-wrap: wrap;
}
.quick-stat-item {
text-align: center;
@media (max-width: 480px) {
min-width: 50px;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: $slate-800;
line-height: 1;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 1.375rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 1.25rem;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 1.125rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 1rem;
}
}
.stat-label {
font-size: 0.75rem;
color: $slate-500;
margin-top: 2px;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 0.8125rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 0.75rem;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 0.6875rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 0.625rem;
}
}
}
}
.user-actions {
display: flex;
align-items: center;
gap: 1rem;
@media (max-width: 768px) {
gap: 0.75rem;
}
@media (max-width: 480px) {
gap: 0.5rem;
}
.action-btn {
padding: 0.5rem 1rem;
border: 1px solid $slate-300;
border-radius: 0.5rem;
background: $white;
color: $slate-700;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
touch-action: manipulation;
white-space: nowrap;
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
// 移动端适配
@media (max-width: 768px) {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
&:hover {
background: $slate-50;
border-color: $slate-400;
}
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
cursor: pointer;
touch-action: manipulation;
// PC端保持一致布局
@media (min-width: 1024px) {
width: 40px;
height: 40px;
font-size: 1rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
width: 36px;
height: 36px;
font-size: 0.9375rem;
}
// 移动端适配
@media (max-width: 768px) {
width: 32px;
height: 32px;
font-size: 0.875rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
width: 28px;
height: 28px;
font-size: 0.75rem;
}
}
}
}
}
// 主要布局
.main-layout {
width: 100vw;
margin: 0 auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
// min-height: calc(100vh - 70px);
// PC端保持一致布局
@media (min-width: 1024px) {
width: 100vw;
// max-width: 1400px;
// padding: 1.5rem;
gap: 1.5rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
width: 90vw;
padding: 1rem;
gap: 1rem;
}
// 移动端适配
@media (max-width: 768px) {
width: 95vw;
padding: 0.75rem;
gap: 0.75rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
width: 98vw;
padding: 0.5rem;
gap: 0.5rem;
}
}
// 智能任务清单(原左侧边栏,现在在上方)
.sidebar {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
overflow: hidden;
height: fit-content;
max-height: 400px;
display: flex;
flex-direction: column;
// PC端保持一致布局
@media (min-width: 1024px) {
border-radius: 1rem;
max-height: 450px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
border-radius: 0.875rem;
max-height: 420px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
}
// 移动端适配
@media (max-width: 768px) {
border-radius: 0.75rem;
max-height: calc(100vh - 120px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
}
// 小屏移动端适配
@media (max-width: 480px) {
border-radius: 0.5rem;
max-height: calc(100vh - 100px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.1),
rgba(118, 75, 162, 0.1)
);
display: flex;
align-items: center;
justify-content: space-between;
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 1.75rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 1.5rem;
}
// 移动端适配
@media (max-width: 768px) {
padding: 1.25rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 1rem;
}
h2 {
margin: 0 0 0.5rem 0;
font-size: 1.125rem;
font-weight: 600;
color: $slate-800;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 1.25rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 1.125rem;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 1rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 0.9375rem;
}
}
.task-summary {
display: flex;
align-items: baseline;
gap: 0.25rem;
.task-count {
font-size: 1.5rem;
font-weight: 700;
color: #667eea;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 1.75rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 1.5rem;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 1.375rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 1.25rem;
}
}
.task-label {
font-size: 0.875rem;
color: $slate-500;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 0.9375rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 0.875rem;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 0.8125rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 0.8125rem;
}
}
}
}
.sidebar-content {
flex: 1;
overflow-y: auto;
}
}
// 主要内容区域
.main-content {
display: flex;
flex-direction: column;
gap: 2rem;
// PC端保持一致布局
@media (min-width: 1024px) {
gap: 2.5rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
gap: 2rem;
}
// 移动端适配
@media (max-width: 768px) {
gap: 1.5rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
gap: 1rem;
}
.detail-section {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
overflow: hidden;
// PC端保持一致布局
@media (min-width: 1024px) {
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
border-radius: 0.875rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
}
// 移动端适配
@media (max-width: 768px) {
border-radius: 0.75rem;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
}
// 小屏移动端适配
@media (max-width: 480px) {
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
.section-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.05),
rgba(118, 75, 162, 0.05)
);
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 1.75rem;
flex-direction: row;
align-items: center;
gap: 2rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 1.5rem;
flex-direction: row;
align-items: center;
gap: 1.5rem;
}
// 移动端适配
@media (max-width: 768px) {
padding: 1.25rem;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 1rem;
gap: 0.75rem;
}
h2 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: $slate-800;
// PC端保持一致布局
@media (min-width: 1024px) {
font-size: 1.25rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
font-size: 1.125rem;
}
// 移动端适配
@media (max-width: 768px) {
font-size: 1rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
font-size: 0.9375rem;
}
}
.section-actions {
display: flex;
gap: 0.75rem;
// PC端保持一致布局
@media (min-width: 1024px) {
gap: 1rem;
flex-wrap: nowrap;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
gap: 0.75rem;
flex-wrap: nowrap;
}
// 移动端适配
@media (max-width: 768px) {
gap: 0.5rem;
width: 100%;
justify-content: flex-start;
flex-wrap: nowrap;
}
// 小屏移动端适配
@media (max-width: 480px) {
gap: 0.375rem;
flex-wrap: wrap;
}
.action-btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
touch-action: manipulation;
white-space: nowrap;
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
// 移动端适配
@media (max-width: 768px) {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
min-width: 60px;
}
&.primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
}
&.secondary {
background: $white;
color: $slate-700;
border: 1px solid $slate-300;
&:hover {
background: $slate-50;
border-color: $slate-400;
}
}
}
}
.view-toggle {
display: flex;
background: $slate-100;
border-radius: 0.5rem;
padding: 0.25rem;
.toggle-btn {
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: $slate-600;
font-size: 0.875rem;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
&.active {
background: $white;
color: $slate-800;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
&:hover:not(.active) {
color: $slate-800;
}
}
}
}
.section-content {
padding: 0.5rem;
@media (max-width: 768px) {
padding: 1.25rem;
}
@media (max-width: 480px) {
padding: 1rem;
}
}
}
.detail-section {
flex: 0 0 auto;
}
}
// 销售漏斗和时间线区域样式
.funnel-section,
.timeline-section,
.raw-data-section {
grid-column: 1 / -1;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
overflow: hidden;
margin-bottom: 1.5rem;
width: 100%;
// PC端保持一致布局
@media (min-width: 1024px) {
border-radius: 1rem;
margin-bottom: 1.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
border-radius: 0.875rem;
margin-bottom: 1.25rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
}
// 移动端适配
@media (max-width: 768px) {
border-radius: 0.75rem;
margin-bottom: 1rem;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
}
// 小屏移动端适配
@media (max-width: 480px) {
border-radius: 0.5rem;
margin-bottom: 0.75rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
.section-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.05),
rgba(118, 75, 162, 0.05)
);
@media (max-width: 768px) {
padding: 1.25rem;
}
@media (max-width: 480px) {
padding: 1rem;
}
h2 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: $slate-800;
@media (max-width: 768px) {
font-size: 1rem;
}
@media (max-width: 480px) {
font-size: 0.9375rem;
}
}
.section-subtitle {
margin: 0.5rem 0 0 0;
font-size: 0.875rem;
color: $slate-600;
font-weight: 400;
@media (max-width: 768px) {
font-size: 0.8125rem;
}
@media (max-width: 480px) {
font-size: 0.75rem;
}
}
}
.section-content {
padding: 1.5rem;
@media (max-width: 768px) {
padding: 1.25rem;
}
@media (max-width: 480px) {
padding: 1rem;
}
}
}
// 独立的数据分析区域 - 跨越整个宽度
.analytics-section-full {
grid-column: 1 / -1;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
overflow: hidden;
width: 100%;
min-height: 400px;
// PC端保持一致布局
@media (min-width: 1024px) {
min-height: 450px;
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
min-height: 380px;
border-radius: 0.875rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
}
// 移动端适配
@media (max-width: 768px) {
border-radius: 0.75rem;
min-height: 320px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
}
// 小屏移动端适配
@media (max-width: 480px) {
border-radius: 0.5rem;
min-height: 280px;
margin-top: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
.section-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.05),
rgba(118, 75, 162, 0.05)
);
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 1.75rem;
flex-direction: row;
align-items: center;
gap: 2rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 1.5rem;
flex-direction: row;
align-items: center;
gap: 1.5rem;
}
// 移动端适配
@media (max-width: 768px) {
padding: 1.25rem;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 1rem;
gap: 0.75rem;
}
h2 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: $slate-800;
@media (max-width: 768px) {
font-size: 1rem;
}
@media (max-width: 480px) {
font-size: 0.9375rem;
}
}
.view-toggle {
display: flex;
background: $slate-100;
border-radius: 0.5rem;
padding: 0.25rem;
// PC端保持一致布局
@media (min-width: 1024px) {
width: auto;
flex-wrap: nowrap;
justify-content: flex-start;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
width: auto;
flex-wrap: nowrap;
justify-content: flex-start;
}
// 移动端适配
@media (max-width: 768px) {
width: 100%;
justify-content: space-between;
flex-wrap: nowrap;
}
// 小屏移动端适配
@media (max-width: 480px) {
flex-wrap: wrap;
gap: 0.25rem;
justify-content: flex-start;
}
.toggle-btn {
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: $slate-600;
font-size: 0.875rem;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
touch-action: manipulation;
white-space: nowrap;
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 0.5rem 1.25rem;
font-size: 0.875rem;
flex: none;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 0.5rem 1rem;
font-size: 0.875rem;
flex: none;
}
// 移动端适配
@media (max-width: 768px) {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
flex: 1;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
min-width: 60px;
flex: none;
}
&.active {
background: $white;
color: $slate-800;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
&:hover:not(.active) {
color: $slate-800;
}
}
}
}
.section-content {
padding: 1.5rem;
// PC端保持一致布局
@media (min-width: 1024px) {
padding: 1.75rem;
}
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
padding: 1.5rem;
}
// 移动端适配
@media (max-width: 768px) {
padding: 1.25rem;
}
// 小屏移动端适配
@media (max-width: 480px) {
padding: 1rem;
}
}
}
</style>