<script setup lang="ts">
|
import type { FormInstance, TablePaginationConfig } from 'ant-design-vue';
|
import type { EnergyFilters, EnergyRecord } from '#/api/energy';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
import { Page } from '@vben/common-ui';
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
import { Alert, AutoComplete, Button, Card, DatePicker, Form, FormItem, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, message } from 'ant-design-vue';
|
import dayjs from 'dayjs';
|
import { createEnergyRecord, deleteEnergyRecord, exportEnergyRecords, getEnergyRecord, getEnergyRecordPage, updateEnergyRecord } from '#/api/energy';
|
import { areaOptions, energyInfo, energyOptions, numberText, productOptions, purposeOptions } from '../shared';
|
|
defineOptions({ name: 'EnergyRecord' });
|
const filters = reactive<EnergyFilters>({});
|
const rows = ref<EnergyRecord[]>([]);
|
const loading = ref(false);
|
const exporting = ref(false);
|
async function exportRecords() {
|
if (filters.startDate && filters.endDate && filters.startDate > filters.endDate) { message.warning('开始日期不能晚于结束日期'); return; }
|
exporting.value = true;
|
try { downloadFileFromBlobPart({ fileName: '能耗台账.xlsx', source: await exportEnergyRecords({ ...filters }) }); }
|
finally { exporting.value = false; }
|
}
|
const loadError = ref(false);
|
const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true });
|
const open = ref(false);
|
const saving = ref(false);
|
const detail = ref(false);
|
const formRef = ref<FormInstance>();
|
const initial = (): EnergyRecord => ({ recordDate: dayjs().format('YYYY-MM-DD'), energyType: 'WATER', usageArea: '', productCategory: '燕窝', usagePurpose: '', quantity: 1, unitPrice: 0, recorder: '', remark: '' });
|
const model = reactive<EnergyRecord>(initial());
|
const unit = computed(() => energyInfo[model.energyType].unit);
|
const estimatedAmount = computed(() => numberText(Number(model.quantity || 0) * Number(model.unitPrice || 0), 2));
|
const title = computed(() => detail.value ? '能耗记录详情' : model.id ? '编辑能耗记录' : '新增能耗记录');
|
const rules = {
|
recordDate: [{ required: true, message: '请选择统计日期' }],
|
energyType: [{ required: true, message: '请选择能源类型' }],
|
usageArea: [{ required: true, whitespace: true, message: '请输入使用区域' }],
|
productCategory: [{ required: true, message: '请选择产品类别' }],
|
usagePurpose: [{ required: true, whitespace: true, message: '请输入用途' }],
|
quantity: [{ required: true, type: 'number' as const, min: 0.001, message: '用量必须大于零' }],
|
unitPrice: [{ required: true, type: 'number' as const, min: 0, message: '单价不能为负数' }],
|
recorder: [{ required: true, whitespace: true, message: '请输入录入人' }],
|
};
|
const columns = [
|
{ title: '统计日期', dataIndex: 'recordDate', width: 120 },
|
{ title: '能源', dataIndex: 'energyType', width: 95 },
|
{ title: '使用区域', dataIndex: 'usageArea', width: 160 },
|
{ title: '产品类别', dataIndex: 'productCategory', width: 130 },
|
{ title: '用途', dataIndex: 'usagePurpose', width: 170 },
|
{ title: '用量', dataIndex: 'quantity', width: 150, align: 'right' as const },
|
{ title: '单价(元/单位)', dataIndex: 'unitPrice', width: 150, align: 'right' as const },
|
{ title: '费用(元)', dataIndex: 'amount', width: 145, align: 'right' as const },
|
{ title: '录入人', dataIndex: 'recorder', width: 110 },
|
{ title: '操作', key: 'action', width: 190, fixed: 'right' as const },
|
];
|
let requestId = 0;
|
async function load(reset = false) {
|
if (filters.startDate && filters.endDate && filters.startDate > filters.endDate) {
|
message.warning('开始日期不能晚于结束日期'); return;
|
}
|
if (reset) pagination.current = 1;
|
const currentId = ++requestId;
|
loading.value = true;
|
loadError.value = false;
|
try {
|
const data = await getEnergyRecordPage({ ...filters, pageNo: pagination.current, pageSize: pagination.pageSize });
|
if (currentId !== requestId) return;
|
rows.value = data.list;
|
pagination.total = data.total;
|
} catch {
|
if (currentId === requestId) { rows.value = []; loadError.value = true; }
|
} finally { if (currentId === requestId) loading.value = false; }
|
}
|
function resetFilters() {
|
Object.keys(filters).forEach(key => delete filters[key as keyof EnergyFilters]);
|
void load(true);
|
}
|
function tableChange(page: TablePaginationConfig) {
|
pagination.current = page.current ?? 1;
|
pagination.pageSize = page.pageSize ?? 10;
|
void load();
|
}
|
async function showForm(row?: EnergyRecord, readonly = false) {
|
const values = row ? await getEnergyRecord(row.id!) : initial();
|
detail.value = readonly;
|
// 清除前一次编辑的编号及详情字段。
|
Object.keys(model).forEach(key => delete (model as unknown as Record<string, unknown>)[key]);
|
Object.assign(model, values);
|
open.value = true;
|
formRef.value?.clearValidate();
|
}
|
async function save() {
|
await formRef.value?.validate();
|
saving.value = true;
|
try {
|
const data = { ...model, usageArea: model.usageArea.trim(), usagePurpose: model.usagePurpose.trim(), recorder: model.recorder.trim() };
|
await (data.id ? updateEnergyRecord(data) : createEnergyRecord(data));
|
message.success('能耗记录已保存');
|
open.value = false;
|
await load();
|
} finally { saving.value = false; }
|
}
|
const deletingId = ref<number>();
|
async function remove(row: EnergyRecord) {
|
deletingId.value = row.id;
|
try {
|
await deleteEnergyRecord(row.id!);
|
message.success('能耗记录已删除');
|
if (rows.value.length === 1 && pagination.current > 1) pagination.current--;
|
await load();
|
} finally { deletingId.value = undefined; }
|
}
|
onMounted(() => load());
|
</script>
|
|
<template>
|
<Page title="能耗记录" description="按天手动记录水、电、蒸汽使用情况,费用自动计算。">
|
<Card>
|
<Form :model="filters" layout="inline" class="energy-filters" @finish="load(true)">
|
<FormItem label="开始日期"><DatePicker v-model:value="filters.startDate" value-format="YYYY-MM-DD" placeholder="开始日期" /></FormItem>
|
<FormItem label="结束日期"><DatePicker v-model:value="filters.endDate" value-format="YYYY-MM-DD" placeholder="结束日期" /></FormItem>
|
<FormItem label="能源"><Select v-model:value="filters.energyType" :options="energyOptions" allow-clear placeholder="全部能源" style="width: 130px" /></FormItem>
|
<FormItem label="区域"><Input v-model:value="filters.usageArea" allow-clear placeholder="输入区域关键词" style="width: 160px" /></FormItem>
|
<FormItem label="产品"><Select v-model:value="filters.productCategory" :options="productOptions" allow-clear placeholder="全部类别" style="width: 150px" /></FormItem>
|
<FormItem><Space><Button type="primary" :loading="loading" @click="load(true)">查询</Button><Button @click="resetFilters">重置</Button></Space></FormItem>
|
</Form>
|
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
<span class="text-base font-semibold">能耗台账</span>
|
<Space><Button v-access:code="['energy:record:export']" :loading="exporting" @click="exportRecords">导出台账</Button><Button v-access:code="['energy:record:create']" type="primary" @click="showForm()">新增能耗记录</Button></Space>
|
</div>
|
<Alert v-if="loadError" type="error" message="能耗记录加载失败,请点击查询重试。" show-icon class="mb-4" />
|
<Table :columns="columns" :data-source="rows" row-key="id" :loading="loading" :pagination="pagination" :scroll="{ x: 1420 }" :locale="{ emptyText: '暂无能耗记录,请新增每日用能信息' }" @change="tableChange">
|
<template #bodyCell="{ column, record }">
|
<template v-if="column.dataIndex === 'energyType'"><Tag>{{ energyInfo[record.energyType as keyof typeof energyInfo].name }}</Tag></template>
|
<template v-else-if="column.dataIndex === 'quantity'">{{ numberText(record.quantity) }} {{ energyInfo[record.energyType as keyof typeof energyInfo].unit }}</template>
|
<template v-else-if="column.dataIndex === 'unitPrice'">{{ numberText(record.unitPrice, 4) }}</template>
|
<template v-else-if="column.dataIndex === 'amount'">{{ numberText(record.amount, 2) }}</template>
|
<template v-else-if="column.key === 'action'">
|
<Space :size="0">
|
<Button v-access:code="['energy:record:query']" type="link" size="small" @click="showForm(record as EnergyRecord, true)">详情</Button>
|
<Button v-access:code="['energy:record:update']" type="link" size="small" @click="showForm(record as EnergyRecord)">编辑</Button>
|
<Popconfirm title="确定删除这条能耗记录?删除后统计数据也会更新。" ok-text="确定" cancel-text="取消" @confirm="remove(record as EnergyRecord)">
|
<Button v-access:code="['energy:record:delete']" type="link" danger size="small" :loading="deletingId === record.id">删除</Button>
|
</Popconfirm>
|
</Space>
|
</template>
|
</template>
|
</Table>
|
</Card>
|
<Modal v-model:open="open" :title="title" :width="760" :confirm-loading="saving" :closable="!saving" :mask-closable="!saving" :keyboard="!saving" :cancel-button-props="{ disabled: saving }" :footer="detail ? null : undefined" ok-text="保存" cancel-text="取消" @ok="save">
|
<Form ref="formRef" :model="model" :rules="rules" :disabled="detail || saving" layout="vertical" class="energy-form">
|
<FormItem label="统计日期" name="recordDate"><DatePicker v-model:value="model.recordDate" value-format="YYYY-MM-DD" :disabled-date="date => date.isAfter(dayjs(), 'day')" class="w-full" /></FormItem>
|
<FormItem label="能源类型" name="energyType"><Select v-model:value="model.energyType" :options="energyOptions" /></FormItem>
|
<FormItem label="使用区域" name="usageArea"><AutoComplete v-model:value="model.usageArea" :options="areaOptions" :maxlength="100" placeholder="选择建议区域或手动输入" /></FormItem>
|
<FormItem label="产品类别" name="productCategory"><Select v-model:value="model.productCategory" :options="productOptions" /></FormItem>
|
<FormItem label="用途 / 工序" name="usagePurpose"><AutoComplete v-model:value="model.usagePurpose" :options="purposeOptions" :maxlength="100" placeholder="如设备清洗、蒸煮提取、灭菌" /></FormItem>
|
<FormItem label="录入人" name="recorder"><Input v-model:value="model.recorder" :maxlength="50" placeholder="请输入姓名" /></FormItem>
|
<FormItem :label="`用量(${unit})`" name="quantity"><InputNumber v-model:value="model.quantity" :min="0.001" :max="999999999999999.999" :precision="3" style="width: 100%" /></FormItem>
|
<FormItem :label="`单价(元/${unit})`" name="unitPrice"><InputNumber v-model:value="model.unitPrice" :min="0" :max="99999999.9999" :precision="4" style="width: 100%" /></FormItem>
|
<div class="col-span-full mb-4 rounded border border-solid border-[hsl(var(--border))] p-3">
|
费用:<strong>{{ detail ? numberText(model.amount, 2) : estimatedAmount }} 元</strong>
|
<div class="mt-1 text-sm text-muted-foreground">按用量 × 单价计算,保存时精确到分。仅记录用量时,单价可填 0。</div>
|
</div>
|
<FormItem label="备注" name="remark" class="col-span-full"><Input.TextArea v-model:value="model.remark" :maxlength="500" show-count :rows="3" placeholder="如计量口径、特殊用能情况等" /></FormItem>
|
</Form>
|
</Modal>
|
</Page>
|
</template>
|
|
<style scoped>
|
.energy-filters { gap: 8px 0; margin-bottom: 24px; }
|
.energy-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 20px; }
|
@media (max-width: 640px) { .energy-form { grid-template-columns: minmax(0, 1fr); } }
|
</style>
|