Files
DJKB/my-vue-app/src/views/topOne/topone.vue
lbw_9527443 3b1c1c03f3 feat: 实现任务管理功能并优化界面显示
- 添加任务列表获取和状态更新API调用
- 修改任务列表组件显示格式和状态标签
- 优化日期格式化处理逻辑
- 调整任务列表样式和交互效果
- 注释掉部分不需要的API调用
2025-08-21 12:57:50 +08:00

2204 lines
41 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"
@periods-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
:format-number="formatNumber"
:get-rank-class="getRankClass"
/>
<!-- 客户类型占比 -->
<customer-type :customer-data="customerTypeRatio" @category-change="getCustomerTypeRatio" />
<!-- 客户迫切解决的问题排行榜 -->
<problem-ranking :ranking-data="problemRankingData" />
</div>
<!-- 第四行详细数据表格和数据详情 -->
<div class="dashboard-row" v-show="false">
<CampManagement />
</div>
<!-- 第五行 -->
<div class="dashboard-row" >
<DetailedDataTable
:table-data="detailData"
:level-tree="levelTree"
v-model:selected-person="selectedPerson"
@filter-change="handleFilterChange"
/>
</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 assigneeOptions"
:key="employee.wechat_id"
:value="employee.wechat_id"
>
{{ 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 axios from "axios";
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 "./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 { getOverallCompanyPerformance,getCompanyDepositConversionRate,getCompanyTotalCallCount,getCompanyNewCustomer,getCompanyConversionRate,getCompanyRealTimeProgress
,getCompanyConversionRateVsLast,getSalesMonthlyPerformance,getCustomerTypeDistribution,getUrgentNeedToAddress,getLevelTree,getDetailedDataTable,assignTasks } 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([]);
const employees = ref([
{ id: 1, name: "张三" }
]);
const showTaskModal = ref(false);
const newTask = reactive({
title: "",
assignee: "",
deadline: "",
description: "",
});
// 获取任务列表
const getTaskList = async () => {
try {
const res = await axios.post('http://192.168.15.56:8890/api/v1/level_five/overview/view_tasks', {}, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
console.log(888888,res)
if (res.data.code === 200) {
const apiTasks = res.data.data.tasks || res.data.data
// 将API数据格式转换为TaskList组件期望的格式
tasks.value = apiTasks.map(task => ({
id: task.task_id,
title: task.task_title,
assignee: task.assignee || '未分配',
deadline: task.expiration_date,
status: task.state === '待处理' ? 'pending' : task.state === '正在处理' ? 'in-progress' : 'completed',
description: task.task_content,
created_at: task.created_at
}))
console.log(777777,tasks.value)
/**
* tasks
:
[,…]
0
:
{task_id: "1755748690560728_22d55cc618784537973481228a15956a", task_title: "55", task_content: "222",…}
created_at
:
"2025-08-21 11:58:10"
expiration_date
:
"20250808"
state
:
"待处理"
task_content
:
"222"
task_id
:
"1755748690560728_22d55cc618784537973481228a15956a"
task_title
:
"55"
1
:
{task_id: "1755745331126891_650206e5b6d345699de3e3e406a2600e", task_title: "测试任务",…}
created_at
:
"2025-08-21 11:02:11"
expiration_date
:
"121221"
state
:
"待处理"
task_content
:
"测试任务"
task_id
:
"1755745331126891_650206e5b6d345699de3e3e406a2600e"
task_title
:
"测试任务"
2
:
{task_id: "1755745330094989_528dd87dc13a4a5bb33c9c272fb1a482", task_title: "测试任务",…}
created_at
:
"2025-08-21 11:02:10"
expiration_date
:
"121221"
state
:
"已完成"
task_content
:
"测试任务"
task_id
:
"1755745330094989_528dd87dc13a4a5bb33c9c272fb1a482"
task_title
:
"测试任务"
*/
}
} catch (error) {
console.error('获取任务列表失败:', error)
}
}
// 下拉框人员
const assigneeOptions = ref([]);
async function name() {
try {
console.log('开始获取下属人员列表...');
const res = await axios.get('http://192.168.15.56:8890/api/v1/level_five/overview/get_subordinates',{
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
assigneeOptions.value = res.data.data;
console.log('assigneeOptions设置后:', assigneeOptions.value);
} catch (error) {
console.error('获取下属人员列表失败:', error);
}
}
// 计算属性
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) => {
if (!dateString) return '';
// 处理 "2025-08-21 11:58:10" 格式的时间字符串
try {
const date = new Date(dateString.replace(' ', 'T'));
if (isNaN(date.getTime())) {
return dateString; // 如果解析失败,返回原字符串
}
return date.toLocaleDateString("zh-CN");
} catch (error) {
return dateString;
}
};
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 = async () => {
if (!newTask.title || !newTask.assignee || !newTask.deadline) {
alert("请填写完整信息");
return;
}
try {
// 构造API请求参数
const params = {
task_title: newTask.title,
task_assignee: [newTask.assignee], // 转换为数组格式
expiration_date: newTask.deadline.replace(/-/g, ''), // 移除日期中的横线
task_content: newTask.description || newTask.title
};
// 调用API
const response = await assignTasks(params);
console.log('任务创建成功:', response);
// 创建本地任务对象用于显示
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;
alert('任务创建成功!');
} catch (error) {
console.error('创建任务失败:', error);
alert('创建任务失败,请重试');
}
};
// 核心数据
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' : 'periods';
const stageOrder = ['线索总数', '加微', '到课', '付定金', '成交'];
const comparisonArray = stageOrder.map(stageName => ({
name: stageName,
count: lastData[stageName] || 0
}));
// 同时返回period和month两个键确保组件能找到对应数据
const result = {
periods: comparisonArray,
month: comparisonArray
};
return result;
});
async function getConversionComparison(data) {
const params={
check_type:data //month periods
}
try {
const res = await getCompanyConversionRateVsLast(params)
console.log(111111,res)
conversionComparison.value = res.data
} 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 = (periods) => {
// 根据期间参数调用相应的函数,这里默认调用红榜数据
getCompanySalesRank('red');
};
async function getCompanySalesRank(Rank) {
const params={
rank_type:Rank,
}
try {
const res = await getSalesMonthlyPerformance(params)
companySalesRank.value = res.data
} catch (error) {
console.error("获取销售月度业绩红黑榜失败:", error);
}
}
// 获取全中心业绩排行榜逻辑已移至 RankingList 组件
// 客户类型占比
const customerTypeRatio = ref({});
async function getCustomerTypeRatio(data) {
const params={
distribution_type:data // child_education territory occupation
}
try {
const res = await getCustomerTypeDistribution(params)
customerTypeRatio.value = res.data
} catch (error) {
console.error("获取客户类型占比失败:", error);
}
}
// 客户迫切解决的问题排行榜
const customerUrgency = ref({});
const problemRankingData = ref([]);
async function getCustomerUrgency() {
try {
const res = await getUrgentNeedToAddress()
customerUrgency.value = res.data
// 将API返回的数据转换为ProblemRanking组件需要的格式
if (res.data && res.data.company_urgent_issue_ratio) {
problemRankingData.value = Object.entries(res.data.company_urgent_issue_ratio).map(([name, value]) => ({
name,
value
}));
}
} catch (error) {
console.error("获取客户迫切解决的问题排行榜失败:", error);
}
}
// 获取级别树
const levelTree = ref({});
async function CusotomGetLevelTree() {
try {
const res = await getLevelTree()
levelTree.value = res.data
} catch (error) {
console.error("获取级别树失败:", error);
}
}
// 获取详细数据表格
const detailData = ref({});
async function getDetailData(params) {
if(params?.center_leader){
try {
const res = await getDetailedDataTable(params)
detailData.value = res.data
} catch (error) {
console.error("获取详细数据表格失败:", error);
}
}else{
try {
const res = await getDetailedDataTable()
detailData.value = res.data
} catch (error) {
console.error("获取详细数据表格失败:", error);
}
}
}
// 处理筛选器变化
const handleFilterChange = (filterParams) => {
console.log('筛选器变化:', filterParams)
getDetailData(filterParams)
}
onMounted(async() => {
// 页面初始化逻辑
await getRealTimeProgress()
await getTotalDeals()
await getTaskList()
await getConversionComparison('month')
await getCompanySalesRank('red')
await getCustomerTypeRatio('child_education')
await getCustomerUrgency()
await CusotomGetLevelTree()
await getDetailData()
await name() // 获取下属人员列表
});
</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-periods {
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;
}
.periods-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>