云
3 天以前 a241b66efcc41e973290a6722fb7a4d79b833ea1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
<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>