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.
599 lines
19 KiB
599 lines
19 KiB
<?php
|
|
|
|
namespace app\api\controller;
|
|
|
|
use app\service\api\member\MemberService;
|
|
use core\base\BaseApiController;
|
|
|
|
/**
|
|
* Dashboard WebView 控制器
|
|
*/
|
|
class Dashboard extends BaseApiController
|
|
{
|
|
/**
|
|
* WebView 页面渲染
|
|
*/
|
|
public function webview()
|
|
{
|
|
$type = $this->request->get('type', 'my_data'); // 页面类型
|
|
$token = $this->request->get('token', ''); // 用户token
|
|
$platform = $this->request->get('platform', 'web'); // 平台标识
|
|
|
|
// 验证token和获取用户信息
|
|
if (empty($token)) {
|
|
return $this->renderErrorPage('缺少用户认证信息');
|
|
}
|
|
|
|
try {
|
|
// 这里应该验证token,暂时跳过验证用于测试
|
|
$userInfo = $this->getMockUserInfo($type);
|
|
|
|
// 根据页面类型渲染不同内容
|
|
$htmlContent = $this->renderDashboardPage($type, $userInfo, $platform);
|
|
|
|
// 输出HTML内容
|
|
return response($htmlContent)->header([
|
|
'Content-Type' => 'text/html; charset=utf-8',
|
|
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
|
'Pragma' => 'no-cache',
|
|
'Expires' => '0'
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
return $this->renderErrorPage('页面加载失败: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 渲染Dashboard页面
|
|
*/
|
|
private function renderDashboardPage($type, $userInfo, $platform)
|
|
{
|
|
// 获取页面数据
|
|
$pageData = $this->getPageData($type, $userInfo);
|
|
|
|
// 页面标题映射
|
|
$titleMap = [
|
|
'my_data' => '我的数据',
|
|
'dept_data' => '部门数据',
|
|
'campus_data' => '校区数据'
|
|
];
|
|
|
|
$pageTitle = $titleMap[$type] ?? '数据统计';
|
|
|
|
// 生成HTML内容
|
|
return $this->generateHTML($pageTitle, $pageData, $platform);
|
|
}
|
|
|
|
/**
|
|
* 获取页面数据
|
|
*/
|
|
private function getPageData($type, $userInfo)
|
|
{
|
|
switch ($type) {
|
|
case 'my_data':
|
|
return $this->getMyData($userInfo);
|
|
case 'dept_data':
|
|
return $this->getDeptData($userInfo);
|
|
case 'campus_data':
|
|
return $this->getCampusData($userInfo);
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取我的数据
|
|
*/
|
|
private function getMyData($userInfo)
|
|
{
|
|
return [
|
|
'stats' => [
|
|
['label' => '本月签约客户', 'value' => 12, 'unit' => '人', 'trend' => '+15%'],
|
|
['label' => '本月完成业绩', 'value' => 85000, 'unit' => '元', 'trend' => '+8%'],
|
|
['label' => '跟进客户数', 'value' => 45, 'unit' => '人', 'trend' => '+5%'],
|
|
['label' => '转化率', 'value' => 26.7, 'unit' => '%', 'trend' => '+2.3%']
|
|
],
|
|
'charts' => [
|
|
'monthly_trend' => [
|
|
'title' => '月度业绩趋势',
|
|
'data' => [65000, 72000, 68000, 75000, 82000, 85000]
|
|
],
|
|
'customer_source' => [
|
|
'title' => '客户来源分布',
|
|
'data' => [
|
|
['name' => '线上推广', 'value' => 35],
|
|
['name' => '转介绍', 'value' => 28],
|
|
['name' => '电话营销', 'value' => 22],
|
|
['name' => '其他', 'value' => 15]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取部门数据
|
|
*/
|
|
private function getDeptData($userInfo)
|
|
{
|
|
return [
|
|
'stats' => [
|
|
['label' => '部门总业绩', 'value' => 520000, 'unit' => '元', 'trend' => '+12%'],
|
|
['label' => '团队人数', 'value' => 8, 'unit' => '人', 'trend' => '0%'],
|
|
['label' => '平均业绩', 'value' => 65000, 'unit' => '元', 'trend' => '+12%'],
|
|
['label' => '部门排名', 'value' => 2, 'unit' => '名', 'trend' => '+1']
|
|
],
|
|
'charts' => [
|
|
'team_performance' => [
|
|
'title' => '团队成员业绩排行',
|
|
'data' => [
|
|
['name' => '张三', 'value' => 85000],
|
|
['name' => '李四', 'value' => 72000],
|
|
['name' => '王五', 'value' => 68000],
|
|
['name' => '赵六', 'value' => 65000]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取校区数据
|
|
*/
|
|
private function getCampusData($userInfo)
|
|
{
|
|
return [
|
|
'stats' => [
|
|
['label' => '校区总业绩', 'value' => 1200000, 'unit' => '元', 'trend' => '+18%'],
|
|
['label' => '部门数量', 'value' => 5, 'unit' => '个', 'trend' => '0%'],
|
|
['label' => '员工总数', 'value' => 32, 'unit' => '人', 'trend' => '+3'],
|
|
['label' => '客户总数', 'value' => 245, 'unit' => '人', 'trend' => '+25']
|
|
],
|
|
'charts' => [
|
|
'dept_performance' => [
|
|
'title' => '部门业绩对比',
|
|
'data' => [
|
|
['name' => '销售一部', 'value' => 320000],
|
|
['name' => '销售二部', 'value' => 280000],
|
|
['name' => '销售三部', 'value' => 260000],
|
|
['name' => '客服部', 'value' => 180000],
|
|
['name' => '行政部', 'value' => 160000]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取模拟用户信息
|
|
*/
|
|
private function getMockUserInfo($type)
|
|
{
|
|
return [
|
|
'id' => 1,
|
|
'name' => '测试员工',
|
|
'department' => '销售部',
|
|
'campus' => '总校区',
|
|
'role' => 'staff'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 生成HTML内容
|
|
*/
|
|
private function generateHTML($title, $data, $platform)
|
|
{
|
|
$statsHtml = '';
|
|
if (!empty($data['stats'])) {
|
|
foreach ($data['stats'] as $stat) {
|
|
$trendColor = strpos($stat['trend'], '+') === 0 ? '#4CAF50' : '#FF5722';
|
|
$statsHtml .= "
|
|
<div class='stat-card'>
|
|
<div class='stat-label'>{$stat['label']}</div>
|
|
<div class='stat-value'>{$stat['value']}<span class='stat-unit'>{$stat['unit']}</span></div>
|
|
<div class='stat-trend' style='color: {$trendColor}'>{$stat['trend']}</div>
|
|
</div>
|
|
";
|
|
}
|
|
}
|
|
|
|
// 生成图表数据的JavaScript
|
|
$chartsScript = '';
|
|
if (!empty($data['charts'])) {
|
|
foreach ($data['charts'] as $chartId => $chart) {
|
|
$chartData = json_encode($chart['data']);
|
|
$chartsScript .= "
|
|
renderChart('{$chartId}', '{$chart['title']}', {$chartData});
|
|
";
|
|
}
|
|
}
|
|
|
|
return "
|
|
<!DOCTYPE html>
|
|
<html lang='zh-CN'>
|
|
<head>
|
|
<meta charset='UTF-8'>
|
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
<title>{$title}</title>
|
|
<script src='https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js'></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background-color: #181A20;
|
|
color: #fff;
|
|
padding: 0;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.container {
|
|
padding: 20px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.page-title {
|
|
text-align: center;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin-bottom: 30px;
|
|
color: #29d3b4;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 15px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: linear-gradient(135deg, #29d3b4 0%, #1a9b7c 100%);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.stat-unit {
|
|
font-size: 12px;
|
|
font-weight: normal;
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.stat-trend {
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.chart-container {
|
|
background-color: #292929;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
margin-bottom: 15px;
|
|
color: #fff;
|
|
}
|
|
|
|
.chart {
|
|
height: 300px;
|
|
width: 100%;
|
|
}
|
|
|
|
.refresh-btn {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
width: 56px;
|
|
height: 56px;
|
|
background: linear-gradient(135deg, #29d3b4 0%, #1a9b7c 100%);
|
|
border-radius: 50%;
|
|
border: none;
|
|
color: #fff;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 12px rgba(41, 211, 180, 0.3);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.refresh-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #29d3b4;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.chart {
|
|
height: 250px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class='container'>
|
|
<h1 class='page-title'>{$title}</h1>
|
|
|
|
<div class='stats-grid'>
|
|
{$statsHtml}
|
|
</div>
|
|
|
|
<div id='charts-container'></div>
|
|
</div>
|
|
|
|
<button class='refresh-btn' onclick='refreshData()'>⟳</button>
|
|
|
|
<script>
|
|
// 图表渲染函数
|
|
function renderChart(chartId, title, data) {
|
|
const container = document.getElementById('charts-container');
|
|
const chartDiv = document.createElement('div');
|
|
chartDiv.className = 'chart-container';
|
|
chartDiv.innerHTML = `
|
|
<div class='chart-title'>\${title}</div>
|
|
<div class='chart' id='\${chartId}'></div>
|
|
`;
|
|
container.appendChild(chartDiv);
|
|
|
|
const chart = echarts.init(document.getElementById(chartId));
|
|
|
|
let option;
|
|
if (Array.isArray(data) && data[0] && typeof data[0] === 'object' && 'name' in data[0]) {
|
|
// 饼图或柱状图
|
|
if (chartId.includes('source') || chartId.includes('distribution')) {
|
|
// 饼图
|
|
option = {
|
|
backgroundColor: 'transparent',
|
|
textStyle: { color: '#fff' },
|
|
tooltip: {
|
|
trigger: 'item',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
legend: {
|
|
orient: 'horizontal',
|
|
bottom: '0%',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
series: [{
|
|
type: 'pie',
|
|
radius: '60%',
|
|
center: ['50%', '45%'],
|
|
data: data,
|
|
itemStyle: {
|
|
borderRadius: 5,
|
|
borderColor: '#181A20',
|
|
borderWidth: 2
|
|
},
|
|
label: {
|
|
color: '#fff'
|
|
}
|
|
}]
|
|
};
|
|
} else {
|
|
// 柱状图
|
|
option = {
|
|
backgroundColor: 'transparent',
|
|
textStyle: { color: '#fff' },
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: data.map(item => item.name),
|
|
axisLabel: { color: '#fff' },
|
|
axisLine: { lineStyle: { color: '#666' } }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLabel: { color: '#fff' },
|
|
axisLine: { lineStyle: { color: '#666' } },
|
|
splitLine: { lineStyle: { color: '#333' } }
|
|
},
|
|
series: [{
|
|
type: 'bar',
|
|
data: data.map(item => item.value),
|
|
itemStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{offset: 0, color: '#29d3b4'},
|
|
{offset: 1, color: '#1a9b7c'}
|
|
]),
|
|
borderRadius: [4, 4, 0, 0]
|
|
}
|
|
}]
|
|
};
|
|
}
|
|
} else {
|
|
// 折线图
|
|
option = {
|
|
backgroundColor: 'transparent',
|
|
textStyle: { color: '#fff' },
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
|
axisLabel: { color: '#fff' },
|
|
axisLine: { lineStyle: { color: '#666' } }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLabel: { color: '#fff' },
|
|
axisLine: { lineStyle: { color: '#666' } },
|
|
splitLine: { lineStyle: { color: '#333' } }
|
|
},
|
|
series: [{
|
|
type: 'line',
|
|
data: data,
|
|
smooth: true,
|
|
itemStyle: { color: '#29d3b4' },
|
|
lineStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
|
{offset: 0, color: '#29d3b4'},
|
|
{offset: 1, color: '#1a9b7c'}
|
|
]),
|
|
width: 3
|
|
},
|
|
areaStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{offset: 0, color: 'rgba(41, 211, 180, 0.3)'},
|
|
{offset: 1, color: 'rgba(41, 211, 180, 0.05)'}
|
|
])
|
|
}
|
|
}]
|
|
};
|
|
}
|
|
|
|
chart.setOption(option);
|
|
|
|
// 响应式
|
|
window.addEventListener('resize', () => {
|
|
chart.resize();
|
|
});
|
|
}
|
|
|
|
// 渲染所有图表
|
|
{$chartsScript}
|
|
|
|
// 刷新数据
|
|
function refreshData() {
|
|
// 发送消息给UniApp
|
|
if (typeof uni !== 'undefined' && uni.postMessage) {
|
|
uni.postMessage({
|
|
data: {
|
|
type: 'refresh'
|
|
}
|
|
});
|
|
} else {
|
|
// 页面刷新
|
|
location.reload();
|
|
}
|
|
}
|
|
|
|
// 页面加载完成后的处理
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('Dashboard页面加载完成');
|
|
|
|
// 通知UniApp页面加载完成
|
|
if (typeof uni !== 'undefined' && uni.postMessage) {
|
|
uni.postMessage({
|
|
data: {
|
|
type: 'loaded'
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>";
|
|
}
|
|
|
|
/**
|
|
* 渲染错误页面
|
|
*/
|
|
private function renderErrorPage($message)
|
|
{
|
|
return response("
|
|
<!DOCTYPE html>
|
|
<html lang='zh-CN'>
|
|
<head>
|
|
<meta charset='UTF-8'>
|
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
<title>页面错误</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background-color: #181A20;
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
padding: 20px;
|
|
}
|
|
.error-container {
|
|
text-align: center;
|
|
max-width: 400px;
|
|
}
|
|
.error-icon {
|
|
font-size: 48px;
|
|
color: #ff5722;
|
|
margin-bottom: 20px;
|
|
}
|
|
.error-title {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
.error-message {
|
|
font-size: 14px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class='error-container'>
|
|
<div class='error-icon'>⚠️</div>
|
|
<div class='error-title'>页面加载失败</div>
|
|
<div class='error-message'>{$message}</div>
|
|
</div>
|
|
</body>
|
|
</html>")->header([
|
|
'Content-Type' => 'text/html; charset=utf-8'
|
|
]);
|
|
}
|
|
}
|