dbzl.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981
  1. <script setup lang="ts">
  2. import { nextTick, onMounted, reactive, ref } from 'vue'
  3. import { clientGet, clientPost } from '@/utils/request.ts'
  4. import type { BaseResponse, PaginationResponse } from '@/utils/type.ts'
  5. import { ElMessage, ElMessageBox } from 'element-plus'
  6. import {
  7. Activity,
  8. BarChart3,
  9. Calendar,
  10. Clock,
  11. Delete,
  12. Edit,
  13. Plus,
  14. Power,
  15. RefreshCw,
  16. Search,
  17. TrendingUp,
  18. Zap
  19. } from 'lucide-vue-next'
  20. import * as echarts from 'echarts'
  21. interface ElectricItem {
  22. id: number
  23. companyName: string
  24. electricNumber: string
  25. createTime: string
  26. }
  27. interface ElectricListResponse extends BaseResponse {
  28. data: PaginationResponse<ElectricItem>
  29. }
  30. interface ElectricResponse extends BaseResponse {
  31. data: ElectricItem
  32. }
  33. interface YearData extends BaseResponse {
  34. data: {
  35. allYearData: number,
  36. meterMonthDataList: number[]
  37. }
  38. }
  39. interface MonthData extends BaseResponse {
  40. data: {
  41. allMonthData: number,
  42. meterDayDataList: number[]
  43. }
  44. }
  45. interface DayData extends BaseResponse {
  46. data: {
  47. dayData: number
  48. }
  49. }
  50. const tableData = ref<ElectricItem[]>([])
  51. const loading = ref(false)
  52. const dialogVisible = ref(false)
  53. const dialogTitle = ref('新增电表')
  54. const isEdit = ref(false)
  55. const selectedIds = ref<number[]>([])
  56. const formRef = ref()
  57. // 图表对话框相关
  58. const chartDialogVisible = ref(false)
  59. const currentElectricNumber = ref('')
  60. const currentCompanyName = ref('')
  61. const chartLoading = ref(false)
  62. // 图表实例引用
  63. const yearChartRef = ref()
  64. const monthChartRef = ref()
  65. // 图表数据
  66. const chartData = reactive({
  67. year: new Date().getFullYear(),
  68. month: new Date().getMonth() + 1,
  69. yearData: {
  70. total: 0,
  71. monthlyData: [] as number[]
  72. },
  73. monthData: {
  74. total: 0,
  75. dailyData: [] as number[]
  76. },
  77. dayData: {
  78. total: 0
  79. }
  80. })
  81. // 分页数据
  82. const pagination = reactive({
  83. pageNum: 1,
  84. pageSize: 10,
  85. total: 0
  86. })
  87. // 搜索表单
  88. const searchForm = reactive({
  89. companyName: '',
  90. electricNumber: ''
  91. })
  92. // 表单数据
  93. const formData = reactive({
  94. id: 0,
  95. companyName: '',
  96. electricNumber: ''
  97. })
  98. // 表单验证规则
  99. const formRules = {
  100. companyName: [
  101. { required: true, message: '请输入公司名称', trigger: 'blur' }
  102. ],
  103. electricNumber: [
  104. { required: true, message: '请输入电表编号', trigger: 'blur' }
  105. ]
  106. }
  107. const getList = async () => {
  108. loading.value = true
  109. try {
  110. const arr: {
  111. value?: string,
  112. column: string,
  113. type: string
  114. }[] = []
  115. arr.push({
  116. column: 'create_time',
  117. type: 'orderByDesc'
  118. })
  119. if (searchForm.companyName) {
  120. arr.push({
  121. value: searchForm.companyName,
  122. column: 'company_name',
  123. type: 'like'
  124. })
  125. }
  126. if (searchForm.electricNumber) {
  127. arr.push({
  128. value: searchForm.electricNumber,
  129. column: 'electric_number',
  130. type: 'like'
  131. })
  132. }
  133. const params = {
  134. pageNum: pagination.pageNum,
  135. pageSize: pagination.pageSize,
  136. conditionJson: encodeURIComponent(JSON.stringify(arr))
  137. }
  138. const res = await clientGet<typeof params, ElectricListResponse>('/infrared/companyElectric/findByPage', {
  139. params
  140. })
  141. if (res.code !== 200) {
  142. ElMessage.error(res.msg)
  143. return
  144. }
  145. tableData.value = res.data.records
  146. pagination.total = res.data.total
  147. } catch (error) {
  148. console.error(error)
  149. ElMessage.error('获取数据失败')
  150. } finally {
  151. loading.value = false
  152. }
  153. }
  154. // API 函数
  155. const getElectricYears = async (electricNumber: string, year: number) => {
  156. const res = await clientGet<{
  157. meterNumber: string,
  158. year: number
  159. }, YearData>('/infrared/infraredReadingMeter/selectByYearAndMonthData', {
  160. params: {
  161. meterNumber: electricNumber,
  162. year
  163. }
  164. })
  165. if (res.code !== 200) {
  166. ElMessage.error(res.msg)
  167. return null
  168. }
  169. return res.data
  170. }
  171. const getElectricMonth = async (electricNumber: string, year: number, month: number) => {
  172. const res = await clientGet<{
  173. meterNumber: string,
  174. year: number,
  175. month: number
  176. }, MonthData>('/infrared/infraredReadingMeter/selectByYearAndMonthWithDayData', {
  177. params: {
  178. meterNumber: electricNumber,
  179. year,
  180. month
  181. }
  182. })
  183. if (res.code !== 200) {
  184. ElMessage.error(res.msg)
  185. return null
  186. }
  187. return res.data
  188. }
  189. const getElectricDay = async (electricNumber: string) => {
  190. const res = await clientGet<{
  191. meterNumber: string
  192. }, DayData>('/infrared/infraredReadingMeter/selectTodayNewestData', {
  193. params: {
  194. meterNumber: electricNumber
  195. }
  196. })
  197. if (res.code !== 200) {
  198. ElMessage.error(res.msg)
  199. return null
  200. }
  201. return res.data
  202. }
  203. // 点击电表编号显示图表
  204. const handleShowChart = async (row: ElectricItem) => {
  205. currentElectricNumber.value = row.electricNumber
  206. currentCompanyName.value = row.companyName
  207. chartDialogVisible.value = true
  208. await loadChartData()
  209. }
  210. // 加载图表数据
  211. const loadChartData = async () => {
  212. chartLoading.value = true
  213. try {
  214. // 并行加载三种数据
  215. const [yearResult, monthResult, dayResult] = await Promise.all([
  216. getElectricYears(currentElectricNumber.value, chartData.year),
  217. getElectricMonth(currentElectricNumber.value, chartData.year, chartData.month),
  218. getElectricDay(currentElectricNumber.value)
  219. ])
  220. if (yearResult) {
  221. chartData.yearData.total = yearResult.allYearData
  222. chartData.yearData.monthlyData = yearResult.meterMonthDataList
  223. }
  224. if (monthResult) {
  225. chartData.monthData.total = monthResult.allMonthData
  226. chartData.monthData.dailyData = monthResult.meterDayDataList
  227. }
  228. if (dayResult) {
  229. chartData.dayData.total = dayResult.dayData
  230. }
  231. // 等待DOM更新后初始化图表
  232. await nextTick()
  233. initCharts()
  234. } catch (error) {
  235. console.error('加载图表数据失败:', error)
  236. ElMessage.error('加载图表数据失败')
  237. } finally {
  238. chartLoading.value = false
  239. }
  240. }
  241. // 初始化图表
  242. const initCharts = () => {
  243. initYearChart()
  244. initMonthChart()
  245. }
  246. // 初始化年度图表
  247. const initYearChart = () => {
  248. if (!yearChartRef.value) return
  249. const chart = echarts.init(yearChartRef.value)
  250. const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  251. const option = {
  252. title: {
  253. text: `${chartData.year}年度用电量统计`,
  254. subtext: `总用电量: ${chartData.yearData.total} kWh`,
  255. left: 'center'
  256. },
  257. tooltip: {
  258. trigger: 'axis',
  259. formatter: '{b}: {c} kWh'
  260. },
  261. xAxis: {
  262. type: 'category',
  263. data: months
  264. },
  265. yAxis: {
  266. type: 'value',
  267. name: '用电量 (kWh)'
  268. },
  269. series: [{
  270. name: '月度用电量',
  271. type: 'bar',
  272. data: chartData.yearData.monthlyData,
  273. itemStyle: {
  274. color: '#409EFF'
  275. }
  276. }]
  277. }
  278. chart.setOption(option)
  279. }
  280. // 初始化月度图表
  281. const initMonthChart = () => {
  282. if (!monthChartRef.value) return
  283. const chart = echarts.init(monthChartRef.value)
  284. const days = Array.from({ length: chartData.monthData.dailyData.length }, (_, i) => `${i + 1}日`)
  285. const option = {
  286. title: {
  287. text: `${chartData.year}年${chartData.month}月用电量统计`,
  288. subtext: `总用电量: ${chartData.monthData.total} kWh`,
  289. left: 'center'
  290. },
  291. tooltip: {
  292. trigger: 'axis',
  293. formatter: '{b}: {c} kWh'
  294. },
  295. xAxis: {
  296. type: 'category',
  297. data: days
  298. },
  299. yAxis: {
  300. type: 'value',
  301. name: '用电量 (kWh)'
  302. },
  303. series: [{
  304. name: '日用电量',
  305. type: 'line',
  306. data: chartData.monthData.dailyData,
  307. smooth: true,
  308. itemStyle: {
  309. color: '#67C23A'
  310. }
  311. }]
  312. }
  313. chart.setOption(option)
  314. }
  315. // 计算用电量等级
  316. const getPowerLevel = (value: number) => {
  317. if (value < 50) return { level: 'low', color: '#67C23A', text: '节能' }
  318. if (value < 100) return { level: 'medium', color: '#E6A23C', text: '正常' }
  319. return { level: 'high', color: '#F56C6C', text: '偏高' }
  320. }
  321. // 年份/月份变化时重新加载数据
  322. const handleYearChange = async () => {
  323. await loadChartData()
  324. }
  325. const handleMonthChange = async () => {
  326. const monthResult = await getElectricMonth(currentElectricNumber.value, chartData.year, chartData.month)
  327. if (monthResult) {
  328. chartData.monthData.total = monthResult.allMonthData
  329. chartData.monthData.dailyData = monthResult.meterDayDataList
  330. await nextTick()
  331. initMonthChart()
  332. }
  333. }
  334. // 其他原有函数保持不变
  335. const add = async (item: { companyName: string, electricNumber: string }) => {
  336. const res = await clientPost<{
  337. companyName: string,
  338. electricNumber: string
  339. }, BaseResponse>('/infrared/companyElectric/save', {
  340. companyName: item.companyName,
  341. electricNumber: item.electricNumber
  342. })
  343. if (res.code !== 200) {
  344. ElMessage.error(res.msg)
  345. return false
  346. }
  347. ElMessage.success('新增成功')
  348. getList()
  349. return true
  350. }
  351. const edit = async (item: { id: number, companyName: string, electricNumber: string }) => {
  352. const res = await clientPost<{
  353. id: number,
  354. companyName: string,
  355. electricNumber: string
  356. }, BaseResponse>('/infrared/companyElectric/update', {
  357. id: item.id,
  358. companyName: item.companyName,
  359. electricNumber: item.electricNumber
  360. })
  361. if (res.code !== 200) {
  362. ElMessage.error(res.msg)
  363. return false
  364. }
  365. ElMessage.success('修改成功')
  366. getList()
  367. return true
  368. }
  369. const deleteBatch = async (ids: string[]) => {
  370. const res = await clientPost<string, BaseResponse>('/infrared/companyElectric/deleteBatch', JSON.stringify(ids))
  371. if (res.code !== 200) {
  372. ElMessage.error(res.msg)
  373. return
  374. }
  375. ElMessage.success('删除成功')
  376. getList()
  377. }
  378. const getById = async (id: number) => {
  379. const res = await clientGet<null, ElectricResponse>('/infrared/companyElectric/getById/' + id)
  380. if (res.code !== 200) {
  381. ElMessage.error(res.msg)
  382. return null
  383. }
  384. return res.data
  385. }
  386. const handleSearch = () => {
  387. pagination.pageNum = 1
  388. getList()
  389. }
  390. const handleReset = () => {
  391. Object.assign(searchForm, {
  392. companyName: '',
  393. electricNumber: ''
  394. })
  395. pagination.pageNum = 1
  396. getList()
  397. }
  398. const handleAdd = () => {
  399. dialogTitle.value = '新增电表'
  400. isEdit.value = false
  401. Object.assign(formData, {
  402. id: 0,
  403. companyName: '',
  404. electricNumber: ''
  405. })
  406. dialogVisible.value = true
  407. }
  408. const handleEdit = async (row: ElectricItem) => {
  409. dialogTitle.value = '编辑电表'
  410. isEdit.value = true
  411. const data = await getById(row.id)
  412. if (data) {
  413. Object.assign(formData, data)
  414. dialogVisible.value = true
  415. }
  416. }
  417. const handleDelete = (row: ElectricItem) => {
  418. ElMessageBox.confirm('确定要删除这条记录吗?', '提示', {
  419. confirmButtonText: '确定',
  420. cancelButtonText: '取消',
  421. type: 'warning'
  422. }).then(() => {
  423. deleteBatch([row.id.toString()])
  424. })
  425. }
  426. const handleBatchDelete = () => {
  427. if (selectedIds.value.length === 0) {
  428. ElMessage.warning('请选择要删除的记录')
  429. return
  430. }
  431. ElMessageBox.confirm(`确定要删除选中的 ${selectedIds.value.length} 条记录吗?`, '提示', {
  432. confirmButtonText: '确定',
  433. cancelButtonText: '取消',
  434. type: 'warning'
  435. }).then(() => {
  436. deleteBatch(selectedIds.value.map(id => id.toString()))
  437. selectedIds.value = []
  438. })
  439. }
  440. const handleSelectionChange = (selection: ElectricItem[]) => {
  441. selectedIds.value = selection.map(item => item.id)
  442. }
  443. const handlePageChange = (page: number) => {
  444. pagination.pageNum = page
  445. getList()
  446. }
  447. const handleSizeChange = (size: number) => {
  448. pagination.pageSize = size
  449. pagination.pageNum = 1
  450. getList()
  451. }
  452. const handleSubmit = async () => {
  453. if (!formRef.value) return
  454. try {
  455. await formRef.value.validate()
  456. let success = false
  457. if (isEdit.value) {
  458. success = await edit(formData)
  459. } else {
  460. success = await add(formData)
  461. }
  462. if (success) {
  463. dialogVisible.value = false
  464. }
  465. } catch (error) {
  466. console.error('表单验证失败', error)
  467. }
  468. }
  469. onMounted(() => {
  470. getList()
  471. })
  472. </script>
  473. <template>
  474. <div class="p-6 bg-gray-50 min-h-screen">
  475. <!-- 页面标题 -->
  476. <div class="mb-6">
  477. <h1 class="text-2xl font-bold text-gray-800 mb-2">智慧电表总览</h1>
  478. <p class="text-gray-600">管理公司电表信息</p>
  479. </div>
  480. <!-- 搜索区域 -->
  481. <div class="bg-white p-4 rounded-lg shadow-sm mb-4">
  482. <el-form :model="searchForm" inline class="search-form">
  483. <el-form-item label="公司名称">
  484. <el-input
  485. v-model="searchForm.companyName"
  486. placeholder="请输入公司名称"
  487. clearable
  488. class="w-200px"
  489. />
  490. </el-form-item>
  491. <el-form-item label="电表编号">
  492. <el-input
  493. v-model="searchForm.electricNumber"
  494. placeholder="请输入电表编号"
  495. clearable
  496. class="w-200px"
  497. />
  498. </el-form-item>
  499. <el-form-item>
  500. <el-button type="primary" @click="handleSearch">
  501. <Search class="w-4 h-4 mr-1" />
  502. 搜索
  503. </el-button>
  504. <el-button @click="handleReset">
  505. <RefreshCw class="w-4 h-4 mr-1" />
  506. 重置
  507. </el-button>
  508. </el-form-item>
  509. </el-form>
  510. </div>
  511. <!-- 操作区域 -->
  512. <div class="bg-white p-4 rounded-lg shadow-sm">
  513. <div class="flex justify-between items-center mb-4">
  514. <div class="flex gap-2">
  515. <el-button type="primary" @click="handleAdd">
  516. <Plus class="w-4 h-4 mr-1" />
  517. 新增电表
  518. </el-button>
  519. <el-button
  520. type="danger"
  521. :disabled="selectedIds.length === 0"
  522. @click="handleBatchDelete"
  523. >
  524. <Delete class="w-4 h-4 mr-1" />
  525. 批量删除
  526. </el-button>
  527. </div>
  528. <div class="text-sm text-gray-500">
  529. 共 {{ pagination.total }} 条记录
  530. </div>
  531. </div>
  532. <!-- 数据表格 -->
  533. <el-table
  534. :data="tableData"
  535. :loading="loading"
  536. @selection-change="handleSelectionChange"
  537. stripe
  538. class="w-full"
  539. >
  540. <el-table-column type="selection" width="55" />
  541. <el-table-column prop="id" label="ID" width="80" />
  542. <el-table-column prop="companyName" label="公司名称" min-width="200" />
  543. <el-table-column prop="electricNumber" label="电表编号" min-width="150">
  544. <template #default="{ row }">
  545. <el-button
  546. type="primary"
  547. text
  548. @click="handleShowChart(row)"
  549. class="p-0 h-auto font-normal"
  550. >
  551. <BarChart3 class="w-4 h-4 mr-1" />
  552. {{ row.electricNumber }}
  553. </el-button>
  554. </template>
  555. </el-table-column>
  556. <el-table-column prop="createTime" label="创建时间" width="180">
  557. <template #default="{ row }">
  558. {{ row.createTime ? new Date(row.createTime).toLocaleString() : '-' }}
  559. </template>
  560. </el-table-column>
  561. <el-table-column label="操作" width="150" fixed="right">
  562. <template #default="{ row }">
  563. <el-button
  564. type="primary"
  565. size="small"
  566. text
  567. @click="handleEdit(row)"
  568. >
  569. <Edit class="w-3 h-3 mr-1" />
  570. 编辑
  571. </el-button>
  572. <el-button
  573. type="danger"
  574. size="small"
  575. text
  576. @click="handleDelete(row)"
  577. >
  578. <Delete class="w-3 h-3 mr-1" />
  579. 删除
  580. </el-button>
  581. </template>
  582. </el-table-column>
  583. </el-table>
  584. <!-- 分页 -->
  585. <div class="flex justify-center mt-4">
  586. <el-pagination
  587. v-model:current-page="pagination.pageNum"
  588. v-model:page-size="pagination.pageSize"
  589. :page-sizes="[10, 20, 50, 100]"
  590. :total="pagination.total"
  591. layout="total, sizes, prev, pager, next, jumper"
  592. @size-change="handleSizeChange"
  593. @current-change="handlePageChange"
  594. />
  595. </div>
  596. </div>
  597. <!-- 新增/编辑弹窗 -->
  598. <el-dialog
  599. v-model="dialogVisible"
  600. :title="dialogTitle"
  601. width="500px"
  602. :close-on-click-modal="false"
  603. >
  604. <el-form
  605. :model="formData"
  606. :rules="formRules"
  607. ref="formRef"
  608. label-width="100px"
  609. class="dialog-form"
  610. >
  611. <el-form-item label="公司名称" prop="companyName">
  612. <el-input
  613. v-model="formData.companyName"
  614. placeholder="请输入公司名称"
  615. clearable
  616. />
  617. </el-form-item>
  618. <el-form-item label="电表编号" prop="electricNumber">
  619. <el-input
  620. v-model="formData.electricNumber"
  621. placeholder="请输入电表编号"
  622. clearable
  623. />
  624. </el-form-item>
  625. </el-form>
  626. <template #footer>
  627. <div class="dialog-footer">
  628. <el-button @click="dialogVisible = false">取消</el-button>
  629. <el-button type="primary" @click="handleSubmit">
  630. {{ isEdit ? '更新' : '新增' }}
  631. </el-button>
  632. </div>
  633. </template>
  634. </el-dialog>
  635. <!-- 图表全屏对话框 -->
  636. <el-dialog
  637. v-model="chartDialogVisible"
  638. :title="`${currentCompanyName} - ${currentElectricNumber} 用电情况分析`"
  639. fullscreen
  640. :close-on-click-modal="false"
  641. >
  642. <div v-loading="chartLoading" class="chart-container">
  643. <!-- 控制面板 -->
  644. <div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
  645. <div class="flex items-center gap-4">
  646. <div class="flex items-center gap-2">
  647. <Calendar class="w-4 h-4" />
  648. <span>年份:</span>
  649. <el-select v-model="chartData.year" @change="handleYearChange" class="w-32">
  650. <el-option
  651. v-for="year in [2021, 2022, 2023, 2024, 2025]"
  652. :key="year"
  653. :label="year"
  654. :value="year"
  655. />
  656. </el-select>
  657. </div>
  658. <div class="flex items-center gap-2">
  659. <Clock class="w-4 h-4" />
  660. <span>月份:</span>
  661. <el-select v-model="chartData.month" @change="handleMonthChange" class="w-32">
  662. <el-option
  663. v-for="month in 12"
  664. :key="month"
  665. :label="`${month}月`"
  666. :value="month"
  667. />
  668. </el-select>
  669. </div>
  670. </div>
  671. <div class="text-sm text-gray-600">
  672. 电表编号: {{ currentElectricNumber }}
  673. </div>
  674. </div>
  675. <!-- 今日用电量卡片区域 -->
  676. <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
  677. <!-- 今日用电量主卡片 -->
  678. <el-card class="today-power-card" shadow="hover">
  679. <template #header>
  680. <div class="flex items-center justify-between">
  681. <div class="flex items-center gap-2">
  682. <div class="p-2 rounded-lg bg-blue-100">
  683. <Zap class="w-5 h-5 text-blue-600" />
  684. </div>
  685. <span class="font-semibold text-gray-700">今日用电量</span>
  686. </div>
  687. <el-tag
  688. :type="getPowerLevel(chartData.dayData.total).level === 'low' ? 'success' : getPowerLevel(chartData.dayData.total).level === 'medium' ? 'warning' : 'danger'"
  689. size="small">
  690. {{ getPowerLevel(chartData.dayData.total).text }}
  691. </el-tag>
  692. </div>
  693. </template>
  694. <div class="text-center py-4">
  695. <div class="text-4xl font-bold mb-2" :style="{ color: getPowerLevel(chartData.dayData.total).color }">
  696. {{ chartData.dayData.total }}
  697. </div>
  698. <div class="text-lg text-gray-500 mb-4">kWh</div>
  699. <div class="flex items-center justify-center gap-2 text-sm text-gray-600">
  700. <Activity class="w-4 h-4" />
  701. <span>实时监控</span>
  702. </div>
  703. </div>
  704. </el-card>
  705. <!-- 月度对比卡片 -->
  706. <el-card shadow="hover">
  707. <template #header>
  708. <div class="flex items-center gap-2">
  709. <div class="p-2 rounded-lg bg-green-100">
  710. <TrendingUp class="w-5 h-5 text-green-600" />
  711. </div>
  712. <span class="font-semibold text-gray-700">本月累计</span>
  713. </div>
  714. </template>
  715. <div class="py-4">
  716. <div class="text-2xl font-bold text-green-600 mb-2">
  717. {{ chartData.monthData.total }}
  718. </div>
  719. <div class="text-sm text-gray-500 mb-3">kWh</div>
  720. <div class="flex items-center justify-between text-xs">
  721. <span class="text-gray-600">日均用电</span>
  722. <span class="font-medium">{{ (chartData.monthData.total / new Date().getDate()).toFixed(1) }} kWh</span>
  723. </div>
  724. </div>
  725. </el-card>
  726. <!-- 年度对比卡片 -->
  727. <el-card shadow="hover">
  728. <template #header>
  729. <div class="flex items-center gap-2">
  730. <div class="p-2 rounded-lg bg-purple-100">
  731. <Power class="w-5 h-5 text-purple-600" />
  732. </div>
  733. <span class="font-semibold text-gray-700">年度累计</span>
  734. </div>
  735. </template>
  736. <div class="py-4">
  737. <div class="text-2xl font-bold text-purple-600 mb-2">
  738. {{ chartData.yearData.total }}
  739. </div>
  740. <div class="text-sm text-gray-500 mb-3">kWh</div>
  741. <div class="flex items-center justify-between text-xs">
  742. <span class="text-gray-600">月均用电</span>
  743. <span class="font-medium">{{ (chartData.yearData.total / 12).toFixed(1) }} kWh</span>
  744. </div>
  745. </div>
  746. </el-card>
  747. </div>
  748. <!-- 用电量趋势分析卡片 -->
  749. <el-card class="mb-6" shadow="hover">
  750. <template #header>
  751. <div class="flex items-center gap-2">
  752. <div class="p-2 rounded-lg bg-orange-100">
  753. <BarChart3 class="w-5 h-5 text-orange-600" />
  754. </div>
  755. <span class="font-semibold text-gray-700">
  756. 用电量趋势分析
  757. <el-tooltip>
  758. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
  759. stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
  760. class="lucide lucide-shield-question-mark-icon lucide-shield-question-mark"><path
  761. d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path
  762. d="M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3" /><path d="M12 17h.01" /></svg>
  763. <template #content>
  764. 50KW/h以下为节能用电<br />
  765. 50KW/h以上为正常<br />
  766. 100KW/h以上为偏高<br />
  767. 预计月度为当日用电量乘以30
  768. </template>
  769. </el-tooltip>
  770. </span>
  771. </div>
  772. </template>
  773. <div class="grid grid-cols-1 md:grid-cols-3 gap-4 py-4">
  774. <div class="text-center p-4 bg-gray-50 rounded-lg">
  775. <div class="text-lg font-semibold text-gray-700 mb-1">今日状态</div>
  776. <div class="text-sm" :style="{ color: getPowerLevel(chartData.dayData.total).color }">
  777. {{ getPowerLevel(chartData.dayData.total).text }}用电
  778. </div>
  779. </div>
  780. <div class="text-center p-4 bg-gray-50 rounded-lg">
  781. <div class="text-lg font-semibold text-gray-700 mb-1">预计月度</div>
  782. <div class="text-sm text-blue-600">
  783. {{ (chartData.dayData.total * 30).toFixed(0) }} kWh
  784. </div>
  785. </div>
  786. <div class="text-center p-4 bg-gray-50 rounded-lg">
  787. <div class="text-lg font-semibold text-gray-700 mb-1">节能建议</div>
  788. <div class="text-sm text-green-600">
  789. {{ chartData.dayData.total < 50 ? '保持良好' : chartData.dayData.total < 100 ? '适度节能' : '加强管控'
  790. }}
  791. </div>
  792. </div>
  793. </div>
  794. </el-card>
  795. <!-- 图表网格 -->
  796. <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
  797. <!-- 年度用电量图表 -->
  798. <el-card shadow="hover">
  799. <template #header>
  800. <div class="flex items-center gap-2">
  801. <BarChart3 class="w-4 h-4 text-blue-600" />
  802. <span class="font-semibold">年度用电量统计</span>
  803. </div>
  804. </template>
  805. <div ref="yearChartRef" class="w-full h-96"></div>
  806. </el-card>
  807. <!-- 月度用电量图表 -->
  808. <el-card shadow="hover">
  809. <template #header>
  810. <div class="flex items-center gap-2">
  811. <TrendingUp class="w-4 h-4 text-green-600" />
  812. <span class="font-semibold">月度用电量统计</span>
  813. </div>
  814. </template>
  815. <div ref="monthChartRef" class="w-full h-96"></div>
  816. </el-card>
  817. </div>
  818. </div>
  819. <template #footer>
  820. <div class="dialog-footer">
  821. <el-button @click="chartDialogVisible = false">关闭</el-button>
  822. </div>
  823. </template>
  824. </el-dialog>
  825. </div>
  826. </template>
  827. <style scoped>
  828. .search-form .el-form-item {
  829. margin-bottom: 0;
  830. }
  831. .dialog-footer {
  832. text-align: right;
  833. }
  834. .chart-container {
  835. min-height: 600px;
  836. }
  837. .today-power-card {
  838. background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
  839. border: none;
  840. }
  841. .today-power-card :deep(.el-card__body) {
  842. padding: 16px;
  843. }
  844. /* UnoCSS 样式补充 */
  845. .w-200px {
  846. width: 200px;
  847. }
  848. .w-32 {
  849. width: 8rem;
  850. }
  851. .h-96 {
  852. height: 24rem;
  853. }
  854. .grid {
  855. display: grid;
  856. }
  857. .grid-cols-1 {
  858. grid-template-columns: repeat(1, minmax(0, 1fr));
  859. }
  860. .gap-6 {
  861. gap: 1.5rem;
  862. }
  863. .gap-4 {
  864. gap: 1rem;
  865. }
  866. .gap-2 {
  867. gap: 0.5rem;
  868. }
  869. @media (min-width: 768px) {
  870. .md\:grid-cols-3 {
  871. grid-template-columns: repeat(3, minmax(0, 1fr));
  872. }
  873. }
  874. @media (min-width: 1024px) {
  875. .lg\:grid-cols-2 {
  876. grid-template-columns: repeat(2, minmax(0, 1fr));
  877. }
  878. .lg\:grid-cols-3 {
  879. grid-template-columns: repeat(3, minmax(0, 1fr));
  880. }
  881. .lg\:col-span-2 {
  882. grid-column: span 2 / span 2;
  883. }
  884. }
  885. </style>