|
@@ -1,11 +1,400 @@
|
|
|
-import { Component } from '@angular/core';
|
|
|
+import { Component, OnInit } from '@angular/core';
|
|
|
+import { CommonModule } from '@angular/common';
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
+import { RouterModule } from '@angular/router';
|
|
|
+import { signal } from '@angular/core';
|
|
|
+
|
|
|
+// 报表类型定义
|
|
|
+export type ReportType =
|
|
|
+ | 'financial_summary' // 财务汇总
|
|
|
+ | 'project_profitability' // 项目盈利能力
|
|
|
+ | 'cash_flow' // 现金流
|
|
|
+ | 'expense_analysis'; // 支出分析
|
|
|
+
|
|
|
+// 时间周期类型
|
|
|
+export type TimePeriod =
|
|
|
+ | 'daily' // 日
|
|
|
+ | 'weekly' // 周
|
|
|
+ | 'monthly' // 月
|
|
|
+ | 'quarterly' // 季度
|
|
|
+ | 'yearly'; // 年
|
|
|
+
|
|
|
+// 图表类型
|
|
|
+export type ChartType =
|
|
|
+ | 'bar' // 柱状图
|
|
|
+ | 'line' // 折线图
|
|
|
+ | 'pie' // 饼图
|
|
|
+ | 'table'; // 表格
|
|
|
+
|
|
|
+// 图表数据点类型
|
|
|
+export interface ChartDataPoint {
|
|
|
+ label: string;
|
|
|
+ value: number;
|
|
|
+ color?: string;
|
|
|
+}
|
|
|
+
|
|
|
+// 报表数据类型
|
|
|
+export interface ReportData {
|
|
|
+ id: string;
|
|
|
+ title: string;
|
|
|
+ type: ReportType;
|
|
|
+ period: TimePeriod;
|
|
|
+ dateRange: { start: Date; end: Date };
|
|
|
+ generatedAt: Date;
|
|
|
+ chartType: ChartType;
|
|
|
+ chartData: ChartDataPoint[];
|
|
|
+ summary?: { [key: string]: number };
|
|
|
+ details?: { [key: string]: any }[];
|
|
|
+}
|
|
|
|
|
|
@Component({
|
|
|
selector: 'app-reports',
|
|
|
- imports: [],
|
|
|
+ imports: [CommonModule, FormsModule, RouterModule],
|
|
|
templateUrl: './reports.html',
|
|
|
styleUrl: './reports.scss'
|
|
|
})
|
|
|
-export class Reports {
|
|
|
+export class Reports implements OnInit {
|
|
|
+ // 报表类型选项
|
|
|
+ reportTypes: { value: ReportType; label: string }[] = [
|
|
|
+ { value: 'financial_summary', label: '财务汇总报表' },
|
|
|
+ { value: 'project_profitability', label: '项目盈利能力报表' },
|
|
|
+ { value: 'cash_flow', label: '现金流报表' },
|
|
|
+ { value: 'expense_analysis', label: '支出分析报表' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 时间周期选项
|
|
|
+ timePeriods: { value: TimePeriod; label: string }[] = [
|
|
|
+ { value: 'daily', label: '按日' },
|
|
|
+ { value: 'weekly', label: '按周' },
|
|
|
+ { value: 'monthly', label: '按月' },
|
|
|
+ { value: 'quarterly', label: '按季度' },
|
|
|
+ { value: 'yearly', label: '按年' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 图表类型选项
|
|
|
+ chartTypes: { value: ChartType; label: string }[] = [
|
|
|
+ { value: 'bar', label: '柱状图' },
|
|
|
+ { value: 'line', label: '折线图' },
|
|
|
+ { value: 'pie', label: '饼图' },
|
|
|
+ { value: 'table', label: '表格' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 当前选择的报表类型
|
|
|
+ selectedReportType = signal<ReportType>('financial_summary');
|
|
|
+
|
|
|
+ // 当前选择的时间周期
|
|
|
+ selectedTimePeriod = signal<TimePeriod>('monthly');
|
|
|
+
|
|
|
+ // 日期范围
|
|
|
+ dateRange = signal<{ start: Date; end: Date }>({
|
|
|
+ start: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
|
|
+ end: new Date()
|
|
|
+ });
|
|
|
+
|
|
|
+ // 当前选择的图表类型
|
|
|
+ selectedChartType = signal<ChartType>('bar');
|
|
|
+
|
|
|
+ // 报表数据
|
|
|
+ reportData = signal<ReportData | null>(null);
|
|
|
+
|
|
|
+ // 生成报表时的加载状态
|
|
|
+ isLoading = signal(false);
|
|
|
+
|
|
|
+ // 安全更新日期范围的开始日期
|
|
|
+ updateDateRangeStart(date: string) {
|
|
|
+ this.dateRange.set({
|
|
|
+ ...this.dateRange(),
|
|
|
+ start: new Date(date)
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全更新日期范围的结束日期
|
|
|
+ updateDateRangeEnd(date: string) {
|
|
|
+ this.dateRange.set({
|
|
|
+ ...this.dateRange(),
|
|
|
+ end: new Date(date)
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 历史报表记录
|
|
|
+ historicalReports = signal<ReportData[]>([]);
|
|
|
+
|
|
|
+ constructor() {}
|
|
|
+
|
|
|
+ ngOnInit(): void {
|
|
|
+ // 加载历史报表记录
|
|
|
+ this.loadHistoricalReports();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载历史报表记录
|
|
|
+ loadHistoricalReports(): void {
|
|
|
+ // 模拟加载历史报表数据
|
|
|
+ const mockHistory: ReportData[] = [
|
|
|
+ {
|
|
|
+ id: 'REP-2023-09-001',
|
|
|
+ title: '2023年9月财务汇总报表',
|
|
|
+ type: 'financial_summary',
|
|
|
+ period: 'monthly',
|
|
|
+ dateRange: {
|
|
|
+ start: new Date(2023, 8, 1),
|
|
|
+ end: new Date(2023, 8, 30)
|
|
|
+ },
|
|
|
+ generatedAt: new Date(2023, 8, 30, 15, 30),
|
|
|
+ chartType: 'bar',
|
|
|
+ chartData: [],
|
|
|
+ summary: {
|
|
|
+ totalRevenue: 120000,
|
|
|
+ totalExpense: 85000,
|
|
|
+ netProfit: 35000
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'REP-2023-08-002',
|
|
|
+ title: '2023年第三季度项目盈利能力报表',
|
|
|
+ type: 'project_profitability',
|
|
|
+ period: 'quarterly',
|
|
|
+ dateRange: {
|
|
|
+ start: new Date(2023, 6, 1),
|
|
|
+ end: new Date(2023, 8, 30)
|
|
|
+ },
|
|
|
+ generatedAt: new Date(2023, 8, 30, 16, 45),
|
|
|
+ chartType: 'pie',
|
|
|
+ chartData: [],
|
|
|
+ summary: {
|
|
|
+ averageProfitMargin: 32.5
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.historicalReports.set(mockHistory);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成报表
|
|
|
+ generateReport(): void {
|
|
|
+ this.isLoading.set(true);
|
|
|
+
|
|
|
+ // 模拟API调用延迟
|
|
|
+ setTimeout(() => {
|
|
|
+ const reportId = `REP-${new Date().getFullYear()}-${(new Date().getMonth() + 1).toString().padStart(2, '0')}-${Date.now().toString().slice(-4)}`;
|
|
|
+ const reportTitle = this.getReportTitle();
|
|
|
+ const chartData = this.generateMockChartData();
|
|
|
+ const summaryData = this.generateMockSummary();
|
|
|
+
|
|
|
+ const newReport: ReportData = {
|
|
|
+ id: reportId,
|
|
|
+ title: reportTitle,
|
|
|
+ type: this.selectedReportType(),
|
|
|
+ period: this.selectedTimePeriod(),
|
|
|
+ dateRange: { ...this.dateRange() },
|
|
|
+ generatedAt: new Date(),
|
|
|
+ chartType: this.selectedChartType(),
|
|
|
+ chartData: chartData,
|
|
|
+ summary: summaryData
|
|
|
+ };
|
|
|
+
|
|
|
+ this.reportData.set(newReport);
|
|
|
+
|
|
|
+ // 添加到历史记录
|
|
|
+ const updatedHistory = [newReport, ...this.historicalReports()];
|
|
|
+ this.historicalReports.set(updatedHistory);
|
|
|
+
|
|
|
+ this.isLoading.set(false);
|
|
|
+ }, 1500);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取报表标题
|
|
|
+ getReportTitle(): string {
|
|
|
+ const reportTypeLabel = this.reportTypes.find(type => type.value === this.selectedReportType())?.label || '';
|
|
|
+ const periodLabel = this.timePeriods.find(period => period.value === this.selectedTimePeriod())?.label || '';
|
|
|
+
|
|
|
+ const startDateStr = this.formatDate(this.dateRange().start);
|
|
|
+ const endDateStr = this.formatDate(this.dateRange().end);
|
|
|
+
|
|
|
+ return `${startDateStr}至${endDateStr}${periodLabel}${reportTypeLabel}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成模拟图表数据
|
|
|
+ generateMockChartData(): ChartDataPoint[] {
|
|
|
+ switch (this.selectedReportType()) {
|
|
|
+ case 'financial_summary':
|
|
|
+ return [
|
|
|
+ { label: '设计费用', value: 45000, color: '#3498db' },
|
|
|
+ { label: '项目管理', value: 30000, color: '#2ecc71' },
|
|
|
+ { label: '材料费用', value: 25000, color: '#f39c12' },
|
|
|
+ { label: '设备租赁', value: 15000, color: '#e74c3c' },
|
|
|
+ { label: '其他收入', value: 10000, color: '#9b59b6' }
|
|
|
+ ];
|
|
|
+ case 'project_profitability':
|
|
|
+ return [
|
|
|
+ { label: '现代住宅设计', value: 15000, color: '#3498db' },
|
|
|
+ { label: '商业办公空间', value: 12000, color: '#2ecc71' },
|
|
|
+ { label: '品牌展厅设计', value: 8000, color: '#f39c12' },
|
|
|
+ { label: '餐饮空间改造', value: 7500, color: '#e74c3c' },
|
|
|
+ { label: '酒店大堂设计', value: 6000, color: '#9b59b6' }
|
|
|
+ ];
|
|
|
+ case 'cash_flow':
|
|
|
+ return [
|
|
|
+ { label: '1月', value: 25000, color: '#3498db' },
|
|
|
+ { label: '2月', value: 28000, color: '#2ecc71' },
|
|
|
+ { label: '3月', value: 32000, color: '#f39c12' },
|
|
|
+ { label: '4月', value: 29000, color: '#e74c3c' },
|
|
|
+ { label: '5月', value: 35000, color: '#9b59b6' },
|
|
|
+ { label: '6月', value: 38000, color: '#1abc9c' }
|
|
|
+ ];
|
|
|
+ case 'expense_analysis':
|
|
|
+ return [
|
|
|
+ { label: '人力成本', value: 40000, color: '#3498db' },
|
|
|
+ { label: '软件工具', value: 15000, color: '#2ecc71' },
|
|
|
+ { label: '办公费用', value: 10000, color: '#f39c12' },
|
|
|
+ { label: '差旅费用', value: 8000, color: '#e74c3c' },
|
|
|
+ { label: '营销费用', value: 12000, color: '#9b59b6' }
|
|
|
+ ];
|
|
|
+ default:
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成模拟汇总数据
|
|
|
+ generateMockSummary(): { [key: string]: number } {
|
|
|
+ const summary: { [key: string]: number } = {};
|
|
|
+
|
|
|
+ switch (this.selectedReportType()) {
|
|
|
+ case 'financial_summary':
|
|
|
+ summary['totalRevenue'] = 120000;
|
|
|
+ summary['totalExpense'] = 75000;
|
|
|
+ summary['netProfit'] = 45000;
|
|
|
+ summary['profitMargin'] = 37.5;
|
|
|
+ break;
|
|
|
+ case 'project_profitability':
|
|
|
+ summary['totalProjects'] = 15;
|
|
|
+ summary['completedProjects'] = 10;
|
|
|
+ summary['averageProfitMargin'] = 32.8;
|
|
|
+ summary['highestProjectProfit'] = 15000;
|
|
|
+ break;
|
|
|
+ case 'cash_flow':
|
|
|
+ summary['beginningBalance'] = 50000;
|
|
|
+ summary['endingBalance'] = 120000;
|
|
|
+ summary['netCashFlow'] = 70000;
|
|
|
+ summary['operatingCashFlow'] = 65000;
|
|
|
+ break;
|
|
|
+ case 'expense_analysis':
|
|
|
+ summary['totalExpense'] = 85000;
|
|
|
+ summary['personnelExpense'] = 40000;
|
|
|
+ summary['operatingExpense'] = 30000;
|
|
|
+ summary['otherExpense'] = 15000;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导出报表
|
|
|
+ exportReport(): void {
|
|
|
+ if (!this.reportData()) {
|
|
|
+ // 显示提示信息
|
|
|
+ console.log('请先生成报表再导出');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 模拟导出功能
|
|
|
+ const report = this.reportData();
|
|
|
+ if (report) {
|
|
|
+ console.log('导出报表:', report.title);
|
|
|
+ alert(`报表"${report.title}"已导出`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查看历史报表
|
|
|
+ viewHistoricalReport(report: ReportData): void {
|
|
|
+ this.reportData.set(report);
|
|
|
+ this.selectedReportType.set(report.type);
|
|
|
+ this.selectedTimePeriod.set(report.period);
|
|
|
+ this.dateRange.set(report.dateRange);
|
|
|
+ this.selectedChartType.set(report.chartType);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化日期
|
|
|
+ formatDate(date: Date | undefined | null): string {
|
|
|
+ if (!date) {
|
|
|
+ return new Date().toLocaleDateString('zh-CN');
|
|
|
+ }
|
|
|
+ return new Date(date).toLocaleDateString('zh-CN');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化金额
|
|
|
+ formatAmount(amount: number): string {
|
|
|
+ return new Intl.NumberFormat('zh-CN', {
|
|
|
+ style: 'currency',
|
|
|
+ currency: 'CNY',
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 0
|
|
|
+ }).format(amount);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化百分比
|
|
|
+ formatPercentage(value: number): string {
|
|
|
+ return `${value.toFixed(1)}%`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取报表类型图标
|
|
|
+ getReportTypeIcon(type: ReportType): string {
|
|
|
+ const iconMap: Record<ReportType, string> = {
|
|
|
+ 'financial_summary': '📊',
|
|
|
+ 'project_profitability': '💰',
|
|
|
+ 'cash_flow': '💹',
|
|
|
+ 'expense_analysis': '📉'
|
|
|
+ };
|
|
|
+ return iconMap[type] || '📋';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置报表参数
|
|
|
+ resetParameters(): void {
|
|
|
+ this.selectedReportType.set('financial_summary');
|
|
|
+ this.selectedTimePeriod.set('monthly');
|
|
|
+ this.dateRange.set({
|
|
|
+ start: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
|
|
+ end: new Date()
|
|
|
+ });
|
|
|
+ this.selectedChartType.set('bar');
|
|
|
+ this.reportData.set(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取图表数据中的最大值(用于柱状图比例计算)
|
|
|
+ maxValue(data: ChartDataPoint[]): number {
|
|
|
+ if (!data || data.length === 0) return 1;
|
|
|
+ return Math.max(...data.map(item => item.value));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算图表数据的总和(用于饼图和占比计算)
|
|
|
+ totalValue(data: ChartDataPoint[]): number {
|
|
|
+ if (!data || data.length === 0) return 0;
|
|
|
+ return data.reduce((sum, item) => sum + item.value, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取饼图切片的变换样式
|
|
|
+ getPieTransform(index: number, data: ChartDataPoint[]): string {
|
|
|
+ const total = this.totalValue(data);
|
|
|
+ if (total === 0 || data.length === 0) return '';
|
|
|
+
|
|
|
+ // 计算当前切片和之前所有切片的总和
|
|
|
+ let cumulativeValue = 0;
|
|
|
+ for (let i = 0; i < index; i++) {
|
|
|
+ cumulativeValue += data[i].value;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算当前切片的起始角度和结束角度
|
|
|
+ const startAngle = (cumulativeValue / total) * 360;
|
|
|
+ const endAngle = ((cumulativeValue + data[index].value) / total) * 360;
|
|
|
|
|
|
+ // 计算中间角度用于定位
|
|
|
+ const midAngle = (startAngle + endAngle) / 2;
|
|
|
+
|
|
|
+ // 生成变换样式
|
|
|
+ // 注意:这里使用简化的实现,实际项目中可能需要更复杂的计算来生成真实的饼图
|
|
|
+ if (index === 0) {
|
|
|
+ return `rotate(${startAngle}deg)`;
|
|
|
+ } else {
|
|
|
+ return `rotate(${startAngle}deg)`;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|