|
@@ -1,18 +1,14 @@
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { RouterModule } from '@angular/router';
|
|
|
import { Subscription } from 'rxjs';
|
|
|
-import { signal } from '@angular/core';
|
|
|
+import { signal, Component, OnInit, AfterViewInit, OnDestroy, computed } from '@angular/core';
|
|
|
import { AdminDashboardService } from './dashboard.service';
|
|
|
-import { WriteContentComponent } from './components/write-content/write-content.component';
|
|
|
// 使用全局echarts变量,不导入模块
|
|
|
|
|
|
-// 确保@Component装饰器存在
|
|
|
-import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core';
|
|
|
-
|
|
|
@Component({
|
|
|
selector: 'app-admin-dashboard',
|
|
|
standalone: true,
|
|
|
- imports: [CommonModule, RouterModule, WriteContentComponent],
|
|
|
+ imports: [CommonModule, RouterModule],
|
|
|
templateUrl: './dashboard.html',
|
|
|
styleUrl: './dashboard.scss'
|
|
|
})
|
|
@@ -27,12 +23,104 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
totalRevenue: signal(1258000)
|
|
|
};
|
|
|
|
|
|
- // 模态框控制
|
|
|
- showWriteModal = signal(false);
|
|
|
+ // 图表周期切换
|
|
|
+ projectPeriod = signal<'6m' | '12m'>('6m');
|
|
|
+ revenuePeriod = signal<'quarter' | 'year'>('quarter');
|
|
|
+
|
|
|
+ // 详情面板
|
|
|
+ detailOpen = signal(false);
|
|
|
+ detailType = signal<'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue' | null>(null);
|
|
|
+ detailTitle = computed(() => {
|
|
|
+ switch (this.detailType()) {
|
|
|
+ case 'totalProjects': return '项目总览';
|
|
|
+ case 'active': return '进行中项目详情';
|
|
|
+ case 'completed': return '已完成项目详情';
|
|
|
+ case 'designers': return '设计师统计详情';
|
|
|
+ case 'customers': return '客户统计详情';
|
|
|
+ case 'revenue': return '收入统计详情';
|
|
|
+ default: return '';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 明细数据与筛选/分页状态
|
|
|
+ detailData = signal<any[]>([]);
|
|
|
+ keyword = signal('');
|
|
|
+ statusFilter = signal('all');
|
|
|
+ dateFrom = signal<string | null>(null);
|
|
|
+ dateTo = signal<string | null>(null);
|
|
|
+ pageIndex = signal(1);
|
|
|
+ pageSize = signal(10);
|
|
|
+
|
|
|
+ // 过滤后的数据
|
|
|
+ filteredData = computed(() => {
|
|
|
+ const type = this.detailType();
|
|
|
+ let data = this.detailData();
|
|
|
+ const kw = this.keyword().trim().toLowerCase();
|
|
|
+ const status = this.statusFilter();
|
|
|
+ const from = this.dateFrom() ? new Date(this.dateFrom() as string).getTime() : null;
|
|
|
+ const to = this.dateTo() ? new Date(this.dateTo() as string).getTime() : null;
|
|
|
+
|
|
|
+ // 关键词过滤(对常见字段做并集匹配)
|
|
|
+ if (kw) {
|
|
|
+ data = data.filter((it: any) => {
|
|
|
+ const text = [it.name, it.projectName, it.customer, it.owner, it.status, it.level, it.invoiceNo]
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(' ')
|
|
|
+ .toLowerCase();
|
|
|
+ return text.includes(kw);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态过滤(不同类型对应不同字段)
|
|
|
+ if (status && status !== 'all') {
|
|
|
+ data = data.filter((it: any) => {
|
|
|
+ switch (type) {
|
|
|
+ case 'active':
|
|
|
+ case 'completed':
|
|
|
+ case 'totalProjects':
|
|
|
+ return (it.status || '').toLowerCase() === status.toLowerCase();
|
|
|
+ case 'designers':
|
|
|
+ return (it.level || '').toLowerCase() === status.toLowerCase();
|
|
|
+ case 'customers':
|
|
|
+ return (it.status || '').toLowerCase() === status.toLowerCase();
|
|
|
+ case 'revenue':
|
|
|
+ return (it.type || '').toLowerCase() === status.toLowerCase();
|
|
|
+ default:
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 时间范围过滤:尝试使用 date/endDate/startDate 三者之一
|
|
|
+ if (from || to) {
|
|
|
+ data = data.filter((it: any) => {
|
|
|
+ const d = it.date || it.endDate || it.startDate;
|
|
|
+ if (!d) return false;
|
|
|
+ const t = new Date(d).getTime();
|
|
|
+ if (from && t < from) return false;
|
|
|
+ if (to && t > to) return false;
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return data;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 分页后的数据
|
|
|
+ pagedData = computed(() => {
|
|
|
+ const size = this.pageSize();
|
|
|
+ const idx = this.pageIndex();
|
|
|
+ const start = (idx - 1) * size;
|
|
|
+ return this.filteredData().slice(start, start + size);
|
|
|
+ });
|
|
|
+
|
|
|
+ totalItems = computed(() => this.filteredData().length);
|
|
|
+ totalPagesComputed = computed(() => Math.max(1, Math.ceil(this.totalItems() / this.pageSize())));
|
|
|
|
|
|
private subscriptions: Subscription = new Subscription();
|
|
|
private projectChart: any | null = null;
|
|
|
private revenueChart: any | null = null;
|
|
|
+ private detailChart: any | null = null;
|
|
|
|
|
|
constructor(private dashboardService: AdminDashboardService) {}
|
|
|
|
|
@@ -48,105 +136,406 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
ngOnDestroy(): void {
|
|
|
this.subscriptions.unsubscribe();
|
|
|
window.removeEventListener('resize', this.handleResize);
|
|
|
- if (this.projectChart) {
|
|
|
- this.projectChart.dispose();
|
|
|
- }
|
|
|
- if (this.revenueChart) {
|
|
|
- this.revenueChart.dispose();
|
|
|
- }
|
|
|
+ this.disposeCharts();
|
|
|
+ }
|
|
|
+
|
|
|
+ private disposeCharts(): void {
|
|
|
+ if (this.projectChart) { this.projectChart.dispose(); this.projectChart = null; }
|
|
|
+ if (this.revenueChart) { this.revenueChart.dispose(); this.revenueChart = null; }
|
|
|
+ if (this.detailChart) { this.detailChart.dispose(); this.detailChart = null; }
|
|
|
}
|
|
|
|
|
|
loadDashboardData(): void {
|
|
|
- // 在实际应用中,这里会从服务加载数据
|
|
|
- // 由于是模拟环境,我们使用模拟数据
|
|
|
+ // 模拟调用服务
|
|
|
this.subscriptions.add(
|
|
|
- this.dashboardService.getDashboardStats().subscribe(data => {
|
|
|
- // 更新统计数据
|
|
|
- // this.stats.totalProjects.set(data.totalProjects);
|
|
|
- // 其他数据更新...
|
|
|
+ this.dashboardService.getDashboardStats().subscribe(() => {
|
|
|
+ // 使用默认模拟数据,必要时可在此更新 signals
|
|
|
})
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ // ====== 顶部两张主图表 ======
|
|
|
initCharts(): void {
|
|
|
- // 初始化项目趋势图
|
|
|
- const projectChartDom = document.getElementById('projectTrendChart');
|
|
|
- if (projectChartDom) {
|
|
|
- this.projectChart = echarts.init(projectChartDom);
|
|
|
- if (this.projectChart) {
|
|
|
- this.projectChart.setOption({
|
|
|
- title: { text: '项目数量趋势', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
- tooltip: { trigger: 'axis' },
|
|
|
- xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月'] },
|
|
|
- yAxis: { type: 'value' },
|
|
|
- series: [{
|
|
|
- name: '新项目',
|
|
|
- type: 'line',
|
|
|
- data: [18, 25, 32, 28, 42, 38],
|
|
|
- lineStyle: { color: '#165DFF' },
|
|
|
- itemStyle: { color: '#165DFF' }
|
|
|
- }, {
|
|
|
- name: '完成项目',
|
|
|
- type: 'line',
|
|
|
- data: [15, 20, 25, 22, 35, 30],
|
|
|
- lineStyle: { color: '#00B42A' },
|
|
|
- itemStyle: { color: '#00B42A' }
|
|
|
- }]
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 初始化收入统计图
|
|
|
- const revenueChartDom = document.getElementById('revenueChart');
|
|
|
- if (revenueChartDom) {
|
|
|
- this.revenueChart = echarts.init(revenueChartDom);
|
|
|
- if (this.revenueChart) {
|
|
|
- this.revenueChart.setOption({
|
|
|
- title: { text: '季度收入统计', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
- tooltip: { trigger: 'item' },
|
|
|
- series: [{
|
|
|
- name: '收入分布',
|
|
|
- type: 'pie',
|
|
|
- radius: '65%',
|
|
|
- data: [
|
|
|
- { value: 350000, name: '第一季度' },
|
|
|
- { value: 420000, name: '第二季度' },
|
|
|
- { value: 488000, name: '第三季度' }
|
|
|
- ],
|
|
|
- emphasis: {
|
|
|
- itemStyle: {
|
|
|
- shadowBlur: 10,
|
|
|
- shadowOffsetX: 0,
|
|
|
- shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
- }
|
|
|
- }
|
|
|
- }]
|
|
|
- });
|
|
|
- }
|
|
|
+ this.initProjectChart();
|
|
|
+ this.initRevenueChart();
|
|
|
+ }
|
|
|
+
|
|
|
+ private initProjectChart(): void {
|
|
|
+ const el = document.getElementById('projectTrendChart');
|
|
|
+ if (!el) return;
|
|
|
+ this.projectChart?.dispose();
|
|
|
+ this.projectChart = echarts.init(el);
|
|
|
+
|
|
|
+ const { x, newProjects, completed } = this.prepareProjectSeries(this.projectPeriod());
|
|
|
+ this.projectChart.setOption({
|
|
|
+ title: { text: '项目数量趋势', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ legend: { data: ['新项目', '完成项目'] },
|
|
|
+ xAxis: { type: 'category', data: x },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ series: [
|
|
|
+ { name: '新项目', type: 'line', data: newProjects, lineStyle: { color: '#165DFF' }, itemStyle: { color: '#165DFF' }, smooth: true },
|
|
|
+ { name: '完成项目', type: 'line', data: completed, lineStyle: { color: '#00B42A' }, itemStyle: { color: '#00B42A' }, smooth: true }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private initRevenueChart(): void {
|
|
|
+ const el = document.getElementById('revenueChart');
|
|
|
+ if (!el) return;
|
|
|
+ this.revenueChart?.dispose();
|
|
|
+ this.revenueChart = echarts.init(el);
|
|
|
+
|
|
|
+ if (this.revenuePeriod() === 'quarter') {
|
|
|
+ this.revenueChart.setOption({
|
|
|
+ title: { text: '季度收入统计', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
+ tooltip: { trigger: 'item' },
|
|
|
+ series: [{
|
|
|
+ type: 'pie', radius: '65%',
|
|
|
+ data: [
|
|
|
+ { value: 350000, name: '第一季度' },
|
|
|
+ { value: 420000, name: '第二季度' },
|
|
|
+ { value: 488000, name: '第三季度' }
|
|
|
+ ],
|
|
|
+ emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.5)' } }
|
|
|
+ }]
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 全年:使用柱状图展示 12 个月收入
|
|
|
+ const months = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
|
|
|
+ const revenue = [120, 140, 160, 155, 180, 210, 230, 220, 240, 260, 280, 300].map(v => v * 1000);
|
|
|
+ this.revenueChart.setOption({
|
|
|
+ title: { text: '全年收入统计', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ xAxis: { type: 'category', data: months },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ series: [{ type: 'bar', data: revenue, itemStyle: { color: '#165DFF' } }]
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private handleResize = (): void => {
|
|
|
- if (this.projectChart) {
|
|
|
- this.projectChart.resize();
|
|
|
+ private prepareProjectSeries(period: '6m' | '12m') {
|
|
|
+ if (period === '6m') {
|
|
|
+ return {
|
|
|
+ x: ['1月','2月','3月','4月','5月','6月'],
|
|
|
+ newProjects: [18, 25, 32, 28, 42, 38],
|
|
|
+ completed: [15, 20, 25, 22, 35, 30]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // 12个月数据(构造平滑趋势)
|
|
|
+ return {
|
|
|
+ x: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
|
|
|
+ newProjects: [12,18,22,26,30,34,36,38,40,42,44,46],
|
|
|
+ completed: [10,14,18,20,24,28,30,31,33,35,37,39]
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ setProjectPeriod(p: '6m' | '12m') {
|
|
|
+ if (this.projectPeriod() !== p) {
|
|
|
+ this.projectPeriod.set(p);
|
|
|
+ this.initProjectChart();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setRevenuePeriod(p: 'quarter' | 'year') {
|
|
|
+ if (this.revenuePeriod() !== p) {
|
|
|
+ this.revenuePeriod.set(p);
|
|
|
+ this.initRevenueChart();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====== 详情面板 ======
|
|
|
+ showPanel(type: 'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue') {
|
|
|
+ this.detailType.set(type);
|
|
|
+ // 重置筛选与分页
|
|
|
+ this.keyword.set('');
|
|
|
+ this.statusFilter.set('all');
|
|
|
+ this.dateFrom.set(null);
|
|
|
+ this.dateTo.set(null);
|
|
|
+ this.pageIndex.set(1);
|
|
|
+
|
|
|
+ // 加载本次类型的明细数据
|
|
|
+ this.loadDetailData(type);
|
|
|
+
|
|
|
+ // 打开抽屉并初始化图表
|
|
|
+ this.detailOpen.set(true);
|
|
|
+ setTimeout(() => this.initDetailChart(), 0);
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
+ }
|
|
|
+
|
|
|
+ closeDetailPanel() {
|
|
|
+ this.detailOpen.set(false);
|
|
|
+ this.detailType.set(null);
|
|
|
+ this.detailChart?.dispose();
|
|
|
+ this.detailChart = null;
|
|
|
+ document.body.style.overflow = 'auto';
|
|
|
+ }
|
|
|
+
|
|
|
+ private initDetailChart() {
|
|
|
+ const el = document.getElementById('detailChart');
|
|
|
+ if (!el) return;
|
|
|
+ this.detailChart?.dispose();
|
|
|
+ this.detailChart = echarts.init(el);
|
|
|
+ const type = this.detailType();
|
|
|
+
|
|
|
+ if (type === 'totalProjects' || type === 'active' || type === 'completed') {
|
|
|
+ const { x, newProjects, completed } = this.prepareProjectSeries('12m');
|
|
|
+ this.detailChart.setOption({
|
|
|
+ title: { text: '项目趋势详情(12个月)', left: 'center' },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ legend: { data: ['新项目','完成项目'] },
|
|
|
+ xAxis: { type: 'category', data: x },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ series: [
|
|
|
+ { name: '新项目', type: 'line', data: newProjects, smooth: true, lineStyle: { color: '#165DFF' } },
|
|
|
+ { name: '完成项目', type: 'line', data: completed, smooth: true, lineStyle: { color: '#00B42A' } }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ return;
|
|
|
}
|
|
|
- if (this.revenueChart) {
|
|
|
- this.revenueChart.resize();
|
|
|
+
|
|
|
+ if (type === 'designers') {
|
|
|
+ this.detailChart.setOption({
|
|
|
+ title: { text: '设计师完成量对比', left: 'center' },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ legend: { data: ['完成','进行中'] },
|
|
|
+ xAxis: { type: 'category', data: ['张','李','王','赵','陈'] },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ series: [
|
|
|
+ { name: '完成', type: 'bar', data: [18,15,12,10,9], itemStyle: { color: '#00B42A' } },
|
|
|
+ { name: '进行中', type: 'bar', data: [8,6,5,4,3], itemStyle: { color: '#165DFF' } }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'customers') {
|
|
|
+ this.detailChart.setOption({
|
|
|
+ title: { text: '客户增长趋势', left: 'center' },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ xAxis: { type: 'category', data: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'] },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ series: [{ name: '客户数', type: 'line', data: [280,300,310,320,330,340,345,350,355,360,368,380], itemStyle: { color: '#4E5BA6' }, smooth: true }]
|
|
|
+ });
|
|
|
+ return;
|
|
|
}
|
|
|
+
|
|
|
+ // revenue
|
|
|
+ this.detailChart.setOption({
|
|
|
+ title: { text: '收入构成(年度)', left: 'center' },
|
|
|
+ tooltip: { trigger: 'item' },
|
|
|
+ series: [{
|
|
|
+ type: 'pie', radius: ['35%','65%'],
|
|
|
+ data: [
|
|
|
+ { value: 520000, name: '设计服务' },
|
|
|
+ { value: 360000, name: '材料供应' },
|
|
|
+ { value: 180000, name: '售后与增值' },
|
|
|
+ { value: 198000, name: '其他' }
|
|
|
+ ]
|
|
|
+ }]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private handleResize = (): void => {
|
|
|
+ this.projectChart?.resize();
|
|
|
+ this.revenueChart?.resize();
|
|
|
+ this.detailChart?.resize();
|
|
|
};
|
|
|
|
|
|
formatCurrency(amount: number): string {
|
|
|
return '¥' + amount.toLocaleString('zh-CN');
|
|
|
}
|
|
|
|
|
|
- // 模态框控制方法
|
|
|
- openWriteModal(): void {
|
|
|
- this.showWriteModal.set(true);
|
|
|
- document.body.style.overflow = 'hidden';
|
|
|
+ // 兼容旧模板调用(已调整为 showPanel)
|
|
|
+ showProjectDetails(status: 'active' | 'completed'): void {
|
|
|
+ this.showPanel(status);
|
|
|
}
|
|
|
+ showCustomersDetails(): void { this.showPanel('customers'); }
|
|
|
+ showFinanceDetails(): void { this.showPanel('revenue'); }
|
|
|
|
|
|
- closeWriteModal(): void {
|
|
|
- this.showWriteModal.set(false);
|
|
|
- document.body.style.overflow = 'auto';
|
|
|
+ // ====== 明细数据:加载、列配置、导出与分页 ======
|
|
|
+ private loadDetailData(type: 'totalProjects' | 'active' | 'completed' | 'designers' | 'customers' | 'revenue') {
|
|
|
+ // 构造模拟数据(足量便于分页演示)
|
|
|
+ const now = new Date();
|
|
|
+ const addDays = (base: Date, days: number) => new Date(base.getTime() + days * 86400000);
|
|
|
+
|
|
|
+ if (type === 'totalProjects' || type === 'active' || type === 'completed') {
|
|
|
+ const status = type === 'active' ? '进行中' : (type === 'completed' ? '已完成' : undefined);
|
|
|
+ const items = Array.from({ length: 42 }).map((_, i) => ({
|
|
|
+ id: 'P' + String(1000 + i),
|
|
|
+ name: `项目 ${i + 1}`,
|
|
|
+ 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)
|
|
|
+ }));
|
|
|
+ this.detailData.set(items);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'designers') {
|
|
|
+ const items = Array.from({ length: 36 }).map((_, i) => ({
|
|
|
+ id: 'D' + String(200 + i),
|
|
|
+ name: ['张一','李二','王三','赵四','陈五','刘六'][i % 6],
|
|
|
+ level: ['junior','mid','senior'][i % 3],
|
|
|
+ completed: 10 + (i % 15),
|
|
|
+ inProgress: 1 + (i % 6),
|
|
|
+ avgCycle: 7 + (i % 10),
|
|
|
+ date: addDays(now, -i).toISOString().slice(0,10)
|
|
|
+ }));
|
|
|
+ this.detailData.set(items);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'customers') {
|
|
|
+ const items = Array.from({ length: 28 }).map((_, i) => ({
|
|
|
+ id: 'C' + String(300 + i),
|
|
|
+ name: ['王先生','李女士','赵先生','陈女士'][i % 4],
|
|
|
+ projects: 1 + (i % 5),
|
|
|
+ lastContact: addDays(now, -i * 2).toISOString().slice(0,10),
|
|
|
+ status: ['潜在','跟进中','已签约'][i % 3],
|
|
|
+ date: addDays(now, -i * 2).toISOString().slice(0,10)
|
|
|
+ }));
|
|
|
+ this.detailData.set(items);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // revenue
|
|
|
+ const items = Array.from({ length: 34 }).map((_, i) => ({
|
|
|
+ invoiceNo: 'INV-' + String(10000 + i),
|
|
|
+ customer: ['华夏地产','远景家装','绿洲装饰','宏图设计'][i % 4],
|
|
|
+ amount: 5000 + (i % 12) * 1500,
|
|
|
+ type: ['service','material','addon'][i % 3],
|
|
|
+ date: addDays(now, -i).toISOString().slice(0,10)
|
|
|
+ }));
|
|
|
+ this.detailData.set(items);
|
|
|
+ }
|
|
|
+
|
|
|
+ getColumns(): { label: string; field: string; formatter?: (v: any) => string }[] {
|
|
|
+ const type = this.detailType();
|
|
|
+ if (type === 'totalProjects' || type === 'active' || type === 'completed') {
|
|
|
+ return [
|
|
|
+ { label: '项目编号', field: 'id' },
|
|
|
+ { label: '项目名称', field: 'name' },
|
|
|
+ { label: '负责人', field: 'owner' },
|
|
|
+ { label: '状态', field: 'status' },
|
|
|
+ { label: '开始日期', field: 'startDate' },
|
|
|
+ { label: '结束日期', field: 'endDate' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ if (type === 'designers') {
|
|
|
+ return [
|
|
|
+ { label: '设计师', field: 'name' },
|
|
|
+ { label: '级别', field: 'level' },
|
|
|
+ { label: '完成量', field: 'completed' },
|
|
|
+ { label: '进行中', field: 'inProgress' },
|
|
|
+ { label: '平均周期(天)', field: 'avgCycle' },
|
|
|
+ { label: '统计日期', field: 'date' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ if (type === 'customers') {
|
|
|
+ return [
|
|
|
+ { label: '客户名', field: 'name' },
|
|
|
+ { label: '项目数', field: 'projects' },
|
|
|
+ { label: '最后联系', field: 'lastContact' },
|
|
|
+ { label: '状态', field: 'status' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ // revenue
|
|
|
+ return [
|
|
|
+ { label: '发票号', field: 'invoiceNo' },
|
|
|
+ { label: '客户', field: 'customer' },
|
|
|
+ { label: '金额', field: 'amount', formatter: (v: any) => this.formatCurrency(Number(v)) },
|
|
|
+ { label: '类型', field: 'type' },
|
|
|
+ { label: '日期', field: 'date' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态选项(随类型变化)
|
|
|
+ getStatusOptions(): { label: string; value: string }[] {
|
|
|
+ const type = this.detailType();
|
|
|
+ if (type === 'totalProjects' || type === 'active' || type === 'completed') {
|
|
|
+ return [
|
|
|
+ { label: '全部状态', value: 'all' },
|
|
|
+ { label: '进行中', value: '进行中' },
|
|
|
+ { label: '已完成', value: '已完成' },
|
|
|
+ { label: '待启动', value: '待启动' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ if (type === 'designers') {
|
|
|
+ return [
|
|
|
+ { label: '全部级别', value: 'all' },
|
|
|
+ { label: 'junior', value: 'junior' },
|
|
|
+ { label: 'mid', value: 'mid' },
|
|
|
+ { label: 'senior', value: 'senior' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ if (type === 'customers') {
|
|
|
+ return [
|
|
|
+ { label: '全部状态', value: 'all' },
|
|
|
+ { label: '潜在', value: '潜在' },
|
|
|
+ { label: '跟进中', value: '跟进中' },
|
|
|
+ { label: '已签约', value: '已签约' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return [
|
|
|
+ { label: '全部类型', value: 'all' },
|
|
|
+ { label: 'service', value: 'service' },
|
|
|
+ { label: 'material', value: 'material' },
|
|
|
+ { label: 'addon', value: 'addon' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 交互:筛选与分页
|
|
|
+ setKeyword(v: string) { this.keyword.set(v); this.pageIndex.set(1); }
|
|
|
+ setStatus(v: string) { this.statusFilter.set(v); this.pageIndex.set(1); }
|
|
|
+ setDateFrom(v: string) { this.dateFrom.set(v || null); this.pageIndex.set(1); }
|
|
|
+ setDateTo(v: string) { this.dateTo.set(v || null); this.pageIndex.set(1); }
|
|
|
+ resetFilters() {
|
|
|
+ this.keyword.set('');
|
|
|
+ this.statusFilter.set('all');
|
|
|
+ this.dateFrom.set(null);
|
|
|
+ this.dateTo.set(null);
|
|
|
+ this.pageIndex.set(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ get totalPages() { return this.totalPagesComputed(); }
|
|
|
+ goToPage(n: number) { const tp = this.totalPagesComputed(); if (n >= 1 && n <= tp) this.pageIndex.set(n); }
|
|
|
+ prevPage() { this.goToPage(this.pageIndex() - 1); }
|
|
|
+ nextPage() { this.goToPage(this.pageIndex() + 1); }
|
|
|
+
|
|
|
+ // 生成页码列表(最多展示 5 个,居中当前页)
|
|
|
+ getPages(): number[] {
|
|
|
+ const total = this.totalPagesComputed();
|
|
|
+ const current = this.pageIndex();
|
|
|
+ const max = 5;
|
|
|
+ let start = Math.max(1, current - Math.floor(max / 2));
|
|
|
+ let end = Math.min(total, start + max - 1);
|
|
|
+ start = Math.max(1, end - max + 1);
|
|
|
+ const pages: number[] = [];
|
|
|
+ for (let i = start; i <= end; i++) pages.push(i);
|
|
|
+ return pages;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导出当前过滤结果为 CSV
|
|
|
+ exportCSV() {
|
|
|
+ const cols = this.getColumns();
|
|
|
+ const rows = this.filteredData();
|
|
|
+ const header = cols.map(c => c.label).join(',');
|
|
|
+ const escape = (val: any) => {
|
|
|
+ if (val === undefined || val === null) return '';
|
|
|
+ const s = String(val).replace(/"/g, '""');
|
|
|
+ return /[",\n]/.test(s) ? `"${s}"` : s;
|
|
|
+ };
|
|
|
+ const lines = rows.map(r => cols.map(c => escape(c.formatter ? c.formatter((r as any)[c.field]) : (r as any)[c.field])).join(','));
|
|
|
+ const csv = [header, ...lines].join('\n');
|
|
|
+ const blob = new Blob(["\ufeff" + csv], { type: 'text/csv;charset=utf-8;' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ const filenameMap: any = { totalProjects: '项目总览', active: '进行中项目', completed: '已完成项目', designers: '设计师统计', customers: '客户统计', revenue: '收入统计' };
|
|
|
+ a.download = `${filenameMap[this.detailType() || 'totalProjects']}-明细.csv`;
|
|
|
+ a.click();
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
}
|
|
|
-}
|
|
|
+}
|