detail.vue 14 KB

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