Files
DJKB/my-vue-app/src/views/topOne/topone.vue
lbw_9527443 8eff57cf05 refactor(topOne): 清理未使用的响应式数据和简化组件
移除多个未使用的响应式数据变量和模板代码
简化销售进度和个人销售排名组件的显示
重构转化对比数据处理逻辑,确保数据格式正确
2025-08-18 16:32:18 +08:00

2377 lines
45 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="dashboard-container">
<!-- 页面标题 -->
<div class="dashboard-header">
<h1>管理者数据看板</h1>
<!-- 头像 -->
<UserDropdown />
</div>
<!-- 第一行核心业绩指标销售实时进度下发任务 -->
<div class="dashboard-row row-1">
<!-- 核心业绩指标 -->
<kpi-metrics :kpi-data="totalDeals" :format-number="formatNumber" />
<!-- 销售实时进度 -->
<sales-progress :sales-data="realTimeProgress" />
<!-- 下发任务 -->
<task-list
:tasks="tasks"
:format-date="formatDate"
:get-task-status-text="getTaskStatusText"
@show-task-modal="showTaskModal = true"
/>
</div>
<!-- 第二行 -->
<div class="dashboard-row row-3">
<!-- 转化漏斗 -->
<funnel-chart
:funnel-data="formattedFunnelData"
:comparison-data="formattedComparisonData"
@time-range-change="handleTimeRangeChange"
/>
<!-- 销售个人业绩排行榜 -->
<personal-sales-ranking
:ranking-data="formattedSalesRankingData"
:format-number="formatNumber"
:get-rank-class="getRankClass"
@period-change="handleRankingPeriodChange"
@ranking-type-change="getCompanySalesRank"
/>
<!-- 优质通话 -->
<quality-calls
:quality-calls="qualityCalls"
@play-call="playCall"
@download-call="downloadCall"
/>
</div>
<!-- 第三行 -->
<div class="dashboard-row row-3">
<!-- 业绩排行榜 -->
<ranking-list
:ranking-data="formattedRankingData"
:format-number="formatNumber"
:get-rank-class="getRankClass"
@period-change="getCenterSalesRank"
/>
<!-- 客户类型占比 -->
<customer-type :customer-data="customerTypeRatio" @category-change="getCustomerTypeRatio" />
<!-- 客户迫切解决的问题排行榜 -->
<problem-ranking :ranking-data="problemRankingData" />
</div>
<!-- 第四行详细数据表格和数据详情 -->
<div class="dashboard-row">
<CampManagement />
</div>
<!-- 第五行 -->
<div class="dashboard-row row-4">
<DetailedDataTable
:table-data="detailData"
:level-tree="levelTree"
v-model:selected-person="selectedPerson"
@filter-change="handleFilterChange"
/>
<DataDetailCard :selected-person="selectedPerson" />
</div>
<!-- 新建任务模态框 -->
<div
v-if="showTaskModal"
class="modal-overlay"
@click="showTaskModal = false"
>
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>新建任务</h3>
<button class="close-btn" @click="showTaskModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>任务标题</label>
<input
v-model="newTask.title"
type="text"
placeholder="请输入任务标题"
/>
</div>
<div class="form-group">
<label>分配给</label>
<select v-model="newTask.assignee">
<option value="">请选择员工</option>
<option
v-for="employee in employees"
:key="employee.id"
:value="employee.name"
>
{{ employee.name }}
</option>
</select>
</div>
<div class="form-group">
<label>截止日期</label>
<input v-model="newTask.deadline" type="date" />
</div>
<div class="form-group">
<label>任务描述</label>
<textarea
v-model="newTask.description"
placeholder="请输入任务描述"
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" @click="showTaskModal = false">
取消
</button>
<button class="confirm-btn" @click="createTask">创建任务</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dashboard-container {
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
padding: 20px;
box-sizing: border-box;
background-color: #f0f2f5;
/* 触摸设备滚动优化 */
-webkit-overflow-scrolling: touch;
}
</style>
<script setup>
import { ref, reactive, computed, onMounted, nextTick } from "vue";
import UserDropdown from "@/components/UserDropdown.vue";
import KpiMetrics from "./components/KpiMetrics.vue";
import SalesProgress from "./components/SalesProgress.vue";
import TaskList from "./components/TaskList.vue";
import FunnelChart from "./components/FunnelChart.vue";
import CustomerProfile from "./components/CustomerProfile.vue";
import CustomerType from "../secondTop/components/CustomerType.vue";
import ProblemRanking from "../secondTop/components/ProblemRanking.vue";
import RankingList from "./components/RankingList.vue";
import PersonalSalesRanking from "./components/PersonalSalesRanking.vue";
import CommunicationData from "./components/CommunicationData.vue";
import QualityCalls from "./components/QualityCalls.vue";
import DataTable from "./components/DataTable.vue";
import DataDetail from "./components/DataDetail.vue";
import CampManagement from "./components/CampManagement.vue";
import DetailedDataTable from "./components/DetailedDataTable.vue";
import DataDetailCard from "./components/DataDetailCard.vue";
import { getOverallCompanyPerformance,getCompanyDepositConversionRate,getCompanyTotalCallCount,getCompanyNewCustomer,getCompanyConversionRate,getCompanyRealTimeProgress
,getCompanyConversionRateVsLast,getSalesMonthlyPerformance,getCenterPerformanceRank,getCustomerTypeDistribution,getUrgentNeedToAddress,getLevelTree,getDetailedDataTable
} from "@/api/top";
const rankingPeriod = ref("month");
const rankingData = ref([
{ id: 1, name: "张三", department: "销售一部", performance: 125000 },
{ id: 2, name: "李四", department: "销售二部", performance: 118000 },
{ id: 3, name: "王五", department: "销售一部", performance: 112000 },
{ id: 4, name: "赵六", department: "销售三部", performance: 98000 },
{ id: 5, name: "钱七", department: "销售二部", performance: 89000 },
]);
const sortField = ref("dealRate");
const sortOrder = ref("desc");
const selectedPerson = ref(null);
const tasks = ref([
{
id: 1,
title: "完成Q4销售目标制定",
assignee: "张三",
deadline: "2024-01-15",
status: "pending",
}
]);
const employees = ref([
{ id: 1, name: "张三" }
]);
const showTaskModal = ref(false);
const newTask = reactive({
title: "",
assignee: "",
deadline: "",
description: "",
});
// 计算属性
const filteredTableData = computed(() => {
let filtered = tableData.value;
// 应用筛选器
if (filters.value.department) {
filtered = filtered.filter(
(item) => item.department === filters.value.department
);
}
if (filters.value.position) {
filtered = filtered.filter(
(item) => item.position === filters.value.position
);
}
// 排序
return filtered.sort((a, b) => {
const aValue = a[sortField.value];
const bValue = b[sortField.value];
if (sortOrder.value === "desc") {
return bValue - aValue;
} else {
return aValue - bValue;
}
});
});
// 方法
const refreshData = () => {
// 刷新数据逻辑
console.log("刷新数据");
};
// 处理时间范围变化
const handleTimeRangeChange = (timeRange) => {
console.log("时间范围变化:", timeRange);
// 根据时间范围重新获取转化对比数据
getConversionComparison(timeRange);
};
const sortBy = (field) => {
if (sortField.value === field) {
sortOrder.value = sortOrder.value === "desc" ? "asc" : "desc";
} else {
sortField.value = field;
sortOrder.value = "desc";
}
};
const getRateClass = (rate) => {
if (rate >= 80) return "high";
if (rate >= 60) return "medium";
return "low";
};
const getRateColor = (rate) => {
if (rate >= 80) return "#4CAF50";
if (rate >= 60) return "#FF9800";
return "#f44336";
};
const getRankClass = (index) => {
if (index === 0) return "gold";
if (index === 1) return "silver";
if (index === 2) return "bronze";
return "";
};
const formatNumber = (num) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + "万";
}
return num.toLocaleString();
};
const getActivityIcon = (type) => {
const icons = {
deal: "icon-check-circle",
lost: "icon-x-circle",
call: "icon-phone",
};
return icons[type] || "icon-info";
};
const formatTime = (timestamp) => {
const now = new Date();
const diff = now - timestamp;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "刚刚";
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
return `${hours}小时前`;
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString("zh-CN");
};
const formatDuration = (minutes) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h${mins}m` : `${mins}m`;
};
const selectPerson = (person) => {
selectedPerson.value = person;
};
const getTaskStatusText = (status) => {
const statusMap = {
pending: "待处理",
"in-progress": "进行中",
completed: "已完成",
};
return statusMap[status] || status;
};
const playCall = (callId) => {
console.log("播放通话录音:", callId);
};
const downloadCall = (callId) => {
console.log("下载通话录音:", callId);
};
const createTask = () => {
if (!newTask.title || !newTask.assignee || !newTask.deadline) {
alert("请填写完整信息");
return;
}
const task = {
id: Date.now(),
title: newTask.title,
assignee: newTask.assignee,
deadline: newTask.deadline,
status: "pending",
};
tasks.value.unshift(task);
// 重置表单
Object.assign(newTask, {
title: "",
assignee: "",
deadline: "",
description: "",
});
showTaskModal.value = false;
};
// 核心数据
const totalDeals = ref({});
// 核心数据--总成交金额
async function getTotalDeals() {
try {
const res1 = await getOverallCompanyPerformance()
const res2=await getCompanyDepositConversionRate()
const res3=await getCompanyTotalCallCount()
const res4=await getCompanyNewCustomer()
const res5=await getCompanyConversionRate()
totalDeals.value={
totalDeal:res1.data, //总成交单数
DingconversionRate:res2.data, //定金转化率
totalCallCount:res3.data, // 总通话
newCustomer:res4.data, //新客户
conversionRate:res5.data,//转化率
}
} catch (error) {
console.error("获取总成交金额失败:", error);
}
}
// 实时进度
const realTimeProgress = ref({});
async function getRealTimeProgress() {
try {
const res = await getCompanyRealTimeProgress()
// console.log(111111,res)
realTimeProgress.value = res.data
} catch (error) {
console.error("获取实时进度失败:", error);
}
}
// 转化对比
const conversionComparison = ref({});
// 计算属性:转换 conversionComparison 数据为 funnelData 格式
const formattedFunnelData = computed(() => {
if (!conversionComparison.value || !conversionComparison.value.company_current_rate) {
return []; // 返回空数组,避免数据格式不匹配
}
const currentData = conversionComparison.value.company_current_rate;
const stageOrder = ['线索总数', '加微', '到课', '付定金', '成交'];
const colors = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#F44336'];
return stageOrder.map((stageName, index) => {
const count = currentData[stageName] || 0;
const totalCount = currentData['线索总数'] || 1;
const percentage = totalCount > 0 ? Math.round((count / totalCount) * 100) : 0;
return {
name: stageName,
count: count,
percentage: percentage,
color: colors[index]
};
});
});
// 计算属性:转换 conversionComparison 数据格式以适配 FunnelChart 组件
const formattedComparisonData = computed(() => {
if (!conversionComparison.value || !conversionComparison.value.company_current_rate) {
return {};
}
const currentData = conversionComparison.value.company_current_rate;
const lastData = conversionComparison.value.company_last_rate;
const checkType = conversionComparison.value.check_type;
// 确保lastData存在
if (!lastData) {
return {};
}
// 根据 check_type 确定时间范围键
const timeRangeKey = checkType === 'month' ? 'month' : 'period';
const stageOrder = ['线索总数', '加微', '到课', '付定金', '成交'];
const comparisonArray = stageOrder.map(stageName => ({
name: stageName,
count: lastData[stageName] || 0
}));
// 同时返回period和month两个键确保组件能找到对应数据
const result = {
period: comparisonArray,
month: comparisonArray
};
return result;
});
async function getConversionComparison(data) {
const params={
check_type:data
}
try {
const res = await getCompanyConversionRateVsLast(params)
console.log(111111,res)
conversionComparison.value = res.data
/**
* data
:
{user_name: "赵世敬", user_level: 5, check_type: "month",…}
check_type
:
"month"
company_current_rate
:
{线索总数: 14051, 加微: 3238, 到课: 7614, 付定金: 168, 成交: 136}
付定金
:
168
到课
:
7614
加微
:
3238
成交
:
136
线索总数
:
14051
company_current_vs_last_rate
:
{线索总数: "-20.16%", 加微: "-2.70%", 到课: "+114.90%", 付定金: "+∞%", 成交: "+∞%"}
付定金
:
"+∞%"
到课
:
"+114.90%"
加微
:
"-2.70%"
成交
:
"+∞%"
线索总数
:
"-20.16%"
company_last_rate
:
{线索总数: 17598, 加微: 3328, 到课: 3543, 付定金: 0, 成交: 0}
付定金
:
0
到课
:
3543
加微
:
3328
成交
:
0
线索总数
:
17598
user_level
:
5
user_name
:
"赵世敬"
*/
} catch (error) {
console.error("获取转化对比失败:", error);
}
}
// 获取全公司销售月度业绩红黑榜 params:{"rank_type": "red" // "rank_type": "black"}
const companySalesRank = ref({});
// 计算属性:转换 companySalesRank 数据格式以适配 PersonalSalesRanking 组件
const formattedSalesRankingData = computed(() => {
if (!companySalesRank.value) {
return [];
}
// 根据 rank_type 选择对应的数据
const rankType = companySalesRank.value.rank_type;
const rankList = rankType === 'red'
? companySalesRank.value.sales_monthly_performance_red
: companySalesRank.value.sales_monthly_performance_black;
if (!rankList) {
return [];
}
return rankList.map((item, index) => ({
id: index + 1,
name: item.name,
department: item.department,
performance: item.deal_count, // 假设每单10000元可根据实际情况调整
deals: item.deal_count,
conversionRate: parseFloat(item.conversion_rate.replace('%', '')),
trend: rankType === 'red' ? 'up' : 'down', // 红榜为上升趋势,黑榜为下降趋势
growth: rankType === 'red' ? Math.random() * 20 : -(Math.random() * 20), // 红榜正增长,黑榜负增长
avatar: '/default-avatar.svg'
}));
});
// 处理销售排行榜期间变化
const handleRankingPeriodChange = (period) => {
// 根据期间参数调用相应的函数,这里默认调用红榜数据
getCompanySalesRank('red');
};
async function getCompanySalesRank(Rank) {
const params={
rank_type:Rank,
}
try {
const res = await getSalesMonthlyPerformance(params)
companySalesRank.value = res.data
/**
* "data": {
"user_name": "赵世敬",
"user_level": 5,
"rank_type": "red",
"sales_monthly_performance_red": [
{
"name": "贾星草",
"department": "洋葱管理层",
"deal_count": 2,
"conversion_rate": "2.78%",
"rank": 1
},
{
"name": "常志洁",
"department": "星火二部--王志恒",
"deal_count": 2,
"conversion_rate": "4.17%",
"rank": 2
},
{
"name": "李俊",
"department": "星火二部--王志恒",
"deal_count": 2,
"conversion_rate": "3.77%",
"rank": 3
},
{
"name": "高有桔",
"department": "勇士一部-张茂华",
"deal_count": 2,
"conversion_rate": "3.12%",
"rank": 4
},
{
"name": "马肖剑",
"department": "星耀三部-周毅",
"deal_count": 1,
"conversion_rate": "1.05%",
"rank": 5
},
{
"name": "刘思雨",
"department": "星耀三部-周毅",
"deal_count": 1,
"conversion_rate": "2.27%",
"rank": 6
},
{
"name": "王慧",
"department": "聚星三部--张卓",
"deal_count": 1,
"conversion_rate": "2.00%",
"rank": 7
},
{
"name": "寇帅杰",
"department": "巅峰一部-贾星草",
"deal_count": 1,
"conversion_rate": "2.13%",
"rank": 8
},
{
"name": "王奥博",
"department": "巅峰二部-纪洋洋",
"deal_count": 1,
"conversion_rate": "1.14%",
"rank": 9
},
{
"name": "董富忠",
"department": "巅峰三部--刘东洋",
"deal_count": 1,
"conversion_rate": "0.95%",
"rank": 10
}
]
}
*/
// 黑榜数据
/**
* "data": {
"user_name": "赵世敬",
"user_level": 5,
"rank_type": "black",
"sales_monthly_performance_black": [
{
"name": "马然",
"department": "美团业务支持部",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 1
},
{
"name": "郭可英",
"department": "技术部",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 2
},
{
"name": "杨启晨",
"department": "星火一部--张瑾",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 3
},
{
"name": "程慧仟",
"department": "星耀三部-周毅",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 4
},
{
"name": "常琳",
"department": "亮剑二部-田贵星",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 5
},
{
"name": "李晓雪",
"department": "星火一部--张瑾",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 6
},
{
"name": "杨朵朵",
"department": "星耀三部-周毅",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 7
},
{
"name": "张明起",
"department": "星耀一部-吕明月",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 8
},
{
"name": "刘英杰",
"department": "星耀一部-吕明月",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 9
},
{
"name": "孟凡玉",
"department": "星耀一部-吕明月",
"deal_count": 0,
"conversion_rate": "0.00%",
"rank": 10
}
]
}
*/
} catch (error) {
console.error("获取销售月度业绩红黑榜失败:", error);
}
}
// 获取全中心业绩排行榜
const centerSalesRank = ref({});
// 计算属性:转换 centerSalesRank 数据格式以适配 ranking-list 组件
const formattedRankingData = computed(() => {
if (!centerSalesRank.value || !centerSalesRank.value.center_performance_periods_rank) {
return rankingData.value; // 返回默认数据
}
const rankList = centerSalesRank.value.center_performance_periods_rank;
return rankList.map((item, index) => ({
id: index + 1,
name: item.center_leader,
performance: item.total_deals,
average_deals_per_member: item.average_deals_per_member
}));
});
async function getCenterSalesRank(data) {
const params={
check_type:data //periods、month、year
}
try {
const res = await getCenterPerformanceRank(params)
console.log(1222222,res)
centerSalesRank.value = res.data
} catch (error) {
console.error("获取全中心业绩排行榜失败:", error);
}
}
// 客户类型占比
const customerTypeRatio = ref({});
async function getCustomerTypeRatio(data) {
const params={
distribution_type:data // child_education territory occupation
}
try {
const res = await getCustomerTypeDistribution(params)
console.log(1222222,res)
customerTypeRatio.value = res.data
} catch (error) {
console.error("获取客户类型占比失败:", error);
}
}
// 客户迫切解决的问题排行榜
const customerUrgency = ref({});
async function getCustomerUrgency() {
try {
const res = await getUrgentNeedToAddress()
console.log(1222222,res)
customerUrgency.value = res.data
} catch (error) {
console.error("获取客户迫切解决的问题排行榜失败:", error);
}
}
// 获取级别树
const levelTree = ref({});
async function CusotomGetLevelTree() {
try {
const res = await getLevelTree()
console.log(1222222,res)
levelTree.value = res.data
/**
* "data": {
"user_name": "赵世敬",
"user_level": 5,
"level_tree": {
"center_leaders": [
{
"name": "郭可英",
"advanced_managers": [
{
"name": "李小燕",
"managers": []
},
{
"name": "郭子奇",
"managers": [
{
"name": "杨朵朵"
},
{
"name": "张明起"
}
]
}
]
},
{
"name": "刘瑞",
"advanced_managers": [
{
"name": "陈盼良",
"managers": [
{
"name": "马然"
},
{
"name": "杨启晨"
},
{
"name": "韦少杰"
}
]
}
]
}
]
}
}
*/
} catch (error) {
console.error("获取级别树失败:", error);
}
}
// 获取详细数据表格
const detailData = ref({});
async function getDetailData(params) {
if(params?.center_leader){
// alert(11111)
try {
const res = await getDetailedDataTable(params)
console.log('详细数据表格:', res)
detailData.value = res.data
} catch (error) {
console.error("获取详细数据表格失败:", error);
}
}else{
// alert(22222)
try {
const res = await getDetailedDataTable()
console.log('详细数据表格:', res)
detailData.value = res.data
} catch (error) {
console.error("获取详细数据表格失败:", error);
}
}
}
// 处理筛选器变化
const handleFilterChange = (filterParams) => {
console.log('筛选器变化:', filterParams)
getDetailData(filterParams)
}
onMounted(async() => {
// 页面初始化逻辑
await getRealTimeProgress()
await getTotalDeals()
await getConversionComparison('month')
await getCompanySalesRank('red')
await getCenterSalesRank('periods')
await getCustomerTypeRatio('child_education')
await getCustomerUrgency()
await CusotomGetLevelTree()
await getDetailData()
});
</script>
<style scoped>
.dashboard-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
max-width: 1400px;
margin: 0 auto;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
width: 100%;
}
.dashboard-header h1 {
font-size: 28px;
font-weight: 600;
color: #1a202c;
margin: 0;
}
.header-actions {
display: flex;
gap: 12px;
}
.refresh-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #4299e1;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.refresh-btn:hover {
background: #3182ce;
}
.metrics-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.metric-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.metric-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.metric-header h3 {
font-size: 16px;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.metric-period {
font-size: 12px;
color: #718096;
background: #edf2f7;
padding: 4px 8px;
border-radius: 4px;
}
.kpi-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 10px;
}
.kpi-item {
text-align: center;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
}
.kpi-label {
font-size: 12px;
color: #718096;
margin-bottom: 8px;
}
.kpi-value {
font-size: 20px;
font-weight: 700;
color: #1a202c;
margin-bottom: 4px;
}
.kpi-trend {
font-size: 12px;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.kpi-trend.positive {
color: #38a169;
background: #f0fff4;
}
.kpi-trend.negative {
color: #e53e3e;
background: #fff5f5;
}
.communication-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 10px;
}
.comm-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
transition: all 0.3s ease;
}
.comm-card:hover {
background: #f1f5f9;
border-color: #cbd5e1;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-content {
flex: 1;
}
.card-label {
font-size: 12px;
color: #64748b;
margin-bottom: 4px;
}
.card-value {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.sales-progress-tips {
display: flex;
flex-direction: column;
gap: 12px;
padding: 10px;
}
.tip-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
}
.tip-item.success {
background: #f0fff4;
color: #38a169;
border-left: 3px solid #38a169;
}
.tip-item.warning {
background: #fffbeb;
color: #d69e2e;
border-left: 3px solid #d69e2e;
}
.tip-item.info {
background: #ebf8ff;
color: #4299e1;
border-left: 3px solid #4299e1;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: #718096;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #1a202c;
}
.stat-value.success {
color: #38a169;
}
.stat-value.danger {
color: #e53e3e;
}
.dashboard-row {
display: grid;
gap: 20px;
margin-bottom: 24px;
}
.row-1 {
grid-template-columns: 2fr 1fr 1fr;
height: 350px;
}
.row-2 {
grid-template-columns: 1fr 1fr 1fr;
height: 300px;
}
.row-3 {
grid-template-columns: 1fr 1fr 1fr;
height: 400px;
}
.row-3 .dashboard-card {
height: 400px;
overflow: hidden;
}
.row-3 .customer-profile {
height: calc(100% - 60px);
overflow-y: auto;
}
.row-4 {
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.table-section,
.detail-section {
height: 600px;
overflow: hidden;
}
.data-table-container {
height: calc(100% - 60px);
overflow-y: auto;
}
.detail-content {
height: calc(100% - 60px);
overflow-y: auto;
}
.data-table tbody tr {
cursor: pointer;
transition: background-color 0.2s ease;
}
.data-table tbody tr:hover {
background-color: #f8fafc;
}
.data-table tbody tr.selected {
background-color: #e0f2fe;
border-left: 4px solid #0ea5e9;
}
.no-selection {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #64748b;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.person-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
}
.detail-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 600;
}
.detail-info h4 {
margin: 0 0 4px 0;
font-size: 20px;
color: #1e293b;
}
.detail-info p {
margin: 0;
color: #64748b;
font-size: 14px;
}
.detail-placeholder {
text-align: center;
padding: 40px 20px;
color: #64748b;
}
.detail-placeholder p:first-child {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: #475569;
}
.placeholder-text {
font-size: 14px;
line-height: 1.6;
opacity: 0.8;
}
.dashboard-row .dashboard-card {
/* height: 400px; */
}
.row-4 .dashboard-card {
height: auto;
min-height: 500px;
}
.dashboard-card.full-width {
width: 100%;
}
.dashboard-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px 16px;
border-bottom: 1px solid #e2e8f0;
}
.card-header h3 {
font-size: 18px;
font-weight: 600;
color: #1a202c;
margin: 0;
}
.period-select {
padding: 4px 8px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 12px;
background: white;
}
.live-indicator {
color: #e53e3e;
font-size: 12px;
font-weight: 600;
}
.view-all-btn,
.add-task-btn {
padding: 6px 12px;
background: #4299e1;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.ranking-list {
padding: 0 24px 24px;
}
.ranking-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid #f7fafc;
}
.ranking-item:last-child {
border-bottom: none;
}
.rank-number {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-weight: 700;
font-size: 14px;
background: #edf2f7;
color: #4a5568;
}
.rank-number.gold {
background: #ffd700;
color: white;
}
.rank-number.silver {
background: #c0c0c0;
color: white;
}
.rank-number.bronze {
background: #cd7f32;
color: white;
}
.employee-info {
flex: 1;
}
.employee-name {
font-weight: 600;
color: #1a202c;
margin-bottom: 2px;
}
.employee-dept {
font-size: 12px;
color: #718096;
}
.performance-value {
font-weight: 700;
color: #1a202c;
}
.funnel-chart {
padding: 24px;
}
.funnel-stage {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.funnel-stage:last-child {
margin-bottom: 0;
}
.stage-bar {
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-radius: 6px;
color: white;
font-weight: 600;
min-width: 120px;
}
.stage-percentage {
font-weight: 600;
color: #4a5568;
min-width: 40px;
}
.activity-feed {
padding: 0 24px 24px;
max-height: 300px;
overflow-y: auto;
}
.activity-item {
display: flex;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid #f7fafc;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.activity-icon.deal {
background: #f0fff4;
color: #38a169;
}
.activity-icon.lost {
background: #fff5f5;
color: #e53e3e;
}
.activity-icon.call {
background: #ebf8ff;
color: #4299e1;
}
.activity-content {
flex: 1;
}
.activity-text {
font-size: 14px;
color: #1a202c;
margin-bottom: 4px;
}
.activity-time {
font-size: 12px;
color: #718096;
}
.quality-calls {
padding: 0 24px 24px;
}
.call-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f7fafc;
}
.call-item:last-child {
border-bottom: none;
}
.caller-name {
font-weight: 600;
color: #1a202c;
margin-bottom: 4px;
}
.call-details {
display: flex;
gap: 12px;
font-size: 12px;
color: #718096;
}
.call-actions {
display: flex;
gap: 8px;
}
.play-btn,
.download-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.play-btn {
background: #ebf8ff;
color: #4299e1;
}
.download-btn {
background: #f7fafc;
color: #4a5568;
}
.customer-profile {
padding: 0 10px;
}
.profile-section {
margin-bottom: 24px;
}
.profile-section:last-child {
margin-bottom: 0;
}
.profile-section h4 {
font-size: 16px;
font-weight: 600;
color: #1a202c;
margin-bottom: 16px;
}
.parent-types {
display: flex;
flex-direction: column;
gap: 12px;
}
.parent-type-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.type-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.type-name {
font-size: 14px;
color: #1a202c;
}
.type-percentage {
font-size: 14px;
font-weight: 600;
color: #4a5568;
}
.type-bar {
height: 8px;
background: #edf2f7;
border-radius: 4px;
overflow: hidden;
}
.type-fill {
height: 100%;
transition: width 0.3s ease;
}
.hot-questions {
display: flex;
flex-direction: column;
gap: 12px;
}
.question-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
}
.question-rank {
width: 24px;
height: 24px;
background: #4299e1;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.question-content {
flex: 1;
}
.question-text {
font-size: 14px;
color: #1a202c;
margin-bottom: 2px;
}
.question-count {
font-size: 12px;
color: #718096;
}
/* 数据表格样式 */
.data-table-container {
padding: 24px;
}
.table-filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.filter-group label {
font-size: 12px;
font-weight: 600;
color: #4a5568;
text-transform: uppercase;
}
.filter-group select {
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: white;
font-size: 14px;
color: #1a202c;
cursor: pointer;
transition: border-color 0.2s;
}
.filter-group select:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
}
.data-table {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.data-table table {
width: 100%;
border-collapse: collapse;
background: white;
}
.data-table th {
background: #f7fafc;
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: #4a5568;
border-bottom: 1px solid #e2e8f0;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.data-table th.sortable {
cursor: pointer;
user-select: none;
position: relative;
}
.data-table th.sortable:hover {
background: #edf2f7;
}
.sort-icon {
margin-left: 4px;
opacity: 0.5;
transition: opacity 0.2s;
}
.sort-icon.active {
opacity: 1;
color: #4299e1;
}
.data-table td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.data-table tr:hover {
background: #f8fafc;
}
.person-info {
display: flex;
align-items: center;
gap: 12px;
}
.person-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #4299e1;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
}
.person-name {
font-weight: 600;
color: #1a202c;
margin-bottom: 2px;
}
.person-position {
font-size: 12px;
color: #718096;
}
.deal-rate {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 80px;
}
.rate-value {
font-weight: 600;
font-size: 14px;
}
.rate-value.high {
color: #4caf50;
}
.rate-value.medium {
color: #ff9800;
}
.rate-value.low {
color: #f44336;
}
.rate-bar {
height: 4px;
background: #edf2f7;
border-radius: 2px;
overflow: hidden;
}
.rate-fill {
height: 100%;
transition: width 0.3s ease;
}
.task-list {
padding: 0 24px 24px;
}
.task-list.compact {
max-height: 320px;
}
.task-list.compact .task-item {
padding: 12px 0;
}
.task-list.compact .task-item .task-title {
font-size: 14px;
margin-bottom: 4px;
}
.task-list.compact .task-meta {
display: flex;
flex-direction: row;
gap: 15px;
font-size: 12px;
}
.task-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px 0;
border-bottom: 1px solid #f7fafc;
}
.task-item:last-child {
border-bottom: none;
}
.task-title {
font-weight: 600;
color: #1a202c;
margin-bottom: 8px;
}
.task-meta {
display: flex;
flex-direction: row;
gap: 15px;
font-size: 12px;
color: #718096;
}
.task-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.task-status.pending {
background: #fef5e7;
color: #d69e2e;
}
.task-status.in-progress {
background: #ebf8ff;
color: #4299e1;
}
.task-status.completed {
background: #f0fff4;
color: #38a169;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 12px;
width: 500px;
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1a202c;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #718096;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1a202c;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
.form-group textarea {
height: 80px;
resize: vertical;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #e2e8f0;
}
.cancel-btn {
padding: 8px 16px;
background: #f7fafc;
color: #4a5568;
border: 1px solid #e2e8f0;
border-radius: 6px;
cursor: pointer;
}
.confirm-btn {
padding: 8px 16px;
background: #4299e1;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
/* 响应式设计 */
/* 大屏幕 (1400px+) */
@media (min-width: 1400px) {
.dashboard-container {
max-width: 1600px;
padding: 30px;
}
}
/* 中大屏幕 (1200px - 1399px) */
@media (max-width: 1399px) {
.dashboard-container {
max-width: 1200px;
}
}
/* 中屏幕 (992px - 1199px) */
@media (max-width: 1199px) {
.dashboard-container {
max-width: 100%;
padding: 20px 15px;
}
.row-1 {
grid-template-columns: 1fr 1fr;
height: auto;
}
.row-2 {
grid-template-columns: 1fr 1fr;
height: auto;
}
.row-3 {
grid-template-columns: 1fr 1fr;
height: auto;
}
.row-4 {
grid-template-columns: 1fr;
}
}
/* 小屏幕 (768px - 991px) */
@media (max-width: 991px) {
.dashboard-container {
padding: 15px 10px;
}
.dashboard-header h1 {
font-size: 24px;
}
.row-1,
.row-2 {
grid-template-columns: 1fr;
gap: 15px;
}
.dashboard-row {
gap: 15px;
margin-bottom: 20px;
}
.card-header {
padding: 15px 20px 12px;
}
.card-header h3 {
font-size: 16px;
}
}
/* 移动端 (576px - 767px) */
@media (max-width: 767px) {
.dashboard-container {
padding: 10px 8px;
}
.dashboard-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
margin-bottom: 20px;
}
.dashboard-header h1 {
font-size: 20px;
}
.refresh-btn {
padding: 6px 12px;
font-size: 12px;
}
.row-1,
.row-2,
.row-3,
.row-4 {
grid-template-columns: 1fr;
gap: 12px;
}
.dashboard-row {
margin-bottom: 15px;
}
.dashboard-card {
border-radius: 8px;
}
.card-header {
padding: 12px 16px 10px;
}
.card-header h3 {
font-size: 14px;
}
.metrics-row {
grid-template-columns: 1fr;
gap: 12px;
}
.kpi-metrics {
grid-template-columns: 1fr;
gap: 12px;
padding: 8px;
}
.communication-cards {
grid-template-columns: 1fr;
gap: 8px;
padding: 8px;
}
.comm-card {
padding: 12px;
}
.table-filters {
grid-template-columns: 1fr;
gap: 12px;
padding: 12px;
}
.data-table th,
.data-table td {
padding: 8px 12px;
font-size: 12px;
}
.person-info {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.person-avatar {
width: 32px;
height: 32px;
font-size: 14px;
}
.modal-content {
width: 95vw;
margin: 10px;
}
.modal-header,
.modal-body,
.modal-footer {
padding: 16px;
}
}
/* 超小屏幕 (最大575px) */
@media (max-width: 575px) {
.dashboard-container {
padding: 8px 5px;
}
.dashboard-header h1 {
font-size: 18px;
}
.refresh-btn {
padding: 4px 8px;
font-size: 11px;
}
.dashboard-row {
gap: 8px;
margin-bottom: 12px;
}
.card-header {
padding: 10px 12px 8px;
}
.card-header h3 {
font-size: 13px;
}
.kpi-value {
font-size: 16px;
}
.kpi-label {
font-size: 10px;
}
.card-value {
font-size: 14px;
}
.card-label {
font-size: 10px;
}
.tip-item {
padding: 6px 8px;
font-size: 12px;
}
.ranking-item {
padding: 8px 0;
}
.rank-number {
width: 24px;
height: 24px;
font-size: 12px;
}
.employee-name {
font-size: 13px;
}
.employee-dept {
font-size: 10px;
}
.performance-value {
font-size: 13px;
}
.data-table th,
.data-table td {
padding: 6px 8px;
font-size: 11px;
}
.person-avatar {
width: 28px;
height: 28px;
font-size: 12px;
}
.person-name {
font-size: 13px;
}
.person-position {
font-size: 10px;
}
}
/* 图标样式 (使用字符代替实际图标) */
.icon-refresh::before {
content: "↻";
}
.icon-plus::before {
content: "+";
}
.icon-check-circle::before {
content: "✓";
}
.icon-x-circle::before {
content: "✗";
}
.icon-phone::before {
content: "☎";
}
.icon-info::before {
content: "";
}
.icon-play::before {
content: "▶";
}
.icon-download::before {
content: "↓";
}
.icon-alert-circle::before {
content: "⚠";
}
.icon-info-circle::before {
content: "";
}
/* 通用响应式优化 */
* {
box-sizing: border-box;
}
/* 防止水平滚动 */
body {
overflow-x: hidden;
}
/* 图片响应式 */
img {
max-width: 100%;
height: auto;
}
/* 表格响应式 */
.data-table {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 按钮触摸优化 */
button {
min-height: 44px;
min-width: 44px;
touch-action: manipulation;
}
/* 小屏幕下的按钮优化 */
@media (max-width: 767px) {
button {
min-height: 40px;
min-width: 40px;
}
}
/* 超小屏幕下的按钮优化 */
@media (max-width: 575px) {
button {
min-height: 36px;
min-width: 36px;
}
}
/* 文本选择优化 */
.dashboard-card {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* 可选择的文本 */
.data-table,
.person-detail,
.modal-content {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
</style>