tuqin
2026-10-28 dc3dbaebcc54feeaa630cc94ecde223d1da2818c
feat(em): 蓝羽纸业新增能耗管理前端

配套后端(同一基线 09-11)。基于 d1013ed3 创建分支,仅新增能耗模块。

新增内容:
- 能耗管理页面 21 个文件:总览看板、计量点管理、能耗数据采集、考核指标、预警、报表
- company.config.ts 增加 LYZY(新疆蓝羽纸业),接口指向 :9020
已添加21个文件
已修改1个文件
3473 ■■■■■ 文件已修改
company.config.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energy/alert/index.ts 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energy/meter/index.ts 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energy/quota/index.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energy/record/index.ts 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energy/statistics/index.ts 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/alert/data.ts 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/alert/index.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/alert/modules/check-modal.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/alert/modules/handle-modal.vue 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/meter/data.ts 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/meter/index.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/meter/modules/form.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/overview/chart-options.ts 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/overview/index.vue 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/quota/data.ts 365 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/quota/index.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/quota/modules/form.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/record/data.ts 277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/record/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/record/modules/form.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energy/report/index.vue 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts
@@ -59,4 +59,10 @@
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://116.172.73.239:9001/admin-api',
  },
  LYZY: {
    name: '新疆蓝羽纸业',
    logo: '/logos/YZFXLogo.png',
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://36.213.150.237:9020/admin-api',
  },
};
src/api/energy/alert/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace EnergyAlertApi {
  /** èƒ½è€—预警 */
  export interface Alert {
    id?: number;
    alertNo?: string;
    meterId?: number;
    meterCode?: string;
    meterName?: string;
    energyType?: number;
    energyTypeName?: string;
    alertType?: number;
    alertTypeName?: string;
    alertLevel?: number;
    alertLevelName?: string;
    alertDate?: string;
    objectType?: number;
    objectTypeName?: string;
    objectId?: number;
    objectName?: string;
    actualValue?: number;
    thresholdValue?: number;
    unit?: string;
    content?: string;
    status?: number;
    statusName?: string;
    handleUserId?: number;
    handleUserName?: string;
    handleTime?: string;
    handleRemark?: string;
    createTime?: Date;
  }
}
/** æŸ¥è¯¢èƒ½è€—预警分页 */
export function getAlertPage(params: PageParam) {
  return requestClient.get<PageResult<EnergyAlertApi.Alert>>(
    '/mes/em/alert/page',
    { params },
  );
}
/** æŸ¥è¯¢èƒ½è€—预警详情 */
export function getAlert(id: number) {
  return requestClient.get<EnergyAlertApi.Alert>(`/mes/em/alert/get?id=${id}`);
}
/** å¤„理能耗预警 */
export function handleAlert(id: number, handleRemark?: string) {
  return requestClient.put('/mes/em/alert/handle', { id, handleRemark });
}
/** é‡æ–°æ£€æµ‹æŒ‡å®šæ—¥æœŸåŒºé—´çš„能耗预警 */
export function checkAlerts(beginDate: string, endDate: string) {
  return requestClient.post<number>('/mes/em/alert/check', null, {
    params: { beginDate, endDate },
  });
}
src/api/energy/meter/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace EnergyMeterApi {
  /** èƒ½è€—计量点 */
  export interface Meter {
    id?: number;
    code?: string;
    name?: string;
    energyType?: number;
    energyTypeName?: string;
    unit?: string;
    meterNo?: string;
    workshopId?: number;
    workshopName?: string;
    lineId?: number;
    lineName?: string;
    machineryId?: number;
    machineryName?: string;
    ratio?: number;
    initialReading?: number;
    coalFactor?: number;
    leakThreshold?: number;
    chargeUserId?: number;
    chargeUserName?: string;
    installDate?: string;
    status?: number;
    statusName?: string;
    remark?: string;
    createTime?: Date;
  }
  /** ä¸‹æ‹‰é€‰é¡¹ */
  export interface Option {
    id?: number;
    code?: string;
    name?: string;
    energyType?: number;
    unit?: string;
    workshopId?: number;
    workshopName?: string;
    lineId?: number;
    lineName?: string;
  }
}
/** æŸ¥è¯¢èƒ½è€—计量点分页 */
export function getMeterPage(params: PageParam) {
  return requestClient.get<PageResult<EnergyMeterApi.Meter>>(
    '/mes/em/meter/page',
    { params },
  );
}
/** æŸ¥è¯¢èƒ½è€—计量点详情 */
export function getMeter(id: number) {
  return requestClient.get<EnergyMeterApi.Meter>(`/mes/em/meter/get?id=${id}`);
}
/** æ–°å¢žèƒ½è€—计量点 */
export function createMeter(data: EnergyMeterApi.Meter) {
  return requestClient.post<number>('/mes/em/meter/create', data);
}
/** ä¿®æ”¹èƒ½è€—计量点 */
export function updateMeter(data: EnergyMeterApi.Meter) {
  return requestClient.put('/mes/em/meter/update', data);
}
/** åˆ é™¤èƒ½è€—计量点 */
export function deleteMeter(id: number) {
  return requestClient.delete(`/mes/em/meter/delete?id=${id}`);
}
/** æŸ¥è¯¢å¯ç”¨ä¸­çš„能耗计量点(下拉选项) */
export function getMeterSimpleList() {
  return requestClient.get<EnergyMeterApi.Option[]>(
    '/mes/em/meter/simple-list',
  );
}
/** æŸ¥è¯¢è½¦é—´ä¸‹æ‹‰é€‰é¡¹ */
export function getWorkshopOptions() {
  return requestClient.get<EnergyMeterApi.Option[]>(
    '/mes/em/meter/workshop-options',
  );
}
/** æŸ¥è¯¢äº§çº¿ä¸‹æ‹‰é€‰é¡¹ */
export function getLineOptions() {
  return requestClient.get<EnergyMeterApi.Option[]>(
    '/mes/em/meter/line-options',
  );
}
/** æŸ¥è¯¢è®¾å¤‡ä¸‹æ‹‰é€‰é¡¹ */
export function getMachineryOptions() {
  return requestClient.get<EnergyMeterApi.Option[]>(
    '/mes/em/meter/machinery-options',
  );
}
src/api/energy/quota/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace EnergyQuotaApi {
  /** èƒ½è€—考核指标 */
  export interface Quota {
    id?: number;
    name?: string;
    energyType?: number;
    energyTypeName?: string;
    objectType?: number;
    objectTypeName?: string;
    objectId?: number;
    objectName?: string;
    periodType?: number;
    periodTypeName?: string;
    quotaValue?: number;
    unit?: string;
    warnRatio?: number;
    alarmRatio?: number;
    momWarnRatio?: number;
    yoyWarnRatio?: number;
    status?: number;
    statusName?: string;
    remark?: string;
    createTime?: Date;
  }
}
/** æŸ¥è¯¢èƒ½è€—考核指标分页 */
export function getQuotaPage(params: PageParam) {
  return requestClient.get<PageResult<EnergyQuotaApi.Quota>>(
    '/mes/em/quota/page',
    { params },
  );
}
/** æŸ¥è¯¢èƒ½è€—考核指标详情 */
export function getQuota(id: number) {
  return requestClient.get<EnergyQuotaApi.Quota>(`/mes/em/quota/get?id=${id}`);
}
/** æ–°å¢žèƒ½è€—考核指标 */
export function createQuota(data: EnergyQuotaApi.Quota) {
  return requestClient.post<number>('/mes/em/quota/create', data);
}
/** ä¿®æ”¹èƒ½è€—考核指标 */
export function updateQuota(data: EnergyQuotaApi.Quota) {
  return requestClient.put('/mes/em/quota/update', data);
}
/** åˆ é™¤èƒ½è€—考核指标 */
export function deleteQuota(id: number) {
  return requestClient.delete(`/mes/em/quota/delete?id=${id}`);
}
src/api/energy/record/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace EnergyRecordApi {
  /** èƒ½è€—抄表记录 */
  export interface Record {
    id?: number;
    meterId?: number;
    meterCode?: string;
    meterName?: string;
    energyType?: number;
    energyTypeName?: string;
    unit?: string;
    ratio?: number;
    coalFactor?: number;
    workshopId?: number;
    workshopName?: string;
    lineId?: number;
    lineName?: string;
    machineryId?: number;
    machineryName?: string;
    recordDate?: string;
    prevReading?: number;
    reading?: number;
    usageAmount?: number;
    price?: number;
    cost?: number;
    source?: number;
    sourceName?: string;
    remark?: string;
    createTime?: Date;
  }
  /** ä¸ŠæœŸè¯»æ•° */
  export interface LastReading {
    prevReading?: number;
    lastDate?: string;
  }
}
/** æŸ¥è¯¢èƒ½è€—抄表记录分页 */
export function getRecordPage(params: PageParam) {
  return requestClient.get<PageResult<EnergyRecordApi.Record>>(
    '/mes/em/record/page',
    { params },
  );
}
/** æŸ¥è¯¢èƒ½è€—抄表记录详情 */
export function getRecord(id: number) {
  return requestClient.get<EnergyRecordApi.Record>(
    `/mes/em/record/get?id=${id}`,
  );
}
/** æ–°å¢žèƒ½è€—抄表记录 */
export function createRecord(data: EnergyRecordApi.Record) {
  return requestClient.post<number>('/mes/em/record/create', data);
}
/** ä¿®æ”¹èƒ½è€—抄表记录 */
export function updateRecord(data: EnergyRecordApi.Record) {
  return requestClient.put('/mes/em/record/update', data);
}
/** åˆ é™¤èƒ½è€—抄表记录 */
export function deleteRecord(id: number) {
  return requestClient.delete(`/mes/em/record/delete?id=${id}`);
}
/** æŸ¥è¯¢è®¡é‡ç‚¹ä¸ŠæœŸè¯»æ•° */
export function getLastReading(
  meterId: number,
  recordDate: string,
  excludeId?: number,
) {
  return requestClient.get<EnergyRecordApi.LastReading>(
    '/mes/em/record/last-reading',
    { params: { meterId, recordDate, excludeId } },
  );
}
src/api/energy/statistics/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
import { requestClient } from '#/api/request';
export namespace EnergyStatisticsApi {
  /** èƒ½æºç±»åž‹ç»Ÿè®¡ */
  export interface TypeStat {
    energyType?: number;
    energyTypeName?: string;
    usage?: number;
    unit?: string;
    cost?: number;
    tce?: number;
  }
  /** è½¦é—´èƒ½è€—统计 */
  export interface WorkshopStat {
    workshopId?: number;
    workshopName?: string;
    tce?: number;
    cost?: number;
  }
  /** èƒ½è€—趋势点 */
  export interface TrendPoint {
    date?: string;
    tce?: number;
    cost?: number;
    electricity?: number;
    water?: number;
    fuel?: number;
    gas?: number;
  }
  /** é‡ç‚¹ç”¨èƒ½è®¾å¤‡ */
  export interface MeterStat {
    meterId?: number;
    meterName?: string;
    energyType?: number;
    energyTypeName?: string;
    usage?: number;
    unit?: string;
    cost?: number;
    tce?: number;
  }
  /** æœ€è¿‘预警 */
  export interface RecentAlert {
    id?: number;
    content?: string;
    alertLevel?: number;
    alertLevelName?: string;
    alertDate?: string;
    status?: number;
    statusName?: string;
  }
  /** èƒ½è€—总览 */
  export interface Overview {
    totalTce?: number;
    totalCost?: number;
    meterCount?: number;
    pendingAlertCount?: number;
    typeStats?: TypeStat[];
    workshopStats?: WorkshopStat[];
    trend?: TrendPoint[];
    topMeters?: MeterStat[];
    recentAlerts?: RecentAlert[];
  }
  /** æŠ¥è¡¨-按能源类型 */
  export interface TypeRow {
    energyType?: number;
    energyTypeName?: string;
    usage?: number;
    unit?: string;
    cost?: number;
    tce?: number;
    lastMonthUsage?: number;
    momRate?: number;
    lastYearUsage?: number;
    yoyRate?: number;
  }
  /** æŠ¥è¡¨-按车间 */
  export interface WorkshopRow {
    workshopId?: number;
    workshopName?: string;
    cost?: number;
    tce?: number;
    lastMonthTce?: number;
    momRate?: number;
    lastYearTce?: number;
    yoyRate?: number;
  }
  /** æœˆåº¦èƒ½è€—报表 */
  export interface Report {
    period?: string;
    beginDate?: string;
    endDate?: string;
    totalTce?: number;
    totalCost?: number;
    lastMonthTce?: number;
    momRate?: number;
    lastYearTce?: number;
    yoyRate?: number;
    typeRows?: TypeRow[];
    workshopRows?: WorkshopRow[];
  }
}
/** æŸ¥è¯¢èƒ½è€—总览 */
export function getOverview(params: { beginDate?: string; endDate?: string }) {
  return requestClient.get<EnergyStatisticsApi.Overview>(
    '/mes/em/statistics/overview',
    { params },
  );
}
/** æŸ¥è¯¢æœˆåº¦èƒ½è€—报表 */
export function getReport(period: string) {
  return requestClient.get<EnergyStatisticsApi.Report>(
    '/mes/em/statistics/report',
    { params: { period } },
  );
}
src/views/energy/alert/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyAlertApi } from '#/api/energy/alert';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'meterName',
      label: '计量点',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入计量点名称',
      },
    },
    {
      fieldName: 'energyType',
      label: '能源类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ENERGY_TYPE, 'number'),
        placeholder: '请选择能源类型',
      },
    },
    {
      fieldName: 'alertType',
      label: '预警类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ALERT_TYPE, 'number'),
        placeholder: '请选择预警类型',
      },
    },
    {
      fieldName: 'alertLevel',
      label: '预警级别',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ALERT_LEVEL, 'number'),
        placeholder: '请选择预警级别',
      },
    },
    {
      fieldName: 'status',
      label: '处理状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ALERT_STATUS, 'number'),
        placeholder: '请选择处理状态',
      },
    },
    {
      fieldName: 'alertDate',
      label: '预警日期',
      component: 'RangePicker',
      componentProps: {
        class: 'w-full',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<EnergyAlertApi.Alert>['columns'] {
  return [
    { field: 'alertNo', title: '预警编号', width: 170 },
    { field: 'alertDate', title: '预警日期', width: 110 },
    { field: 'meterName', title: '计量点/对象', minWidth: 170 },
    {
      field: 'energyType',
      title: '能源类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_ENERGY_TYPE },
      },
    },
    {
      field: 'alertType',
      title: '预警类型',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_ALERT_TYPE },
      },
    },
    {
      field: 'alertLevel',
      title: '级别',
      width: 90,
      slots: {
        default: 'alertLevel',
      },
    },
    {
      field: 'actualValue',
      title: '实际值 / é˜ˆå€¼',
      width: 180,
      slots: {
        default: 'valueCompare',
      },
    },
    { field: 'content', title: '预警内容', minWidth: 260, showOverflow: 'tooltip' },
    {
      field: 'status',
      title: '处理状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_ALERT_STATUS },
      },
    },
    { field: 'handleUserName', title: '处理人', width: 100 },
    {
      field: 'handleTime',
      title: '处理时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/energy/alert/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyAlertApi } from '#/api/energy/alert';
import { Page, useVbenModal } from '@vben/common-ui';
import { Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAlertPage } from '#/api/energy/alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import CheckModalComponent from './modules/check-modal.vue';
import HandleModalComponent from './modules/handle-modal.vue';
defineOptions({ name: 'EnergyAlert' });
const [HandleModal, handleModalApi] = useVbenModal({
  connectedComponent: HandleModalComponent,
  destroyOnClose: true,
});
const [CheckModal, checkModalApi] = useVbenModal({
  connectedComponent: CheckModalComponent,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** é‡æ–°æ£€æµ‹é¢„è­¦ */
function handleCheck() {
  checkModalApi.open();
}
/** å¤„理预警 */
function handleHandle(row: EnergyAlertApi.Alert) {
  handleModalApi.setData({ id: row.id }).open();
}
/** æŸ¥çœ‹é¢„è­¦ */
function handleDetail(row: EnergyAlertApi.Alert) {
  handleModalApi.setData({ id: row.id, readonly: true }).open();
}
/** é¢„警级别标签颜色 */
function getLevelColor(level?: number) {
  if (level === 3) {
    return 'red';
  }
  return level === 2 ? 'orange' : 'blue';
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getAlertPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<EnergyAlertApi.Alert>,
});
</script>
<template>
  <Page auto-content-height>
    <HandleModal @success="handleRefresh" />
    <CheckModal @success="handleRefresh" />
    <Grid table-title="能耗预警列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '重新检测',
              type: 'primary',
              icon: ACTION_ICON.SEARCH,
              auth: ['mes:em-alert:handle'],
              onClick: handleCheck,
            },
          ]"
        />
      </template>
      <template #alertLevel="{ row }">
        <Tag :color="getLevelColor(row.alertLevel)">
          {{ row.alertLevelName || '-' }}
        </Tag>
      </template>
      <template #valueCompare="{ row }">
        <span>
          {{ row.actualValue ?? '-' }}{{ row.unit || '' }}
          <span class="text-muted-foreground">
            / {{ row.thresholdValue ?? '-' }}{{ row.unit || '' }}
          </span>
        </span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '处理',
              type: 'link',
              auth: ['mes:em-alert:handle'],
              ifShow: row.status === 0,
              onClick: handleHandle.bind(null, row),
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/energy/alert/modules/check-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenForm } from '#/adapter/form';
import { checkAlerts } from '#/api/energy/alert';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-1',
  layout: 'horizontal',
  schema: [
    {
      fieldName: 'range',
      label: '检测日期区间',
      component: 'RangePicker',
      componentProps: {
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
      defaultValue: [
        dayjs().startOf('month').format('YYYY-MM-DD'),
        dayjs().format('YYYY-MM-DD'),
      ],
      help: '按该区间内的抄表记录重新执行定额、环比、同比、跑冒滴漏检测',
      rules: 'required',
    },
  ],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const values = await formApi.getValues();
    const range = values.range as string[] | undefined;
    if (!range?.[0] || !range?.[1]) {
      message.warning('请选择检测日期区间');
      return;
    }
    modalApi.lock();
    try {
      const count = await checkAlerts(range[0], range[1]);
      emit('success');
      message.success(`检测完成,共检测 ${count ?? 0} æ¡æŠ„表记录`);
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal title="重新检测能耗预警" class="w-1/3">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/energy/alert/modules/handle-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,117 @@
<script lang="ts" setup>
import type { EnergyAlertApi } from '#/api/energy/alert';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Descriptions, message, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getAlert, handleAlert } from '#/api/energy/alert';
const emit = defineEmits(['success']);
const alert = ref<EnergyAlertApi.Alert>();
const readonly = ref(false);
const getTitle = computed(() =>
  readonly.value ? '查看能耗预警' : '处理能耗预警',
);
const levelColor = computed(() => {
  const level = alert.value?.alertLevel;
  return level === 3 ? 'red' : level === 2 ? 'orange' : 'blue';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 90,
  },
  wrapperClass: 'grid-cols-1',
  layout: 'horizontal',
  schema: [
    {
      fieldName: 'handleRemark',
      label: '处理说明',
      component: 'Textarea',
      componentProps: {
        placeholder: '请填写处理措施,如:已安排检修、更换密封件等',
        rows: 4,
      },
      rules: 'required',
    },
  ],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (readonly.value || !alert.value?.id) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    try {
      const values = await formApi.getValues();
      await handleAlert(alert.value.id, values.handleRemark as string);
      emit('success');
      message.success('预警已处理');
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      alert.value = undefined;
      return;
    }
    const data = modalApi.getData<{ id: number; readonly?: boolean }>();
    readonly.value = !!data.readonly;
    formApi.setDisabled(readonly.value);
    modalApi.setState({ showConfirmButton: !readonly.value });
    modalApi.lock();
    try {
      alert.value = await getAlert(data.id);
      await formApi.setValues({
        handleRemark: alert.value?.handleRemark,
      });
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/5">
    <div class="mx-4 mb-4">
      <Descriptions :column="1" bordered size="small">
        <Descriptions.Item label="预警编号">
          {{ alert?.alertNo || '-' }}
        </Descriptions.Item>
        <Descriptions.Item label="计量点/对象">
          {{ alert?.meterName || '-' }}
        </Descriptions.Item>
        <Descriptions.Item label="预警类型">
          {{ alert?.alertTypeName || '-' }}
        </Descriptions.Item>
        <Descriptions.Item label="预警级别">
          <Tag :color="levelColor">{{ alert?.alertLevelName || '-' }}</Tag>
        </Descriptions.Item>
        <Descriptions.Item label="预警内容">
          {{ alert?.content || '-' }}
        </Descriptions.Item>
        <Descriptions.Item v-if="alert?.handleUserName" label="处理人">
          {{ alert.handleUserName }} {{ alert.handleTime }}
        </Descriptions.Item>
      </Descriptions>
    </div>
    <Form class="mx-4" />
  </Modal>
</template>
src/views/energy/meter/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,352 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyMeterApi } from '#/api/energy/meter';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import {
  getLineOptions,
  getMachineryOptions,
  getWorkshopOptions,
} from '#/api/energy/meter';
import { getSimpleUserList } from '#/api/system/user';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** èƒ½æºç±»åž‹é»˜è®¤è®¡é‡å•位与折标煤系数 */
export const ENERGY_DEFAULTS: Record<
  number,
  { coalFactor: number; unit: string }
> = {
  1: { coalFactor: 0.1229, unit: 'kWh' },
  2: { coalFactor: 0.2429, unit: 't' },
  3: { coalFactor: 1.2, unit: 'L' },
  4: { coalFactor: 1.33, unit: 'm³' },
};
/** æ–°å¢ž/修改计量点的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '计量点编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计量点编码,如 EM-DL-001',
      },
      dependencies: {
        triggerFields: ['id'],
        componentProps: (values) => ({ disabled: !!values.id }),
      },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '计量点名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计量点名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'energyType',
      label: '能源类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ENERGY_TYPE, 'number'),
        placeholder: '请选择能源类型',
      },
      help: '切换能源类型后,计量单位与折标煤系数会自动带入默认值',
      rules: 'selectRequired',
    },
    {
      fieldName: 'unit',
      label: '计量单位',
      component: 'Input',
      componentProps: {
        placeholder: '如 kWh、t、L、m³',
      },
      rules: 'required',
    },
    {
      fieldName: 'meterNo',
      label: '表计编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入现场表计编号',
      },
    },
    {
      fieldName: 'workshopId',
      label: '所属车间',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getWorkshopOptions,
        labelField: 'name',
        placeholder: '请选择所属车间',
        valueField: 'id',
      },
    },
    {
      fieldName: 'lineId',
      label: '所属产线',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getLineOptions,
        labelField: 'name',
        placeholder: '请选择所属产线',
        valueField: 'id',
      },
    },
    {
      fieldName: 'machineryId',
      label: '关联设备',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getMachineryOptions,
        labelField: 'name',
        placeholder: '请选择重点用能设备',
        valueField: 'id',
      },
      help: '选择设备后,若未选车间,系统会按设备归属自动补齐车间',
    },
    {
      fieldName: 'ratio',
      label: '倍率',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 4,
        placeholder: '请输入倍率',
      },
      help: '用量 = (期末读数 - æœŸåˆè¯»æ•°) Ã— å€çއ',
      rules: z.number().default(1),
    },
    {
      fieldName: 'initialReading',
      label: '初始读数',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入初始读数',
      },
      help: '第一条抄表记录的期初读数',
      rules: z.number().default(0),
    },
    {
      fieldName: 'coalFactor',
      label: '折标煤系数',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 4,
        placeholder: '请输入折标煤系数',
      },
      help: '吨标煤/计量单位,默认按能源类型带入',
    },
    {
      fieldName: 'leakThreshold',
      label: '跑冒滴漏阈值',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入单日用量阈值',
      },
      help: '单日用量超过该值时触发「疑似跑冒滴漏」预警;留空则不检测',
    },
    {
      fieldName: 'chargeUserId',
      label: '责任人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择责任人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'installDate',
      label: '安装日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        disabled: formType === 'detail',
        format: 'YYYY-MM-DD',
        placeholder: '请选择安装日期',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.MES_EM_METER_STATUS, 'number'),
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '计量点编码',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入计量点编码',
      },
    },
    {
      fieldName: 'name',
      label: '计量点名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入计量点名称',
      },
    },
    {
      fieldName: 'energyType',
      label: '能源类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ENERGY_TYPE, 'number'),
        placeholder: '请选择能源类型',
      },
    },
    {
      fieldName: 'workshopId',
      label: '所属车间',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getWorkshopOptions,
        labelField: 'name',
        placeholder: '请选择所属车间',
        valueField: 'id',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_METER_STATUS, 'number'),
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<EnergyMeterApi.Meter>['columns'] {
  return [
    {
      field: 'code',
      title: '计量点编码',
      minWidth: 140,
      slots: {
        default: 'code',
      },
    },
    { field: 'name', title: '计量点名称', minWidth: 170 },
    {
      field: 'energyType',
      title: '能源类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_ENERGY_TYPE },
      },
    },
    { field: 'unit', title: '单位', width: 80 },
    { field: 'meterNo', title: '表计编号', width: 120 },
    { field: 'workshopName', title: '所属车间', width: 120 },
    { field: 'lineName', title: '所属产线', width: 120 },
    { field: 'machineryName', title: '关联设备', minWidth: 140 },
    {
      field: 'ratio',
      title: '倍率',
      width: 90,
      align: 'right',
    },
    {
      field: 'coalFactor',
      title: '折标煤系数',
      width: 110,
      align: 'right',
    },
    {
      field: 'leakThreshold',
      title: '滴漏阈值',
      width: 100,
      align: 'right',
    },
    { field: 'chargeUserName', title: '责任人', width: 100 },
    {
      field: 'status',
      title: '状态',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_METER_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 170,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/energy/meter/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyMeterApi } from '#/api/energy/meter';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteMeter, getMeterPage } from '#/api/energy/meter';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'EnergyMeter' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ–°å¢žè®¡é‡ç‚¹ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è®¡é‡ç‚¹ */
function handleDetail(row: EnergyMeterApi.Meter) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑计量点 */
function handleEdit(row: EnergyMeterApi.Meter) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤è®¡é‡ç‚¹ */
async function handleDelete(row: EnergyMeterApi.Meter) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteMeter(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getMeterPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<EnergyMeterApi.Meter>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="能耗计量点列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['能耗计量点']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:em-meter:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #code="{ row }">
        <a @click="handleDetail(row)">{{ row.code }}</a>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:em-meter:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:em-meter:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/energy/meter/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { EnergyMeterApi } from '#/api/energy/meter';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createMeter, getMeter, updateMeter } from '#/api/energy/meter';
import { $t } from '#/locales';
import { ENERGY_DEFAULTS, useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const lastEnergyType = ref<number>();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看能耗计量点';
  }
  return formType.value === 'update' ? '修改能耗计量点' : '新增能耗计量点';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  handleValuesChange: (values, changedFields) => {
    if (!changedFields.includes('energyType')) {
      return;
    }
    const current = values.energyType as number | undefined;
    const previous = lastEnergyType.value;
    lastEnergyType.value = current;
    if (!current) {
      return;
    }
    const config = ENERGY_DEFAULTS[current];
    const previousConfig = previous ? ENERGY_DEFAULTS[previous] : undefined;
    if (!config) {
      return;
    }
    // ä»…在单位/系数为空或仍是上一能源类型的默认值时替换,避免覆盖手工调整值
    if (!values.unit || values.unit === previousConfig?.unit) {
      formApi.setFieldValue('unit', config.unit);
    }
    if (
      values.coalFactor === undefined ||
      values.coalFactor === null ||
      values.coalFactor === previousConfig?.coalFactor
    ) {
      formApi.setFieldValue('coalFactor', config.coalFactor);
    }
  },
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as EnergyMeterApi.Meter;
    try {
      if (formType.value === 'create') {
        await createMeter(data);
      } else {
        await updateMeter(data);
      }
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      lastEnergyType.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema(formType.value) });
    formApi.setDisabled(formType.value === 'detail');
    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      const meter = await getMeter(data.id);
      lastEnergyType.value = meter.energyType;
      await formApi.setValues(meter);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/energy/overview/chart-options.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
import type { EChartsOption } from '@vben/plugins/echarts';
/** èƒ½è€—趋势:综合能耗柱状 + æˆæœ¬æŠ˜çº¿ï¼ˆåŒ Y è½´ï¼‰ */
export function getEnergyTrendChartOptions(
  dates: string[],
  tce: number[],
  cost: number[],
): EChartsOption {
  return {
    grid: { bottom: 50, left: 60, right: 60, top: 30 },
    legend: { bottom: 0, data: ['综合能耗', '能耗成本'] },
    series: [
      {
        barMaxWidth: 28,
        data: tce,
        itemStyle: { borderRadius: [4, 4, 0, 0], color: '#409EFF' },
        name: '综合能耗',
        type: 'bar',
      },
      {
        data: cost,
        itemStyle: { color: '#E6A23C' },
        name: '能耗成本',
        smooth: true,
        type: 'line',
        yAxisIndex: 1,
      },
    ],
    tooltip: { axisPointer: { type: 'cross' }, trigger: 'axis' },
    xAxis: {
      data: dates,
      type: 'category',
      axisLabel: { hideOverlap: true },
    },
    yAxis: [
      { name: '吨标煤', type: 'value' },
      {
        name: '元',
        splitLine: { show: false },
        type: 'value',
      },
    ],
  };
}
/** èƒ½æºç»“构:按折标煤占比的饼图 */
export function getEnergyTypeChartOptions(
  data: Array<{ color?: string; name: string; value: number }>,
): EChartsOption {
  return {
    legend: { bottom: 0, type: 'scroll' },
    series: [
      {
        avoidLabelOverlap: true,
        data,
        emphasis: { label: { fontSize: 14, fontWeight: 'bold', show: true } },
        itemStyle: { borderColor: '#fff', borderRadius: 6, borderWidth: 2 },
        label: { formatter: '{b}\n{d}%', show: true },
        radius: ['42%', '70%'],
        type: 'pie',
      },
    ],
    tooltip: { formatter: '{b}: {c} å¨æ ‡ç…¤ ({d}%)', trigger: 'item' },
  };
}
/** è½¦é—´èƒ½è€—排行:横向柱状图 */
export function getWorkshopChartOptions(
  names: string[],
  values: number[],
): EChartsOption {
  return {
    grid: { bottom: 20, left: 100, right: 40, top: 10 },
    series: [
      {
        barMaxWidth: 18,
        data: values,
        itemStyle: { borderRadius: [0, 4, 4, 0], color: '#7C3AED' },
        label: { position: 'right', show: true },
        name: '综合能耗',
        type: 'bar',
      },
    ],
    tooltip: {
      axisPointer: { type: 'shadow' },
      formatter: '{b}: {c} å¨æ ‡ç…¤',
      trigger: 'axis',
    },
    xAxis: { name: '吨标煤', type: 'value' },
    yAxis: { data: names, inverse: true, type: 'category' },
  };
}
src/views/energy/overview/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
<script lang="ts" setup>
import type { TableColumnsType } from 'ant-design-vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { EnergyStatisticsApi } from '#/api/energy/statistics';
import { computed, h, onMounted, ref } from 'vue';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Card, Col, RangePicker, Row, Spin, Table, Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
import { getOverview } from '#/api/energy/statistics';
import {
  getEnergyTrendChartOptions,
  getEnergyTypeChartOptions,
  getWorkshopChartOptions,
} from './chart-options';
defineOptions({ name: 'EnergyOverview' });
type DateRange = [string, string] | undefined;
/** èƒ½æºç±»åž‹é¢œè‰² */
const ENERGY_COLORS: Record<number, string> = {
  1: '#409EFF',
  2: '#36CFC9',
  3: '#E6A23C',
  4: '#67C23A',
};
const loading = ref(false);
const dateRange = ref<DateRange>([
  dayjs().startOf('month').format('YYYY-MM-DD'),
  dayjs().format('YYYY-MM-DD'),
]);
const overview = ref<EnergyStatisticsApi.Overview>({});
const trendChartRef = ref<EchartsUIType>();
const typeChartRef = ref<EchartsUIType>();
const workshopChartRef = ref<EchartsUIType>();
const { renderEcharts: renderTrend } = useEcharts(trendChartRef);
const { renderEcharts: renderType } = useEcharts(typeChartRef);
const { renderEcharts: renderWorkshop } = useEcharts(workshopChartRef);
/** æŠ˜æ ‡ç…¤åˆè®¡ï¼ˆå¨ï¼‰ */
const totalTce = computed(() => Number(overview.value.totalTce ?? 0));
/** èƒ½è€—成本合计(元) */
const totalCost = computed(() => Number(overview.value.totalCost ?? 0));
/** ç”¨é‡æœ€å¤§çš„能源类型 */
const topEnergyType = computed(() => {
  const stats = overview.value.typeStats ?? [];
  return [...stats].sort((a, b) => Number(b.tce ?? 0) - Number(a.tce ?? 0))[0];
});
const topMeterColumns: TableColumnsType<EnergyStatisticsApi.MeterStat> = [
  { title: '排名', width: 60, key: 'rank', align: 'center' },
  { title: '计量点', dataIndex: 'meterName', ellipsis: true },
  { title: '能源类型', dataIndex: 'energyTypeName', width: 90 },
  {
    title: '折标煤(吨)',
    dataIndex: 'tce',
    width: 110,
    align: 'right',
    customRender: ({ record }) => formatNumber(record.tce),
  },
  {
    title: '费用(元)',
    dataIndex: 'cost',
    width: 120,
    align: 'right',
    customRender: ({ record }) => formatNumber(record.cost),
  },
];
const alertColumns: TableColumnsType<EnergyStatisticsApi.RecentAlert> = [
  { title: '预警日期', dataIndex: 'alertDate', width: 110 },
  {
    title: '级别',
    dataIndex: 'alertLevel',
    width: 90,
    customRender: ({ record }) => {
      const level = record.alertLevel;
      const color = level === 3 ? 'red' : level === 2 ? 'orange' : 'blue';
      return h(Tag, { color }, () => record.alertLevelName || '-');
    },
  },
  { title: '预警内容', dataIndex: 'content', ellipsis: true },
  {
    title: '状态',
    dataIndex: 'status',
    width: 100,
    customRender: ({ record }) => {
      const color = record.status === 0 ? 'red' : 'green';
      return h(Tag, { color }, () => record.statusName || '-');
    },
  },
];
/** æ•°å­—格式化 */
function formatNumber(value?: number, digits = 2) {
  if (value === undefined || value === null) {
    return '-';
  }
  return Number(value).toLocaleString('zh-CN', {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  });
}
/** åŠ è½½èƒ½è€—æ€»è§ˆ */
async function loadData() {
  loading.value = true;
  try {
    const beginDate = dateRange.value?.[0];
    const endDate = dateRange.value?.[1];
    overview.value = await getOverview({ beginDate, endDate });
    // è¶‹åŠ¿å›¾
    const trend = overview.value.trend ?? [];
    await renderTrend(
      getEnergyTrendChartOptions(
        trend.map((item) => item.date?.slice(5) ?? ''),
        trend.map((item) => Number(item.tce ?? 0)),
        trend.map((item) => Number(item.cost ?? 0)),
      ),
    );
    // èƒ½æºç»“构饼图
    const typeStats = overview.value.typeStats ?? [];
    await renderType(
      getEnergyTypeChartOptions(
        typeStats.map((item) => ({
          color: ENERGY_COLORS[item.energyType ?? 0],
          name: item.energyTypeName ?? '',
          value: Number(item.tce ?? 0),
        })),
      ),
    );
    // è½¦é—´æŽ’行(按折标煤倒序)
    const workshops = [...(overview.value.workshopStats ?? [])].sort(
      (a, b) => Number(b.tce ?? 0) - Number(a.tce ?? 0),
    );
    await renderWorkshop(
      getWorkshopChartOptions(
        workshops.map((item) => item.workshopName ?? '未归属车间'),
        workshops.map((item) => Number(item.tce ?? 0)),
      ),
    );
  } finally {
    loading.value = false;
  }
}
/** åˆ‡æ¢ç»Ÿè®¡åŒºé—´ */
function handleRangeChange() {
  loadData();
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Page>
    <Card class="mb-4" size="small">
      <div class="flex flex-wrap items-center gap-3">
        <span class="text-muted-foreground text-sm">统计区间</span>
        <RangePicker
          v-model:value="dateRange"
          :allow-clear="false"
          class="w-[260px]"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          @change="handleRangeChange"
        />
        <span class="text-muted-foreground text-xs">
          é»˜è®¤ç»Ÿè®¡æœ¬æœˆè‡³ä»Šï¼›ç»¼åˆèƒ½è€—按各能源折标煤系数折算
        </span>
      </div>
    </Card>
    <Spin :spinning="loading">
      <!-- KPI æŒ‡æ ‡ -->
      <Row :gutter="16">
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="flex items-center gap-4">
              <div
                class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
                style="background: linear-gradient(135deg, #409eff, #66b1ff)"
              >
                <IconifyIcon class="size-7" icon="lucide:leaf" />
              </div>
              <div>
                <div class="text-muted-foreground mb-1 text-sm">综合能耗</div>
                <div class="flex items-baseline gap-1">
                  <CountTo
                    class="text-2xl font-bold leading-tight text-[#409eff]"
                    :decimals="2"
                    :duration="1500"
                    :end-val="totalTce"
                  />
                  <span class="text-muted-foreground text-xs">吨标煤</span>
                </div>
                <div class="text-muted-foreground mt-1 text-xs">
                  <template v-if="topEnergyType">
                    å æ¯”最高:{{ topEnergyType.energyTypeName }}
                  </template>
                  <template v-else>暂无能耗数据</template>
                </div>
              </div>
            </div>
          </Card>
        </Col>
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="flex items-center gap-4">
              <div
                class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
                style="background: linear-gradient(135deg, #e6a23c, #ebb563)"
              >
                <IconifyIcon class="size-7" icon="lucide:coins" />
              </div>
              <div>
                <div class="text-muted-foreground mb-1 text-sm">能耗成本</div>
                <div class="flex items-baseline gap-1">
                  <CountTo
                    class="text-2xl font-bold leading-tight text-[#e6a23c]"
                    :decimals="2"
                    :duration="1500"
                    :end-val="totalCost"
                  />
                  <span class="text-muted-foreground text-xs">元</span>
                </div>
                <div class="text-muted-foreground mt-1 text-xs">
                  æŒ‰æŠ„表记录单价归集
                </div>
              </div>
            </div>
          </Card>
        </Col>
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="flex items-center gap-4">
              <div
                class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
                style="background: linear-gradient(135deg, #67c23a, #85ce61)"
              >
                <IconifyIcon class="size-7" icon="lucide:gauge" />
              </div>
              <div>
                <div class="text-muted-foreground mb-1 text-sm">启用计量点</div>
                <div class="flex items-baseline gap-1">
                  <CountTo
                    class="text-2xl font-bold leading-tight text-[#67c23a]"
                    :duration="1500"
                    :end-val="overview.meterCount ?? 0"
                  />
                  <span class="text-muted-foreground text-xs">个</span>
                </div>
                <div class="text-muted-foreground mt-1 text-xs">
                  è¦†ç›–电、水、燃油、天然气
                </div>
              </div>
            </div>
          </Card>
        </Col>
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="flex items-center gap-4">
              <div
                class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
                style="background: linear-gradient(135deg, #f56c6c, #f89898)"
              >
                <IconifyIcon class="size-7" icon="lucide:bell-ring" />
              </div>
              <div>
                <div class="text-muted-foreground mb-1 text-sm">待处理预警</div>
                <div class="flex items-baseline gap-1">
                  <CountTo
                    class="text-2xl font-bold leading-tight text-[#f56c6c]"
                    :duration="1500"
                    :end-val="overview.pendingAlertCount ?? 0"
                  />
                  <span class="text-muted-foreground text-xs">条</span>
                </div>
                <div class="text-muted-foreground mt-1 text-xs">
                  è¶…定额、环比/同比异常、跑冒滴漏
                </div>
              </div>
            </div>
          </Card>
        </Col>
      </Row>
      <!-- è¶‹åŠ¿ + èƒ½æºç»“æž„ -->
      <Row :gutter="16" class="mb-4">
        <Col :lg="16" :md="24" :sm="24" :xs="24" class="mb-4">
          <Card title="能耗趋势">
            <EchartsUI ref="trendChartRef" class="h-[320px] w-full" />
          </Card>
        </Col>
        <Col :lg="8" :md="24" :sm="24" :xs="24" class="mb-4">
          <Card title="能源结构(折标煤)">
            <EchartsUI ref="typeChartRef" class="h-[320px] w-full" />
          </Card>
        </Col>
      </Row>
      <!-- è½¦é—´æŽ’行 + é‡ç‚¹è®¾å¤‡ -->
      <Row :gutter="16" class="mb-4">
        <Col :lg="12" :md="24" :sm="24" :xs="24" class="mb-4">
          <Card title="车间能耗排行">
            <EchartsUI ref="workshopChartRef" class="h-[320px] w-full" />
          </Card>
        </Col>
        <Col :lg="12" :md="24" :sm="24" :xs="24" class="mb-4">
          <Card title="重点用能设备 TOP10">
            <Table
              :columns="topMeterColumns"
              :data-source="overview.topMeters ?? []"
              :pagination="false"
              :scroll="{ y: 275 }"
              size="small"
            >
              <template #bodyCell="{ column, index }">
                <template v-if="column.key === 'rank'">
                  {{ index + 1 }}
                </template>
              </template>
            </Table>
          </Card>
        </Col>
      </Row>
      <!-- æœ€è¿‘预警 -->
      <Card title="最近预警">
        <Table
          :columns="alertColumns"
          :data-source="overview.recentAlerts ?? []"
          :pagination="false"
          size="small"
        />
      </Card>
    </Spin>
  </Page>
</template>
src/views/energy/quota/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,365 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyQuotaApi } from '#/api/energy/quota';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import {
  getLineOptions,
  getMachineryOptions,
  getWorkshopOptions,
} from '#/api/energy/meter';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改考核指标的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'name',
      label: '指标名称',
      component: 'Input',
      componentProps: {
        placeholder: '如 1#车间用电日定额',
      },
      rules: 'required',
    },
    {
      fieldName: 'energyType',
      label: '能源类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ENERGY_TYPE, 'number'),
        placeholder: '请选择能源类型',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'objectType',
      label: '考核对象类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_EM_QUOTA_OBJECT_TYPE, 'number'),
        placeholder: '请选择考核对象类型',
      },
      help: '对象越具体优先级越高:设备 > äº§çº¿ > è½¦é—´ > å…¨åŽ‚',
      rules: 'selectRequired',
    },
    {
      fieldName: 'workshopId',
      label: '考核车间',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getWorkshopOptions,
        labelField: 'name',
        placeholder: '请选择考核车间',
        valueField: 'id',
      },
      dependencies: {
        triggerFields: ['objectType'],
        show: (values) => values.objectType === 1,
      },
    },
    {
      fieldName: 'lineId',
      label: '考核产线',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getLineOptions,
        labelField: 'name',
        placeholder: '请选择考核产线',
        valueField: 'id',
      },
      dependencies: {
        triggerFields: ['objectType'],
        show: (values) => values.objectType === 2,
      },
    },
    {
      fieldName: 'machineryId',
      label: '考核设备',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getMachineryOptions,
        labelField: 'name',
        placeholder: '请选择考核设备',
        valueField: 'id',
      },
      dependencies: {
        triggerFields: ['objectType'],
        show: (values) => values.objectType === 3,
      },
    },
    {
      fieldName: 'periodType',
      label: '考核周期',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_EM_QUOTA_PERIOD, 'number'),
        placeholder: '请选择考核周期',
      },
      help: '日定额按当日用量比较;月定额按当月累计用量比较',
      rules: 'selectRequired',
    },
    {
      fieldName: 'quotaValue',
      label: '定额值',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入定额值',
      },
      rules: 'required',
    },
    {
      fieldName: 'unit',
      label: '计量单位',
      component: 'Input',
      componentProps: {
        placeholder: '如 kWh、t、L、m³',
      },
      help: '选择能源类型后自动带入默认单位',
    },
    {
      fieldName: 'warnRatio',
      label: '预警比例(%)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入预警比例',
      },
      help: '实际用量超过「定额 Ã— (1 + æ¯”例)」触发警告',
      rules: z.number().default(10),
    },
    {
      fieldName: 'alarmRatio',
      label: '严重预警比例(%)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入严重预警比例',
      },
      help: '超过该比例时预警级别升级为「严重」',
      rules: z.number().default(20),
    },
    {
      fieldName: 'momWarnRatio',
      label: '环比阈值(%)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入环比异常阈值',
      },
      help: '较上一周期增幅超过该值时触发环比异常预警',
      rules: z.number().default(30),
    },
    {
      fieldName: 'yoyWarnRatio',
      label: '同比阈值(%)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入同比异常阈值',
      },
      help: '较去年同期增幅超过该值时触发同比异常预警',
      rules: z.number().default(30),
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.MES_EM_METER_STATUS, 'number'),
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
        disabled: formType === 'detail',
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'name',
      label: '指标名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入指标名称',
      },
    },
    {
      fieldName: 'energyType',
      label: '能源类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ENERGY_TYPE, 'number'),
        placeholder: '请选择能源类型',
      },
    },
    {
      fieldName: 'objectType',
      label: '考核对象类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_QUOTA_OBJECT_TYPE, 'number'),
        placeholder: '请选择考核对象类型',
      },
    },
    {
      fieldName: 'periodType',
      label: '考核周期',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_QUOTA_PERIOD, 'number'),
        placeholder: '请选择考核周期',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_METER_STATUS, 'number'),
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<EnergyQuotaApi.Quota>['columns'] {
  return [
    { field: 'name', title: '指标名称', minWidth: 180 },
    {
      field: 'energyType',
      title: '能源类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_ENERGY_TYPE },
      },
    },
    {
      field: 'objectType',
      title: '对象类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_QUOTA_OBJECT_TYPE },
      },
    },
    { field: 'objectName', title: '考核对象', minWidth: 140 },
    {
      field: 'periodType',
      title: '考核周期',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_QUOTA_PERIOD },
      },
    },
    {
      field: 'quotaValue',
      title: '定额值',
      width: 130,
      align: 'right',
      slots: {
        default: 'quotaValue',
      },
    },
    {
      field: 'warnRatio',
      title: '预警比例',
      width: 100,
      align: 'right',
      formatter: ({ cellValue }) => `${cellValue ?? 0}%`,
    },
    {
      field: 'alarmRatio',
      title: '严重比例',
      width: 100,
      align: 'right',
      formatter: ({ cellValue }) => `${cellValue ?? 0}%`,
    },
    {
      field: 'momWarnRatio',
      title: '环比阈值',
      width: 100,
      align: 'right',
      formatter: ({ cellValue }) => `${cellValue ?? 0}%`,
    },
    {
      field: 'yoyWarnRatio',
      title: '同比阈值',
      width: 100,
      align: 'right',
      formatter: ({ cellValue }) => `${cellValue ?? 0}%`,
    },
    {
      field: 'status',
      title: '状态',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_METER_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 170,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/energy/quota/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyQuotaApi } from '#/api/energy/quota';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteQuota, getQuotaPage } from '#/api/energy/quota';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'EnergyQuota' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ–°å¢žè€ƒæ ¸æŒ‡æ ‡ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è€ƒæ ¸æŒ‡æ ‡ */
function handleDetail(row: EnergyQuotaApi.Quota) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑考核指标 */
function handleEdit(row: EnergyQuotaApi.Quota) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤è€ƒæ ¸æŒ‡æ ‡ */
async function handleDelete(row: EnergyQuotaApi.Quota) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteQuota(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getQuotaPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<EnergyQuotaApi.Quota>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="能耗考核指标">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['考核指标']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:em-quota:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #quotaValue="{ row }">
        <span>{{ row.quotaValue }} {{ row.unit }}</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:em-quota:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:em-quota:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/energy/quota/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { EnergyQuotaApi } from '#/api/energy/quota';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createQuota, getQuota, updateQuota } from '#/api/energy/quota';
import { $t } from '#/locales';
import { ENERGY_DEFAULTS } from '../../meter/data';
import { useFormSchema } from '../data';
/** è€ƒæ ¸å¯¹è±¡ç±»åž‹å¯¹åº”的表单字段 */
const OBJECT_TYPE_FIELDS: Record<number, string> = {
  1: 'workshopId',
  2: 'lineId',
  3: 'machineryId',
};
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看能耗考核指标';
  }
  return formType.value === 'update' ? '修改能耗考核指标' : '新增能耗考核指标';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  handleValuesChange: (values, changedFields) => {
    if (!changedFields.includes('energyType') || !values.energyType) {
      return;
    }
    const config = ENERGY_DEFAULTS[Number(values.energyType)];
    if (config && !values.unit) {
      formApi.setFieldValue('unit', config.unit);
    }
  },
});
/** æŠŠè¡¨å•里的对象字段映射为后端统一的 objectId */
function buildSaveData(values: Record<string, unknown>): EnergyQuotaApi.Quota {
  const objectType = Number(values.objectType ?? 0);
  const objectField = OBJECT_TYPE_FIELDS[objectType];
  return {
    id: values.id ? Number(values.id) : undefined,
    name: values.name as string | undefined,
    energyType: values.energyType as number | undefined,
    objectType,
    objectId: objectField ? Number(values[objectField]) || undefined : undefined,
    periodType: values.periodType as number | undefined,
    quotaValue: values.quotaValue as number | undefined,
    unit: values.unit as string | undefined,
    warnRatio: values.warnRatio as number | undefined,
    alarmRatio: values.alarmRatio as number | undefined,
    momWarnRatio: values.momWarnRatio as number | undefined,
    yoyWarnRatio: values.yoyWarnRatio as number | undefined,
    status: values.status as number | undefined,
    remark: values.remark as string | undefined,
  };
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = buildSaveData(await formApi.getValues());
    if (data.objectType !== 0 && !data.objectId) {
      message.warning('请选择考核对象');
      return;
    }
    modalApi.lock();
    try {
      if (formType.value === 'create') {
        await createQuota(data);
      } else {
        await updateQuota(data);
      }
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema(formType.value) });
    formApi.setDisabled(formType.value === 'detail');
    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      const quota = await getQuota(data.id);
      await formApi.setValues(quota);
      const objectField = OBJECT_TYPE_FIELDS[quota.objectType ?? 0];
      if (objectField && quota.objectId) {
        await formApi.setFieldValue(objectField, quota.objectId);
      }
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/energy/record/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,277 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyRecordApi } from '#/api/energy/record';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getMeterSimpleList, getWorkshopOptions } from '#/api/energy/meter';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改抄表记录的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'meterId',
      label: '计量点',
      component: 'ApiSelect',
      componentProps: {
        api: getMeterSimpleList,
        labelField: 'name',
        placeholder: '请选择能耗计量点',
        valueField: 'id',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'recordDate',
      label: '抄表日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择抄表日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'prevReading',
      label: '期初读数',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        disabled: true,
        precision: 2,
      },
      help: '自动取该计量点上一笔抄表记录的期末读数,首笔取初始读数',
    },
    {
      fieldName: 'reading',
      label: '期末读数',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入本期表计读数',
      },
      rules: 'required',
    },
    {
      fieldName: 'unit',
      label: '计量单位',
      component: 'Input',
      componentProps: {
        class: '!w-full',
        disabled: true,
        placeholder: '随计量点自动带入',
      },
    },
    {
      fieldName: 'usageAmount',
      label: '本期用量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        disabled: true,
        precision: 4,
      },
      help: '用量 =(期末读数 - æœŸåˆè¯»æ•°ï¼‰Ã— å€çއ',
    },
    {
      fieldName: 'price',
      label: '单价',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 4,
        placeholder: '请输入能源单价',
      },
      help: '元/计量单位,用于归集能耗成本',
    },
    {
      fieldName: 'cost',
      label: '本期费用',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        disabled: true,
        precision: 2,
      },
      help: '费用 = ç”¨é‡ Ã— å•ä»·',
    },
    {
      fieldName: 'source',
      label: '数据来源',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_RECORD_SOURCE, 'number'),
        placeholder: '请选择数据来源',
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'meterId',
      label: '计量点',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getMeterSimpleList,
        labelField: 'name',
        placeholder: '请选择能耗计量点',
        valueField: 'id',
      },
    },
    {
      fieldName: 'energyType',
      label: '能源类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_ENERGY_TYPE, 'number'),
        placeholder: '请选择能源类型',
      },
    },
    {
      fieldName: 'workshopId',
      label: '所属车间',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getWorkshopOptions,
        labelField: 'name',
        placeholder: '请选择所属车间',
        valueField: 'id',
      },
    },
    {
      fieldName: 'source',
      label: '数据来源',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_EM_RECORD_SOURCE, 'number'),
        placeholder: '请选择数据来源',
      },
    },
    {
      fieldName: 'recordDate',
      label: '抄表日期',
      component: 'RangePicker',
      componentProps: {
        class: 'w-full',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<EnergyRecordApi.Record>['columns'] {
  return [
    {
      field: 'recordDate',
      title: '抄表日期',
      width: 115,
    },
    { field: 'meterCode', title: '计量点编码', width: 140 },
    { field: 'meterName', title: '计量点名称', minWidth: 170 },
    {
      field: 'energyType',
      title: '能源类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_ENERGY_TYPE },
      },
    },
    { field: 'workshopName', title: '所属车间', width: 120 },
    {
      field: 'prevReading',
      title: '期初读数',
      width: 110,
      align: 'right',
    },
    {
      field: 'reading',
      title: '期末读数',
      width: 110,
      align: 'right',
    },
    {
      field: 'usageAmount',
      title: '本期用量',
      width: 120,
      align: 'right',
      slots: {
        default: 'usageAmount',
      },
    },
    {
      field: 'price',
      title: '单价',
      width: 90,
      align: 'right',
    },
    {
      field: 'cost',
      title: '本期费用(元)',
      width: 120,
      align: 'right',
    },
    {
      field: 'source',
      title: '数据来源',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_EM_RECORD_SOURCE },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 170,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/energy/record/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { EnergyRecordApi } from '#/api/energy/record';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteRecord, getRecordPage } from '#/api/energy/record';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'EnergyRecord' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ–°å¢žæŠ„表记录 */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹æŠ„表记录 */
function handleDetail(row: EnergyRecordApi.Record) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑抄表记录 */
function handleEdit(row: EnergyRecordApi.Record) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤æŠ„表记录 */
async function handleDelete(row: EnergyRecordApi.Record) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.meterName]),
    duration: 0,
  });
  try {
    await deleteRecord(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.meterName]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getRecordPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<EnergyRecordApi.Record>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="能耗抄表记录">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['抄表记录']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:em-record:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #usageAmount="{ row }">
        <span>{{ row.usageAmount }} {{ row.unit }}</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:em-record:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:em-record:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  `${row.meterName} ${row.recordDate} çš„æŠ„表记录`,
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/energy/record/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { EnergyMeterApi } from '#/api/energy/meter';
import type { EnergyRecordApi } from '#/api/energy/record';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getMeter } from '#/api/energy/meter';
import {
  createRecord,
  getLastReading,
  getRecord,
  updateRecord,
} from '#/api/energy/record';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const recordId = ref<number>();
const meterInfo = ref<EnergyMeterApi.Meter>();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看能耗抄表记录';
  }
  return formType.value === 'update' ? '修改能耗抄表记录' : '新增能耗抄表记录';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  handleValuesChange: (values, changedFields) => {
    if (changedFields.includes('meterId')) {
      void syncMeterAndReading();
      return;
    }
    if (changedFields.includes('recordDate')) {
      void syncPrevReading();
      return;
    }
    if (
      changedFields.includes('reading') ||
      changedFields.includes('price')
    ) {
      void recalc();
    }
  },
});
/** æŒ‰é€‰ä¸­è®¡é‡ç‚¹åˆ·æ–°å€çއ/单位,并重新带入上期读数 */
async function syncMeterAndReading() {
  const { meterId } = await formApi.getValues();
  if (!meterId) {
    meterInfo.value = undefined;
    return;
  }
  meterInfo.value = await getMeter(Number(meterId));
  await formApi.setFieldValue('unit', meterInfo.value?.unit ?? '');
  await syncPrevReading();
}
/** å¸¦å…¥ä¸ŠæœŸè¯»æ•°ï¼ˆç¼–辑时排除当前记录) */
async function syncPrevReading() {
  const { meterId, recordDate } = await formApi.getValues();
  if (!meterId || !recordDate) {
    return;
  }
  if (!meterInfo.value || meterInfo.value.id !== Number(meterId)) {
    meterInfo.value = await getMeter(Number(meterId));
    await formApi.setFieldValue('unit', meterInfo.value?.unit ?? '');
  }
  const result = await getLastReading(
    Number(meterId),
    String(recordDate),
    recordId.value,
  );
  await formApi.setFieldValue('prevReading', result?.prevReading ?? 0);
  await recalc();
}
/** æ ¹æ®è¯»æ•°ã€å€çŽ‡ã€å•ä»·å›žç®—ç”¨é‡ä¸Žè´¹ç”¨ */
async function recalc() {
  const values = await formApi.getValues();
  const ratio = Number(meterInfo.value?.ratio ?? 1);
  const prevReading = Number(values.prevReading ?? 0);
  const reading = Number(values.reading ?? 0);
  const price = Number(values.price ?? 0);
  const usage = reading >= prevReading ? (reading - prevReading) * ratio : 0;
  await formApi.setFieldValue('usageAmount', Number(usage.toFixed(4)));
  await formApi.setFieldValue('cost', Number((usage * price).toFixed(2)));
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as EnergyRecordApi.Record;
    delete data.cost;
    delete data.unit;
    delete data.usageAmount;
    try {
      if (formType.value === 'create') {
        await createRecord(data);
      } else {
        await updateRecord(data);
      }
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      recordId.value = undefined;
      meterInfo.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema() });
    formApi.setDisabled(formType.value === 'detail');
    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      recordId.value = data.id;
      const record = await getRecord(data.id);
      if (record.meterId) {
        meterInfo.value = await getMeter(record.meterId);
      }
      await formApi.setValues(record);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/energy/report/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,284 @@
<script lang="ts" setup>
import type { TableColumnsType } from 'ant-design-vue';
import type { Dayjs } from 'dayjs';
import type { EnergyStatisticsApi } from '#/api/energy/statistics';
import { computed, h, onMounted, ref } from 'vue';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Card, Col, DatePicker, Row, Spin, Table } from 'ant-design-vue';
import dayjs from 'dayjs';
import { getReport } from '#/api/energy/statistics';
defineOptions({ name: 'EnergyReport' });
const loading = ref(false);
const period = ref<Dayjs | undefined>(dayjs());
const report = ref<EnergyStatisticsApi.Report>({});
const typeColumns: TableColumnsType<EnergyStatisticsApi.TypeRow> = [
  { title: '能源类型', dataIndex: 'energyTypeName', width: 120 },
  {
    title: '本月用量',
    dataIndex: 'usage',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.usage),
  },
  { title: '单位', dataIndex: 'unit', width: 80 },
  {
    title: '折标煤(吨)',
    dataIndex: 'tce',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.tce),
  },
  {
    title: '本月费用(元)',
    dataIndex: 'cost',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.cost),
  },
  {
    title: '上月用量',
    dataIndex: 'lastMonthUsage',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.lastMonthUsage),
  },
  {
    title: '环比',
    dataIndex: 'momRate',
    width: 110,
    align: 'right',
    customRender: ({ record }) => renderRate(record.momRate),
  },
  {
    title: '去年同月用量',
    dataIndex: 'lastYearUsage',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.lastYearUsage),
  },
  {
    title: '同比',
    dataIndex: 'yoyRate',
    width: 110,
    align: 'right',
    customRender: ({ record }) => renderRate(record.yoyRate),
  },
];
const workshopColumns: TableColumnsType<EnergyStatisticsApi.WorkshopRow> = [
  { title: '车间', dataIndex: 'workshopName', minWidth: 160 },
  {
    title: '本月折标煤(吨)',
    dataIndex: 'tce',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.tce),
  },
  {
    title: '本月费用(元)',
    dataIndex: 'cost',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.cost),
  },
  {
    title: '上月折标煤(吨)',
    dataIndex: 'lastMonthTce',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.lastMonthTce),
  },
  {
    title: '环比',
    dataIndex: 'momRate',
    width: 110,
    align: 'right',
    customRender: ({ record }) => renderRate(record.momRate),
  },
  {
    title: '去年同月折标煤(吨)',
    dataIndex: 'lastYearTce',
    align: 'right',
    customRender: ({ record }) => formatNumber(record.lastYearTce),
  },
  {
    title: '同比',
    dataIndex: 'yoyRate',
    width: 110,
    align: 'right',
    customRender: ({ record }) => renderRate(record.yoyRate),
  },
];
const totalTce = computed(() => Number(report.value.totalTce ?? 0));
const totalCost = computed(() => Number(report.value.totalCost ?? 0));
/** æ•°å­—格式化 */
function formatNumber(value?: number, digits = 2) {
  if (value === undefined || value === null) {
    return '-';
  }
  return Number(value).toLocaleString('zh-CN', {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  });
}
/** æ¯”率文本 */
function formatRate(rate?: number) {
  if (rate === undefined || rate === null) {
    return '-';
  }
  const value = Number(rate);
  const sign = value > 0 ? '+' : '';
  return `${sign}${value.toFixed(2)}%`;
}
/** æ¯”率颜色(上升红色、下降绿色) */
function getRateClass(rate?: number) {
  if (rate === undefined || rate === null) {
    return '';
  }
  const value = Number(rate);
  return value > 0 ? 'text-[#f56c6c]' : value < 0 ? 'text-[#67c23a]' : '';
}
/** è¡¨æ ¼å†…比率渲染 */
function renderRate(rate?: number) {
  return h('span', { class: getRateClass(rate) }, formatRate(rate));
}
/** åŠ è½½æœˆåº¦æŠ¥è¡¨ */
async function loadData() {
  loading.value = true;
  try {
    report.value = await getReport(
      period.value?.format('YYYY-MM') ?? dayjs().format('YYYY-MM'),
    );
  } finally {
    loading.value = false;
  }
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Page>
    <Card class="mb-4" size="small">
      <div class="flex flex-wrap items-center gap-3">
        <span class="text-muted-foreground text-sm">报表月份</span>
        <DatePicker
          v-model:value="period"
          :allow-clear="false"
          class="w-[140px]"
          format="YYYY-MM"
          picker="month"
          @change="loadData"
        />
        <span class="text-muted-foreground text-xs">
          çŽ¯æ¯”å¯¹æ¯”ä¸Šä¸€è‡ªç„¶æœˆï¼ŒåŒæ¯”å¯¹æ¯”åŽ»å¹´åŒæœˆï¼›åŸºæ•°ä¸º 0 æ—¶æ¯”率显示为 -
        </span>
      </div>
    </Card>
    <Spin :spinning="loading">
      <Row :gutter="16">
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="flex items-center gap-4">
              <div
                class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
                style="background: linear-gradient(135deg, #409eff, #66b1ff)"
              >
                <IconifyIcon class="size-7" icon="lucide:leaf" />
              </div>
              <div>
                <div class="text-muted-foreground mb-1 text-sm">综合能耗</div>
                <div class="flex items-baseline gap-1">
                  <CountTo
                    class="text-2xl font-bold leading-tight text-[#409eff]"
                    :decimals="2"
                    :duration="1500"
                    :end-val="totalTce"
                  />
                  <span class="text-muted-foreground text-xs">吨标煤</span>
                </div>
              </div>
            </div>
          </Card>
        </Col>
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="flex items-center gap-4">
              <div
                class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
                style="background: linear-gradient(135deg, #e6a23c, #ebb563)"
              >
                <IconifyIcon class="size-7" icon="lucide:coins" />
              </div>
              <div>
                <div class="text-muted-foreground mb-1 text-sm">能耗成本</div>
                <div class="flex items-baseline gap-1">
                  <CountTo
                    class="text-2xl font-bold leading-tight text-[#e6a23c]"
                    :decimals="2"
                    :duration="1500"
                    :end-val="totalCost"
                  />
                  <span class="text-muted-foreground text-xs">元</span>
                </div>
              </div>
            </div>
          </Card>
        </Col>
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="text-muted-foreground mb-2 text-sm">综合能耗环比</div>
            <div class="text-2xl font-bold">
              <span :class="getRateClass(report.momRate)">
                {{ formatRate(report.momRate) }}
              </span>
            </div>
            <div class="text-muted-foreground mt-1 text-xs">
              ä¸Šæœˆ {{ formatNumber(report.lastMonthTce) }} å¨æ ‡ç…¤
            </div>
          </Card>
        </Col>
        <Col :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
          <Card>
            <div class="text-muted-foreground mb-2 text-sm">综合能耗同比</div>
            <div class="text-2xl font-bold">
              <span :class="getRateClass(report.yoyRate)">
                {{ formatRate(report.yoyRate) }}
              </span>
            </div>
            <div class="text-muted-foreground mt-1 text-xs">
              åŽ»å¹´åŒæœˆ {{ formatNumber(report.lastYearTce) }} å¨æ ‡ç…¤
            </div>
          </Card>
        </Col>
      </Row>
      <Card :title="`按能源类型统计(${report.period || ''})`" class="mb-4">
        <Table
          :columns="typeColumns"
          :data-source="report.typeRows ?? []"
          :pagination="false"
          size="small"
        />
      </Card>
      <Card :title="`按车间统计(${report.period || ''})`">
        <Table
          :columns="workshopColumns"
          :data-source="report.workshopRows ?? []"
          :pagination="false"
          size="small"
        />
      </Card>
    </Spin>
  </Page>
</template>