Files
DJKB/my-vue-app/src/views/topOne/topone.vue
lbw_9527443 d4daed2ec1 feat(api): 更新优秀录音文件接口路径并添加缓存系统
refactor(views): 在多个视图组件中实现数据缓存机制

为API接口更新路径并添加全面的缓存系统,包括:
1. 修改优秀录音文件接口路径
2. 实现30分钟有效期的缓存机制
3. 添加缓存管理功能(清除、查看状态)
4. 在topOne、secondTop和seniorManager视图组件中应用缓存
5. 开发环境下暴露缓存管理函数方便调试
2025-08-27 14:04:04 +08:00

2140 lines
40 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" />
<!-- 各中心营期阶段 -->
<period-stage />
</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="excellentRecord"
@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>
</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 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 PeriodStage from "./components/PeriodStage.vue";
import { getOverallCompanyPerformance,getCompanyDepositConversionRate,getCompanyTotalCallCount,getCompanyNewCustomer,getCompanyConversionRate,getCompanyRealTimeProgress
,getCompanyConversionRateVsLast,getSalesMonthlyPerformance,getCustomerTypeDistribution,getUrgentNeedToAddress,getLevelTree,getDetailedDataTable,getExcellentRecordFile } from "@/api/top";
import { useUserStore } from "@/stores/user.js";
// 缓存系统
const cache = new Map();
const CACHE_DURATION = 30 * 60 * 1000; // 30分钟
// 缓存工具函数
const getCacheKey = (functionName, params = {}) => {
return `${functionName}_${JSON.stringify(params)}`;
};
const isValidCache = (cacheItem) => {
return cacheItem && (Date.now() - cacheItem.timestamp) < CACHE_DURATION;
};
const setCache = (key, data) => {
cache.set(key, {
data,
timestamp: Date.now()
});
};
const getCache = (key) => {
const cacheItem = cache.get(key);
if (isValidCache(cacheItem)) {
return cacheItem.data;
}
return null;
};
// 带缓存的API调用包装器
const withCache = async (cacheKey, apiCall) => {
const cachedData = getCache(cacheKey);
if (cachedData) {
console.log(`使用缓存数据: ${cacheKey}`);
return cachedData;
}
try {
const result = await apiCall();
setCache(cacheKey, result);
console.log(`缓存新数据: ${cacheKey}`);
return result;
} catch (error) {
console.error(`API调用失败: ${cacheKey}`, error);
throw error;
}
};
// 清除缓存函数
const clearCache = () => {
cache.clear();
console.log('所有缓存已清除');
};
// 清除特定缓存
const clearSpecificCache = (functionName, params = {}) => {
const cacheKey = getCacheKey(functionName, params);
cache.delete(cacheKey);
console.log(`已清除缓存: ${cacheKey}`);
};
// 获取缓存状态信息
const getCacheInfo = () => {
const cacheEntries = Array.from(cache.entries());
const validEntries = cacheEntries.filter(([key, value]) => isValidCache(value));
const expiredEntries = cacheEntries.filter(([key, value]) => !isValidCache(value));
// 清除过期缓存
expiredEntries.forEach(([key]) => cache.delete(key));
return {
totalCached: validEntries.length,
expiredCleaned: expiredEntries.length,
cacheKeys: validEntries.map(([key]) => key)
};
};
// 强制刷新所有数据(清除缓存并重新获取)
const forceRefreshAllData = async () => {
clearCache();
console.log('开始强制刷新所有数据...');
await getRealTimeProgress();
await getTotalDeals();
await getConversionComparison('month');
await getCompanySalesRank('red');
await getCustomerTypeRatio('child_education');
await getCustomerUrgency();
await CusotomGetLevelTree();
await getDetailData();
await CenterExcellentRecord();
console.log('所有数据刷新完成');
};
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 userStore = useUserStore();
// 计算属性
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 = async () => {
// 强制刷新所有数据
await forceRefreshAllData();
};
// 处理时间范围变化
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 playCall = (callId) => {
console.log("播放通话录音:", callId);
};
const downloadCall = (callId) => {
console.log("下载通话录音:", callId);
};
// 核心数据
const totalDeals = ref({});
// 核心数据--总成交金额
async function getTotalDeals() {
try {
const cacheKey = getCacheKey('getTotalDeals');
const cachedResult = getCache(cacheKey);
if (cachedResult) {
console.log('使用缓存数据: getTotalDeals');
totalDeals.value = cachedResult;
return;
}
const res1 = await getOverallCompanyPerformance()
const res2=await getCompanyDepositConversionRate()
const res3=await getCompanyTotalCallCount()
const res4=await getCompanyNewCustomer()
const res5=await getCompanyConversionRate()
const result = {
totalDeal:res1.data, //总成交单数
DingconversionRate:res2.data, //定金转化率
totalCallCount:res3.data, // 总通话
newCustomer:res4.data, //新客户
conversionRate:res5.data,//转化率
};
totalDeals.value = result;
setCache(cacheKey, result);
console.log('缓存新数据: getTotalDeals');
} catch (error) {
console.error("获取总成交金额失败:", error);
}
}
// 实时进度
const realTimeProgress = ref({});
async function getRealTimeProgress() {
try {
const cacheKey = getCacheKey('getRealTimeProgress');
const result = await withCache(cacheKey, async () => {
const res = await getCompanyRealTimeProgress();
return res.data;
});
realTimeProgress.value = result;
} 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 cacheKey = getCacheKey('getConversionComparison', params);
const result = await withCache(cacheKey, async () => {
const res = await getCompanyConversionRateVsLast(params);
return res.data;
});
console.log(111111,result);
conversionComparison.value = result;
} 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 cacheKey = getCacheKey('getCompanySalesRank', params);
const result = await withCache(cacheKey, async () => {
const res = await getSalesMonthlyPerformance(params);
return res.data;
});
companySalesRank.value = result;
} catch (error) {
console.error("获取销售月度业绩红黑榜失败:", error);
}
}
// 获取全中心业绩排行榜逻辑已移至 RankingList 组件
// 客户类型占比
const customerTypeRatio = ref({});
async function getCustomerTypeRatio(data) {
const params={
distribution_type:data // child_education territory occupation
}
try {
const cacheKey = getCacheKey('getCustomerTypeRatio', params);
const result = await withCache(cacheKey, async () => {
const res = await getCustomerTypeDistribution(params);
return res.data;
});
customerTypeRatio.value = result;
} catch (error) {
console.error("获取客户类型占比失败:", error);
}
}
// 客户迫切解决的问题排行榜
const customerUrgency = ref({});
const problemRankingData = ref([]);
async function getCustomerUrgency() {
try {
const cacheKey = getCacheKey('getCustomerUrgency');
const result = await withCache(cacheKey, async () => {
const res = await getUrgentNeedToAddress();
return res.data;
});
customerUrgency.value = result;
// 将API返回的数据转换为ProblemRanking组件需要的格式
if (result && result.company_urgent_issue_ratio) {
problemRankingData.value = Object.entries(result.company_urgent_issue_ratio).map(([name, value]) => ({
name,
value
}));
}
} catch (error) {
console.error("获取客户迫切解决的问题排行榜失败:", error);
}
}
// 获取级别树
const levelTree = ref({});
async function CusotomGetLevelTree() {
try {
const cacheKey = getCacheKey('CusotomGetLevelTree');
const result = await withCache(cacheKey, async () => {
const res = await getLevelTree();
return res.data;
});
levelTree.value = result;
} catch (error) {
console.error("获取级别树失败:", error);
}
}
// 获取详细数据表格
const detailData = ref({});
async function getDetailData(params) {
try {
const cacheKey = getCacheKey('getDetailData', params || {});
const result = await withCache(cacheKey, async () => {
const res = params?.center_leader
? await getDetailedDataTable(params)
: await getDetailedDataTable();
return res.data;
});
detailData.value = result;
} catch (error) {
console.error("获取详细数据表格失败:", error);
}
}
// 处理筛选器变化
const handleFilterChange = (filterParams) => {
console.log('筛选器变化:', filterParams)
getDetailData(filterParams)
}
// 优秀录音
const excellentRecord = ref({});
async function CenterExcellentRecord() {
const params={
user_level:userStore.userInfo.user_level.toString(),
user_name:userStore.userInfo.username
}
try {
const cacheKey = getCacheKey('CenterExcellentRecord', params);
const result = await withCache(cacheKey, async () => {
const res = await getExcellentRecordFile(params);
return res.data.excellent_record_list;
});
excellentRecord.value = result;
console.log(111111,result);
} catch (error) {
console.error("获取优秀录音失败:", error);
}
}
onMounted(async() => {
// 页面初始化逻辑
console.log('页面初始化,开始加载数据...');
await getRealTimeProgress()
await getTotalDeals()
await getConversionComparison('month')
await getCompanySalesRank('red')
await getCustomerTypeRatio('child_education')
await getCustomerUrgency()
await CusotomGetLevelTree()
await getDetailData()
await CenterExcellentRecord()
// 输出缓存状态信息
const cacheInfo = getCacheInfo();
console.log('数据加载完成,缓存状态:', cacheInfo);
// 在开发环境下暴露缓存管理函数到全局,方便调试
if (import.meta.env.DEV) {
window.dashboardCache = {
clearCache,
clearSpecificCache,
getCacheInfo,
forceRefreshAllData,
cache: cache
};
console.log('开发模式:缓存管理函数已暴露到 window.dashboardCache');
}
});
</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>