重构销售时间线组件,分离不同阶段的客户数据管理: 1. 不再将后三个阶段和成交阶段的客户合并到customersList 2. 为各阶段客户数据添加独立处理逻辑 3. 优化课程显示逻辑,优先使用class_num字段 4. 简化总数计算逻辑
1428 lines
38 KiB
Vue
1428 lines
38 KiB
Vue
<template>
|
||
<div class="sales-dashboard">
|
||
<!-- 悬浮待办组件 -->
|
||
<FloatingTodo />
|
||
|
||
<!-- 页面加载状态 -->
|
||
<Loading :visible="isPageLoading" text="正在加载数据..." />
|
||
<!-- 顶部导航栏 -->
|
||
<!-- 数据分析区域 - 独立占据整行 -->
|
||
<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">
|
||
<!-- 数据分析区域加载状态 -->
|
||
<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="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">
|
||
<!-- 销售时间线加载状态 -->
|
||
<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"
|
||
@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 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 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} 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
|
||
}
|
||
// STATE
|
||
const selectedContactId = ref(null);
|
||
const contextPanelRef = 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);
|
||
|
||
// 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 problemDistribution = res.data.problem_distribution
|
||
urgentProblemData.value = Object.entries(problemDistribution).map(([name, percentage]) => ({
|
||
name: name,
|
||
value: parseInt(percentage.replace('%', '')) || 0
|
||
}))
|
||
}
|
||
} 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) {
|
||
MOCK_DATA.formFields = res.data
|
||
|
||
}
|
||
} 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(() => ({
|
||
labels: ["线索", "沟通", "意向", "预约", "成交"],
|
||
data: MOCK_DATA.personalFunnel,
|
||
}));
|
||
|
||
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();
|
||
}
|
||
|
||
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 = (contact) => {
|
||
// TODO: 实现表单数据查看逻辑
|
||
};
|
||
|
||
const handleViewChatData = (contact) => {
|
||
// TODO: 实现聊天记录查看逻辑
|
||
};
|
||
|
||
const handleViewCallData = (contact) => {
|
||
// TODO: 实现通话录音查看逻辑
|
||
};
|
||
|
||
// LIFECYCLE HOOKS
|
||
onMounted(async () => {
|
||
try {
|
||
isPageLoading.value = true
|
||
await getTimeline()
|
||
|
||
// 等待数据加载完成后选择默认客户
|
||
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: 450px;
|
||
}
|
||
|
||
@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)
|
||
);
|
||
|
||
// 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>
|