refactor(views): 在多个视图组件中实现数据缓存机制 为API接口更新路径并添加全面的缓存系统,包括: 1. 修改优秀录音文件接口路径 2. 实现30分钟有效期的缓存机制 3. 添加缓存管理功能(清除、查看状态) 4. 在topOne、secondTop和seniorManager视图组件中应用缓存 5. 开发环境下暴露缓存管理函数方便调试
2140 lines
40 KiB
Vue
2140 lines
40 KiB
Vue
<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>
|