TotalProgress.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. <template>
  2. <div class="p-base p-b-base m-b-base radius-base fill-blank total-progress-box">
  3. <base-form size="small" :model="model" :items="items">
  4. <template #form-item-button>
  5. <el-button class="m-l-base" :loading="loading" type="primary" @click="onSearch">查询</el-button>
  6. </template>
  7. </base-form>
  8. <div class="total-progress-info">
  9. <div class="p-t-base p-b-extra-small table-title">整体进度</div>
  10. <div class="flex m-t-base">
  11. <base-table
  12. class="left-table"
  13. border
  14. stripe
  15. size="small"
  16. highlight-current-row
  17. :columns="totalColumns"
  18. :data="totalProgressData"
  19. @current-change="onCurrentChange"
  20. >
  21. <template #empty>暂无数据</template>
  22. </base-table>
  23. <div
  24. v-if="canLoadTopChart"
  25. class="p-b-extra-small overflow-hidden chart-info flex-1"
  26. :style="{ height: topChartHeight + 'px' }"
  27. >
  28. <vue-e-charts class="full" :option="totalChartsOption" autoresize></vue-e-charts>
  29. </div>
  30. </div>
  31. </div>
  32. </div>
  33. <div class="p-base radius-base fill-blank group-progress-box">
  34. <div class="flex direction-column p-r-base">
  35. <div class="flex justify-between">
  36. <span class="table-title p-t-small p-b-extra-small">小组进度</span>
  37. <span v-show="currentMainName" class="flex items-center current-main-name">
  38. <svg-icon class="m-r-medium-mini" name="question"></svg-icon>
  39. <span>{{ currentMainName }}</span>
  40. </span>
  41. </div>
  42. <div class="flex-1 overflow-hidden">
  43. <base-table border stripe size="small" height="100%" :columns="groupColumns" :data="groupProgressData">
  44. <template #empty>暂无数据</template>
  45. </base-table>
  46. </div>
  47. </div>
  48. <div class="chart-info m-t-base" style="height: 300px">
  49. <vue-e-charts v-if="currentMainName" class="full" :option="groupChartsOption" autoresize></vue-e-charts>
  50. </div>
  51. </div>
  52. </template>
  53. <script setup lang="ts" name="TotalProgress">
  54. /** 评卷进度 */
  55. import { reactive, watch, computed, ref, nextTick } from 'vue'
  56. import { ElButton } from 'element-plus'
  57. import VueECharts from 'vue-echarts'
  58. import { minus } from '@/utils/common'
  59. import BaseForm from '@/components/element/BaseForm.vue'
  60. import BaseTable from '@/components/element/BaseTable.vue'
  61. import SvgIcon from '@/components/common/SvgIcon.vue'
  62. import useFetch from '@/hooks/useFetch'
  63. import useOptions from '@/hooks/useOptions'
  64. import useVueECharts from '@/hooks/useVueECharts'
  65. import { usePX } from '@/hooks/useVW'
  66. import type { EChartsOption } from 'echarts'
  67. import type { ExtractApiResponse } from '@/api/api'
  68. import type { EpFormItem, EpTableColumn } from 'global-type'
  69. const { provideInitOption } = useVueECharts()
  70. provideInitOption({ renderer: 'svg' })
  71. const canLoadTopChart = ref(false)
  72. // const { subjectList, dataModel, changeModelValue } = useOptions(['subject'])
  73. const { subjectList, mainQuestionList, changeModelValue, dataModel, onOptionInit, isExpert, isLeader } = useOptions([
  74. 'subject',
  75. 'question',
  76. ])
  77. const model = reactive<any>({
  78. subjectCode: dataModel.subject || '',
  79. //todo
  80. questionMainNumber: null,
  81. // hasGroupLeaderScore: [],
  82. })
  83. watch(
  84. dataModel,
  85. () => {
  86. model.subjectCode = dataModel.subject || ''
  87. model.questionMainNumber = dataModel.question
  88. },
  89. { deep: true, immediate: true }
  90. )
  91. const items = computed<EpFormItem[]>(() => [
  92. {
  93. labelWidth: '52px',
  94. label: '科目',
  95. slotType: 'select',
  96. prop: 'subjectCode',
  97. slot: { options: subjectList.value, onChange: changeModelValue('subject'), disabled: !isExpert.value },
  98. rowKey: 'row-1',
  99. colProp: { span: 5 },
  100. },
  101. {
  102. labelWidth: '52px',
  103. slotType: 'select',
  104. label: '大题',
  105. prop: 'questionMainNumber',
  106. slot: {
  107. placeholder: '选择大题',
  108. options: mainQuestionList.value,
  109. onChange: changeModelValue('question'),
  110. disabled: !isExpert.value && !isLeader.value,
  111. },
  112. rowKey: 'row-1',
  113. colProp: { span: 5 },
  114. },
  115. // {
  116. // prop: 'hasGroupLeaderScore',
  117. // label: '',
  118. // slotType: 'checkbox',
  119. // labelWidth: '20px',
  120. // slot: {
  121. // options: [{ label: 'true', slotLabel: '包含组长给分' }],
  122. // },
  123. // rowKey: 'row-1',
  124. // colProp: { span: 3 },
  125. // },
  126. {
  127. rowKey: 'row-1',
  128. slotName: 'button',
  129. colProp: { span: 5 },
  130. },
  131. ])
  132. /** 整体进度 */
  133. type TotalProgress = ExtractArrayValue<ExtractApiResponse<'getMarkProgress'>>
  134. const getMainName = (row?: TotalProgress) => {
  135. const { questionMainName: name, questionMainNumber: number } = row || {}
  136. return [number, name].filter(Boolean).join('-')
  137. }
  138. const totalColumns: EpTableColumn<TotalProgress>[] = [
  139. {
  140. label: '大题',
  141. formatter: getMainName,
  142. fixed: 'left',
  143. },
  144. { label: '试卷总量', prop: 'totalPaper', align: 'center', sortable: true, minWidth: 90 },
  145. { label: '已完成', prop: 'finishCount', align: 'center', sortable: true, minWidth: 80 },
  146. {
  147. label: '完成比',
  148. align: 'center',
  149. minWidth: 80,
  150. formatter(row) {
  151. return `${row.finishRate}%`
  152. },
  153. sortable: true,
  154. },
  155. {
  156. label: '待完成',
  157. align: 'center',
  158. minWidth: 80,
  159. formatter(row) {
  160. return `${minus(row.totalPaper, row.finishCount)}`
  161. },
  162. sortable: true,
  163. },
  164. {
  165. label: '待完成比',
  166. align: 'center',
  167. minWidth: 95,
  168. formatter(row) {
  169. return `${minus(100, row.finishRate)}%`
  170. },
  171. sortable: true,
  172. },
  173. {
  174. // label: '预计耗时(分)',
  175. label: '预计耗时',
  176. align: 'center',
  177. minWidth: 95,
  178. prop: 'takeTime',
  179. // formatter(row) {
  180. // // return `${parseFloat((row.takeTime / 60 / 60).toFixed(2))}`
  181. // return `${parseFloat((row.takeTime / 60 / 60).toFixed(5))}`
  182. // },
  183. // sortable: true,
  184. },
  185. ]
  186. const { fetch: getMarkProgress, result: markProgressResult, loading } = useFetch('getMarkProgress')
  187. /** 将汇总行放到数组最后 */
  188. const totalProgressData = computed(() => {
  189. if (!markProgressResult?.value) return []
  190. const arr = markProgressResult.value.slice(0)
  191. const totalIndex = arr.findIndex((v) => v.questionMainNumber === 0)
  192. const [total] = arr.splice(totalIndex, 1)
  193. arr.push(total)
  194. return arr
  195. })
  196. const topChartHeight = computed(() => {
  197. return ((markProgressResult.value as any) || []).length * 44 + 60
  198. })
  199. watch(
  200. () => topChartHeight.value,
  201. (val: any) => {
  202. if (val > 1) {
  203. nextTick(() => {
  204. canLoadTopChart.value = true
  205. })
  206. }
  207. }
  208. )
  209. // watch(
  210. // [() => model.subjectCode, () => model.questionMainNumber],
  211. // (v) => {
  212. // v && getMarkProgress(model)
  213. // },
  214. // { immediate: true }
  215. // )
  216. function onSearch() {
  217. getMarkProgress(model)
  218. }
  219. onOptionInit(onSearch)
  220. const currentMainQuestion = ref<TotalProgress>()
  221. const currentMainName = computed(() => {
  222. return getMainName(currentMainQuestion.value)
  223. })
  224. const onCurrentChange = (row: TotalProgress) => {
  225. if (row.questionMainNumber !== 0) {
  226. currentMainQuestion.value = row
  227. }
  228. }
  229. const getYAxisData = (field: keyof TotalProgress | 'name', data?: TotalProgress[]) => {
  230. if (!data) {
  231. return []
  232. }
  233. return data.map((v) => {
  234. if (field === 'name') {
  235. return getMainName(v)
  236. }
  237. return v[field]
  238. })
  239. }
  240. const totalChartsOption = computed<EChartsOption>(() => {
  241. return {
  242. tooltip: {
  243. show: true,
  244. trigger: 'item',
  245. formatter(params: any) {
  246. let prev = params.seriesName + '<br />' + params.name + ': ' + params.value
  247. if (params.seriesName === '完成比') {
  248. return prev + '%'
  249. } else {
  250. return prev
  251. }
  252. },
  253. },
  254. grid: {
  255. top: 26,
  256. bottom: -20,
  257. left: 20,
  258. right: 30,
  259. containLabel: true,
  260. },
  261. legend: {
  262. right: 0,
  263. top: 0,
  264. itemWidth: 14,
  265. data: ['试卷总量', '已完成', '完成比'],
  266. },
  267. yAxis: {
  268. axisLine: { show: false },
  269. axisTick: { show: false },
  270. splitLine: { show: false },
  271. inverse: true,
  272. axisLabel: {
  273. align: 'right',
  274. verticalAlign: 'bottom',
  275. },
  276. data: getYAxisData('name', markProgressResult?.value),
  277. },
  278. xAxis: [
  279. {
  280. position: 'top',
  281. type: 'value',
  282. splitLine: { show: true },
  283. },
  284. {
  285. position: 'bottom',
  286. type: 'value',
  287. show: false,
  288. axisLabel: {
  289. formatter: `{value}%`,
  290. },
  291. splitLine: { show: false },
  292. },
  293. ],
  294. series: [
  295. {
  296. name: '试卷总量',
  297. type: 'bar',
  298. barWidth: 11,
  299. barGap: '-200%',
  300. itemStyle: {
  301. color: '#0064FF',
  302. },
  303. data: getYAxisData('totalPaper', markProgressResult?.value),
  304. },
  305. {
  306. name: '已完成',
  307. type: 'bar',
  308. barWidth: 11,
  309. barGap: '-200%',
  310. itemStyle: {
  311. color: '#3AD500',
  312. },
  313. data: getYAxisData('finishCount', markProgressResult?.value),
  314. },
  315. {
  316. name: '完成比',
  317. type: 'bar',
  318. // barWidth: 44,
  319. barWidth: 11,
  320. barGap: '-200%',
  321. showBackground: true,
  322. xAxisIndex: 1,
  323. itemStyle: {
  324. color: 'rgba(0, 186, 151,0.3)',
  325. },
  326. label: {
  327. show: true,
  328. color: '#444',
  329. fontSize: 10,
  330. formatter({ value }) {
  331. return value > 0 ? `${value}%` : ''
  332. },
  333. position: 'insideTopRight',
  334. },
  335. data: getYAxisData('finishRate', markProgressResult?.value),
  336. },
  337. ],
  338. }
  339. })
  340. /** 小组进度 */
  341. type GroupProgress = ExtractArrayValue<ExtractApiResponse<'getMarkProgressByGroup'>>
  342. const groupColumns: EpTableColumn<GroupProgress>[] = [
  343. { label: '小组', formatter: (row) => (row.markingGroupNumber ? `第${row.markingGroupNumber}组` : '全体') },
  344. { label: '完成总量', prop: 'finishCount', align: 'center', sortable: true },
  345. {
  346. label: '完成比',
  347. prop: 'finishRate',
  348. align: 'center',
  349. formatter(row) {
  350. return `${row.finishRate}%`
  351. },
  352. sortable: true,
  353. },
  354. { label: '当日已完成', prop: 'dayFinishCount', align: 'center', sortable: true },
  355. {
  356. label: '当日完成比',
  357. prop: 'dayFinishRate',
  358. align: 'center',
  359. formatter(row) {
  360. return `${row.dayFinishRate}%`
  361. },
  362. sortable: true,
  363. },
  364. ]
  365. const { fetch: getMarkProgressByGroup, result: groupProgressResult } = useFetch('getMarkProgressByGroup')
  366. /** 将汇总行放到数组最后 */
  367. const groupProgressData = computed(() => {
  368. if (!groupProgressResult?.value) return []
  369. const arr = groupProgressResult.value.slice(0)
  370. const totalIndex = arr.findIndex((v) => v.markingGroupNumber === 0)
  371. const [total] = arr.splice(totalIndex, 1)
  372. arr.push(total)
  373. return arr
  374. })
  375. watch(
  376. currentMainQuestion,
  377. (v) => {
  378. v?.questionMainNumber &&
  379. getMarkProgressByGroup({ subjectCode: model.subjectCode, questionMainNumber: v.questionMainNumber })
  380. },
  381. { immediate: true }
  382. )
  383. const getXAxisData = (field: keyof GroupProgress, data?: GroupProgress[]) => {
  384. if (!data) {
  385. return []
  386. }
  387. const getValue = (data: GroupProgress, v: keyof GroupProgress) => {
  388. if (v === 'markingGroupNumber') {
  389. return `第${data[v]}组`
  390. }
  391. return data[v]
  392. }
  393. return data.filter((v) => v.markingGroupNumber !== 0).map((v) => getValue(v, field))
  394. }
  395. const groupChartsOption = computed<EChartsOption>(() => {
  396. return {
  397. tooltip: {
  398. show: true,
  399. trigger: 'item',
  400. formatter(params: any) {
  401. let prev = params.seriesName + '<br />' + params.name + ': ' + params.value
  402. if (params.seriesName === '完成比') {
  403. return prev + '%'
  404. } else {
  405. return prev
  406. }
  407. },
  408. },
  409. legend: {
  410. right: 0,
  411. itemWidth: 14,
  412. data: ['完成总量', '当日已完成', '完成比'],
  413. },
  414. xAxis: {
  415. axisLine: { show: false },
  416. axisTick: { show: false },
  417. splitLine: { show: false },
  418. axisLabel: {
  419. align: 'right',
  420. },
  421. data: getXAxisData('markingGroupNumber', groupProgressResult?.value),
  422. },
  423. yAxis: [
  424. {
  425. type: 'value',
  426. },
  427. {
  428. type: 'value',
  429. axisLabel: {
  430. formatter: `{value}%`,
  431. },
  432. splitLine: { show: false },
  433. },
  434. ],
  435. series: [
  436. {
  437. name: '完成总量',
  438. type: 'bar',
  439. barWidth: 20,
  440. itemStyle: {
  441. color: '#0064FF',
  442. },
  443. data: getXAxisData('finishCount', groupProgressResult?.value),
  444. },
  445. {
  446. name: '当日已完成',
  447. type: 'bar',
  448. barWidth: 20,
  449. itemStyle: {
  450. color: '#3AD500',
  451. },
  452. data: getXAxisData('dayFinishCount', groupProgressResult?.value),
  453. },
  454. {
  455. name: '完成比',
  456. type: 'bar',
  457. barWidth: 20,
  458. // showBackground: true,
  459. barGap: '-200%',
  460. yAxisIndex: 1,
  461. itemStyle: {
  462. color: 'rgba(0, 186, 151,0.3)',
  463. },
  464. data: getXAxisData('finishRate', groupProgressResult?.value),
  465. },
  466. ],
  467. }
  468. })
  469. </script>
  470. <style scoped lang="scss">
  471. .total-progress-box {
  472. height: 323px;
  473. .left-table {
  474. width: 62%;
  475. // flex: 1;
  476. }
  477. .total-progress-info {
  478. border-top: $OnePixelLine;
  479. // height: 253px;
  480. }
  481. }
  482. // .table-info {
  483. // // width: 720px;
  484. // border-right: $OnePixelLine;
  485. .table-title {
  486. font-size: $MediumFont;
  487. }
  488. // }
  489. .group-progress-box {
  490. .current-main-name {
  491. background-color: $BaseBgColor;
  492. font-size: $SmallFont;
  493. color: $PrimaryPlusFontColor;
  494. border-radius: 4px;
  495. &:not(:empty) {
  496. padding: 2px 4px;
  497. }
  498. }
  499. }
  500. </style>