index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <template>
  2. <div class="alarmHistoryPage">
  3. <div class="searchBar">
  4. <a-form layout="inline" @keydown.enter="searchHandler">
  5. <a-form-item label="设备ID">
  6. <a-input
  7. v-model:value.trim="searchState.clientId"
  8. placeholder="设备ID"
  9. :maxlength="12"
  10. show-count
  11. allow-clear
  12. />
  13. </a-form-item>
  14. <a-form-item label="创建时间">
  15. <range-picker
  16. v-model:start="searchState.createTimeStart"
  17. v-model:end="searchState.createTimeEnd"
  18. @change="searchHandler"
  19. />
  20. </a-form-item>
  21. <a-form-item label="事件类型">
  22. <a-select
  23. v-model:value="searchState.eventType"
  24. placeholder="请选择"
  25. style="width: 150px"
  26. :options="eventTypeList"
  27. @change="searchHandler"
  28. />
  29. </a-form-item>
  30. <a-form-item>
  31. <a-space>
  32. <a-button type="primary" @click="searchHandler"> 搜索 </a-button>
  33. <a-button @click="resetHandler"> 重置 </a-button>
  34. </a-space>
  35. </a-form-item>
  36. </a-form>
  37. </div>
  38. <div class="tableCard">
  39. <div class="tableCard-header">
  40. <div class="tableCard-header-title">告警历史列表</div>
  41. </div>
  42. <a-table :columns="columns" :data-source="tableList" :loading="loading" :pagination="false">
  43. <template #bodyCell="{ column, record }">
  44. <template v-if="column.key === 'pose'">
  45. {{ record.poseName }}
  46. </template>
  47. <template v-if="column.key === 'eventType'">
  48. {{ record.eventTypeName }}
  49. </template>
  50. <template v-if="column.key === 'info'">
  51. <div class="eventInfo">
  52. <div v-if="record.info?.start_time">开始时间:{{ record.info?.start_time }}</div>
  53. <div v-if="record.info?.end_time">结束时间:{{ record.info?.end_time }}</div>
  54. <div v-if="record.info?.stay_time">停留时长:{{ record.info?.stay_time }}(秒)</div>
  55. <div v-if="record.info?.count">次数:{{ record.info?.count }}</div>
  56. <a-collapse ghost accordion v-if="record.info?.event_list && !initPanel">
  57. <template #expandIcon="{ isActive }">
  58. <caret-right-outlined :rotate="isActive ? 90 : 0" />
  59. </template>
  60. <a-collapse-panel :header="`事件列表 (${record.info?.event_list?.length ?? 0}条)`">
  61. <ScrollContainer style="max-height: 110px">
  62. <div
  63. v-for="(event, index) in record.info?.event_list"
  64. :key="index"
  65. class="eventInfo-list"
  66. >
  67. <div v-if="event.start_time">开始时间:{{ event.start_time }}</div>
  68. <div v-if="event.end_time">结束时间:{{ event.end_time }}</div>
  69. <div v-if="event.stay_time">停留时长:{{ event.stay_time }}(秒)</div>
  70. <div v-if="event.absence_time">消失时长:{{ event.absence_time }}(秒)</div>
  71. <span class="showIndex">{{ index + 1 }}</span>
  72. </div>
  73. </ScrollContainer>
  74. </a-collapse-panel>
  75. </a-collapse>
  76. </div>
  77. </template>
  78. </template>
  79. </a-table>
  80. <base-pagination
  81. v-if="tableTotal > 0"
  82. v-model:current="current"
  83. v-model:pageSize="pageSize"
  84. :total="tableTotal"
  85. @change="paginationChange"
  86. @showSizeChange="paginationSizeChange"
  87. ></base-pagination>
  88. </div>
  89. <alarmPlanModal
  90. v-if="alarmPlanVisible"
  91. v-model:open="alarmPlanVisible"
  92. :title="alarmPlanTitle"
  93. type="template"
  94. :alarm-plan-id="alarmPlanId"
  95. :data="alarmPlanDataWithType"
  96. :area="{
  97. width: 400,
  98. length: 400,
  99. ranges: [-200, 200, -200, 200],
  100. }"
  101. @success="fetchList"
  102. ></alarmPlanModal>
  103. </div>
  104. </template>
  105. <script setup lang="ts">
  106. import { ref, onActivated, computed } from 'vue'
  107. import { columns } from './const'
  108. import { useSearch } from '@/hooks/useSearch'
  109. // import { useRouter } from 'vue-router'
  110. import * as alarmApi from '@/api/alarm'
  111. import alarmPlanModal from '@/views/device/detail/components/alarmPlanModal/index.vue'
  112. // import { message } from 'ant-design-vue'
  113. import { useDictName } from '@/hooks/useDictName'
  114. import * as statsApi from '@/api/stats'
  115. import type { StatsAlarmQueryDataRow } from '@/api/stats/types'
  116. import { CaretRightOutlined } from '@ant-design/icons-vue'
  117. // const router = useRouter()
  118. defineOptions({
  119. name: 'AlarmHistoryIndex',
  120. })
  121. interface SearchData {
  122. createTimeStart: string // 创建时间开始
  123. createTimeEnd: string // 创建时间结束
  124. type?: ID // 事件类型 一般滞留
  125. eventType?: ID // 事件类型 异常滞留
  126. clientId?: ID // 设备ID
  127. }
  128. const defaultSearch: SearchData = {
  129. createTimeStart: '',
  130. createTimeEnd: '',
  131. type: null,
  132. eventType: null,
  133. clientId: null,
  134. }
  135. const [searchState, resetHandler] = useSearch(defaultSearch, { afterReset: () => searchHandler() })
  136. const tableList = ref<StatsAlarmQueryDataRow[]>([])
  137. const tableTotal = ref<number>(0)
  138. const current = ref<number>(1)
  139. const pageSize = ref<number>(10)
  140. const alarmList = ref<StatsAlarmQueryDataRow[]>([])
  141. // 分页变化
  142. const paginationChange = (current: number, pageSize: number) => {
  143. console.log('change', current, pageSize)
  144. fetchList()
  145. }
  146. // 分页大小变化
  147. const paginationSizeChange = (current: number, pageSize: number) => {
  148. console.log('showSizeChange', current, pageSize)
  149. }
  150. const eventTypeList = ref<{ label: string; value: string }[]>([])
  151. // 获取事件类型下拉列表
  152. const fetchEventTypeList = async () => {
  153. try {
  154. const res = await alarmApi.getAlarmEventTypeList()
  155. console.log('获取事件类型下拉列表成功✅', res)
  156. const data = res.data
  157. eventTypeList.value =
  158. (Array.isArray(data) &&
  159. data.map((item) => ({
  160. label: item.eventDesc,
  161. value: item.eventVal,
  162. }))) ||
  163. []
  164. } catch (err) {
  165. console.log('获取事件类型下拉列表失败❌', err)
  166. }
  167. }
  168. const { dictNameMap: alarmEventTypeName } = useDictName(eventTypeList)
  169. const loading = ref(false)
  170. // 获取列表
  171. const fetchList = async () => {
  172. console.log('fetchList')
  173. try {
  174. loading.value = true
  175. initPanel.value = true
  176. const res = await statsApi.statsAlarmQuery({
  177. pageNo: current.value,
  178. pageSize: pageSize.value,
  179. createTimeStart: searchState.createTimeStart,
  180. createTimeEnd: searchState.createTimeEnd,
  181. eventType: searchState.eventType as ID,
  182. clientId: searchState.clientId,
  183. })
  184. console.log('✅ 获取告警统计数据成功', res)
  185. const { rows, total } = res.data
  186. alarmList.value = rows as StatsAlarmQueryDataRow[]
  187. alarmList.value.forEach((item) => {
  188. item.eventTypeName = alarmEventTypeName(item.eventType)
  189. try {
  190. item.info = item.info && JSON.parse(item.info)
  191. } catch (error) {
  192. item.info = ''
  193. console.log('❌ 解析info失败', error)
  194. }
  195. })
  196. tableList.value = alarmList.value
  197. tableTotal.value = Number(total)
  198. loading.value = false
  199. initPanel.value = false
  200. } catch (error) {
  201. console.error('❌ 获取告警统计数据失败', error)
  202. loading.value = false
  203. initPanel.value = false
  204. }
  205. }
  206. // 搜索
  207. const searchHandler = async () => {
  208. console.log('searchState', searchState)
  209. current.value = 1
  210. pageSize.value = 10
  211. await fetchList()
  212. }
  213. const alarmPlanVisible = ref(false) // 告警计划弹窗
  214. const alarmPlanId = ref<number | null>(null) // 当前编辑的告警计划id
  215. const alarmPlanTitle = ref<string>('添加告警计划')
  216. type AlarmPlan = {
  217. id: number
  218. uuid: ID
  219. name: string
  220. clientId: string
  221. enable: SwitchType
  222. region: string
  223. eventVal: number
  224. alarmTimePlanId: ID
  225. thresholdTime: ID
  226. mergeTime: ID
  227. param: string
  228. createTime: string
  229. updateTime: string
  230. remark: string | null
  231. alarmTimePlan: {
  232. createId: ID
  233. updateId: ID
  234. createTime: ID
  235. updateTime: ID
  236. isDeleted: SwitchType | null
  237. remark: ID
  238. id: ID
  239. startDate: string
  240. stopDate: string
  241. timeRange: string
  242. monthDays: string
  243. weekdays: string
  244. }
  245. }
  246. interface AlarmPlanItem {
  247. id?: number
  248. loading?: boolean
  249. [key: string]: unknown
  250. }
  251. const alarmPlanData = ref<AlarmPlanItem | undefined>(undefined)
  252. const alarmPlanDataWithType = computed(() => alarmPlanData.value as AlarmPlan)
  253. onActivated(async () => {
  254. await fetchEventTypeList().catch((err) => {
  255. console.error('fetchEventTypeList 失败', err)
  256. })
  257. await fetchList().catch((err) => {
  258. console.error('fetchList 失败', err)
  259. })
  260. })
  261. const initPanel = ref<boolean>(false)
  262. </script>
  263. <style scoped lang="less">
  264. .alarmHistoryPage {
  265. .searchBar {
  266. padding: 20px;
  267. background-color: #fff;
  268. margin-bottom: 20px;
  269. display: flex;
  270. justify-content: space-between;
  271. .ant-form {
  272. flex-grow: 1;
  273. }
  274. :deep(.ant-form-inline .ant-form-item) {
  275. margin-bottom: 16px !important;
  276. }
  277. }
  278. .tableCard {
  279. background-color: #fff;
  280. &-header {
  281. display: flex;
  282. justify-content: space-between;
  283. padding: 20px;
  284. &-title {
  285. font-size: 18px;
  286. font-weight: 600;
  287. }
  288. }
  289. .eventInfo {
  290. display: flex;
  291. flex-direction: column;
  292. align-items: flex-start;
  293. padding: 10px;
  294. border-radius: 4px;
  295. &-list {
  296. display: flex;
  297. flex-direction: column;
  298. align-items: flex-start;
  299. background-color: #eee;
  300. padding: 12px;
  301. border-radius: 5px;
  302. margin-bottom: 8px;
  303. position: relative;
  304. .showIndex {
  305. position: absolute;
  306. right: 0;
  307. bottom: 0;
  308. font-size: 12px;
  309. color: #fff;
  310. padding: 0 5px;
  311. border-radius: 5px 0 5px 0;
  312. background-color: #ccc;
  313. }
  314. }
  315. }
  316. }
  317. }
  318. :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
  319. padding: 0 !important;
  320. }
  321. :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header > .ant-collapse-header-text) {
  322. flex: none;
  323. }
  324. :deep(.ant-collapse .ant-collapse-content > .ant-collapse-content-box) {
  325. padding: 0 !important;
  326. }
  327. </style>