wangrong
2026-09-26 8e6c592cb1940a909d14a013de35767effbbad0a
修改首页显示bug
2个文件已修改
99 ■■■■■ 已修改文件
src/modules/base/middleware/authority.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/modules/report/service/screen.ts 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/modules/base/middleware/authority.ts
@@ -100,7 +100,9 @@
          if (
            new RegExp(`^${adminUrl}?.*/comm/`).test(url) ||
            // 字典接口
            url == '/admin/dict/info/data'
            url == '/admin/dict/info/data' ||
            // 驾驶舱首页汇总:菜单「首页」没有按钮权限,未登记 report:screen:query 时非超管会 403
            url == '/admin/report/screen/query'
          ) {
            await next();
            return;
src/modules/report/service/screen.ts
@@ -28,7 +28,7 @@
const ITEM_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#a855f7', '#06b6d4', '#ef4444', '#94a3b8'];
/** 日表列与垃圾种类编码。首页分类统计只累加其中状态为启用的编码。 */
/** 日表列与垃圾种类编码,首页 KPI/趋势口径以本表为准,不读种类表启用状态。 */
const WASTE_FIELDS: {
  code: string;
  weight: string;
@@ -51,6 +51,9 @@
    group: (idx === '006' ? 'elec' : idx === '004' ? 'fabric' : undefined) as 'elec' | 'fabric' | undefined,
  })),
];
/** 五大类;子类已滚入 SW62,总量不再把 SW62001–007 加一遍。 */
const MAIN_WASTE_FIELDS = WASTE_FIELDS.filter(f => /^SW6[0-4]$/.test(f.code));
type OrdersByType = {
  axis: string[];
@@ -143,21 +146,21 @@
      safe('年度站点日汇总', () => this.sumSite(yearStart, yearEnd), emptySum),
    ]);
    const enabledWaste = await safe(
      '启用垃圾种类',
      () => this.enabledWaste(),
      { names: new Map<string, string>(), codes: new Set<string>() }
    const wasteNames = await safe(
      '垃圾种类名称',
      () => this.wasteNames(),
      new Map<string, string>()
    );
    const wasteMix = this.buildWasteMix(period, enabledWaste);
    const recycleItems = this.buildRecycleItems(period, enabledWaste);
    const wasteMix = this.buildWasteMix(period, wasteNames);
    const recycleItems = this.buildRecycleItems(period, wasteNames);
    const { axis, trend } = await safe(
      '回收趋势',
      () => this.buildTrend(range || 'month', dates, enabledWaste.codes),
      () => this.buildTrend(range || 'month', dates),
      { axis: [], trend: { weight: [], value: [], carbon: [] } }
    );
    const yearCarbon = await safe(
      '年度碳减排',
      () => this.buildYearCarbon(enabledWaste.codes),
      () => this.buildYearCarbon(),
      Array.from({ length: 12 }, () => 0)
    );
    const ranks = await safe(
@@ -208,10 +211,10 @@
      sites: [],
    });
    const periodStat = this.statEnabled(period, enabledWaste.codes);
    const todayStat = this.statEnabled(todayRow, enabledWaste.codes);
    const totalStat = this.statEnabled(totalRow, enabledWaste.codes);
    const yearStat = this.statEnabled(yearRow, enabledWaste.codes);
    const periodStat = this.statKpi(period);
    const todayStat = this.statKpi(todayRow);
    const totalStat = this.statKpi(totalRow);
    const yearStat = this.statKpi(yearRow);
    return {
      range: range || 'month',
@@ -357,6 +360,11 @@
      .addSelect('SUM(a.timeSw63)', 'timeSw63')
      .addSelect('SUM(a.timeSw64)', 'timeSw64')
      .addSelect('SUM(a.carbonTotal)', 'carbonTotal')
      .addSelect('SUM(a.carbonSw60)', 'carbonSw60')
      .addSelect('SUM(a.carbonSw61)', 'carbonSw61')
      .addSelect('SUM(a.carbonSw62)', 'carbonSw62')
      .addSelect('SUM(a.carbonSw63)', 'carbonSw63')
      .addSelect('SUM(a.carbonSw64)', 'carbonSw64')
      .addSelect('SUM(a.estimatedSalesSw62)', 'estimatedSalesSw62');
    for (const i of ['001', '002', '003', '004', '005', '006', '007']) {
@@ -371,34 +379,30 @@
    return (await qb.getRawOne()) || {};
  }
  /** 名称取全部种类;codes 仍只含启用项,供 KPI/趋势沿用原口径。分类图不再用 codes 过滤。 */
  private async enabledWaste() {
  /** 种类名称仅作展示,不参与 KPI 过滤。 */
  private async wasteNames() {
    const list = await this.wasteTypeRepo.find({
      select: ['code', 'name', 'status'],
      select: ['code', 'name'],
    });
    const names = new Map<string, string>();
    const codes = new Set<string>();
    list.forEach(e => {
      const code = String(e.code).toUpperCase();
      names.set(code, e.name);
      if (Number(e.status) === 1) {
        codes.add(code);
      }
      names.set(String(e.code).toUpperCase(), e.name);
    });
    return { names, codes };
    return names;
  }
  /** 只累加启用种类对应的日表列,避免禁用种类进入重量、碳排和销售额。 */
  private statEnabled(row: any, codes: Set<string>) {
    const on = WASTE_FIELDS.filter(f => codes.has(f.code));
    const sum = (pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined) =>
      on.reduce((s, f) => s + n(row?.[pick(f)]), 0);
  /** 按 WASTE_FIELDS 汇总;总量用五大类,分组指标用 group 字段。 */
  private statKpi(row: any) {
    const sum = (
      fields: typeof WASTE_FIELDS,
      pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined
    ) => fields.reduce((s, f) => s + n(row?.[pick(f)]), 0);
    const ofGroup = (group: 'recyclable' | 'bulk' | 'elec' | 'fabric') =>
      on.filter(f => f.group === group).reduce((s, f) => s + n(row?.[f.weight]), 0);
      WASTE_FIELDS.filter(f => f.group === group).reduce((s, f) => s + n(row?.[f.weight]), 0);
    return {
      weight: sum(f => f.weight),
      carbon: sum(f => f.carbon),
      sales: sum(f => f.sales),
      weight: sum(MAIN_WASTE_FIELDS, f => f.weight),
      carbon: sum(MAIN_WASTE_FIELDS, f => f.carbon),
      sales: sum(MAIN_WASTE_FIELDS, f => f.sales),
      recyclable: ofGroup('recyclable'),
      bulk: ofGroup('bulk'),
      elec: ofGroup('elec'),
@@ -406,16 +410,14 @@
    };
  }
  private enabledExpr(codes: Set<string>, pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined) {
    const cols = WASTE_FIELDS.filter(f => codes.has(f.code))
      .map(pick)
      .filter((col): col is string => !!col);
  private kpiExpr(pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined) {
    const cols = MAIN_WASTE_FIELDS.map(pick).filter((col): col is string => !!col);
    if (!cols.length) return '0';
    return cols.map(col => `IFNULL(a.${col},0)`).join(' + ');
  }
  /** 首页分类全量返回,不按种类启用状态裁剪;0 值由前端图表隐藏。 */
  private buildWasteMix(row: any, waste: { names: Map<string, string>; codes: Set<string> }) {
  private buildWasteMix(row: any, names: Map<string, string>) {
    const items = [
      { code: 'SW62', key: 'Sw62' },
      { code: 'SW64', key: 'Sw64' },
@@ -424,7 +426,7 @@
      { code: 'SW60', key: 'Sw60' },
    ];
    return items.map(e => ({
      name: waste.names.get(e.code) || this.defaultWasteName(e.code),
      name: names.get(e.code) || this.defaultWasteName(e.code),
      code: e.code,
      weight: round(n(row[`weight${e.key}`]), 1),
      value: e.code === 'SW62' ? round(n(row.estimatedSalesSw62), 0) : 0,
@@ -446,12 +448,12 @@
  }
  /** 可回收 7 子类全量返回;碳值保留符号(减排可为负),图表侧取绝对值展示。 */
  private buildRecycleItems(row: any, waste: { names: Map<string, string>; codes: Set<string> }) {
  private buildRecycleItems(row: any, names: Map<string, string>) {
    const fallback = ['废纸', '塑料', '金属', '织物', '玻璃', '电子产品', '其他可回收'];
    return ['001', '002', '003', '004', '005', '006', '007']
      .map((idx, i) => ({ idx, i, code: `SW62${idx}` }))
      .map(e => ({
        name: waste.names.get(e.code) || fallback[e.i],
        name: names.get(e.code) || fallback[e.i],
        code: e.code,
        weight: round(n(row[`weightSw62${e.idx}`]), 1),
        value: round(n(row[`estimatedSalesSw62${e.idx}`]), 0),
@@ -462,8 +464,7 @@
  private async buildTrend(
    range: string,
    dates: { start: string; end: string; startTime: string; endTime: string },
    codes: Set<string>
    dates: { start: string; end: string; startTime: string; endTime: string }
  ) {
    const trendStart =
      range === 'today' ? moment().subtract(6, 'day').format('YYYY-MM-DD') : dates.start;
@@ -472,9 +473,9 @@
    const qb = this.dailySite
      .createQueryBuilder('a')
      .select('a.date', 'date')
      .addSelect(`SUM(${this.enabledExpr(codes, f => f.weight)})`, 'weight')
      .addSelect(`SUM(${this.enabledExpr(codes, f => f.sales)})`, 'value')
      .addSelect(`SUM(${this.enabledExpr(codes, f => f.carbon)})`, 'carbon')
      .addSelect(`SUM(${this.kpiExpr(f => f.weight)})`, 'weight')
      .addSelect(`SUM(${this.kpiExpr(f => f.sales)})`, 'value')
      .addSelect(`SUM(${this.kpiExpr(f => f.carbon)})`, 'carbon')
      .where('a.date BETWEEN :start AND :end', { start: trendStart, end: trendEnd })
      .groupBy('a.date')
      .orderBy('a.date', 'ASC');
@@ -527,13 +528,13 @@
    return { axis, trend: { weight, value, carbon } };
  }
  private async buildYearCarbon(codes: Set<string>) {
  private async buildYearCarbon() {
    const start = moment().startOf('year').format('YYYY-MM-DD');
    const end = moment().endOf('year').format('YYYY-MM-DD');
    const qb = this.dailySite
      .createQueryBuilder('a')
      .select('MONTH(a.date)', 'm')
      .addSelect(`SUM(${this.enabledExpr(codes, f => f.carbon)})`, 'carbon')
      .addSelect(`SUM(${this.kpiExpr(f => f.carbon)})`, 'carbon')
      .where('a.date BETWEEN :start AND :end', { start, end })
      .groupBy('MONTH(a.date)');
    this.applyDept(qb, 'a');