Dmesg.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <template>
  2. <div class="dmesg-container">
  3. <div class="dmesg-filter">
  4. <div class="d-flex align-items-center flex-wrap">
  5. <fixed-label-filter :label="$t('compute.ops_time')" class="mr-2 mb-2">
  6. <div class="d-flex align-items-center">
  7. <a-date-picker
  8. v-model="filterForm.startValue"
  9. :disabled-date="disabledStartDate"
  10. show-time
  11. format="YYYY-MM-DD HH:mm:ss"
  12. :placeholder="$t('compute.text_230')"
  13. :open="startOpen"
  14. @openChange="handleStartOpenChange"
  15. @change="handleTimeChange"
  16. style="width: 200px; margin-right: 5px;" />
  17. ~
  18. <a-date-picker
  19. v-model="filterForm.endValue"
  20. :disabled-date="disabledEndDate"
  21. show-time
  22. format="YYYY-MM-DD HH:mm:ss"
  23. :placeholder="$t('compute.text_231')"
  24. :open="endOpen"
  25. @openChange="handleEndOpenChange"
  26. @change="handleTimeChange"
  27. style="width: 200px;margin-left: 5px;" />
  28. </div>
  29. </fixed-label-filter>
  30. <fixed-label-filter :label="$t('compute.log_level')" class="mr-2 mb-2">
  31. <a-select
  32. v-model="filterForm.logLevels"
  33. mode="multiple"
  34. allow-clear
  35. style="min-width: 200px"
  36. @change="handleFilterChange">
  37. <a-select-option v-for="level in logLevelOptions" :key="level.value" :value="level.value">
  38. {{ level.label }}
  39. </a-select-option>
  40. </a-select>
  41. </fixed-label-filter>
  42. <fixed-label-filter :label="$t('compute.log_info')" class="mr-2 mb-2">
  43. <a-input
  44. v-model="filterForm.notes"
  45. allow-clear
  46. style="width: 200px"
  47. @input="handleFilterInput" />
  48. </fixed-label-filter>
  49. <div class="mb-2">
  50. <a-button @click="handleResetFilter">
  51. {{ $t('common.reset') }}
  52. </a-button>
  53. </div>
  54. </div>
  55. </div>
  56. <div
  57. ref="scrollContainer"
  58. class="dmesg-scroll-container"
  59. @scroll="handleScroll">
  60. <div v-if="loading && dataList.length === 0" class="loading-wrapper">
  61. <a-spin size="large" />
  62. </div>
  63. <div v-else-if="dataList.length === 0" class="empty-wrapper">
  64. <a-empty :description="$t('common.notData')" />
  65. </div>
  66. <div v-else class="dmesg-list">
  67. <div
  68. v-for="(item, index) in dataList"
  69. :key="index"
  70. class="dmesg-item">
  71. <span style="font-weight: bold;">></span> {{ formatItem(item) }}
  72. </div>
  73. <div v-if="loadingMore" class="loading-more">
  74. <a-spin size="small" />
  75. <span class="ml-2">{{ $t('common_67') }}</span>
  76. </div>
  77. <div v-if="noMoreData && dataList.length > 0" class="no-more-data">
  78. {{ $t('common.load_no_more') }}
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. </template>
  84. <script>
  85. import WindowsMixin from '@/mixins/windows'
  86. export default {
  87. name: 'Dmesg',
  88. mixins: [WindowsMixin],
  89. props: {
  90. resId: {
  91. type: String,
  92. required: true,
  93. },
  94. data: {
  95. type: Object,
  96. required: true,
  97. },
  98. },
  99. data () {
  100. return {
  101. dataList: [],
  102. loading: false,
  103. loadingMore: false,
  104. noMoreData: false,
  105. nextMarker: null,
  106. pageSize: 50,
  107. scrollContainer: null,
  108. filterForm: {
  109. startValue: null,
  110. endValue: null,
  111. logLevels: [],
  112. notes: '',
  113. },
  114. startOpen: false,
  115. endOpen: false,
  116. logLevelOptions: ['emerg', 'alert', 'crit', 'err', 'warning'].map(level => ({
  117. label: level,
  118. value: level,
  119. })),
  120. debounceTimer: null,
  121. currentFilterParams: {},
  122. previousStartValue: null,
  123. previousEndValue: null,
  124. }
  125. },
  126. created () {
  127. this.fetchData()
  128. },
  129. mounted () {
  130. this.scrollContainer = this.$refs.scrollContainer
  131. },
  132. beforeDestroy () {
  133. if (this.debounceTimer) {
  134. clearTimeout(this.debounceTimer)
  135. }
  136. },
  137. methods: {
  138. async fetchData (isLoadMore = false) {
  139. if (this.loading || this.loadingMore) return
  140. if (isLoadMore) {
  141. this.loadingMore = true
  142. } else {
  143. this.loading = true
  144. this.dataList = []
  145. this.noMoreData = false
  146. }
  147. try {
  148. const manager = new this.$Manager('hostdmesgs', 'v1')
  149. const params = {
  150. limit: this.pageSize,
  151. scope: this.$store.getters.scope,
  152. filter: [],
  153. }
  154. if (this.nextMarker) {
  155. params.paging_marker = this.nextMarker
  156. }
  157. // 添加过滤条件 - 只有 since 或只有 until 都可以进行过滤
  158. const hasStartValue = this.filterForm.startValue && this.$moment(this.filterForm.startValue).isValid()
  159. const hasEndValue = this.filterForm.endValue && this.$moment(this.filterForm.endValue).isValid()
  160. if (hasStartValue) {
  161. const startTime = this.$moment(this.filterForm.startValue).utc().format('YYYY-MM-DD HH:mm:ss')
  162. params.since = startTime
  163. }
  164. if (hasEndValue) {
  165. const endTime = this.$moment(this.filterForm.endValue).utc().format('YYYY-MM-DD HH:mm:ss')
  166. params.until = endTime
  167. }
  168. if (this.filterForm.logLevels && this.filterForm.logLevels.length > 0) {
  169. params.log_levels = this.filterForm.logLevels
  170. }
  171. if (this.filterForm.notes && this.filterForm.notes.trim()) {
  172. const notesFilter = `notes.contains("${this.filterForm.notes.trim()}")`
  173. params.filter.push(notesFilter)
  174. }
  175. this.currentFilterParams = params
  176. const response = await manager.list({
  177. params: {
  178. ...params,
  179. obj_id: this.resId,
  180. obj_type: 'host',
  181. show_dmesg_log: true,
  182. },
  183. })
  184. const newData = response.data?.data || response.data?.items || response.data || []
  185. const nextMarker = response.data?.next_marker || response.data?.marker || null
  186. if (isLoadMore) {
  187. this.dataList = [...this.dataList, ...newData]
  188. } else {
  189. this.dataList = newData
  190. }
  191. this.nextMarker = nextMarker
  192. this.noMoreData = !nextMarker
  193. // 加载完成后滚动到顶部(非 loadMore 方式)
  194. if (!isLoadMore) {
  195. this.$nextTick(() => {
  196. this.scrollToTop()
  197. })
  198. }
  199. } catch (error) {
  200. this.$message.error(error.message || this.$t('compute.text_155'))
  201. } finally {
  202. this.loading = false
  203. this.loadingMore = false
  204. }
  205. },
  206. handleScroll (event) {
  207. const container = event.target
  208. const scrollTop = container.scrollTop
  209. const scrollHeight = container.scrollHeight
  210. const clientHeight = container.clientHeight
  211. // 当滚动到距离底部 100px 时加载更多
  212. if (scrollHeight - scrollTop - clientHeight < 100) {
  213. if (this.nextMarker && !this.loadingMore && !this.noMoreData) {
  214. this.fetchData(true)
  215. }
  216. }
  217. },
  218. scrollToTop () {
  219. if (this.scrollContainer) {
  220. this.scrollContainer.scrollTop = 0
  221. }
  222. },
  223. scrollToBottom () {
  224. if (this.scrollContainer) {
  225. this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight
  226. }
  227. },
  228. formatItem (item) {
  229. return `${item.ops_time ? this.$moment(item.ops_time).format('YYYY-MM-DD HH:mm:ss') : '-'} [${item.log_level || '-'}] ${item.notes || ''}`
  230. },
  231. handleFilterChange () {
  232. // 清除之前的防抖定时器
  233. if (this.debounceTimer) {
  234. clearTimeout(this.debounceTimer)
  235. }
  236. // 立即执行过滤
  237. this.doFilter()
  238. },
  239. handleFilterInput () {
  240. // 清除之前的防抖定时器
  241. if (this.debounceTimer) {
  242. clearTimeout(this.debounceTimer)
  243. }
  244. // 设置新的防抖定时器,500ms 后执行
  245. this.debounceTimer = setTimeout(() => {
  246. this.doFilter()
  247. }, 500)
  248. },
  249. doFilter () {
  250. // 重置分页和标记
  251. this.nextMarker = null
  252. this.dataList = []
  253. this.noMoreData = false
  254. // 更新保存的时间值
  255. this.previousStartValue = this.filterForm.startValue ? this.$moment(this.filterForm.startValue).valueOf() : null
  256. this.previousEndValue = this.filterForm.endValue ? this.$moment(this.filterForm.endValue).valueOf() : null
  257. // 重新加载数据
  258. this.fetchData(false)
  259. },
  260. handleResetFilter () {
  261. // 清除防抖定时器
  262. if (this.debounceTimer) {
  263. clearTimeout(this.debounceTimer)
  264. this.debounceTimer = null
  265. }
  266. // 重置过滤条件
  267. this.filterForm = {
  268. startValue: null,
  269. endValue: null,
  270. logLevels: [],
  271. notes: '',
  272. }
  273. this.startOpen = false
  274. this.endOpen = false
  275. // 重置保存的时间值
  276. this.previousStartValue = null
  277. this.previousEndValue = null
  278. // 重置分页和标记
  279. this.nextMarker = null
  280. this.dataList = []
  281. this.noMoreData = false
  282. // 重新加载数据
  283. this.fetchData(false)
  284. },
  285. disabledStartDate (startValue) {
  286. const endValue = this.filterForm.endValue
  287. if (!startValue || !endValue) {
  288. return false
  289. }
  290. return startValue.valueOf() > endValue.valueOf()
  291. },
  292. disabledEndDate (endValue) {
  293. const startValue = this.filterForm.startValue
  294. if (!endValue || !startValue) {
  295. return false
  296. }
  297. return endValue.valueOf() < startValue.valueOf()
  298. },
  299. handleStartOpenChange (open) {
  300. this.startOpen = open
  301. if (open) {
  302. this.endOpen = false
  303. // 弹框打开时,保存当前值
  304. this.previousStartValue = this.filterForm.startValue ? this.$moment(this.filterForm.startValue).valueOf() : null
  305. } else {
  306. // 弹框关闭时,检查时间是否发生改动
  307. this.checkTimeChangeAndFilter('start')
  308. }
  309. },
  310. handleEndOpenChange (open) {
  311. this.endOpen = open
  312. if (open) {
  313. this.startOpen = false
  314. // 弹框打开时,保存当前值
  315. this.previousEndValue = this.filterForm.endValue ? this.$moment(this.filterForm.endValue).valueOf() : null
  316. } else {
  317. // 弹框关闭时,检查时间是否发生改动
  318. this.checkTimeChangeAndFilter('end')
  319. }
  320. },
  321. checkTimeChangeAndFilter (type) {
  322. let currentValue = null
  323. let previousValue = null
  324. if (type === 'start') {
  325. currentValue = this.filterForm.startValue ? this.$moment(this.filterForm.startValue).valueOf() : null
  326. previousValue = this.previousStartValue
  327. } else if (type === 'end') {
  328. currentValue = this.filterForm.endValue ? this.$moment(this.filterForm.endValue).valueOf() : null
  329. previousValue = this.previousEndValue
  330. }
  331. // 如果时间值发生了变化,执行过滤
  332. if (currentValue !== previousValue) {
  333. this.doFilter()
  334. }
  335. },
  336. handleTimeChange (e) {
  337. if (!e) this.doFilter()
  338. },
  339. },
  340. }
  341. </script>
  342. <style lang="less" scoped>
  343. .dmesg-container {
  344. height: 100%;
  345. display: flex;
  346. flex-direction: column;
  347. overflow: hidden;
  348. }
  349. .dmesg-filter {
  350. background-color: #fff;
  351. flex-shrink: 0;
  352. z-index: 1;
  353. position: relative;
  354. }
  355. .dmesg-scroll-container {
  356. flex: 1;
  357. overflow-y: auto;
  358. padding: 16px;
  359. background-color: #f5f5f5;
  360. min-height: 0;
  361. position: relative;
  362. }
  363. .loading-wrapper,
  364. .empty-wrapper {
  365. display: flex;
  366. align-items: center;
  367. justify-content: center;
  368. position: absolute;
  369. top: 0;
  370. left: 0;
  371. right: 0;
  372. bottom: 0;
  373. min-height: 300px;
  374. background-color: transparent;
  375. }
  376. .dmesg-list {
  377. .dmesg-item {
  378. padding: 5px 3px;
  379. // background-color: #fff;
  380. font-family: 'Courier New';
  381. font-size: 14px;
  382. line-height: 1.6;
  383. word-break: break-all;
  384. white-space: pre-wrap;
  385. // border-left: 3px solid #1890ff;
  386. // &:hover {
  387. // background-color: #f0f0f0;
  388. // }
  389. }
  390. .loading-more {
  391. display: flex;
  392. align-items: center;
  393. justify-content: center;
  394. padding: 16px;
  395. color: #999;
  396. }
  397. .no-more-data {
  398. text-align: center;
  399. padding: 16px;
  400. color: #999;
  401. font-size: 12px;
  402. }
  403. }
  404. </style>