index.vue 10 KB

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