detail.vue 13 KB

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