|
@@ -3,6 +3,8 @@ import { RouterModule } from '@angular/router';
|
|
|
import { Subscription } from 'rxjs';
|
|
|
import { signal, Component, OnInit, AfterViewInit, OnDestroy, computed } from '@angular/core';
|
|
|
import { AdminDashboardService } from './dashboard.service';
|
|
|
+import { FmodeQuery, FmodeObject, FmodeUser } from 'fmode-ng/core';
|
|
|
+import { WxworkAuth } from 'fmode-ng/core';
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
|
@Component({
|
|
@@ -121,11 +123,45 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
private projectChart: any | null = null;
|
|
|
private revenueChart: any | null = null;
|
|
|
private detailChart: any | null = null;
|
|
|
+ private wxAuth: WxworkAuth;
|
|
|
+ private currentUser: FmodeUser | null = null;
|
|
|
|
|
|
- constructor(private dashboardService: AdminDashboardService) {}
|
|
|
+ constructor(private dashboardService: AdminDashboardService) {
|
|
|
+ this.initAuth();
|
|
|
+ }
|
|
|
|
|
|
- ngOnInit(): void {
|
|
|
- this.loadDashboardData();
|
|
|
+ async ngOnInit(): Promise<void> {
|
|
|
+ await this.authenticateAndLoadData();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化企业微信认证
|
|
|
+ private initAuth(): void {
|
|
|
+ try {
|
|
|
+ this.wxAuth = new WxworkAuth({
|
|
|
+ cid: 'cDL6R1hgSi' // 公司帐套ID
|
|
|
+ });
|
|
|
+ console.log('✅ 管理员仪表板企业微信认证初始化成功');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 管理员仪表板企业微信认证初始化失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 认证并加载数据
|
|
|
+ private async authenticateAndLoadData(): Promise<void> {
|
|
|
+ try {
|
|
|
+ // 执行企业微信认证和登录
|
|
|
+ const { user } = await this.wxAuth.authenticateAndLogin();
|
|
|
+ this.currentUser = user;
|
|
|
+
|
|
|
+ if (user) {
|
|
|
+ console.log('✅ 管理员登录成功:', user.get('username'));
|
|
|
+ this.loadDashboardData();
|
|
|
+ } else {
|
|
|
+ console.error('❌ 管理员登录失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 管理员认证过程出错:', error);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
ngAfterViewInit(): void {
|
|
@@ -145,8 +181,99 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
if (this.detailChart) { this.detailChart.dispose(); this.detailChart = null; }
|
|
|
}
|
|
|
|
|
|
- loadDashboardData(): void {
|
|
|
- // 加载统计数据
|
|
|
+ async loadDashboardData(): Promise<void> {
|
|
|
+ try {
|
|
|
+ // 加载项目统计数据
|
|
|
+ await this.loadProjectStats();
|
|
|
+
|
|
|
+ // 加载用户统计数据
|
|
|
+ await this.loadUserStats();
|
|
|
+
|
|
|
+ // 加载收入统计数据
|
|
|
+ await this.loadRevenueStats();
|
|
|
+
|
|
|
+ console.log('✅ 管理员仪表板数据加载完成');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 管理员仪表板数据加载失败:', error);
|
|
|
+ // 降级到模拟数据
|
|
|
+ this.loadMockData();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载项目统计数据
|
|
|
+ private async loadProjectStats(): Promise<void> {
|
|
|
+ try {
|
|
|
+ const projectQuery = new FmodeQuery('Project');
|
|
|
+
|
|
|
+ // 总项目数
|
|
|
+ const totalProjects = await projectQuery.count();
|
|
|
+ this.stats.totalProjects.set(totalProjects);
|
|
|
+
|
|
|
+ // 进行中项目数
|
|
|
+ projectQuery.equalTo('status', '进行中');
|
|
|
+ const activeProjects = await projectQuery.count();
|
|
|
+ this.stats.activeProjects.set(activeProjects);
|
|
|
+
|
|
|
+ // 已完成项目数
|
|
|
+ projectQuery.equalTo('status', '已完成');
|
|
|
+ const completedProjects = await projectQuery.count();
|
|
|
+ this.stats.completedProjects.set(completedProjects);
|
|
|
+
|
|
|
+ console.log(`✅ 项目统计: 总计${totalProjects}, 进行中${activeProjects}, 已完成${completedProjects}`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 项目统计加载失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载用户统计数据
|
|
|
+ private async loadUserStats(): Promise<void> {
|
|
|
+ try {
|
|
|
+ // 设计师统计
|
|
|
+ const designerQuery = new FmodeQuery('Profile');
|
|
|
+ designerQuery.equalTo('role', 'designer');
|
|
|
+ const designers = await designerQuery.count();
|
|
|
+ this.stats.totalDesigners.set(designers);
|
|
|
+
|
|
|
+ // 客户统计
|
|
|
+ const customerQuery = new FmodeQuery('Profile');
|
|
|
+ customerQuery.equalTo('role', 'customer');
|
|
|
+ const customers = await customerQuery.count();
|
|
|
+ this.stats.totalCustomers.set(customers);
|
|
|
+
|
|
|
+ console.log(`✅ 用户统计: 设计师${designers}, 客户${customers}`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 用户统计加载失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载收入统计数据
|
|
|
+ private async loadRevenueStats(): Promise<void> {
|
|
|
+ try {
|
|
|
+ // 从订单表计算总收入
|
|
|
+ const orderQuery = new FmodeQuery('Order');
|
|
|
+ orderQuery.equalTo('status', 'paid');
|
|
|
+
|
|
|
+ const orders = await orderQuery.find();
|
|
|
+ let totalRevenue = 0;
|
|
|
+
|
|
|
+ for (const order of orders) {
|
|
|
+ const amount = order.get('amount') || 0;
|
|
|
+ totalRevenue += amount;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.stats.totalRevenue.set(totalRevenue);
|
|
|
+ console.log(`✅ 收入统计: 总收入 ¥${totalRevenue.toLocaleString()}`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 收入统计加载失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 降级到模拟数据
|
|
|
+ private loadMockData(): void {
|
|
|
+ console.warn('⚠️ 使用模拟数据');
|
|
|
this.subscriptions.add(
|
|
|
this.dashboardService.getDashboardStats().subscribe(stats => {
|
|
|
this.stats.totalProjects.set(stats.totalProjects);
|
|
@@ -250,7 +377,7 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
// ====== 详情面板 ======
|
|
|
- showPanel(type: 'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue') {
|
|
|
+ async showPanel(type: 'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue') {
|
|
|
this.detailType.set(type);
|
|
|
// 重置筛选与分页
|
|
|
this.keyword.set('');
|
|
@@ -260,7 +387,7 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
this.pageIndex.set(1);
|
|
|
|
|
|
// 加载本次类型的明细数据
|
|
|
- this.loadDetailData(type);
|
|
|
+ await this.loadDetailData(type);
|
|
|
|
|
|
// 打开抽屉并初始化图表
|
|
|
this.detailOpen.set(true);
|
|
@@ -359,8 +486,114 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
showFinanceDetails(): void { this.showPanel('revenue'); }
|
|
|
|
|
|
// ====== 明细数据:加载、列配置、导出与分页 ======
|
|
|
- private loadDetailData(type: 'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue') {
|
|
|
- // 构造模拟数据(足量便于分页演示)
|
|
|
+ private async loadDetailData(type: 'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue') {
|
|
|
+ try {
|
|
|
+ switch (type) {
|
|
|
+ case 'totalProjects':
|
|
|
+ case 'active':
|
|
|
+ case 'completed':
|
|
|
+ await this.loadProjectDetailData(type);
|
|
|
+ break;
|
|
|
+ case 'designers':
|
|
|
+ await this.loadDesignerDetailData();
|
|
|
+ break;
|
|
|
+ case 'customers':
|
|
|
+ await this.loadCustomerDetailData();
|
|
|
+ break;
|
|
|
+ case 'revenue':
|
|
|
+ await this.loadRevenueDetailData();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 详情数据加载失败:', error);
|
|
|
+ this.loadMockDetailData(type);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载项目详情数据
|
|
|
+ private async loadProjectDetailData(type: 'totalProjects' | 'active' | 'completed'): Promise<void> {
|
|
|
+ const projectQuery = new FmodeQuery('Project');
|
|
|
+
|
|
|
+ if (type === 'active') {
|
|
|
+ projectQuery.equalTo('status', '进行中');
|
|
|
+ } else if (type === 'completed') {
|
|
|
+ projectQuery.equalTo('status', '已完成');
|
|
|
+ }
|
|
|
+
|
|
|
+ const projects = await projectQuery.descending('createdAt').find();
|
|
|
+
|
|
|
+ const detailItems = projects.map((project: FmodeObject) => ({
|
|
|
+ id: project.id,
|
|
|
+ name: project.get('name') || '未命名项目',
|
|
|
+ owner: project.get('owner')?.get('name') || '未分配',
|
|
|
+ status: project.get('status') || '未知',
|
|
|
+ startDate: project.get('startDate') ? new Date(project.get('startDate')).toISOString().slice(0,10) : '',
|
|
|
+ endDate: project.get('endDate') ? new Date(project.get('endDate')).toISOString().slice(0,10) : '',
|
|
|
+ date: project.get('createdAt') ? new Date(project.get('createdAt')).toISOString().slice(0,10) : ''
|
|
|
+ }));
|
|
|
+
|
|
|
+ this.detailData.set(detailItems);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载设计师详情数据
|
|
|
+ private async loadDesignerDetailData(): Promise<void> {
|
|
|
+ const designerQuery = new FmodeQuery('Profile');
|
|
|
+ designerQuery.equalTo('role', 'designer');
|
|
|
+
|
|
|
+ const designers = await designerQuery.descending('createdAt').find();
|
|
|
+
|
|
|
+ const detailItems = designers.map((designer: FmodeObject) => ({
|
|
|
+ id: designer.id,
|
|
|
+ name: designer.get('name') || '未命名',
|
|
|
+ level: designer.get('level') || 'junior',
|
|
|
+ completed: designer.get('completedProjects') || 0,
|
|
|
+ inProgress: designer.get('activeProjects') || 0,
|
|
|
+ avgCycle: designer.get('avgCycle') || 7,
|
|
|
+ date: designer.get('createdAt') ? new Date(designer.get('createdAt')).toISOString().slice(0,10) : ''
|
|
|
+ }));
|
|
|
+
|
|
|
+ this.detailData.set(detailItems);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载客户详情数据
|
|
|
+ private async loadCustomerDetailData(): Promise<void> {
|
|
|
+ const customerQuery = new FmodeQuery('Profile');
|
|
|
+ customerQuery.equalTo('role', 'customer');
|
|
|
+
|
|
|
+ const customers = await customerQuery.descending('createdAt').find();
|
|
|
+
|
|
|
+ const detailItems = customers.map((customer: FmodeObject) => ({
|
|
|
+ id: customer.id,
|
|
|
+ name: customer.get('name') || '未命名',
|
|
|
+ projects: customer.get('projectCount') || 0,
|
|
|
+ lastContact: customer.get('lastContactAt') ? new Date(customer.get('lastContactAt')).toISOString().slice(0,10) : '',
|
|
|
+ status: customer.get('status') || '潜在',
|
|
|
+ date: customer.get('createdAt') ? new Date(customer.get('createdAt')).toISOString().slice(0,10) : ''
|
|
|
+ }));
|
|
|
+
|
|
|
+ this.detailData.set(detailItems);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载收入详情数据
|
|
|
+ private async loadRevenueDetailData(): Promise<void> {
|
|
|
+ const orderQuery = new FmodeQuery('Order');
|
|
|
+ orderQuery.equalTo('status', 'paid');
|
|
|
+
|
|
|
+ const orders = await orderQuery.descending('createdAt').find();
|
|
|
+
|
|
|
+ const detailItems = orders.map((order: FmodeObject) => ({
|
|
|
+ invoiceNo: order.get('invoiceNo') || `INV-${order.id}`,
|
|
|
+ customer: order.get('customer')?.get('name') || '未知客户',
|
|
|
+ amount: order.get('amount') || 0,
|
|
|
+ type: order.get('type') || 'service',
|
|
|
+ date: order.get('createdAt') ? new Date(order.get('createdAt')).toISOString().slice(0,10) : ''
|
|
|
+ }));
|
|
|
+
|
|
|
+ this.detailData.set(detailItems);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 降级到模拟详情数据
|
|
|
+ private loadMockDetailData(type: 'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue'): void {
|
|
|
const now = new Date();
|
|
|
const addDays = (base: Date, days: number) => new Date(base.getTime() + days * 86400000);
|
|
|
|
|
@@ -372,7 +605,8 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
owner: ['张三','李四','王五','赵六'][i % 4],
|
|
|
status: status || (i % 3 === 0 ? '进行中' : (i % 3 === 1 ? '已完成' : '待启动')),
|
|
|
startDate: addDays(now, -60 + i).toISOString().slice(0,10),
|
|
|
- endDate: addDays(now, -30 + i).toISOString().slice(0,10)
|
|
|
+ endDate: addDays(now, -30 + i).toISOString().slice(0,10),
|
|
|
+ date: addDays(now, -i).toISOString().slice(0,10)
|
|
|
}));
|
|
|
this.detailData.set(items);
|
|
|
return;
|