You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
590 lines
25 KiB
590 lines
25 KiB
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{$pageTitle}</title>
|
|
<link rel="stylesheet" href="/static/css/dashboard.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- 页面头部 -->
|
|
<div class="header">
|
|
<h1><i class="fas fa-chart-line"></i> {$pageTitle}</h1>
|
|
<div class="user-info">
|
|
<span><i class="fas fa-user"></i> 欢迎,{$userInfo.name}</span>
|
|
<span class="role-badge">
|
|
<i class="fas fa-briefcase"></i>
|
|
{{ userInfo.role === 'market_staff' ? '市场人员' : '市场经理' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 数据筛选器 -->
|
|
<div class="filter-container">
|
|
<div class="filter-item">
|
|
<label>时间范围:</label>
|
|
<select v-model="timeRange" @change="updateData">
|
|
<option value="current_month">本月</option>
|
|
<option value="last_month">上月</option>
|
|
<option value="current_quarter">本季度</option>
|
|
<option value="current_year">本年</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-item">
|
|
<label>数据类型:</label>
|
|
<select v-model="dataType" @change="updateData">
|
|
<option value="all">全部数据</option>
|
|
<option value="performance">业绩数据</option>
|
|
<option value="resource">资源数据</option>
|
|
<option value="income">收益数据</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-item">
|
|
<button @click="exportData" class="btn-export">
|
|
<i class="fas fa-download"></i> 导出数据
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 统计卡片 -->
|
|
<div class="stats-container">
|
|
<div v-for="stat in pageData.stats" :key="stat.label" class="stat-card">
|
|
<div class="stat-icon">
|
|
<i :class="getIconClass(stat.label)"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-value">{{ formatValue(stat.value) }}{{ stat.unit }}</div>
|
|
<div class="stat-label">{{ stat.label }}</div>
|
|
<div class="stat-trend" :class="getTrendClass(stat.trend)">
|
|
<i :class="getTrendIcon(stat.trend)"></i>
|
|
{{ stat.trend }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 资源分析 -->
|
|
<div v-if="pageData.resource_analysis" class="section">
|
|
<h2><i class="fas fa-users"></i> 资源分析</h2>
|
|
<div class="charts-container">
|
|
<div class="chart-card">
|
|
<h3><i class="fas fa-chart-pie"></i> 渠道分布</h3>
|
|
<canvas id="channelChart" width="400" height="300"></canvas>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h3><i class="fas fa-bullseye"></i> 来源分布</h3>
|
|
<canvas id="sourceChart" width="400" height="300"></canvas>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h3><i class="fas fa-filter"></i> 转化漏斗</h3>
|
|
<canvas id="funnelChart" width="400" height="300"></canvas>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h3><i class="fas fa-chart-line"></i> 月度趋势</h3>
|
|
<canvas id="trendChart" width="400" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 收益分析 -->
|
|
<div v-if="pageData.income_analysis" class="section">
|
|
<h2><i class="fas fa-dollar-sign"></i> 收益分析</h2>
|
|
<div class="charts-container">
|
|
<div class="chart-card">
|
|
<h3><i class="fas fa-chart-bar"></i> 提成明细</h3>
|
|
<canvas id="commissionChart" width="400" height="300"></canvas>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h3><i class="fas fa-gift"></i> 奖励历史</h3>
|
|
<canvas id="bonusChart" width="400" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 团队数据(经理权限) -->
|
|
<div v-if="pageData.team_overview" class="section">
|
|
<h2><i class="fas fa-users-cog"></i> 团队数据</h2>
|
|
<div class="team-overview">
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-users"></i> 团队成员</h3>
|
|
<div class="overview-value">{{ pageData.team_overview.total_members }}</div>
|
|
<div class="overview-desc">活跃成员</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-user-plus"></i> 团队资源</h3>
|
|
<div class="overview-value">{{ pageData.team_overview.total_resources }}</div>
|
|
<div class="overview-desc">本月新增</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-user-check"></i> 团队成交</h3>
|
|
<div class="overview-value">{{ pageData.team_overview.total_converted }}</div>
|
|
<div class="overview-desc">本月成交</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-chart-line"></i> 团队业绩</h3>
|
|
<div class="overview-value">{{ formatValue(pageData.team_overview.total_performance) }}</div>
|
|
<div class="overview-desc">本月业绩</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ranking-section">
|
|
<h3><i class="fas fa-trophy"></i> 团队成员排名</h3>
|
|
<div class="ranking-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>排名</th>
|
|
<th>姓名</th>
|
|
<th>资源数</th>
|
|
<th>成交数</th>
|
|
<th>业绩</th>
|
|
<th>提成</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(member, index) in pageData.member_ranking" :key="member.staff_id">
|
|
<td>
|
|
<span class="rank-badge" :class="getRankClass(index)">
|
|
{{ index + 1 }}
|
|
</span>
|
|
</td>
|
|
<td>{{ member.staff_name }}</td>
|
|
<td>{{ member.resource_count }}</td>
|
|
<td>{{ member.converted_count }}</td>
|
|
<td>{{ formatValue(member.performance) }}</td>
|
|
<td>{{ formatValue(member.commission) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 部门数据 -->
|
|
<div v-if="pageData.dept_overview" class="section">
|
|
<h2><i class="fas fa-building"></i> 部门数据</h2>
|
|
<div class="team-overview">
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-sitemap"></i> 部门总数</h3>
|
|
<div class="overview-value">{{ pageData.dept_overview.total_depts }}</div>
|
|
<div class="overview-desc">活跃部门</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-users"></i> 部门资源</h3>
|
|
<div class="overview-value">{{ pageData.dept_overview.total_resources }}</div>
|
|
<div class="overview-desc">本月新增</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-chart-line"></i> 部门业绩</h3>
|
|
<div class="overview-value">{{ formatValue(pageData.dept_overview.total_performance) }}</div>
|
|
<div class="overview-desc">本月业绩</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<h3><i class="fas fa-percentage"></i> 转化率</h3>
|
|
<div class="overview-value">{{ pageData.dept_overview.conversion_rate }}%</div>
|
|
<div class="overview-desc">平均转化</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ranking-section">
|
|
<h3><i class="fas fa-chart-bar"></i> 部门业绩排名</h3>
|
|
<div class="ranking-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>排名</th>
|
|
<th>部门名称</th>
|
|
<th>业绩</th>
|
|
<th>占比</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(dept, index) in pageData.dept_ranking" :key="dept.dept_id">
|
|
<td>
|
|
<span class="rank-badge" :class="getRankClass(index)">
|
|
{{ index + 1 }}
|
|
</span>
|
|
</td>
|
|
<td>{{ dept.dept_name }}</td>
|
|
<td>{{ formatValue(dept.performance) }}</td>
|
|
<td>{{ getPercentage(dept.performance, pageData.dept_overview.total_performance) }}%</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 加载提示 -->
|
|
<div v-if="loading" class="loading-overlay">
|
|
<div class="loading-spinner">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<p>数据加载中...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp } = Vue;
|
|
|
|
createApp({
|
|
data() {
|
|
return {
|
|
pageData: <?= json_encode($pageData) ?>,
|
|
userInfo: <?= json_encode($userInfo) ?>,
|
|
loading: false,
|
|
timeRange: 'current_month',
|
|
dataType: 'all'
|
|
};
|
|
},
|
|
mounted() {
|
|
this.initCharts();
|
|
this.initEventListeners();
|
|
},
|
|
methods: {
|
|
formatValue(value) {
|
|
if (typeof value === 'number') {
|
|
return value.toLocaleString();
|
|
}
|
|
return value;
|
|
},
|
|
getIconClass(label) {
|
|
const iconMap = {
|
|
'本月新增资源': 'fas fa-user-plus',
|
|
'本月成交客户': 'fas fa-user-check',
|
|
'本月业绩': 'fas fa-dollar-sign',
|
|
'本月提成': 'fas fa-gift'
|
|
};
|
|
return iconMap[label] || 'fas fa-chart-bar';
|
|
},
|
|
getTrendClass(trend) {
|
|
return trend.startsWith('+') ? 'trend-up' : 'trend-down';
|
|
},
|
|
getTrendIcon(trend) {
|
|
return trend.startsWith('+') ? 'fas fa-arrow-up' : 'fas fa-arrow-down';
|
|
},
|
|
getRankClass(index) {
|
|
const classes = ['rank-first', 'rank-second', 'rank-third'];
|
|
return classes[index] || 'rank-other';
|
|
},
|
|
getPercentage(value, total) {
|
|
if (total === 0) return 0;
|
|
return Math.round((value / total) * 100);
|
|
},
|
|
updateData() {
|
|
this.loading = true;
|
|
// 模拟数据更新
|
|
setTimeout(() => {
|
|
this.loading = false;
|
|
this.showNotification('数据已更新', 'success');
|
|
}, 1000);
|
|
},
|
|
exportData() {
|
|
this.showNotification('数据导出功能开发中...', 'info');
|
|
},
|
|
showNotification(message, type = 'info') {
|
|
// 创建通知元素
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification notification-${type}`;
|
|
notification.innerHTML = `
|
|
<i class="fas fa-${type === 'success' ? 'check-circle' : 'info-circle'}"></i>
|
|
${message}
|
|
`;
|
|
document.body.appendChild(notification);
|
|
|
|
// 3秒后移除
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 3000);
|
|
},
|
|
initEventListeners() {
|
|
// 响应式处理
|
|
window.addEventListener('resize', () => {
|
|
this.resizeCharts();
|
|
});
|
|
},
|
|
initCharts() {
|
|
// 渠道分布图
|
|
if (this.pageData.resource_analysis && this.pageData.resource_analysis.channel_distribution) {
|
|
this.createPieChart('channelChart', this.pageData.resource_analysis.channel_distribution);
|
|
}
|
|
|
|
// 来源分布图
|
|
if (this.pageData.resource_analysis && this.pageData.resource_analysis.source_distribution) {
|
|
this.createPieChart('sourceChart', this.pageData.resource_analysis.source_distribution);
|
|
}
|
|
|
|
// 转化漏斗图
|
|
if (this.pageData.resource_analysis && this.pageData.resource_analysis.conversion_funnel) {
|
|
this.createFunnelChart('funnelChart', this.pageData.resource_analysis.conversion_funnel);
|
|
}
|
|
|
|
// 月度趋势图
|
|
if (this.pageData.resource_analysis && this.pageData.resource_analysis.monthly_trend) {
|
|
this.createLineChart('trendChart', this.pageData.resource_analysis.monthly_trend);
|
|
}
|
|
|
|
// 提成明细图
|
|
if (this.pageData.income_analysis && this.pageData.income_analysis.commission_breakdown) {
|
|
this.createBarChart('commissionChart', this.pageData.income_analysis.commission_breakdown);
|
|
}
|
|
|
|
// 奖励历史图
|
|
if (this.pageData.income_analysis && this.pageData.income_analysis.bonus_history) {
|
|
this.createBarChart('bonusChart', this.pageData.income_analysis.bonus_history);
|
|
}
|
|
},
|
|
createPieChart(canvasId, data) {
|
|
const ctx = document.getElementById(canvasId);
|
|
if (!ctx) return;
|
|
|
|
new Chart(ctx, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: data.map(item => item.name),
|
|
datasets: [{
|
|
data: data.map(item => item.value),
|
|
backgroundColor: [
|
|
'#FF6384',
|
|
'#36A2EB',
|
|
'#FFCE56',
|
|
'#4BC0C0',
|
|
'#9966FF',
|
|
'#FF9F40'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
padding: 20,
|
|
usePointStyle: true
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.label || '';
|
|
const value = context.parsed;
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(1);
|
|
return `${label}: ${value} (${percentage}%)`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
createFunnelChart(canvasId, data) {
|
|
const ctx = document.getElementById(canvasId);
|
|
if (!ctx) return;
|
|
|
|
new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map(item => item.stage),
|
|
datasets: [{
|
|
label: '转化率',
|
|
data: data.map(item => item.rate),
|
|
backgroundColor: '#36A2EB',
|
|
borderColor: '#1E88E5',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 100,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value + '%';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return `转化率: ${context.parsed.y}%`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
createLineChart(canvasId, data) {
|
|
const ctx = document.getElementById(canvasId);
|
|
if (!ctx) return;
|
|
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map(item => item.month),
|
|
datasets: [{
|
|
label: '资源数量',
|
|
data: data.map(item => item.count),
|
|
borderColor: '#36A2EB',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
stepSize: 1
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
createBarChart(canvasId, data) {
|
|
const ctx = document.getElementById(canvasId);
|
|
if (!ctx) return;
|
|
|
|
new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map(item => item.type || item.month),
|
|
datasets: [{
|
|
label: '金额',
|
|
data: data.map(item => item.amount),
|
|
backgroundColor: '#36A2EB',
|
|
borderColor: '#1E88E5',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return '¥' + value.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return '金额: ¥' + context.parsed.y.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
resizeCharts() {
|
|
// 重新调整图表大小
|
|
Chart.instances.forEach(chart => {
|
|
chart.resize();
|
|
});
|
|
}
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
|
|
<style>
|
|
/* 通知样式 */
|
|
.notification {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
color: white;
|
|
font-weight: 500;
|
|
z-index: 1000;
|
|
animation: slideIn 0.3s ease-out;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.notification-success {
|
|
background: linear-gradient(135deg, #10b981, #059669);
|
|
}
|
|
|
|
.notification-info {
|
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
|
}
|
|
|
|
.notification i {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(100%);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* 加载动画 */
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.loading-spinner {
|
|
text-align: center;
|
|
color: white;
|
|
}
|
|
|
|
.loading-spinner i {
|
|
font-size: 48px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.loading-spinner p {
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|
|
</body>
|
|
</html>
|