powerErrorStat.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <template>
  2. <div class="power-error-stat-page">
  3. <a-card :bordered="false">
  4. <a-row :gutter="[12, 12]" align="middle">
  5. <a-col :xs="24" :md="6">
  6. <StationSelector v-model:value="query.stationCode" :options="stationOptions" :multiple="false" />
  7. </a-col>
  8. <a-col :xs="24" :md="4">
  9. <a-select v-model:value="query.forecastType" :options="forecastTypeOptions" class="full-control" />
  10. </a-col>
  11. <a-col :xs="24" :md="5">
  12. <a-date-picker
  13. v-model:value="query.startTime"
  14. format="YYYY-MM-DD"
  15. value-format="YYYY-MM-DD"
  16. placeholder="起始时间"
  17. class="full-control"
  18. />
  19. </a-col>
  20. <a-col :xs="24" :md="5">
  21. <a-date-picker
  22. v-model:value="query.endTime"
  23. format="YYYY-MM-DD"
  24. value-format="YYYY-MM-DD"
  25. placeholder="截止时间"
  26. class="full-control"
  27. />
  28. </a-col>
  29. <a-col :xs="24" :md="4">
  30. <a-space>
  31. <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
  32. <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
  33. </a-space>
  34. </a-col>
  35. </a-row>
  36. </a-card>
  37. <a-card :bordered="false" class="content-card">
  38. <a-tabs v-model:activeKey="viewMode" class="content-tabs" @change="handleTabChange">
  39. <a-tab-pane key="curve" tab="曲线">
  40. <PowerLineChart :x-axis="curveAxis" :series="curveSeries" height="360px" y-axis-name="kW" />
  41. </a-tab-pane>
  42. <a-tab-pane key="accuracy" tab="精度">
  43. <BusinessDataTable
  44. :columns="accuracyColumns"
  45. :data-source="accuracyRows"
  46. :pagination="{ pageSize: 10 }"
  47. :scroll-x="'max-content'"
  48. />
  49. </a-tab-pane>
  50. </a-tabs>
  51. </a-card>
  52. </div>
  53. </template>
  54. <script setup name="PowerErrorStat">
  55. import {computed, onMounted, reactive, ref} from 'vue';
  56. import {message} from 'ant-design-vue';
  57. import dayjs from 'dayjs';
  58. import {saveAs} from 'file-saver';
  59. import {blobValidate} from '@/utils/bearjia';
  60. import {listStation} from '@/api/config/station';
  61. import {
  62. exportPowerErrorStatAccuracy,
  63. exportPowerErrorStatCurve,
  64. listPowerErrorStatAccuracy,
  65. listPowerErrorStatCurve
  66. } from '@/api/data/powerErrorStat';
  67. import BusinessDataTable from '../components/BusinessDataTable.vue';
  68. import PowerLineChart from '../components/PowerLineChart.vue';
  69. import StationSelector from '../components/StationSelector.vue';
  70. import {buildLineFromRows, normalizeRows} from '../components/dataAdapter';
  71. import {toStationOptions} from '../components/stationAdapter';
  72. const loading = ref(false);
  73. const exporting = ref(false);
  74. const stationOptions = ref([]);
  75. const curveRows = ref([]);
  76. const accuracyRows = ref([]);
  77. const viewMode = ref('curve');
  78. const query = reactive({
  79. stationCode: undefined,
  80. forecastType: 'short',
  81. startTime: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
  82. endTime: dayjs().format('YYYY-MM-DD')
  83. });
  84. const forecastTypeOptions = [
  85. {label: '超短期预测', value: 'ultra'},
  86. {label: '短期预测', value: 'short'},
  87. {label: '中期预测', value: 'medium'}
  88. ];
  89. const accuracyColumns = [
  90. {title: '日期', dataIndex: 'time', width: 120},
  91. {title: '均方根误差', dataIndex: 'rmse', width: 120},
  92. {title: '平均绝对误差', dataIndex: 'mae', width: 130},
  93. {title: '相关性系数', dataIndex: 'correlation', width: 120},
  94. {title: '准确率', dataIndex: 'passRate', width: 120},
  95. {title: '合格率', dataIndex: 'acc', width: 120}
  96. ];
  97. const queryValid = computed(() => {
  98. if (!query.startTime || !query.endTime) return false;
  99. const start = dayjs(query.startTime);
  100. const end = dayjs(query.endTime);
  101. if (!start.isValid() || !end.isValid()) return false;
  102. return !start.isAfter(end);
  103. });
  104. const curveAxis = computed(() => curveRows.value.map((item) => item.time));
  105. const curveSeries = computed(() => buildLineFromRows(curveRows.value, [
  106. {name: '实测功率', color: '#1677ff', keys: ['power']},
  107. {name: '预测功率', color: '#52c41a', keys: ['forecastPower']}
  108. ]).series);
  109. const loadStationOptions = async () => {
  110. const res = await listStation({pageNum: 1, pageSize: 500});
  111. stationOptions.value = toStationOptions(normalizeRows(res));
  112. if (!query.stationCode && stationOptions.value.length) {
  113. query.stationCode = stationOptions.value[0].value;
  114. }
  115. };
  116. const buildParams = () => ({
  117. stationCode: query.stationCode,
  118. forecastType: query.forecastType,
  119. startTime: query.startTime,
  120. endTime: query.endTime
  121. });
  122. const fetchCurve = async () => {
  123. const res = await listPowerErrorStatCurve(buildParams());
  124. curveRows.value = normalizeRows(res);
  125. };
  126. const fetchAccuracy = async () => {
  127. const res = await listPowerErrorStatAccuracy(buildParams());
  128. accuracyRows.value = normalizeRows(res);
  129. };
  130. const handleQuery = async () => {
  131. if (!query.stationCode) {
  132. message.warning('请先选择场站');
  133. return;
  134. }
  135. if (!queryValid.value) return;
  136. loading.value = true;
  137. try {
  138. if (viewMode.value === 'accuracy') {
  139. await fetchAccuracy();
  140. } else {
  141. await fetchCurve();
  142. }
  143. } finally {
  144. loading.value = false;
  145. }
  146. };
  147. // 切换 Tab: 查询对应数据并绘制曲线图/Table
  148. const handleTabChange = (key) => {
  149. viewMode.value = key;
  150. handleQuery();
  151. };
  152. const handleExport = async () => {
  153. if (!query.stationCode) {
  154. message.warning('请先选择场站');
  155. return;
  156. }
  157. if (!queryValid.value) return;
  158. exporting.value = true;
  159. try {
  160. const res = viewMode.value === 'accuracy'
  161. ? await exportPowerErrorStatAccuracy(buildParams())
  162. : await exportPowerErrorStatCurve(buildParams());
  163. const isFile = await blobValidate(res);
  164. if (isFile) {
  165. const prefix = viewMode.value === 'accuracy' ? '功率误差统计精度' : '功率误差统计曲线';
  166. const name = `${prefix}-${query.startTime}-${query.endTime}.xlsx`;
  167. saveAs(new Blob([res]), name);
  168. message.success('导出成功');
  169. } else {
  170. const text = await res.text();
  171. let msg = '导出失败';
  172. try {
  173. msg = JSON.parse(text)?.msg || msg;
  174. } catch (e) {
  175. /* ignore */
  176. }
  177. message.error(msg);
  178. }
  179. } finally {
  180. exporting.value = false;
  181. }
  182. };
  183. onMounted(async () => {
  184. await loadStationOptions();
  185. await handleQuery();
  186. });
  187. </script>
  188. <style lang="less" scoped>
  189. .power-error-stat-page {
  190. padding: 16px;
  191. }
  192. .full-control {
  193. width: 100%;
  194. }
  195. .content-card {
  196. margin-top: 16px;
  197. }
  198. </style>