index.vue 10 KB

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