detail.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <template>
  2. <div class="app-container" v-loading="loading">
  3. <evaluation-title :title="title" :sub-title="subTitle" nav-back-button></evaluation-title>
  4. <el-card>
  5. <mx-condition :query-params="queryParams" :require-fields="requireFields" :local-data="localData"
  6. @query="handleQuery"></mx-condition>
  7. </el-card>
  8. <mx-table :rows="detailTable.rows" :prop-defines="detailTable.columns" border class="mt20 elective-flow-table">
  9. <template #group-header="{label, key}">
  10. <div class="fx-row jc-cen">
  11. <span>{{ label }}</span>
  12. <el-popover trigger="hover" :disabled="!!!detailTable.groupHeaders[key].text" class="mx-generation-rank">
  13. <div v-for="(desc,idx) in detailTable.groupHeaders[key].descriptions" :key="idx">
  14. <span>{{ desc.description }}</span> <span class="bold">{{ desc.value }}</span></div>
  15. <el-tag slot="reference" size="mini" class="round-y ml3">{{ detailTable.groupHeaders[key].text }}</el-tag>
  16. </el-popover>
  17. </div>
  18. </template>
  19. <template #group-flow="{value}">
  20. <elective-flow-major v-if="value.matchedMajors.length" :icon-classes="['f-primary']"
  21. :matched-majors="value.matchedMajors"></elective-flow-major>
  22. <div class="fx-row fx-cen-cen">
  23. <span v-if="value.text">{{ value.text }}</span>
  24. <span v-else v-html="'&#12288'"></span>
  25. </div>
  26. <elective-flow-rank-descriptor v-if="value.rankDescriptors"
  27. :rank-descriptors="value.rankDescriptors"></elective-flow-rank-descriptor>
  28. </template>
  29. <template #flow-action="{row}">
  30. <el-link @click="handleFlowLog(row)" :underline="false">查看</el-link>
  31. <el-popover v-if="row['enableForce']" :ref="'force_'+row['studentId']" width="80" trigger="click"
  32. popper-class="zero-padding-popover">
  33. <div class="fx-column">
  34. <el-divider>调剂</el-divider>
  35. <el-button v-for="(g,i) in prevData.groups" :key="i" class="ml0" plain type="text"
  36. :disabled="g.groupId==row['disableForceGroupId']"
  37. @click="handleForceAdjust(g, row)&&$refs['force_'+row['studentId']].doClose()">{{ g.groupName }}
  38. </el-button>
  39. </div>
  40. <el-link slot="reference" :underline="false" type="warning" class="ml10">调剂</el-link>
  41. </el-popover>
  42. </template>
  43. </mx-table>
  44. <pagination :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
  45. @pagination="loadGenerationDetails"></pagination>
  46. <el-dialog title="选科历史记录" v-if="logVisible" :visible.sync="logVisible" :width="logDialogWidth">
  47. <elective-generation-flow-log :groups="prevData.groups" :histories="logRow.histories"
  48. :matched-majors="this.majorsMap[logRow['studentId']]"/>
  49. </el-dialog>
  50. </div>
  51. </template>
  52. <script>
  53. import config from '@/common/mx-config'
  54. import transferMixin from '@/components/mx-transfer-mixin'
  55. import groupTranslateMixin from '@/components/Cache/modules/mx-select-translate-mixin'
  56. import { mapGetters } from 'vuex'
  57. import MxCondition from '@/components/MxCondition/mx-condition'
  58. import {
  59. enrollByForce,
  60. getElectiveGenerationDetails,
  61. getGenerationOptionalMajorsBatch
  62. } from '@/api/webApi/elective/generation'
  63. import ElectiveGenerationFlowLog from '@/views/elective/generation/components/elective-generation-flow-log'
  64. import ElectiveFlowMajor from '@/views/elective/generation/components/elective-flow-major'
  65. import ElectiveFlowRankDescriptor from '@/views/elective/generation/components/elective-flow-rank-descriptor'
  66. export default {
  67. components: { ElectiveFlowRankDescriptor, ElectiveFlowMajor, ElectiveGenerationFlowLog, MxCondition },
  68. mixins: [transferMixin, groupTranslateMixin],
  69. name: 'generation-detail',
  70. computed: {
  71. ...mapGetters(['school']),
  72. title() {
  73. const y = (this.prevData.year + '').tailingFix('学年')
  74. const s = this.school.schoolName
  75. const n = this.prevData.roundName
  76. return y + s + n
  77. },
  78. subTitle() {
  79. const g = Object.values(config.electiveGenerationOptions).find(opt => opt.value == this.prevData.queryGeneration)
  80. const hideGeneration = g == config.electiveGenerationOptions.init || g == config.electiveGenerationOptions.terminate
  81. return hideGeneration ? '' : g?.title || ''
  82. },
  83. localData() {
  84. this.queryParams.generation = this.prevData.queryGeneration
  85. this.queryParams.generationQueryCode = this.prevData.queryCode
  86. this.queryParams.generationGroupId = this.prevData.queryGroupId
  87. return {
  88. ignoreGroupCategories: this.prevData.ignoreGroupCategories,
  89. categories: this.prevData.queryableCategories,
  90. groups: this.prevData.groups
  91. }
  92. },
  93. detailTable() {
  94. if (!this.majorsMap) return {}
  95. if (!this.prevData.queryableCategories.length) return {}
  96. if (!this.detailWrapper?.groupDescriptors?.length) return {}
  97. // fixed columns
  98. const queryCategory = this.prevData.queryableCategories.find(i => i.id == this.queryParams.generationQueryCode)
  99. const ignoreGroups = this.prevData.ignoreGroupCategories.includes(this.queryParams.generationQueryCode)
  100. const columns = {
  101. className: { label: '班级' },
  102. studentName: { label: '姓名' }
  103. }
  104. if (!ignoreGroups) {
  105. columns.groupName = { label: queryCategory.detailName || '组合' }
  106. columns.datetime = { label: '时间', minWidth: '110px' }
  107. }
  108. // extension generation columns & custom group header
  109. const rows = this.detailWrapper.details // todo: need clone?
  110. const groupHeaders = {}
  111. const groupKeyPrefix = 'group_'
  112. if (!ignoreGroups) {
  113. this.prevData.groups.forEach(group => {
  114. const groupKey = groupKeyPrefix + group.groupId
  115. columns[groupKey] = {
  116. label: group.groupName,
  117. slotHeader: 'group-header',
  118. slot: 'group-flow',
  119. minWidth: '180px'
  120. }
  121. groupHeaders[groupKey] = this.getGroupHeaderDescription(group.groupId)
  122. rows.forEach(row => {
  123. row[groupKey] = this.getGroupDescription(row, group.groupId, this.majorsMap)
  124. })
  125. })
  126. }
  127. columns.actions = { label: '操作', slot: 'flow-action', minWidth: '100px' }
  128. return {
  129. rows,
  130. columns,
  131. groupHeaders
  132. }
  133. },
  134. logDialogWidth() {
  135. const expectedWidth = (this.prevData.groups.length + 3) * 160 // 假定elective-generation-flow-log 单格宽160
  136. const finalWidth = Math.min(expectedWidth, window.innerWidth * 0.8)
  137. return finalWidth + 'px'
  138. }
  139. },
  140. data() {
  141. return {
  142. loading: false,
  143. requireFields: ['generationQueryCode'],
  144. queryParams: {
  145. pageNo: 1,
  146. pageSize: 20,
  147. generation: '',
  148. generationQueryCode: '',
  149. generationGroupId: ''
  150. },
  151. // query data
  152. total: 0,
  153. detailWrapper: null,
  154. majorsMap: null,
  155. // log
  156. logVisible: false,
  157. logRow: {}
  158. }
  159. },
  160. methods: {
  161. handleQuery() {
  162. this.queryParams.pageNo = 1
  163. this.loadGenerationDetails()
  164. },
  165. loadGenerationDetails() {
  166. const params = {
  167. pageNo: this.queryParams.pageNo,
  168. pageSize: this.queryParams.pageSize,
  169. roundId: this.prevData.roundId,
  170. generation: this.queryParams.generation,
  171. groupId: this.queryParams.generationGroupId,
  172. queryCode: this.queryParams.generationQueryCode
  173. }
  174. this.loading = true
  175. this.majorsMap = null
  176. getElectiveGenerationDetails(params).then(res => {
  177. this.total = res['total']
  178. this.detailWrapper = res.data
  179. const studentIds = res.data.details?.map(d => d['studentId']).toString()
  180. return getGenerationOptionalMajorsBatch({ studentIds })
  181. }).then(res => {
  182. this.majorsMap = res.data
  183. }).finally(() => this.loading = false)
  184. },
  185. getGroupHeaderDescription(groupId) {
  186. const statistic = this.detailWrapper.groupDescriptors.find(d => d.groupId == groupId)
  187. if (!statistic?.descriptors?.length) return {}
  188. const descriptors = statistic.descriptors.reverse()
  189. return {
  190. text: descriptors.map(d => d.value).join('/'),
  191. descriptions: descriptors
  192. }
  193. },
  194. getGroupDescription(row, groupId) {
  195. const matchedMajors = (this.majorsMap[row['studentId']]
  196. ?.majors?.filter(m => m['matchedGroupIds'].some(id => id == groupId)) || [])
  197. .groupBy(m => m.collegeName)
  198. const current = this.prevData.activeGeneration
  199. const options = Object.values(config.electiveGenerationOptions)
  200. const histories = row['histories'] || []
  201. const filterHistories = []
  202. for (let g = current; g > 0; g--) {
  203. const opt = options.find(opt => opt.value == g)
  204. const groupHistories = histories.filter(h => h.generation == g && h.groupId == groupId)
  205. if (groupHistories.length) filterHistories.push(groupHistories)
  206. // if (g < current && opt.decisionMaking) break // TODO: 仅迭代至最近的决策代(可能需要调整)
  207. }
  208. if (!filterHistories.length) return { matchedMajors }
  209. filterHistories.reverse() // 还原顺序
  210. const mergedHistories = filterHistories.reduce((prev, cur) => {
  211. prev.push(...cur)
  212. return prev
  213. }, [])
  214. return {
  215. matchedMajors,
  216. text: mergedHistories.map(h => h.description).join('/'),
  217. histories: mergedHistories,
  218. rankDescriptors: mergedHistories.last(i => !!i.rankDescriptors?.length)?.rankDescriptors
  219. }
  220. },
  221. handleFlowLog(row) {
  222. this.logRow = row
  223. this.logVisible = true
  224. },
  225. handleForceAdjust(group, row) {
  226. let message = `确认将'${row.studentName}'调剂至'${group.groupName}'?!`
  227. if (row['disableForceGroupId'] > 0) {
  228. message += `\n 当前录取组合'${this.translateGroup(row['disableForceGroupId'])}'`
  229. }
  230. this.$confirm(message, '强制调剂提醒', { type: 'warning' }).then(() => {
  231. enrollByForce(group.groupId, row['studentId']).then(res => {
  232. this.loadGenerationDetails() // refresh
  233. })
  234. })
  235. return true
  236. }
  237. }
  238. }
  239. </script>
  240. <style>
  241. @import url('./components/elective-flow-table-style.css');
  242. </style>