index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <template>
  2. <div class="analysis-monitor-view">
  3. <div class="flex items-center justify-end p-l-base p-r-base fill-blank view-header">
  4. <div class="pointer circle-button" custom-1 @click="toggleSetting(true)"><svg-icon name="setting" /></div>
  5. <el-button type="primary" :loading="loading" @click="onRefresh">刷新</el-button>
  6. </div>
  7. <div class="content-wrap p-base flex">
  8. <div class="data-card-view">
  9. <div
  10. v-for="card in cards"
  11. :key="card.dataField"
  12. class="radius-base full fill-blank data-card"
  13. :class="{ active: activeCard?.dataField === card.dataField }"
  14. @click="setActiveCard(card)"
  15. >
  16. <div class="flex direction-column radius-base full fill-blank data-card-content">
  17. <div class="p-base card-title">{{ card.title }}</div>
  18. <div class="p-l-extra-base p-r-mini p-b-extra-small flex-1 overflow-hidden">
  19. <base-table
  20. border
  21. v-bind="getTableProps(card.hasAll)"
  22. size="small"
  23. height="100%"
  24. :columns="getColumns(card.valueLabel)"
  25. :data="getData(sortableResult[card.dataField], card.hasAll)"
  26. >
  27. <template #empty>
  28. <empty :image-size="100"></empty>
  29. </template>
  30. </base-table>
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. <div class="p-base radius-base m-l-auto fill-blank data-detail-view">
  36. <div class="m-b-mini card-title">系统监控</div>
  37. <base-table
  38. v-if="activeCard"
  39. border
  40. stripe
  41. size="small"
  42. :columns="getColumns(activeCard.valueLabel)"
  43. :data="sortableResult[activeCard.dataField]"
  44. >
  45. </base-table>
  46. </div>
  47. </div>
  48. <base-dialog v-model="visibleSetting" title="参数设置" :footer="false" destroy-on-close>
  49. <base-form :label-width="'88px'" :model="model" :items="formItems">
  50. <el-form-item>
  51. <confirm-button @confirm="onConfirm" @cancel="toggleSetting(false)"></confirm-button>
  52. </el-form-item>
  53. </base-form>
  54. </base-dialog>
  55. </div>
  56. </template>
  57. <script setup lang="tsx" name="AnalysisMonitoring">
  58. /** 决策分析-监控 */
  59. import { computed, reactive, ref, watch, nextTick } from 'vue'
  60. import { useRouter } from 'vue-router'
  61. import { ElButton, ElFormItem } from 'element-plus'
  62. import { omit } from 'lodash-es'
  63. import { useIntervalFn } from '@vueuse/core'
  64. import { localStorage } from '@/plugins/storage'
  65. import useFetch from '@/hooks/useFetch'
  66. import useOptions from '@/hooks/useOptions'
  67. import useVW from '@/hooks/useVW'
  68. import SvgIcon from '@/components/common/SvgIcon.vue'
  69. import BaseTable from '@/components/element/BaseTable.vue'
  70. import BaseDialog from '@/components/element/BaseDialog.vue'
  71. import BaseForm from '@/components/element/BaseForm.vue'
  72. import ConfirmButton from '@/components/common/ConfirmButton.vue'
  73. import Empty from '@/components/common/Empty.vue'
  74. import type { EpTableColumn, EpFormItem, EpTableProps } from 'global-type'
  75. import type { ExtractApiParams, ExtractApiResponse } from '@/api/api'
  76. const { push } = useRouter()
  77. const { subjectList, mainQuestionList, groupListWithAll, dataModel, changeModelValue, initFinish, onOptionInit } =
  78. useOptions(['subject', 'question', 'group'])
  79. const model = reactive<
  80. Omit<ExtractApiParams<'getStatistics'>, 'markingGroupNumbers'> & {
  81. refresh: number
  82. markingGroupNumbers?: number
  83. }
  84. >({
  85. subjectCode: dataModel.subject || '',
  86. questionMainNumber: dataModel.question,
  87. markingGroupNumbers: dataModel.group,
  88. refresh: localStorage.get('MONITORING_REFRESH_RATE') || 0,
  89. })
  90. const modelToFetchModel = () => {
  91. return Object.assign({}, model, {
  92. markingGroupNumbers:
  93. typeof model.markingGroupNumbers === 'number' ? [model.markingGroupNumbers] : model.markingGroupNumbers,
  94. })
  95. }
  96. const fetchModel = reactive<ExtractApiParams<'getStatistics'> & { refresh: number }>(modelToFetchModel())
  97. watch(fetchModel, () => {
  98. localStorage.set('MONITORING_REFRESH_RATE', fetchModel.refresh)
  99. })
  100. watch(
  101. dataModel,
  102. () => {
  103. model.subjectCode = dataModel.subject
  104. model.questionMainNumber = dataModel.question
  105. model.markingGroupNumbers = dataModel.group
  106. },
  107. { immediate: true }
  108. )
  109. const formItems = computed<EpFormItem[]>(() => [
  110. {
  111. label: '科目',
  112. slotType: 'select',
  113. prop: 'subjectCode',
  114. slot: {
  115. options: subjectList.value,
  116. disabled: true,
  117. },
  118. },
  119. {
  120. label: '大题',
  121. slotType: 'select',
  122. prop: 'questionMainNumber',
  123. slot: {
  124. options: mainQuestionList.value,
  125. disabled: true,
  126. },
  127. },
  128. {
  129. label: '小组',
  130. slotType: 'select',
  131. prop: 'markingGroupNumbers',
  132. slot: {
  133. options: groupListWithAll.value,
  134. onChange: changeModelValue('group'),
  135. },
  136. },
  137. {
  138. label: '自动刷新',
  139. slotType: 'select',
  140. prop: 'refresh',
  141. slot: {
  142. options: [3, 5, 10, 0].map((n) => ({ value: n, label: n ? `${n}分钟` : '不自动刷新' })),
  143. },
  144. },
  145. ])
  146. interface Card {
  147. dataField: string
  148. title: string
  149. valueLabel: string
  150. hasAll: boolean
  151. }
  152. const cards: Card[] = [
  153. {
  154. dataField: 'totalPaperList',
  155. title: '评卷份数',
  156. valueLabel: '份数',
  157. hasAll: true,
  158. },
  159. {
  160. dataField: 'xyRelateList',
  161. title: '相关系数',
  162. valueLabel: '相关系数',
  163. hasAll: true,
  164. },
  165. {
  166. dataField: 'avgList',
  167. title: '平均分',
  168. valueLabel: '平均分',
  169. hasAll: true,
  170. },
  171. {
  172. dataField: 'stdList',
  173. title: '标准差',
  174. valueLabel: '标准差',
  175. hasAll: true,
  176. },
  177. {
  178. dataField: 'scoreTopLowList',
  179. title: '近5分钟最高最低分',
  180. valueLabel: '分数',
  181. hasAll: false,
  182. },
  183. {
  184. dataField: 'objSubRateList',
  185. title: '近5分钟客主比',
  186. valueLabel: '最高',
  187. hasAll: false,
  188. },
  189. {
  190. dataField: 'objSubAvgRateList',
  191. title: '平均客主比',
  192. valueLabel: '平均最高',
  193. hasAll: false,
  194. },
  195. {
  196. dataField: 'integrationList',
  197. title: '综合',
  198. valueLabel: '综合指数',
  199. hasAll: false,
  200. },
  201. ]
  202. /** 跳转抽查详情 */
  203. const viewMarkDetail = (row: ExtractArrayValue<ExtractRecordValue<ExtractApiResponse<'getStatistics'>>>) => {
  204. push({
  205. name: 'AnalysisViewMarked',
  206. params: {
  207. markerId: row.markerId,
  208. },
  209. query: {
  210. markerName: row.markerName,
  211. },
  212. })
  213. }
  214. const getTableProps: (hasAll: boolean) => EpTableProps = (hasAll) => {
  215. return {
  216. highlightCurrentRow: false,
  217. rowClassName({ rowIndex }) {
  218. const startIndex = hasAll ? 1 : 0
  219. const splitIndex = hasAll ? 3 : 2
  220. if (rowIndex >= startIndex && rowIndex <= splitIndex) {
  221. return 'top-three-row'
  222. } else if (rowIndex >= splitIndex + 1 && rowIndex <= splitIndex + 3) {
  223. return 'last-three-row'
  224. }
  225. return ''
  226. },
  227. }
  228. }
  229. const getColumns = (
  230. valueLabel: string
  231. ): EpTableColumn<ExtractArrayValue<ExtractRecordValue<ExtractApiResponse<'getStatistics'>>>>[] => {
  232. return [
  233. {
  234. label: '老师ID',
  235. align: 'center',
  236. formatter(row) {
  237. return row.markerId === 0 ? (
  238. '全体'
  239. ) : (
  240. <ElButton type="primary" link onClick={() => viewMarkDetail(row)}>
  241. {row.markerName}
  242. </ElButton>
  243. )
  244. },
  245. },
  246. { label: valueLabel, prop: 'value', align: 'center' },
  247. ]
  248. }
  249. const activeCard = ref<Card>()
  250. const setActiveCard = (v: Card) => {
  251. activeCard.value = v
  252. }
  253. const visibleSetting = ref<boolean>(false)
  254. const toggleSetting = (visible: boolean) => {
  255. visibleSetting.value = visible
  256. }
  257. const interval = computed(() => fetchModel.refresh * 60 * 1000)
  258. const { fetch, result, loading } = useFetch('getStatistics')
  259. const getData = (data: ExtractRecordValue<ExtractApiResponse<'getStatistics'>>, hasAll: boolean) => {
  260. return data?.slice(0, hasAll ? 4 : 3).concat(data?.slice(hasAll ? 4 : 3).slice(-3)) || []
  261. }
  262. const sortableResult = computed<typeof result.value>(() => {
  263. if (!result.value) return {}
  264. return cards.reduce((final, { dataField, hasAll }) => {
  265. let resultData = result.value[dataField]?.filter((v) => hasAll || v.markerId) || []
  266. if (dataField === 'scoreTopLowList') {
  267. const scoreTopList = result.value?.['scoreTopList']?.filter((v) => hasAll || v.markerId) || []
  268. const scoreLowList = result.value?.['scoreLowList']?.filter((v) => hasAll || v.markerId) || []
  269. resultData = scoreTopList
  270. .concat(scoreLowList)
  271. ?.filter((d, i, arr) => d && i === arr.findIndex((v) => v.markerId === d.markerId))
  272. }
  273. const arr = [...resultData]
  274. const totalIndex = arr.findIndex((v) => v.markerId === 0)
  275. if (totalIndex >= 0) {
  276. const [total] = arr.splice(totalIndex, 1)
  277. total && arr.unshift(total)
  278. }
  279. final[dataField] = arr
  280. return final
  281. }, {} as typeof result.value)
  282. })
  283. const onRefresh = () => {
  284. let date = new Date()
  285. let year = date.getFullYear()
  286. let month: number | string = date.getMonth() + 1
  287. let day: number | string = date.getDate()
  288. month < 10 && (month = '0' + month)
  289. day < 10 && (day = '0' + day)
  290. if (fetchModel.subjectCode && fetchModel.questionMainNumber) {
  291. fetch({
  292. ...omit(fetchModel, 'refresh'),
  293. startTime: `${year}${month}${day}000000`,
  294. endTime: `${year}${month}${day}235959`,
  295. })
  296. }
  297. }
  298. const onConfirm = () => {
  299. Object.assign(fetchModel, modelToFetchModel())
  300. onRefresh()
  301. toggleSetting(false)
  302. }
  303. onOptionInit(onConfirm)
  304. const { pause, isActive, resume } = useIntervalFn(onRefresh, interval, { immediate: false })
  305. watch([interval, initFinish], () => {
  306. if (!initFinish.value) {
  307. return
  308. }
  309. if (interval.value <= 0 && isActive.value) {
  310. pause()
  311. } else {
  312. resume()
  313. }
  314. })
  315. </script>
  316. <style scoped lang="scss">
  317. .analysis-monitor-view {
  318. .content-wrapper {
  319. height: 784px;
  320. }
  321. .view-header {
  322. height: 52px;
  323. .circle-button {
  324. width: 32px;
  325. height: 32px;
  326. border-radius: 50%;
  327. background-color: #eee;
  328. color: #666;
  329. display: grid;
  330. place-items: center;
  331. margin-right: $ExtraSmallGapSpace;
  332. }
  333. }
  334. .data-card-view {
  335. // width: 1118px;
  336. width: calc(((100% - 40px) / 5) * 4 + 30px);
  337. height: 706px;
  338. flex-wrap: wrap;
  339. align-content: flex-start;
  340. display: flex;
  341. justify-content: space-between;
  342. .data-card {
  343. // width: 272px;
  344. width: calc((100% - 30px) / 4);
  345. height: 348px;
  346. padding-top: 2px;
  347. &.active {
  348. background-color: #32c5ff;
  349. box-shadow: 0px 16px 16px 0px rgba(0, 0, 0, 0.1);
  350. }
  351. &:nth-child(n + 5) {
  352. margin-top: $ExtraSmallGapSpace;
  353. }
  354. }
  355. }
  356. .data-detail-view {
  357. width: calc((100% - 40px) / 5);
  358. height: 706px;
  359. overflow: auto;
  360. }
  361. .card-title {
  362. color: $BlockTitleColor;
  363. font-size: $BaseFont;
  364. line-height: 1;
  365. }
  366. ::v-deep(.el-table) {
  367. .el-table__body-wrapper {
  368. .el-table__body {
  369. .top-three-row {
  370. background: rgba(0, 186, 151, 0.1);
  371. }
  372. .last-three-row {
  373. background: rgba(255, 114, 59, 0.1);
  374. }
  375. .el-table__cell {
  376. padding: 5px 0;
  377. }
  378. }
  379. }
  380. }
  381. }
  382. </style>