7 changed files with 3321 additions and 61 deletions
@ -0,0 +1,202 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace app\api\controller; |
||||
|
|
||||
|
use app\service\api\member\MemberService; |
||||
|
use core\base\BaseApiController; |
||||
|
use think\facade\View; |
||||
|
|
||||
|
/** |
||||
|
* 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] ?? '数据统计'; |
||||
|
|
||||
|
// 使用视图模板渲染页面 |
||||
|
return View::fetch('dashboard/main', [ |
||||
|
'pageTitle' => $pageTitle, |
||||
|
'pageData' => $pageData, |
||||
|
'platform' => $platform, |
||||
|
'userInfo' => $userInfo |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取页面数据 |
||||
|
*/ |
||||
|
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' |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 渲染错误页面 |
||||
|
*/ |
||||
|
private function renderErrorPage($message) |
||||
|
{ |
||||
|
$errorHtml = View::fetch('dashboard/error', [ |
||||
|
'message' => $message |
||||
|
]); |
||||
|
|
||||
|
return response($errorHtml)->header([ |
||||
|
'Content-Type' => 'text/html; charset=utf-8' |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,827 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace app\api\controller; |
||||
|
|
||||
|
use app\service\api\member\MemberService; |
||||
|
use core\base\BaseApiController; |
||||
|
use think\facade\View; |
||||
|
use think\facade\Db; |
||||
|
|
||||
|
/** |
||||
|
* 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->getUserInfo($token); |
||||
|
|
||||
|
// 根据页面类型渲染不同内容 |
||||
|
$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()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取用户信息 |
||||
|
*/ |
||||
|
private function getUserInfo($token) |
||||
|
{ |
||||
|
// TODO: 实现token验证逻辑,这里暂时使用测试数据 |
||||
|
// 根据教务系统角色使用指南,需要支持市场人员和管理者角色 |
||||
|
return [ |
||||
|
'id' => 7, // 麒麟老师,市场人员 |
||||
|
'name' => '麒麟老师', |
||||
|
'role' => 'market_staff', |
||||
|
'campus_id' => 1, |
||||
|
'dept_id' => 1, |
||||
|
'is_manager' => false, |
||||
|
'staff_id' => 7 |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 渲染Dashboard页面 |
||||
|
*/ |
||||
|
private function renderDashboardPage($type, $userInfo, $platform) |
||||
|
{ |
||||
|
// 获取页面数据 |
||||
|
$pageData = $this->getPageData($type, $userInfo); |
||||
|
|
||||
|
// 页面标题映射 |
||||
|
$titleMap = [ |
||||
|
'my_data' => '我的数据', |
||||
|
'team_data' => '团队数据', |
||||
|
'dept_data' => '部门数据', |
||||
|
'campus_data' => '校区数据' |
||||
|
]; |
||||
|
|
||||
|
$pageTitle = $titleMap[$type] ?? '数据统计'; |
||||
|
|
||||
|
// 使用视图模板渲染页面 |
||||
|
return View::fetch('dashboard/main', [ |
||||
|
'pageTitle' => $pageTitle, |
||||
|
'pageData' => $pageData, |
||||
|
'platform' => $platform, |
||||
|
'userInfo' => $userInfo |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取页面数据 |
||||
|
*/ |
||||
|
private function getPageData($type, $userInfo) |
||||
|
{ |
||||
|
switch ($type) { |
||||
|
case 'my_data': |
||||
|
return $this->getMyData($userInfo); |
||||
|
case 'team_data': |
||||
|
return $this->getTeamData($userInfo); |
||||
|
case 'dept_data': |
||||
|
return $this->getDeptData($userInfo); |
||||
|
case 'campus_data': |
||||
|
return $this->getCampusData($userInfo); |
||||
|
default: |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取我的数据(市场人员) |
||||
|
*/ |
||||
|
private function getMyData($userInfo) |
||||
|
{ |
||||
|
$staffId = $userInfo['staff_id']; |
||||
|
$currentMonth = date('Y-m-01'); |
||||
|
|
||||
|
return [ |
||||
|
// 核心统计指标 |
||||
|
'stats' => [ |
||||
|
[ |
||||
|
'label' => '本月新增资源', |
||||
|
'value' => $this->getResourceCount($staffId, $currentMonth), |
||||
|
'unit' => '个', |
||||
|
'trend' => $this->getResourceTrend($staffId) |
||||
|
], |
||||
|
[ |
||||
|
'label' => '本月成交客户', |
||||
|
'value' => $this->getConvertedCount($staffId, $currentMonth), |
||||
|
'unit' => '人', |
||||
|
'trend' => $this->getConversionTrend($staffId) |
||||
|
], |
||||
|
[ |
||||
|
'label' => '本月业绩', |
||||
|
'value' => $this->getPerformance($staffId, $currentMonth), |
||||
|
'unit' => '元', |
||||
|
'trend' => $this->getPerformanceTrend($staffId) |
||||
|
], |
||||
|
[ |
||||
|
'label' => '本月提成', |
||||
|
'value' => $this->getCommission($staffId, $currentMonth), |
||||
|
'unit' => '元', |
||||
|
'trend' => $this->getCommissionTrend($staffId) |
||||
|
] |
||||
|
], |
||||
|
|
||||
|
// 资源分析 |
||||
|
'resource_analysis' => [ |
||||
|
'channel_distribution' => $this->getChannelDistribution($staffId), |
||||
|
'source_distribution' => $this->getSourceDistribution($staffId), |
||||
|
'conversion_funnel' => $this->getConversionFunnel($staffId), |
||||
|
'monthly_trend' => $this->getMonthlyTrend($staffId) |
||||
|
], |
||||
|
|
||||
|
// 收益分析 |
||||
|
'income_analysis' => [ |
||||
|
'commission_breakdown' => $this->getCommissionBreakdown($staffId), |
||||
|
'bonus_history' => $this->getBonusHistory($staffId), |
||||
|
'income_trend' => $this->getIncomeTrend($staffId) |
||||
|
] |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队数据(经理权限) |
||||
|
*/ |
||||
|
private function getTeamData($userInfo) |
||||
|
{ |
||||
|
$campusId = $userInfo['campus_id']; |
||||
|
$deptId = $userInfo['dept_id']; |
||||
|
$currentMonth = date('Y-m-01'); |
||||
|
|
||||
|
return [ |
||||
|
// 团队总览 |
||||
|
'team_overview' => [ |
||||
|
'total_members' => $this->getTeamMemberCount($campusId, $deptId), |
||||
|
'total_resources' => $this->getTeamResourceCount($campusId, $deptId, $currentMonth), |
||||
|
'total_converted' => $this->getTeamConvertedCount($campusId, $deptId, $currentMonth), |
||||
|
'total_performance' => $this->getTeamPerformance($campusId, $deptId, $currentMonth) |
||||
|
], |
||||
|
|
||||
|
// 团队成员排名 |
||||
|
'member_ranking' => $this->getTeamMemberRanking($campusId, $deptId, $currentMonth), |
||||
|
|
||||
|
// 团队业绩分布 |
||||
|
'performance_distribution' => $this->getTeamPerformanceDistribution($campusId, $deptId, $currentMonth) |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取部门数据 |
||||
|
*/ |
||||
|
private function getDeptData($userInfo) |
||||
|
{ |
||||
|
$campusId = $userInfo['campus_id']; |
||||
|
$currentMonth = date('Y-m-01'); |
||||
|
|
||||
|
return [ |
||||
|
// 部门总览 |
||||
|
'dept_overview' => [ |
||||
|
'total_depts' => $this->getDeptCount($campusId), |
||||
|
'total_resources' => $this->getDeptResourceCount($campusId, $currentMonth), |
||||
|
'total_performance' => $this->getDeptPerformance($campusId, $currentMonth), |
||||
|
'conversion_rate' => $this->getDeptConversionRate($campusId, $currentMonth) |
||||
|
], |
||||
|
|
||||
|
// 部门排名 |
||||
|
'dept_ranking' => $this->getDeptRanking($campusId, $currentMonth) |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取校区数据 |
||||
|
*/ |
||||
|
private function getCampusData($userInfo) |
||||
|
{ |
||||
|
$campusId = $userInfo['campus_id']; |
||||
|
$currentMonth = date('Y-m-01'); |
||||
|
|
||||
|
return [ |
||||
|
// 校区总览 |
||||
|
'campus_overview' => [ |
||||
|
'total_performance' => $this->getCampusPerformance($campusId, $currentMonth), |
||||
|
'total_resources' => $this->getCampusResourceCount($campusId, $currentMonth), |
||||
|
'total_converted' => $this->getCampusConvertedCount($campusId, $currentMonth), |
||||
|
'total_staff' => $this->getCampusStaffCount($campusId) |
||||
|
], |
||||
|
|
||||
|
// 校区部门对比 |
||||
|
'dept_comparison' => $this->getCampusDeptComparison($campusId, $currentMonth) |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
// ========== 数据查询方法 ========== |
||||
|
|
||||
|
/** |
||||
|
* 获取资源数量 |
||||
|
*/ |
||||
|
private function getResourceCount($staffId, $startDate) |
||||
|
{ |
||||
|
return Db::table('school_customer_resources') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->where('created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取成交客户数量 |
||||
|
*/ |
||||
|
private function getConvertedCount($staffId, $startDate) |
||||
|
{ |
||||
|
return Db::table('school_customer_resources r') |
||||
|
->join('school_order_table o', 'r.id = o.resource_id') |
||||
|
->where('r.consultant', $staffId) |
||||
|
->where('r.deleted_at', 0) |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->where('r.created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取业绩数据 |
||||
|
*/ |
||||
|
private function getPerformance($staffId, $startDate) |
||||
|
{ |
||||
|
$result = Db::table('school_order_table') |
||||
|
->where('staff_id', $staffId) |
||||
|
->where('order_type', '1') |
||||
|
->where('order_status', 'paid') |
||||
|
->where('payment_time', '>=', $startDate) |
||||
|
->sum('order_amount'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取提成数据 |
||||
|
*/ |
||||
|
private function getCommission($staffId, $startDate) |
||||
|
{ |
||||
|
$result = Db::table('school_performance_records') |
||||
|
->where('staff_id', $staffId) |
||||
|
->whereIn('performance_type', ['sales', 'marketing', 'consultant']) |
||||
|
->where('order_status', 'completed') |
||||
|
->where('created_at', '>=', $startDate) |
||||
|
->sum('performance_value'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取渠道分布 |
||||
|
*/ |
||||
|
private function getChannelDistribution($staffId) |
||||
|
{ |
||||
|
$results = Db::table('school_customer_resources') |
||||
|
->field('source_channel, COUNT(*) as count') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->group('source_channel') |
||||
|
->select(); |
||||
|
|
||||
|
$channelMap = [ |
||||
|
'1' => '线上推广', |
||||
|
'2' => '电话营销', |
||||
|
'3' => '地推活动', |
||||
|
'4' => '转介绍', |
||||
|
'5' => '其他' |
||||
|
]; |
||||
|
|
||||
|
$data = []; |
||||
|
foreach ($results as $row) { |
||||
|
$channelName = $channelMap[$row['source_channel']] ?? '其他'; |
||||
|
$data[] = [ |
||||
|
'name' => $channelName, |
||||
|
'value' => $row['count'] |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return $data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取来源分布 |
||||
|
*/ |
||||
|
private function getSourceDistribution($staffId) |
||||
|
{ |
||||
|
$results = Db::table('school_customer_resources') |
||||
|
->field('source, COUNT(*) as count') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->whereNotNull('source') |
||||
|
->where('source', '<>', '') |
||||
|
->group('source') |
||||
|
->select(); |
||||
|
|
||||
|
$sourceMap = [ |
||||
|
'1' => '官网', |
||||
|
'2' => '微信', |
||||
|
'3' => '抖音', |
||||
|
'4' => '小红书', |
||||
|
'5' => '其他' |
||||
|
]; |
||||
|
|
||||
|
$data = []; |
||||
|
foreach ($results as $row) { |
||||
|
$sourceName = $sourceMap[$row['source']] ?? $row['source']; |
||||
|
$data[] = [ |
||||
|
'name' => $sourceName, |
||||
|
'value' => $row['count'] |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return $data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取转化漏斗 |
||||
|
*/ |
||||
|
private function getConversionFunnel($staffId) |
||||
|
{ |
||||
|
// 总资源数 |
||||
|
$totalResources = Db::table('school_customer_resources') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->count(); |
||||
|
|
||||
|
// 有效联系数(简化处理) |
||||
|
$contactedResources = $totalResources; |
||||
|
|
||||
|
// 意向客户数 |
||||
|
$intentionResources = Db::table('school_customer_resources') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->where('initial_intent', 'high') |
||||
|
->count(); |
||||
|
|
||||
|
// 成交客户数 |
||||
|
$convertedResources = Db::table('school_customer_resources r') |
||||
|
->join('school_order_table o', 'r.id = o.resource_id') |
||||
|
->where('r.consultant', $staffId) |
||||
|
->where('r.deleted_at', 0) |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->count(); |
||||
|
|
||||
|
return [ |
||||
|
['stage' => '新增资源', 'count' => $totalResources, 'rate' => 100], |
||||
|
['stage' => '有效联系', 'count' => $contactedResources, 'rate' => round($contactedResources * 100 / $totalResources, 1)], |
||||
|
['stage' => '意向客户', 'count' => $intentionResources, 'rate' => round($intentionResources * 100 / $totalResources, 1)], |
||||
|
['stage' => '成交客户', 'count' => $convertedResources, 'rate' => round($convertedResources * 100 / $totalResources, 1)] |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取月度趋势 |
||||
|
*/ |
||||
|
private function getMonthlyTrend($staffId) |
||||
|
{ |
||||
|
$results = Db::table('school_customer_resources') |
||||
|
->field("DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count") |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->group('month') |
||||
|
->order('month DESC') |
||||
|
->limit(6) |
||||
|
->select(); |
||||
|
|
||||
|
return array_reverse($results); // 按时间正序 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取提成明细 |
||||
|
*/ |
||||
|
private function getCommissionBreakdown($staffId) |
||||
|
{ |
||||
|
$results = Db::table('school_performance_records') |
||||
|
->field('performance_type, SUM(performance_value) as total_amount, COUNT(*) as count') |
||||
|
->where('staff_id', $staffId) |
||||
|
->whereIn('performance_type', ['sales', 'marketing', 'consultant']) |
||||
|
->where('order_status', 'completed') |
||||
|
->group('performance_type') |
||||
|
->select(); |
||||
|
|
||||
|
$typeMap = [ |
||||
|
'sales' => '销售提成', |
||||
|
'marketing' => '营销提成', |
||||
|
'consultant' => '咨询提成' |
||||
|
]; |
||||
|
|
||||
|
$data = []; |
||||
|
foreach ($results as $row) { |
||||
|
$typeName = $typeMap[$row['performance_type']] ?? $row['performance_type']; |
||||
|
$data[] = [ |
||||
|
'type' => $typeName, |
||||
|
'amount' => $row['total_amount'], |
||||
|
'count' => $row['count'] |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return $data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取奖励历史 |
||||
|
*/ |
||||
|
private function getBonusHistory($staffId) |
||||
|
{ |
||||
|
return Db::table('school_salary') |
||||
|
->field('salary_month, other_subsidies as amount') |
||||
|
->where('staff_id', $staffId) |
||||
|
->where('other_subsidies', '>', 0) |
||||
|
->order('salary_month DESC') |
||||
|
->limit(6) |
||||
|
->select(); |
||||
|
} |
||||
|
|
||||
|
// ========== 团队数据查询方法 ========== |
||||
|
|
||||
|
/** |
||||
|
* 获取团队成员数量 |
||||
|
*/ |
||||
|
private function getTeamMemberCount($campusId, $deptId) |
||||
|
{ |
||||
|
return Db::table('school_personnel p') |
||||
|
->join('school_campus_person_role cpr', 'p.id = cpr.person_id') |
||||
|
->where('cpr.campus_id', $campusId) |
||||
|
->where('cpr.dept_id', $deptId) |
||||
|
->where('p.status', 1) |
||||
|
->where('p.deleted_at', '0') |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队资源数量 |
||||
|
*/ |
||||
|
private function getTeamResourceCount($campusId, $deptId, $startDate) |
||||
|
{ |
||||
|
$members = Db::table('school_personnel p') |
||||
|
->join('school_campus_person_role cpr', 'p.id = cpr.person_id') |
||||
|
->where('cpr.campus_id', $campusId) |
||||
|
->where('cpr.dept_id', $deptId) |
||||
|
->where('p.status', 1) |
||||
|
->where('p.deleted_at', '0') |
||||
|
->column('id'); |
||||
|
|
||||
|
return Db::table('school_customer_resources') |
||||
|
->whereIn('consultant', $members) |
||||
|
->where('deleted_at', 0) |
||||
|
->where('created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队成交数量 |
||||
|
*/ |
||||
|
private function getTeamConvertedCount($campusId, $deptId, $startDate) |
||||
|
{ |
||||
|
$members = Db::table('school_personnel p') |
||||
|
->join('school_campus_person_role cpr', 'p.id = cpr.person_id') |
||||
|
->where('cpr.campus_id', $campusId) |
||||
|
->where('cpr.dept_id', $deptId) |
||||
|
->where('p.status', 1) |
||||
|
->where('p.deleted_at', '0') |
||||
|
->column('id'); |
||||
|
|
||||
|
return Db::table('school_customer_resources r') |
||||
|
->join('school_order_table o', 'r.id = o.resource_id') |
||||
|
->whereIn('r.consultant', $members) |
||||
|
->where('r.deleted_at', 0) |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->where('r.created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队业绩 |
||||
|
*/ |
||||
|
private function getTeamPerformance($campusId, $deptId, $startDate) |
||||
|
{ |
||||
|
$members = Db::table('school_personnel p') |
||||
|
->join('school_campus_person_role cpr', 'p.id = cpr.person_id') |
||||
|
->where('cpr.campus_id', $campusId) |
||||
|
->where('cpr.dept_id', $deptId) |
||||
|
->where('p.status', 1) |
||||
|
->where('p.deleted_at', '0') |
||||
|
->column('id'); |
||||
|
|
||||
|
$result = Db::table('school_order_table') |
||||
|
->whereIn('staff_id', $members) |
||||
|
->where('order_type', '1') |
||||
|
->where('order_status', 'paid') |
||||
|
->where('payment_time', '>=', $startDate) |
||||
|
->sum('order_amount'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队成员排名 |
||||
|
*/ |
||||
|
private function getTeamMemberRanking($campusId, $deptId, $startDate) |
||||
|
{ |
||||
|
// 获取团队成员列表 |
||||
|
$members = Db::table('school_personnel p') |
||||
|
->join('school_campus_person_role cpr', 'p.id = cpr.person_id') |
||||
|
->where('cpr.campus_id', $campusId) |
||||
|
->where('cpr.dept_id', $deptId) |
||||
|
->where('p.status', 1) |
||||
|
->where('p.deleted_at', '0') |
||||
|
->column('id, name'); |
||||
|
|
||||
|
$ranking = []; |
||||
|
foreach ($members as $member) { |
||||
|
$resourceCount = $this->getResourceCount($member['id'], $startDate); |
||||
|
$convertedCount = $this->getConvertedCount($member['id'], $startDate); |
||||
|
$performance = $this->getPerformance($member['id'], $startDate); |
||||
|
$commission = $this->getCommission($member['id'], $startDate); |
||||
|
|
||||
|
$ranking[] = [ |
||||
|
'staff_id' => $member['id'], |
||||
|
'staff_name' => $member['name'], |
||||
|
'resource_count' => $resourceCount, |
||||
|
'converted_count' => $convertedCount, |
||||
|
'performance' => $performance, |
||||
|
'commission' => $commission |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
// 按业绩排序 |
||||
|
usort($ranking, function($a, $b) { |
||||
|
return $b['performance'] <=> $a['performance']; |
||||
|
}); |
||||
|
|
||||
|
return $ranking; |
||||
|
} |
||||
|
|
||||
|
// ========== 部门数据查询方法 ========== |
||||
|
|
||||
|
/** |
||||
|
* 获取部门数量 |
||||
|
*/ |
||||
|
private function getDeptCount($campusId) |
||||
|
{ |
||||
|
return Db::table('school_departments') |
||||
|
->where('deleted_at', 0) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取部门资源数量 |
||||
|
*/ |
||||
|
private function getDeptResourceCount($campusId, $startDate) |
||||
|
{ |
||||
|
return Db::table('school_customer_resources') |
||||
|
->where('campus', $campusId) |
||||
|
->where('deleted_at', 0) |
||||
|
->where('created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取部门业绩 |
||||
|
*/ |
||||
|
private function getDeptPerformance($campusId, $startDate) |
||||
|
{ |
||||
|
$result = Db::table('school_order_table') |
||||
|
->where('campus_id', $campusId) |
||||
|
->where('order_type', '1') |
||||
|
->where('order_status', 'paid') |
||||
|
->where('payment_time', '>=', $startDate) |
||||
|
->sum('order_amount'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取部门转化率 |
||||
|
*/ |
||||
|
private function getDeptConversionRate($campusId, $startDate) |
||||
|
{ |
||||
|
$totalResources = Db::table('school_customer_resources') |
||||
|
->where('campus', $campusId) |
||||
|
->where('deleted_at', 0) |
||||
|
->where('created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
|
||||
|
if ($totalResources == 0) return 0; |
||||
|
|
||||
|
$convertedResources = Db::table('school_customer_resources r') |
||||
|
->join('school_order_table o', 'r.id = o.resource_id') |
||||
|
->where('r.campus', $campusId) |
||||
|
->where('r.deleted_at', 0) |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->where('r.created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
|
||||
|
return round($convertedResources * 100 / $totalResources, 1); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取部门排名 |
||||
|
*/ |
||||
|
private function getDeptRanking($campusId, $startDate) |
||||
|
{ |
||||
|
$depts = Db::table('school_departments') |
||||
|
->where('deleted_at', 0) |
||||
|
->select(); |
||||
|
|
||||
|
$ranking = []; |
||||
|
foreach ($depts as $dept) { |
||||
|
$performance = Db::table('school_order_table') |
||||
|
->where('campus_id', $campusId) |
||||
|
->where('order_type', '1') |
||||
|
->where('order_status', 'paid') |
||||
|
->where('payment_time', '>=', $startDate) |
||||
|
->sum('order_amount'); |
||||
|
|
||||
|
$ranking[] = [ |
||||
|
'dept_id' => $dept['id'], |
||||
|
'dept_name' => $dept['department_name'], |
||||
|
'performance' => $performance ?: 0 |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
// 按业绩排序 |
||||
|
usort($ranking, function($a, $b) { |
||||
|
return $b['performance'] <=> $a['performance']; |
||||
|
}); |
||||
|
|
||||
|
return $ranking; |
||||
|
} |
||||
|
|
||||
|
// ========== 校区数据查询方法 ========== |
||||
|
|
||||
|
/** |
||||
|
* 获取校区业绩 |
||||
|
*/ |
||||
|
private function getCampusPerformance($campusId, $startDate) |
||||
|
{ |
||||
|
$result = Db::table('school_order_table') |
||||
|
->where('campus_id', $campusId) |
||||
|
->where('order_type', '1') |
||||
|
->where('order_status', 'paid') |
||||
|
->where('payment_time', '>=', $startDate) |
||||
|
->sum('order_amount'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取校区资源数量 |
||||
|
*/ |
||||
|
private function getCampusResourceCount($campusId, $startDate) |
||||
|
{ |
||||
|
return Db::table('school_customer_resources') |
||||
|
->where('campus', $campusId) |
||||
|
->where('deleted_at', 0) |
||||
|
->where('created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取校区成交数量 |
||||
|
*/ |
||||
|
private function getCampusConvertedCount($campusId, $startDate) |
||||
|
{ |
||||
|
return Db::table('school_customer_resources r') |
||||
|
->join('school_order_table o', 'r.id = o.resource_id') |
||||
|
->where('r.campus', $campusId) |
||||
|
->where('r.deleted_at', 0) |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->where('r.created_at', '>=', $startDate) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取校区员工数量 |
||||
|
*/ |
||||
|
private function getCampusStaffCount($campusId) |
||||
|
{ |
||||
|
return Db::table('school_personnel p') |
||||
|
->join('school_campus_person_role cpr', 'p.id = cpr.person_id') |
||||
|
->where('cpr.campus_id', $campusId) |
||||
|
->where('p.status', 1) |
||||
|
->where('p.deleted_at', '0') |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取校区部门对比 |
||||
|
*/ |
||||
|
private function getCampusDeptComparison($campusId, $startDate) |
||||
|
{ |
||||
|
$depts = Db::table('school_departments') |
||||
|
->where('deleted_at', 0) |
||||
|
->select(); |
||||
|
|
||||
|
$comparison = []; |
||||
|
foreach ($depts as $dept) { |
||||
|
$performance = Db::table('school_order_table') |
||||
|
->join('school_customer_resources r', 'r.id = o.resource_id') |
||||
|
->where('o.campus_id', $campusId) |
||||
|
->where('r.consultant', '!=', '') |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->where('o.payment_time', '>=', $startDate) |
||||
|
->sum('o.order_amount'); |
||||
|
|
||||
|
$comparison[] = [ |
||||
|
'dept_id' => $dept['id'], |
||||
|
'dept_name' => $dept['department_name'], |
||||
|
'performance' => $performance ?: 0 |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return $comparison; |
||||
|
} |
||||
|
|
||||
|
// ========== 趋势计算方法 ========== |
||||
|
|
||||
|
/** |
||||
|
* 计算资源趋势 |
||||
|
*/ |
||||
|
private function getResourceTrend($staffId) |
||||
|
{ |
||||
|
$currentMonth = date('Y-m-01'); |
||||
|
$lastMonth = date('Y-m-01', strtotime('-1 month')); |
||||
|
|
||||
|
$current = $this->getResourceCount($staffId, $currentMonth); |
||||
|
$last = $this->getResourceCount($staffId, $lastMonth); |
||||
|
|
||||
|
if ($last == 0) return '+0%'; |
||||
|
|
||||
|
$trend = round(($current - $last) * 100 / $last, 1); |
||||
|
return $trend >= 0 ? '+' . $trend . '%' : $trend . '%'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算转化趋势 |
||||
|
*/ |
||||
|
private function getConversionTrend($staffId) |
||||
|
{ |
||||
|
// 简化处理,返回示例数据 |
||||
|
return '+8%'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算业绩趋势 |
||||
|
*/ |
||||
|
private function getPerformanceTrend($staffId) |
||||
|
{ |
||||
|
// 简化处理,返回示例数据 |
||||
|
return '+12%'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算提成趋势 |
||||
|
*/ |
||||
|
private function getCommissionTrend($staffId) |
||||
|
{ |
||||
|
// 简化处理,返回示例数据 |
||||
|
return '+15%'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 渲染错误页面 |
||||
|
*/ |
||||
|
private function renderErrorPage($message) |
||||
|
{ |
||||
|
$errorHtml = View::fetch('dashboard/error', [ |
||||
|
'message' => $message |
||||
|
]); |
||||
|
|
||||
|
return response($errorHtml)->header([ |
||||
|
'Content-Type' => 'text/html; charset=utf-8' |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,423 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace app\api\controller; |
||||
|
|
||||
|
use core\base\BaseApiController; |
||||
|
use think\facade\Db; |
||||
|
|
||||
|
/** |
||||
|
* 市场统计数据API控制器 |
||||
|
*/ |
||||
|
class MarketStats extends BaseApiController |
||||
|
{ |
||||
|
/** |
||||
|
* 获取个人统计数据 |
||||
|
*/ |
||||
|
public function getPersonalStats() |
||||
|
{ |
||||
|
$staffId = $this->request->post('staff_id'); |
||||
|
$startDate = $this->request->post('start_date'); |
||||
|
$endDate = $this->request->post('end_date'); |
||||
|
|
||||
|
if (empty($staffId)) { |
||||
|
return fail('缺少员工ID'); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$stats = [ |
||||
|
'resource_count' => $this->getResourceCount($staffId, $startDate, $endDate), |
||||
|
'converted_count' => $this->getConvertedCount($staffId, $startDate, $endDate), |
||||
|
'performance' => $this->getPerformance($staffId, $startDate, $endDate), |
||||
|
'commission' => $this->getCommission($staffId, $startDate, $endDate), |
||||
|
'bonus' => $this->getBonus($staffId, $startDate, $endDate) |
||||
|
]; |
||||
|
|
||||
|
return success($stats); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队统计数据 |
||||
|
*/ |
||||
|
public function getTeamStats() |
||||
|
{ |
||||
|
$campusId = $this->request->post('campus_id'); |
||||
|
$deptId = $this->request->post('dept_id'); |
||||
|
$startDate = $this->request->post('start_date'); |
||||
|
$endDate = $this->request->post('end_date'); |
||||
|
|
||||
|
if (empty($campusId) || empty($deptId)) { |
||||
|
return fail('缺少校区或部门信息'); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$stats = [ |
||||
|
'team_members' => $this->getTeamMembers($campusId, $deptId), |
||||
|
'team_performance' => $this->getTeamPerformance($campusId, $deptId, $startDate, $endDate), |
||||
|
'ranking' => $this->getTeamRanking($campusId, $deptId, $startDate, $endDate) |
||||
|
]; |
||||
|
|
||||
|
return success($stats); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取渠道分布统计 |
||||
|
*/ |
||||
|
public function getChannelDistribution() |
||||
|
{ |
||||
|
$staffId = $this->request->post('staff_id'); |
||||
|
$startDate = $this->request->post('start_date'); |
||||
|
$endDate = $this->request->post('end_date'); |
||||
|
|
||||
|
try { |
||||
|
$distribution = Db::table('school_customer_resources') |
||||
|
->field('source_channel, COUNT(*) as count') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->group('source_channel') |
||||
|
->select(); |
||||
|
|
||||
|
return success($distribution); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取来源分布统计 |
||||
|
*/ |
||||
|
public function getSourceDistribution() |
||||
|
{ |
||||
|
$staffId = $this->request->post('staff_id'); |
||||
|
$startDate = $this->request->post('start_date'); |
||||
|
$endDate = $this->request->post('end_date'); |
||||
|
|
||||
|
try { |
||||
|
$distribution = Db::table('school_customer_resources') |
||||
|
->field('source, COUNT(*) as count') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->whereNotNull('source') |
||||
|
->where('source', '<>', '') |
||||
|
->group('source') |
||||
|
->select(); |
||||
|
|
||||
|
return success($distribution); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取转化漏斗数据 |
||||
|
*/ |
||||
|
public function getConversionFunnel() |
||||
|
{ |
||||
|
$staffId = $this->request->post('staff_id'); |
||||
|
$startDate = $this->request->post('start_date'); |
||||
|
$endDate = $this->request->post('end_date'); |
||||
|
|
||||
|
try { |
||||
|
// 总资源数 |
||||
|
$totalResources = Db::table('school_customer_resources') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->count(); |
||||
|
|
||||
|
// 成交客户数 |
||||
|
$convertedResources = Db::table('school_customer_resources r') |
||||
|
->join('school_order_table o', 'r.id = o.resource_id') |
||||
|
->where('r.consultant', $staffId) |
||||
|
->where('r.deleted_at', 0) |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('r.created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('r.created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->count(); |
||||
|
|
||||
|
$funnel = [ |
||||
|
'total_resources' => $totalResources, |
||||
|
'converted_resources' => $convertedResources, |
||||
|
'conversion_rate' => $totalResources > 0 ? round($convertedResources * 100 / $totalResources, 1) : 0 |
||||
|
]; |
||||
|
|
||||
|
return success($funnel); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取月度趋势数据 |
||||
|
*/ |
||||
|
public function getMonthlyTrend() |
||||
|
{ |
||||
|
$staffId = $this->request->post('staff_id'); |
||||
|
$months = $this->request->post('months', 6); |
||||
|
|
||||
|
try { |
||||
|
$results = Db::table('school_customer_resources') |
||||
|
->field("DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count") |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->group('month') |
||||
|
->order('month DESC') |
||||
|
->limit($months) |
||||
|
->select(); |
||||
|
|
||||
|
return success(array_reverse($results)); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取提成明细数据 |
||||
|
*/ |
||||
|
public function getCommissionBreakdown() |
||||
|
{ |
||||
|
$staffId = $this->request->post('staff_id'); |
||||
|
$startDate = $this->request->post('start_date'); |
||||
|
$endDate = $this->request->post('end_date'); |
||||
|
|
||||
|
try { |
||||
|
$results = Db::table('school_performance_records') |
||||
|
->field('performance_type, SUM(performance_value) as total_amount, COUNT(*) as count') |
||||
|
->where('staff_id', $staffId) |
||||
|
->whereIn('performance_type', ['sales', 'marketing', 'consultant']) |
||||
|
->where('order_status', 'completed') |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->group('performance_type') |
||||
|
->select(); |
||||
|
|
||||
|
return success($results); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取奖励历史数据 |
||||
|
*/ |
||||
|
public function getBonusHistory() |
||||
|
{ |
||||
|
$staffId = $this->request->post('staff_id'); |
||||
|
$months = $this->request->post('months', 6); |
||||
|
|
||||
|
try { |
||||
|
$results = Db::table('school_salary') |
||||
|
->field('salary_month, other_subsidies as amount') |
||||
|
->where('staff_id', $staffId) |
||||
|
->where('other_subsidies', '>', 0) |
||||
|
->order('salary_month DESC') |
||||
|
->limit($months) |
||||
|
->select(); |
||||
|
|
||||
|
return success($results); |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
return fail($e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ========== 私有查询方法 ========== |
||||
|
|
||||
|
/** |
||||
|
* 获取资源数量 |
||||
|
*/ |
||||
|
private function getResourceCount($staffId, $startDate = null, $endDate = null) |
||||
|
{ |
||||
|
return Db::table('school_customer_resources') |
||||
|
->where('consultant', $staffId) |
||||
|
->where('deleted_at', 0) |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取成交客户数量 |
||||
|
*/ |
||||
|
private function getConvertedCount($staffId, $startDate = null, $endDate = null) |
||||
|
{ |
||||
|
return Db::table('school_customer_resources r') |
||||
|
->join('school_order_table o', 'r.id = o.resource_id') |
||||
|
->where('r.consultant', $staffId) |
||||
|
->where('r.deleted_at', 0) |
||||
|
->where('o.order_type', '1') |
||||
|
->where('o.order_status', 'paid') |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('r.created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('r.created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->count(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取业绩数据 |
||||
|
*/ |
||||
|
private function getPerformance($staffId, $startDate = null, $endDate = null) |
||||
|
{ |
||||
|
$result = Db::table('school_order_table') |
||||
|
->where('staff_id', $staffId) |
||||
|
->where('order_type', '1') |
||||
|
->where('order_status', 'paid') |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('payment_time', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('payment_time', '<=', $endDate); |
||||
|
}) |
||||
|
->sum('order_amount'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取提成数据 |
||||
|
*/ |
||||
|
private function getCommission($staffId, $startDate = null, $endDate = null) |
||||
|
{ |
||||
|
$result = Db::table('school_performance_records') |
||||
|
->where('staff_id', $staffId) |
||||
|
->whereIn('performance_type', ['sales', 'marketing', 'consultant']) |
||||
|
->where('order_status', 'completed') |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('created_at', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('created_at', '<=', $endDate); |
||||
|
}) |
||||
|
->sum('performance_value'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取奖励数据 |
||||
|
*/ |
||||
|
private function getBonus($staffId, $startDate = null, $endDate = null) |
||||
|
{ |
||||
|
$result = Db::table('school_salary') |
||||
|
->where('staff_id', $staffId) |
||||
|
->where('other_subsidies', '>', 0) |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('salary_month', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('salary_month', '<=', $endDate); |
||||
|
}) |
||||
|
->sum('other_subsidies'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队成员 |
||||
|
*/ |
||||
|
private function getTeamMembers($campusId, $deptId) |
||||
|
{ |
||||
|
return Db::table('school_personnel p') |
||||
|
->join('school_campus_person_role cpr', 'p.id = cpr.person_id') |
||||
|
->where('cpr.campus_id', $campusId) |
||||
|
->where('cpr.dept_id', $deptId) |
||||
|
->where('p.status', 1) |
||||
|
->where('p.deleted_at', '0') |
||||
|
->field('p.id, p.name') |
||||
|
->select(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队业绩 |
||||
|
*/ |
||||
|
private function getTeamPerformance($campusId, $deptId, $startDate = null, $endDate = null) |
||||
|
{ |
||||
|
$members = $this->getTeamMembers($campusId, $deptId); |
||||
|
$memberIds = array_column($members, 'id'); |
||||
|
|
||||
|
$result = Db::table('school_order_table') |
||||
|
->whereIn('staff_id', $memberIds) |
||||
|
->where('order_type', '1') |
||||
|
->where('order_status', 'paid') |
||||
|
->when($startDate, function($query) use ($startDate) { |
||||
|
return $query->whereTime('payment_time', '>=', $startDate); |
||||
|
}) |
||||
|
->when($endDate, function($query) use ($endDate) { |
||||
|
return $query->whereTime('payment_time', '<=', $endDate); |
||||
|
}) |
||||
|
->sum('order_amount'); |
||||
|
|
||||
|
return $result ?: 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取团队排名 |
||||
|
*/ |
||||
|
private function getTeamRanking($campusId, $deptId, $startDate = null, $endDate = null) |
||||
|
{ |
||||
|
$members = $this->getTeamMembers($campusId, $deptId); |
||||
|
$memberIds = array_column($members, 'id'); |
||||
|
|
||||
|
$ranking = []; |
||||
|
foreach ($members as $member) { |
||||
|
$performance = $this->getPerformance($member['id'], $startDate, $endDate); |
||||
|
$ranking[] = [ |
||||
|
'staff_id' => $member['id'], |
||||
|
'staff_name' => $member['name'], |
||||
|
'performance' => $performance |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
// 按业绩排序 |
||||
|
usort($ranking, function($a, $b) { |
||||
|
return $b['performance'] <=> $a['performance']; |
||||
|
}); |
||||
|
|
||||
|
return $ranking; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,590 @@ |
|||||
|
<!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> |
||||
@ -0,0 +1,569 @@ |
|||||
|
/* 市场人员业绩管理系统 - Dashboard样式 */ |
||||
|
|
||||
|
* { |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
min-height: 100vh; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
/* 页面头部 */ |
||||
|
.header { |
||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.95) 0%, rgba(118, 75, 162, 0.95) 100%); |
||||
|
color: white; |
||||
|
padding: 20px 30px; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1); |
||||
|
backdrop-filter: blur(10px); |
||||
|
} |
||||
|
|
||||
|
.header h1 { |
||||
|
font-size: 28px; |
||||
|
font-weight: 700; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.header h1 i { |
||||
|
font-size: 32px; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 15px; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.role-badge { |
||||
|
background: rgba(255, 255, 255, 0.2); |
||||
|
padding: 8px 16px; |
||||
|
border-radius: 25px; |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 6px; |
||||
|
border: 1px solid rgba(255, 255, 255, 0.3); |
||||
|
} |
||||
|
|
||||
|
.role-badge i { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
/* 数据筛选器 */ |
||||
|
.filter-container { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
margin: 20px; |
||||
|
padding: 20px; |
||||
|
border-radius: 15px; |
||||
|
display: flex; |
||||
|
gap: 20px; |
||||
|
align-items: center; |
||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
||||
|
backdrop-filter: blur(10px); |
||||
|
} |
||||
|
|
||||
|
.filter-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.filter-item label { |
||||
|
font-weight: 600; |
||||
|
color: #374151; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.filter-item select { |
||||
|
padding: 10px 15px; |
||||
|
border: 2px solid #e5e7eb; |
||||
|
border-radius: 8px; |
||||
|
background: white; |
||||
|
color: #374151; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.2s ease; |
||||
|
} |
||||
|
|
||||
|
.filter-item select:hover { |
||||
|
border-color: #667eea; |
||||
|
} |
||||
|
|
||||
|
.filter-item select:focus { |
||||
|
outline: none; |
||||
|
border-color: #667eea; |
||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); |
||||
|
} |
||||
|
|
||||
|
.btn-export { |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
color: white; |
||||
|
border: none; |
||||
|
padding: 12px 20px; |
||||
|
border-radius: 8px; |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.2s ease; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.btn-export:hover { |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); |
||||
|
} |
||||
|
|
||||
|
/* 统计卡片容器 */ |
||||
|
.stats-container { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
||||
|
gap: 25px; |
||||
|
padding: 0 20px 20px; |
||||
|
} |
||||
|
|
||||
|
/* 统计卡片 */ |
||||
|
.stat-card { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 20px; |
||||
|
padding: 25px; |
||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 20px; |
||||
|
transition: all 0.3s ease; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.stat-card:hover { |
||||
|
transform: translateY(-5px); |
||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
.stat-icon { |
||||
|
width: 70px; |
||||
|
height: 70px; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
color: white; |
||||
|
font-size: 28px; |
||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); |
||||
|
} |
||||
|
|
||||
|
.stat-content { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.stat-value { |
||||
|
font-size: 32px; |
||||
|
font-weight: 700; |
||||
|
color: #1f2937; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
.stat-label { |
||||
|
font-size: 14px; |
||||
|
color: #6b7280; |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.stat-trend { |
||||
|
font-size: 13px; |
||||
|
font-weight: 600; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
} |
||||
|
|
||||
|
.trend-up { |
||||
|
color: #10b981; |
||||
|
} |
||||
|
|
||||
|
.trend-down { |
||||
|
color: #ef4444; |
||||
|
} |
||||
|
|
||||
|
/* 区域容器 */ |
||||
|
.section { |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.section h2 { |
||||
|
font-size: 24px; |
||||
|
font-weight: 700; |
||||
|
color: white; |
||||
|
margin-bottom: 25px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.section h2 i { |
||||
|
font-size: 28px; |
||||
|
} |
||||
|
|
||||
|
/* 图表容器 */ |
||||
|
.charts-container { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); |
||||
|
gap: 25px; |
||||
|
} |
||||
|
|
||||
|
.chart-card { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 20px; |
||||
|
padding: 25px; |
||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.chart-card h3 { |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
color: #374151; |
||||
|
margin-bottom: 20px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.chart-card h3 i { |
||||
|
font-size: 18px; |
||||
|
color: #667eea; |
||||
|
} |
||||
|
|
||||
|
/* 团队总览 */ |
||||
|
.team-overview { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
||||
|
gap: 20px; |
||||
|
margin-bottom: 30px; |
||||
|
} |
||||
|
|
||||
|
.overview-card { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 15px; |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.overview-card:hover { |
||||
|
transform: translateY(-3px); |
||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
.overview-card h3 { |
||||
|
font-size: 14px; |
||||
|
color: #6b7280; |
||||
|
margin-bottom: 10px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.overview-card h3 i { |
||||
|
font-size: 16px; |
||||
|
color: #667eea; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
.overview-value { |
||||
|
font-size: 36px; |
||||
|
font-weight: 700; |
||||
|
color: #1f2937; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
.overview-desc { |
||||
|
font-size: 12px; |
||||
|
color: #9ca3af; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
/* 排名区域 */ |
||||
|
.ranking-section h3 { |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin-bottom: 20px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.ranking-section h3 i { |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
|
||||
|
.ranking-table { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 15px; |
||||
|
overflow: hidden; |
||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.ranking-table table { |
||||
|
width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
} |
||||
|
|
||||
|
.ranking-table th, |
||||
|
.ranking-table td { |
||||
|
padding: 15px 20px; |
||||
|
text-align: left; |
||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
|
||||
|
.ranking-table th { |
||||
|
background: rgba(102, 126, 234, 0.1); |
||||
|
font-weight: 600; |
||||
|
color: #374151; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.ranking-table th i { |
||||
|
margin-right: 5px; |
||||
|
color: #667eea; |
||||
|
} |
||||
|
|
||||
|
.ranking-table td { |
||||
|
font-size: 14px; |
||||
|
color: #374151; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.ranking-table tr:hover { |
||||
|
background: rgba(102, 126, 234, 0.05); |
||||
|
} |
||||
|
|
||||
|
.ranking-table tr:last-child td { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
/* 排名徽章 */ |
||||
|
.rank-badge { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 28px; |
||||
|
height: 28px; |
||||
|
border-radius: 50%; |
||||
|
font-size: 12px; |
||||
|
font-weight: 700; |
||||
|
color: white; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); |
||||
|
} |
||||
|
|
||||
|
.rank-first { |
||||
|
background: linear-gradient(135deg, #ffd700, #ffed4e); |
||||
|
color: #92400e; |
||||
|
} |
||||
|
|
||||
|
.rank-second { |
||||
|
background: linear-gradient(135deg, #c0c0c0, #e5e5e5); |
||||
|
color: #374151; |
||||
|
} |
||||
|
|
||||
|
.rank-third { |
||||
|
background: linear-gradient(135deg, #cd7f32, #e5a25d); |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.rank-other { |
||||
|
background: linear-gradient(135deg, #6b7280, #9ca3af); |
||||
|
} |
||||
|
|
||||
|
/* 响应式设计 */ |
||||
|
@media (max-width: 1200px) { |
||||
|
.stats-container { |
||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
||||
|
} |
||||
|
|
||||
|
.charts-container { |
||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 768px) { |
||||
|
.header { |
||||
|
flex-direction: column; |
||||
|
gap: 15px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.filter-container { |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
gap: 15px; |
||||
|
} |
||||
|
|
||||
|
.stats-container { |
||||
|
grid-template-columns: 1fr; |
||||
|
padding: 0 15px 15px; |
||||
|
} |
||||
|
|
||||
|
.charts-container { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
|
||||
|
.team-overview { |
||||
|
grid-template-columns: repeat(2, 1fr); |
||||
|
} |
||||
|
|
||||
|
.ranking-table { |
||||
|
overflow-x: auto; |
||||
|
} |
||||
|
|
||||
|
.ranking-table table { |
||||
|
min-width: 600px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.header h1 { |
||||
|
font-size: 24px; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.stat-card { |
||||
|
flex-direction: column; |
||||
|
text-align: center; |
||||
|
gap: 15px; |
||||
|
} |
||||
|
|
||||
|
.stat-icon { |
||||
|
width: 60px; |
||||
|
height: 60px; |
||||
|
font-size: 24px; |
||||
|
} |
||||
|
|
||||
|
.team-overview { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
|
||||
|
.overview-value { |
||||
|
font-size: 28px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 动画效果 */ |
||||
|
@keyframes fadeInUp { |
||||
|
from { |
||||
|
opacity: 0; |
||||
|
transform: translateY(30px); |
||||
|
} |
||||
|
to { |
||||
|
opacity: 1; |
||||
|
transform: translateY(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.stat-card, |
||||
|
.chart-card, |
||||
|
.overview-card { |
||||
|
animation: fadeInUp 0.6s ease-out; |
||||
|
} |
||||
|
|
||||
|
.chart-card:nth-child(2) { |
||||
|
animation-delay: 0.1s; |
||||
|
} |
||||
|
|
||||
|
.chart-card:nth-child(3) { |
||||
|
animation-delay: 0.2s; |
||||
|
} |
||||
|
|
||||
|
.chart-card:nth-child(4) { |
||||
|
animation-delay: 0.3s; |
||||
|
} |
||||
|
|
||||
|
/* 滚动条样式 */ |
||||
|
::-webkit-scrollbar { |
||||
|
width: 8px; |
||||
|
} |
||||
|
|
||||
|
::-webkit-scrollbar-track { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
::-webkit-scrollbar-thumb { |
||||
|
background: rgba(255, 255, 255, 0.3); |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
::-webkit-scrollbar-thumb:hover { |
||||
|
background: rgba(255, 255, 255, 0.5); |
||||
|
} |
||||
|
|
||||
|
/* 加载动画 */ |
||||
|
.loading-spinner { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.loading-spinner i { |
||||
|
font-size: 48px; |
||||
|
color: #667eea; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
from { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
to { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 特殊效果 */ |
||||
|
.stat-card::before { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
height: 2px; |
||||
|
background: linear-gradient(90deg, #667eea, #764ba2, #667eea); |
||||
|
border-radius: 20px 20px 0 0; |
||||
|
opacity: 0.7; |
||||
|
} |
||||
|
|
||||
|
.overview-card::before { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
height: 2px; |
||||
|
background: linear-gradient(90deg, #667eea, #764ba2); |
||||
|
border-radius: 15px 15px 0 0; |
||||
|
opacity: 0.5; |
||||
|
} |
||||
Loading…
Reference in new issue