detail.vue 12 KB

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