1611 lines
44 KiB
Vue
1611 lines
44 KiB
Vue
<template>
|
||
<div class="sales-dashboard">
|
||
<!-- 页面加载状态 -->
|
||
<Loading :visible="isPageLoading" text="正在加载数据..." />
|
||
<!-- 顶部导航栏 -->
|
||
<!-- 销售时间线区域 -->
|
||
<section class="timeline-section">
|
||
<div class="section-header">
|
||
<!-- 动态顶栏:根据是否有路由参数显示不同内容 -->
|
||
<!-- 路由跳转时的顶栏:面包屑 + 姓名 -->
|
||
<div v-if="isRouteNavigation" class="route-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div class="breadcrumb" style="display: flex; flex-direction: column;">
|
||
<span class="breadcrumb-item" @click="goBack">团队管理 >{{ routeUserName }}</span>
|
||
<span class="breadcrumb-item current"> 数据驾驶舱</span>
|
||
</div>
|
||
<div class="user-name">
|
||
{{ routeUserName }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自己登录时的顶栏:原有样式 -->
|
||
<template v-else>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||
<h1 class="app-title">销售驾驶舱</h1>
|
||
<div
|
||
class="quick-stats"
|
||
style="display: flex; align-items: center; gap: 30px"
|
||
>
|
||
</div>
|
||
<UserDropdown />
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="section-content">
|
||
<!-- 销售时间线加载状态 -->
|
||
<div v-if="isTimelineLoading" class="section-loading">
|
||
<div class="loading-spinner"></div>
|
||
<div class="loading-text">正在加载销售时间线...</div>
|
||
</div>
|
||
<!-- 销售时间线内容 -->
|
||
<SalesTimelineWithTaskList
|
||
v-else
|
||
:data="timelineData"
|
||
@stage-select="handleStageSelect"
|
||
:selected-stage="selectedStage"
|
||
:contacts="filteredContacts"
|
||
:selected-contact-id="selectedContactId"
|
||
:course-customers="courseCustomers"
|
||
:kpi-data="kpiData"
|
||
:customers-list="customersList"
|
||
:customers-count="customersCount"
|
||
:pay-money-customers-list="payMoneyCustomersList"
|
||
:pay-money-customers-count="payMoneyCustomersCount"
|
||
@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"
|
||
:form-info="formInfo"
|
||
:chat-info="chatRecords"
|
||
:call-info="callRecords"
|
||
@view-form-data="handleViewFormData"
|
||
@view-chat-data="handleViewChatData"
|
||
@view-call-data="handleViewCallData"
|
||
@analyze-sop="handleAnalyzeSop" />
|
||
</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
|
||
ref="customerDetailRef"
|
||
:selected-contact="selectedContact"
|
||
:form-info="formInfo"
|
||
:chat-records="chatRecords"
|
||
:call-records="callRecords" />
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</div>
|
||
<section class="analytics-section-full" style="width: 100%;">
|
||
|
||
<div class="section-content">
|
||
<!-- 数据分析区域加载状态 -->
|
||
<div v-if="isKpiLoading || isStatisticsLoading || isUrgentProblemLoading" class="section-loading">
|
||
<div class="loading-spinner"></div>
|
||
<div class="loading-text">正在加载数据分析...</div>
|
||
</div>
|
||
<!-- 数据分析内容 -->
|
||
<PersonalDashboard
|
||
v-else
|
||
:kpi-data="kpiData"
|
||
:funnel-data="funnelData"
|
||
:contact-time-data="goldContactTime"
|
||
:statistics-data="statisticsData"
|
||
:urgent-problem-data="urgentProblemData"
|
||
/>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import { useUserStore } from "@/stores/user";
|
||
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 UserDropdown from "@/components/UserDropdown.vue";
|
||
import Loading from "@/components/Loading.vue";
|
||
import {getCustomerAttendance,getTodayCall,getProblemDistribution,getTableFillingRate,getAverageResponseTime,
|
||
getWeeklyActiveCommunicationRate,getTimeoutResponseRate,getCustomerCallInfo,getCustomerChatInfo,getCustomerFormInfo,
|
||
getConversionRateAndAllocatedData,getCustomerAttendanceAfterClass4,getPayMoneyCustomers,getSalesFunnel,getGoldContactTime} from "@/api/api.js"
|
||
|
||
// 路由实例
|
||
const router = useRouter();
|
||
|
||
// 用户store
|
||
const userStore = useUserStore();
|
||
|
||
// 获取通用请求参数的函数
|
||
const getRequestParams = () => {
|
||
const params = {}
|
||
// 只从路由参数获取
|
||
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level
|
||
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
|
||
// 如果路由有参数,使用路由参数
|
||
if (routeUserLevel) {
|
||
params.user_level = routeUserLevel.toString()
|
||
}
|
||
if (routeUserName) {
|
||
params.user_name = routeUserName
|
||
}
|
||
|
||
return params
|
||
}
|
||
|
||
// 判断是否为路由导航(有路由参数)
|
||
const isRouteNavigation = computed(() => {
|
||
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
|
||
return !!routeUserName
|
||
})
|
||
|
||
// 获取路由传递的用户名
|
||
const routeUserName = computed(() => {
|
||
return router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name || ''
|
||
})
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
router.go(-1)
|
||
}
|
||
// STATE
|
||
const selectedContactId = ref(null);
|
||
const contextPanelRef = ref(null);
|
||
const customerDetailRef = ref(null);
|
||
const selectedStage = ref('全部'); // 选中的销售阶段
|
||
|
||
const isPageLoading = ref(true); // 页面整体加载状态
|
||
const isKpiLoading = ref(false); // KPI数据加载状态
|
||
const isStatisticsLoading = ref(false); // 统计数据加载状态
|
||
const isUrgentProblemLoading = ref(false); // 紧急问题数据加载状态
|
||
const isTimelineLoading = 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([]);
|
||
|
||
// 客户总数
|
||
const customersCount = ref(0);
|
||
|
||
// 课程客户数据(课1-4)
|
||
const courseCustomers = ref({});
|
||
|
||
// 成交阶段客户数据
|
||
const payMoneyCustomersList = ref([]);
|
||
const payMoneyCustomersCount = ref(0);
|
||
|
||
// 表单信息
|
||
const formInfo = ref({});
|
||
// 通话记录
|
||
const callRecords = ref([]);
|
||
// 聊天记录
|
||
const chatRecords = 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() {
|
||
isKpiLoading.value = true
|
||
try {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
|
||
// 今日通话数据
|
||
const res = await getTodayCall(hasParams ? params : undefined)
|
||
if (res.code === 200) {
|
||
kpiDataState.totalCalls = res.data.today_call
|
||
}
|
||
|
||
// 转化率、分配数据量、加微率
|
||
const conversionRes = await getConversionRateAndAllocatedData(hasParams ? params : undefined)
|
||
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
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('获取核心KPI数据失败:', error)
|
||
} finally {
|
||
isKpiLoading.value = false
|
||
}
|
||
}
|
||
// 获取统计数据
|
||
async function getStatisticsData() {
|
||
isStatisticsLoading.value = true
|
||
try {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
|
||
// 获取表单填写率
|
||
const fillingRateRes = await getTableFillingRate(hasParams ? params : undefined)
|
||
if (fillingRateRes.code === 200) {
|
||
statisticsData.formCompletionRate = fillingRateRes.data.filling_rate
|
||
}
|
||
|
||
// 获取平均响应时间
|
||
const avgResponseRes = await getAverageResponseTime(hasParams ? params : undefined)
|
||
if (avgResponseRes.code === 200) {
|
||
statisticsData.averageResponseTime = avgResponseRes.data.average_minutes
|
||
}
|
||
|
||
// 获取客户沟通率
|
||
const communicationRes = await getWeeklyActiveCommunicationRate(hasParams ? params : undefined)
|
||
if (communicationRes.code === 200) {
|
||
statisticsData.customerCommunicationRate = communicationRes.data.communication_rate
|
||
}
|
||
|
||
// 获取超时响应率
|
||
const timeoutRes = await getTimeoutResponseRate(hasParams ? params : undefined)
|
||
if (timeoutRes.code === 200) {
|
||
statisticsData.timeoutResponseRate = timeoutRes.data.overtime_rate_600
|
||
statisticsData.severeTimeoutRate = timeoutRes.data.overtime_rate_800
|
||
}
|
||
} catch (error) {
|
||
console.error('获取统计数据失败:', error)
|
||
} finally {
|
||
isStatisticsLoading.value = false
|
||
}
|
||
}
|
||
// 客户迫切解决的问题
|
||
async function getUrgentProblem() {
|
||
isUrgentProblemLoading.value = true
|
||
try {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
|
||
const res = await getProblemDistribution(hasParams ? params : undefined)
|
||
if(res.code === 200) {
|
||
// 将API返回的对象格式转换为数组格式
|
||
const problemDistributionCount = res.data.problem_distribution_count
|
||
urgentProblemData.value = Object.entries(problemDistributionCount).map(([name, value]) => ({ name, value }))
|
||
}
|
||
} catch (error) {
|
||
console.error('获取紧急问题数据失败:', error)
|
||
} finally {
|
||
isUrgentProblemLoading.value = false
|
||
}
|
||
}
|
||
// 时间线
|
||
async function getTimeline() {
|
||
isTimelineLoading.value = true
|
||
try {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
// 前6个阶段
|
||
const res = await getCustomerAttendance(hasParams ? params : undefined)
|
||
if(res.code === 200) {
|
||
// 处理时间线数据
|
||
if (res.data.timeline) {
|
||
timelineData.value = Object.entries(res.data.timeline).map(([name, count]) => ({
|
||
name: name,
|
||
value: parseInt(count) || 0
|
||
}))
|
||
}
|
||
// 处理客户列表数据
|
||
if (res.data.all_customers_list) {
|
||
customersList.value = res.data.all_customers_list
|
||
}
|
||
// 处理客户总数
|
||
if (res.data.all_customers_count) {
|
||
customersCount.value = res.data.all_customers_count
|
||
}
|
||
}
|
||
// 后4个阶段
|
||
const classRes = await getCustomerAttendanceAfterClass4(hasParams ? params : undefined)
|
||
if(classRes.code === 200) {
|
||
// 处理课1-4阶段的客户数据
|
||
if (classRes.data.class_customers_list) {
|
||
// 存储课1-4阶段的原始数据,根据pay_status设置正确的type
|
||
courseCustomers.value['课1-4'] = classRes.data.class_customers_list.map(customer => {
|
||
let customerType = '课1-4'; // 默认类型
|
||
|
||
// 根据pay_status设置具体的type
|
||
if (customer.pay_status === '未支付' || customer.pay_status === '点击未支付') {
|
||
customerType = '点击未支付';
|
||
} else if (customer.pay_status === '付定金') {
|
||
customerType = '付定金';
|
||
} else if (customer.pay_status === '定金转化' || customer.pay_status === '已转化') {
|
||
customerType = '定金转化';
|
||
}
|
||
|
||
return {
|
||
id: customer.customer_name,
|
||
name: customer.customer_name,
|
||
phone: customer.phone,
|
||
profession: customer.customer_occupation,
|
||
education: customer.customer_child_education,
|
||
avatar: customer.weChat_avatar || '/default-avatar.svg',
|
||
time: customer.messages_last_time,
|
||
type: customerType, // 根据pay_status设置的type
|
||
health: 75, // 默认健康度
|
||
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,
|
||
class_num: Object.keys(customer.class_situation || {}), // 添加class_num字段
|
||
pay_status: customer.pay_status,
|
||
records: []
|
||
};
|
||
})
|
||
|
||
// 根据pay_status筛选后三个阶段的客户
|
||
const allClassCustomers = classRes.data.class_customers_list
|
||
|
||
// 点击未支付阶段 - 可以根据具体业务逻辑调整筛选条件
|
||
const unpaidCustomers = allClassCustomers.filter(customer =>
|
||
customer.pay_status === '未支付' || customer.pay_status === '点击未支付'
|
||
)
|
||
// 付定金阶段
|
||
const depositCustomers = allClassCustomers.filter(customer =>
|
||
customer.pay_status === '付定金'
|
||
)
|
||
// 定价转化阶段 - 可以根据具体业务逻辑调整筛选条件
|
||
const conversionCustomers = allClassCustomers.filter(customer =>
|
||
customer.pay_status === '定金转化' || customer.pay_status === '已转化'
|
||
)
|
||
|
||
// 将筛选后的客户添加到对应的customersList中
|
||
const formatCustomerForList = (customer, type) => ({
|
||
customer_name: customer.customer_name,
|
||
phone: customer.phone,
|
||
customer_occupation: customer.customer_occupation,
|
||
customer_child_education: customer.customer_child_education,
|
||
latest_message_time: customer.messages_last_time,
|
||
customer_avatar_url: customer.weChat_avatar,
|
||
type: type,
|
||
class_num: Object.keys(customer.class_situation || {}),
|
||
class_situation: customer.class_situation,
|
||
scrm_user_main_code: customer.scrm_user_main_code,
|
||
weChat_avatar: customer.weChat_avatar,
|
||
pay_status: customer.pay_status
|
||
})
|
||
// 后三个阶段的客户数据已存储在courseCustomers['课1-4']中,不需要合并到customersList
|
||
}
|
||
}
|
||
// 成交阶段
|
||
const payRes = await getPayMoneyCustomers(hasParams ? params : undefined)
|
||
if(payRes.code === 200) {
|
||
// 处理成交阶段的客户数据
|
||
if (payRes.data.pay_money_customers_list) {
|
||
payMoneyCustomersList.value = payRes.data.pay_money_customers_list
|
||
}
|
||
// 处理成交阶段客户总数
|
||
if (payRes.data.pay_money_customers_count) {
|
||
payMoneyCustomersCount.value = payRes.data.pay_money_customers_count
|
||
}
|
||
// 成交阶段客户数据已存储在payMoneyCustomersList中,不需要合并到customersList
|
||
}
|
||
} catch (error) {
|
||
console.error('获取时间线数据失败:', error)
|
||
} finally {
|
||
isTimelineLoading.value = false
|
||
}
|
||
}
|
||
// 获取客户表单
|
||
async function getCustomerForm() {
|
||
if (!selectedContact.value || !selectedContact.value.name) {
|
||
console.warn('无法获取客户表单:客户信息不完整');
|
||
return;
|
||
}
|
||
const routeParams = getRequestParams()
|
||
const params = {
|
||
user_name: routeParams.user_name || userStore.userInfo.username,
|
||
customer_name: selectedContact.value.name,
|
||
}
|
||
try {
|
||
const res = await getCustomerFormInfo(params)
|
||
if(res.code === 200) {
|
||
formInfo.value = res.data
|
||
}
|
||
} catch (error) {
|
||
// 静默处理错误
|
||
}
|
||
}
|
||
// 聊天记录
|
||
async function getCustomerChat() {
|
||
if (!selectedContact.value || !selectedContact.value.name) {
|
||
console.warn('无法获取客户聊天记录:客户信息不完整');
|
||
return;
|
||
}
|
||
const routeParams = getRequestParams()
|
||
const params = {
|
||
user_name: routeParams.user_name || userStore.userInfo.username,
|
||
customer_name: selectedContact.value.name,
|
||
}
|
||
try {
|
||
const res = await getCustomerChatInfo(params)
|
||
if(res.code === 200) {
|
||
chatRecords.value = res.data
|
||
console.log('聊天数据获取成功:', res.data)
|
||
console.log('chatRecords.value:', chatRecords.value)
|
||
} else {
|
||
console.log('聊天数据获取失败:', res)
|
||
}
|
||
} catch (error) {
|
||
// 静默处理错误
|
||
}
|
||
}
|
||
// 通话记录
|
||
async function getCustomerCall() {
|
||
if (!selectedContact.value || !selectedContact.value.name) {
|
||
console.warn('无法获取客户通话记录:客户信息不完整');
|
||
return;
|
||
}
|
||
const routeParams = getRequestParams()
|
||
const params = {
|
||
user_name: routeParams.user_name || userStore.userInfo.username,
|
||
customer_name: selectedContact.value.name,
|
||
}
|
||
try {
|
||
const res = await getCustomerCallInfo(params)
|
||
if(res.code === 200) {
|
||
callRecords.value = res.data
|
||
console.log('Call Records Data from API:', res.data)
|
||
console.log('callRecords.value after assignment:', callRecords.value)
|
||
/**
|
||
* "data": {
|
||
"user_name": "常琳",
|
||
"customer_name": "191桐桐爸爸高一男(婧)",
|
||
"record_file_addr_list": [
|
||
"http://192.168.3.112:5000/api/record/download/杨振彦-20分钟通话-25-08-19_07-23-37-744009-835.mp3"
|
||
]
|
||
}
|
||
*/
|
||
}
|
||
} catch (error) {
|
||
// 静默处理错误
|
||
}
|
||
}
|
||
// 为组件准备数据
|
||
const kpiData = computed(() => kpiDataState);
|
||
// COMPUTED PROPERTIES
|
||
const selectedContact = computed(() => {
|
||
// 优先从API数据中查找
|
||
if (formattedCustomersList.value.length > 0) {
|
||
return formattedCustomersList.value.find((c) => c.id === selectedContactId.value) || null;
|
||
}
|
||
// 否则从MOCK数据中查找
|
||
return MOCK_DATA.contacts.find((c) => c.id === selectedContactId.value) || null;
|
||
});
|
||
|
||
const funnelData = computed(() => {
|
||
if (!SalesFunnel.value || !SalesFunnel.value.sale_funnel) {
|
||
return {
|
||
labels: ["线索总数", "有效沟通", "到课数据", "预付定金", "成功签单"],
|
||
data: [0, 0, 0, 0, 0]
|
||
};
|
||
}
|
||
|
||
const funnel = SalesFunnel.value.sale_funnel;
|
||
return {
|
||
labels: ["线索总数", "有效沟通", "到课数据", "预付定金", "成功签单"],
|
||
data: [
|
||
funnel.线索总数 || 0,
|
||
funnel.有效沟通 || 0,
|
||
funnel.到课数据 || 0,
|
||
funnel.预付定金 || 0,
|
||
funnel.成功签单 || 0
|
||
]
|
||
};
|
||
});
|
||
|
||
const contactTimeData = computed(() => ({
|
||
labels: MOCK_DATA.contactTimeAnalysis.labels,
|
||
data: MOCK_DATA.contactTimeAnalysis.data,
|
||
}));
|
||
|
||
// 格式化客户列表数据
|
||
const formattedCustomersList = computed(() => {
|
||
if (!customersList.value || customersList.value.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
return customersList.value.map(customer => ({
|
||
id: customer.customer_name, // 使用客户姓名作为唯一标识
|
||
name: customer.customer_name,
|
||
phone: customer.phone,
|
||
profession: customer.customer_occupation,
|
||
education: customer.customer_child_education,
|
||
lastMessageTime: customer.latest_message_time,
|
||
avatarUrl: customer.customer_avatar_url,
|
||
type: customer.type,
|
||
class_num: customer.class_num, // 确保字段名一致
|
||
// 添加一些默认值以兼容现有组件
|
||
salesStage: customer.type || '待联系',
|
||
priority: customer.type === '待联系' ? 'high' : 'normal',
|
||
time: customer.latest_message_time || '未知'
|
||
}));
|
||
});
|
||
|
||
// 根据选中阶段筛选联系人
|
||
const filteredContacts = computed(() => {
|
||
// 优先使用当前筛选的客户数据(来自阶段选择)
|
||
if (currentFilteredCustomers.value.length > 0) {
|
||
return currentFilteredCustomers.value;
|
||
}
|
||
|
||
// 对于课1-4阶段,如果没有currentFilteredCustomers数据,返回空数组
|
||
// 避免使用formattedCustomersList中type可能为undefined的数据
|
||
if (selectedStage.value === '课1-4') {
|
||
return [];
|
||
}
|
||
// 如果有API数据,使用API数据
|
||
if (formattedCustomersList.value.length > 0) {
|
||
if (selectedStage.value === 'all' || selectedStage.value === '全部') {
|
||
return formattedCustomersList.value;
|
||
}
|
||
return formattedCustomersList.value.filter(contact => contact.salesStage === selectedStage.value);
|
||
}
|
||
|
||
// 否则使用MOCK数据
|
||
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();
|
||
await getCustomerChat();
|
||
await getCustomerCall();
|
||
}
|
||
contextPanelRef.value?.scrollIntoView({
|
||
behavior: "smooth",
|
||
block: "center",
|
||
});
|
||
});
|
||
};
|
||
|
||
// 存储当前筛选的客户数据
|
||
const currentFilteredCustomers = ref([]);
|
||
|
||
// 处理时间线阶段选择
|
||
const handleStageSelect = (stage, extraData = null) => {
|
||
selectedStage.value = stage;
|
||
|
||
// 如果传递了筛选后的客户数据,使用这些数据
|
||
if (extraData && extraData.filteredCustomers) {
|
||
|
||
|
||
// 将筛选后的客户数据转换为contacts格式
|
||
const filteredContacts = extraData.filteredCustomers.map(customer => ({
|
||
id: customer.customer_name || customer.id,
|
||
name: customer.customer_name || customer.name,
|
||
phone: customer.phone,
|
||
profession: customer.customer_occupation || customer.profession,
|
||
education: customer.customer_child_education || customer.education,
|
||
lastMessageTime: customer.latest_message_time || customer.time,
|
||
avatarUrl: customer.customer_avatar_url || customer.avatar,
|
||
avatar: customer.customer_avatar_url || customer.avatar || '/default-avatar.svg',
|
||
type: customer.type,
|
||
classNum: customer.class_num,
|
||
class_num: customer.class_num, // 确保字段名一致
|
||
salesStage: customer.type || stage,
|
||
priority: customer.type === '待联系' ? 'high' : 'normal',
|
||
time: customer.latest_message_time || customer.time || '未知',
|
||
health: customer.health || 75,
|
||
// 保留原始数据
|
||
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
|
||
}));
|
||
|
||
// 更新当前筛选的客户数据
|
||
currentFilteredCustomers.value = filteredContacts;
|
||
|
||
|
||
} else if (extraData && extraData.isCourseStage) {
|
||
// 处理课1-4阶段的课程数据(保持原有逻辑)
|
||
|
||
|
||
const courseContacts = extraData.courseData.map(customer => ({
|
||
id: customer.id,
|
||
name: customer.name,
|
||
time: customer.time,
|
||
profession: customer.profession,
|
||
education: customer.education,
|
||
avatar: customer.avatar,
|
||
type: customer.type || '课1-4', // 保持原有type字段,如果没有则默认为课1-4
|
||
salesStage: customer.type || '课1-4', // 使用customer.type作为salesStage
|
||
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,
|
||
class_num: customer.class_num, // 添加class_num字段
|
||
pay_status: customer.pay_status, // 添加pay_status字段
|
||
records: customer.records
|
||
}));
|
||
|
||
currentFilteredCustomers.value = courseContacts;
|
||
|
||
} else {
|
||
// 如果没有额外数据,清空筛选结果,使用默认逻辑
|
||
currentFilteredCustomers.value = [];
|
||
}
|
||
};
|
||
const handleViewFormData = async (contact) => {
|
||
// 获取客户表单数据
|
||
await getCustomerForm();
|
||
console.log('表单数据已加载:', formInfo.value);
|
||
};
|
||
|
||
const handleViewChatData = async (contact) => {
|
||
console.log('查看聊天数据:', contact)
|
||
await getCustomerChatInfo({
|
||
customerId: selectedContact.value?.customerId || 1
|
||
})
|
||
console.log('聊天数据已更新:', chatRecords.value)
|
||
};
|
||
|
||
const handleViewCallData = (contact) => {
|
||
// TODO: 实现通话录音查看逻辑
|
||
};
|
||
|
||
// 处理SOP分析事件
|
||
const handleAnalyzeSop = (analyzeData) => {
|
||
console.log('收到SOP分析请求:', analyzeData);
|
||
if (customerDetailRef.value && analyzeData.content) {
|
||
customerDetailRef.value.startSopAnalysis(analyzeData.content);
|
||
}
|
||
};
|
||
// 销售漏斗
|
||
const SalesFunnel = ref([])
|
||
async function CenterGetSalesFunnel() {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
const res = await getSalesFunnel(hasParams?params:undefined)
|
||
if(res.code === 200){
|
||
SalesFunnel.value = res.data
|
||
/**
|
||
* "data": {
|
||
"user_name": "常琳",
|
||
"user_level": 1,
|
||
"sale_funnel": {
|
||
"线索总数": 11,
|
||
"有效沟通": 9,
|
||
"到课数据": 8,
|
||
"预付定金": 0,
|
||
"成功签单": 0
|
||
}
|
||
}
|
||
*/
|
||
}
|
||
}
|
||
// 黄金联络时间段
|
||
const goldContactTime = ref([])
|
||
async function CenterGetGoldContactTime() {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
const res = await getGoldContactTime(hasParams?params:undefined)
|
||
if(res.code === 200){
|
||
goldContactTime.value = res.data
|
||
}
|
||
}
|
||
|
||
// LIFECYCLE HOOKS
|
||
onMounted(async () => {
|
||
try {
|
||
isPageLoading.value = true
|
||
await getCoreKpi()
|
||
await CenterGetGoldContactTime()
|
||
await CenterGetSalesFunnel()
|
||
await getCustomerForm()
|
||
await getCustomerChat()
|
||
await getUrgentProblem()
|
||
await getCustomerCall()
|
||
await getTimeline()
|
||
await getCustomerPayMoney()
|
||
|
||
// 等待数据加载完成后选择默认客户
|
||
await nextTick();
|
||
|
||
// 优先从API数据中选择第一个客户
|
||
if (formattedCustomersList.value.length > 0) {
|
||
selectedContactId.value = formattedCustomersList.value[0].id;
|
||
} else {
|
||
// 否则选择MOCK数据中的第一个高优先级联系人
|
||
const highPriorityContacts = MOCK_DATA.contacts.filter(
|
||
(contact) => contact.priority === "high"
|
||
);
|
||
if (highPriorityContacts.length > 0) {
|
||
selectedContactId.value = highPriorityContacts[0].id;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// 静默处理错误
|
||
} finally {
|
||
isPageLoading.value = false
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
// 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;
|
||
|
||
// 响应式混合器
|
||
@mixin mobile {
|
||
@media (max-width: 768px) {
|
||
@content;
|
||
}
|
||
}
|
||
|
||
@mixin tablet {
|
||
@media (max-width: 1023px) and (min-width: 769px) {
|
||
@content;
|
||
}
|
||
}
|
||
|
||
@mixin desktop {
|
||
@media (min-width: 1024px) {
|
||
@content;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
font-size: 16px;
|
||
line-height: 1.6;
|
||
|
||
@include mobile {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
*, *::before, *::after {
|
||
box-sizing: border-box;
|
||
}
|
||
}
|
||
// 主要布局
|
||
.main-layout {
|
||
width: 100vw;
|
||
margin: 0 auto;
|
||
padding: 1rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
|
||
@include desktop {
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
@include tablet {
|
||
width: 90vw;
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
max-height: 450px;
|
||
}
|
||
|
||
@include tablet {
|
||
border-radius: 0.875rem;
|
||
max-height: 420px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
padding: 1.75rem;
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
font-size: 1.75rem;
|
||
}
|
||
|
||
@include mobile {
|
||
font-size: 1.375rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
font-size: 1.25rem;
|
||
}
|
||
}
|
||
|
||
.task-label {
|
||
font-size: 0.875rem;
|
||
color: $slate-500;
|
||
|
||
@include desktop {
|
||
font-size: 0.9375rem;
|
||
}
|
||
|
||
@include mobile {
|
||
font-size: 0.8125rem;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.sidebar-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
}
|
||
|
||
// 主要内容区域
|
||
.main-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2rem;
|
||
|
||
@include desktop {
|
||
gap: 2.5rem;
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include tablet {
|
||
border-radius: 0.875rem;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
@include mobile {
|
||
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)
|
||
);
|
||
|
||
@include desktop {
|
||
padding: 1.75rem;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 2rem;
|
||
}
|
||
|
||
@include tablet {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
@include mobile {
|
||
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%;
|
||
|
||
@include tablet {
|
||
border-radius: 0.875rem;
|
||
margin-bottom: 1.25rem;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
min-height: 150px;
|
||
}
|
||
|
||
@include tablet {
|
||
min-height: 380px;
|
||
border-radius: 0.875rem;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
@include mobile {
|
||
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)
|
||
);
|
||
|
||
// 路由导航顶栏样式
|
||
.route-header {
|
||
width: 100%;
|
||
padding: 0 2rem;
|
||
|
||
.breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
|
||
.breadcrumb-item {
|
||
color: $slate-600;
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
|
||
&:not(.current) {
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
|
||
&:hover {
|
||
color: $primary;
|
||
}
|
||
}
|
||
|
||
&.current {
|
||
color: $slate-800;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.breadcrumb-separator {
|
||
color: $slate-400;
|
||
font-size: 0.875rem;
|
||
}
|
||
}
|
||
|
||
.user-name {
|
||
color: $slate-800;
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
padding: 0.5rem 1rem;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border-radius: 0.5rem;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
|
||
@include desktop {
|
||
width: auto;
|
||
flex-wrap: nowrap;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
@include tablet {
|
||
width: auto;
|
||
flex-wrap: nowrap;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
padding: 0.5rem 1.25rem;
|
||
flex: none;
|
||
}
|
||
|
||
@include tablet {
|
||
padding: 0.5rem 1rem;
|
||
flex: none;
|
||
}
|
||
|
||
@include mobile {
|
||
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;
|
||
|
||
@include desktop {
|
||
padding: 1.75rem;
|
||
}
|
||
|
||
@include mobile {
|
||
padding: 1.25rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
padding: 1rem;
|
||
}
|
||
}
|
||
|
||
// 局部加载状态样式
|
||
.section-loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 3rem;
|
||
min-height: 200px;
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 3px solid #e2e8f0;
|
||
border-top: 3px solid #3b82f6;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.loading-text {
|
||
color: #64748b;
|
||
font-size: 0.9rem;
|
||
}
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
}
|
||
|
||
</style>
|