feat(em): 蓝羽纸业新增能耗管理前端
配套后端(同一基线 09-11)。基于 d1013ed3 创建分支,仅新增能耗模块。
新增内容:
- 能耗管理页面 21 个文件:总览看板、计量点管理、能耗数据采集、考核指标、预警、报表
- company.config.ts 增加 LYZY(新疆蓝羽纸业),接口指向 :9020
| | |
| | | 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', |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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', |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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}`); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 } }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 } }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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' }, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |