Ver código fonte

选科 - generation - code completed

hare8999@163.com 3 anos atrás
pai
commit
3c7e7677c5

+ 4 - 4
doc/Mind/ElectiveGeneration.cs

@@ -337,7 +337,7 @@ namespace mxdemo.Mind
         /// <summary>
         /// 明细肯定是通过 某个组合某个学生 来呈现的
         /// </summary>
-        public interface IElectiveGengerationDetail
+        public interface IElectiveGenerationDetail
         {
             long id { get; set; }
             EnumElectiveGeneration generation { get; set; }
@@ -359,7 +359,7 @@ namespace mxdemo.Mind
             string description;  // 描述文字
         }
 
-        public class ElectiveGenerationFlowHistory // : IElectiveGengerationDetail 可以考虑接收IElectiveGengerationDetail接口约束,也可以不,因为history只会展示关键信息
+        public class ElectiveGenerationFlowHistory // : IElectiveGenerationDetail 可以考虑接收IElectiveGenerationDetail接口约束,也可以不,因为history只会展示关键信息
         {
             EnumElectiveGeneration generation; // <=ElectiveGenerationDetail.generation
             int groupId; // ElectiveGenerationFlowData.groupId
@@ -379,7 +379,7 @@ namespace mxdemo.Mind
         /// 明细的学生组合信息, 主要包含报名信息;
         /// 还有一部分自选专业的信息,需要借助其它接口
         /// </summary>
-        public class ElectiveGenerationDetail : IElectiveGengerationDetail
+        public class ElectiveGenerationDetail : IElectiveGenerationDetail
         {
             ElectiveGenerationFlowHistory[] histories; // 4.19 当前student截止到当前代的全部generation flow data数据,但需要转换为业务格式
 
@@ -413,7 +413,7 @@ namespace mxdemo.Mind
         /// </summary>
         public class ElectiveGenerationOptionalMajor
         {
-            ElectiveOptionalMajor[] marjors; // 自选专业列表
+            ElectiveOptionalMajor[] majors; // 自选专业列表
 
             int bestMatchedGroupId; // 最佳匹配
         }

+ 97 - 0
mock/modules/elective-generation.js

@@ -58,6 +58,7 @@ module.exports = [
                   category: 'actualCount',
                   queryCode: Random.cname(),
                   displayName: '已报名',
+                  detailName: '报名组合',
                   values: mockGroups.map(groupId => ({
                     groupId: groupId,
                     value: Random.integer(120, 400),
@@ -73,6 +74,7 @@ module.exports = [
                   category: 'unfinishedCount',
                   queryCode: Random.cname(),
                   displayName: '未报名',
+                  detailName: '',
                   values: [{
                     groupId: 0,
                     value: Random.integer(0, 10),
@@ -102,6 +104,7 @@ module.exports = [
                   category: 'approvedCount',
                   queryCode: Random.cname(),
                   displayName: '正常录取',
+                  detailName: '录取组合',
                   values: mockGroups.map(groupId => ({
                     groupId: groupId,
                     value: Random.integer(120, 300),
@@ -115,6 +118,7 @@ module.exports = [
                   category: 'forcedCount',
                   queryCode: Random.cname(),
                   displayName: '调剂录取',
+                  detailName: '调剂组合',
                   values: mockGroups.map(groupId => ({
                     groupId: groupId,
                     value: Random.integer(0, 10),
@@ -147,6 +151,7 @@ module.exports = [
                     category: 'approvedCount',
                     queryCode: Random.cname(),
                     displayName: '录取人数',
+                    detailName: '录取组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -160,6 +165,7 @@ module.exports = [
                     category: 'adjustCount',
                     queryCode: Random.cname(),
                     displayName: '调剂人数',
+                    detailName: '调剂组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -173,6 +179,7 @@ module.exports = [
                     category: 'matchedCount',
                     queryCode: Random.cname(),
                     displayName: '专业符合',
+                    detailName: '推荐组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -186,6 +193,7 @@ module.exports = [
                     category: 'nonmatchedCount',
                     queryCode: Random.cname(),
                     displayName: '专业不符',
+                    detailName: '推荐组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -204,6 +212,7 @@ module.exports = [
                     category: 'matchedApproved',
                     queryCode: Random.cname(),
                     displayName: '专业符合同意',
+                    detailName: '报名组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -217,6 +226,7 @@ module.exports = [
                     category: 'matchedNotOptional',
                     queryCode: Random.cname(),
                     displayName: '专业符合改选',
+                    detailName: '报名组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -230,6 +240,7 @@ module.exports = [
                     category: 'matchedRejected',
                     queryCode: Random.cname(),
                     displayName: '专业符合拒绝',
+                    detailName: '拒绝组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -243,6 +254,7 @@ module.exports = [
                     category: 'matchedNonaction',
                     queryCode: Random.cname(),
                     displayName: '专业符合未填',
+                    detailName: '推荐组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -256,6 +268,7 @@ module.exports = [
                     category: 'matchedRankout',
                     queryCode: Random.cname(),
                     displayName: '专业符合被挤出',
+                    detailName: '报名组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -269,6 +282,7 @@ module.exports = [
                     category: 'nonmatchedApproved',
                     queryCode: Random.cname(),
                     displayName: '专业符合已填',
+                    detailName: '报名组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -282,6 +296,7 @@ module.exports = [
                     category: 'nonmatchedRejected',
                     queryCode: Random.cname(),
                     displayName: '不可调剂拒绝',
+                    detailName: '报名组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -295,6 +310,7 @@ module.exports = [
                     category: 'nonmatchedNonaction',
                     queryCode: Random.cname(),
                     displayName: '不可调剂未填',
+                    detailName: '报名组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -313,6 +329,7 @@ module.exports = [
                     category: 'approvedCount',
                     queryCode: Random.cname(),
                     displayName: '补录录取',
+                    detailName: '录取组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -326,6 +343,7 @@ module.exports = [
                     category: 'forcedCount',
                     queryCode: Random.cname(),
                     displayName: '调剂录取',
+                    detailName: '录取组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -339,6 +357,7 @@ module.exports = [
                     category: 'matchedCount',
                     queryCode: Random.cname(),
                     displayName: '可调剂人数',
+                    detailName: '调剂组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -352,6 +371,7 @@ module.exports = [
                     category: 'nonmatchedCount',
                     queryCode: Random.cname(),
                     displayName: '不可调剂人数',
+                    detailName: '报名组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -370,6 +390,7 @@ module.exports = [
                     category: 'approvedCount',
                     queryCode: Random.cname(),
                     displayName: '补录录取',
+                    detailName: '录取组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -383,6 +404,7 @@ module.exports = [
                     category: 'forcedCount',
                     queryCode: Random.cname(),
                     displayName: '调剂录取',
+                    detailName: '录取组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -401,6 +423,7 @@ module.exports = [
                     category: 'forcedCount',
                     queryCode: Random.cname(),
                     displayName: '调剂录取',
+                    detailName: '录取组合',
                     values: mockGroups.map(groupId => ({
                       groupId: groupId,
                       value: Random.integer(120, 400),
@@ -433,5 +456,79 @@ module.exports = [
         data: results
       }
     }
+  },
+  {
+    url: '/mock/front/report/getElectiveGenerationDetails',
+    type: 'get',
+    response: config => {
+      return {
+        code: 200,
+        msg: 'success',
+        total: Random.integer(5, 200),
+        data: {
+          groupDescriptors: mockGroups.map(groupId => {
+            return {
+              groupId,
+              descriptors: [
+                { key: 'setting', value: Random.integer(100, 9999), description: '设置人数' },
+                { key: 'actual', value: Random.integer(0, 9999), description: '实报人数' }
+              ]
+            }
+          }),
+          'details|5-20': [
+            {
+              'id|+1': 1000,
+              'roundId': 1,
+              'studentId|+1': 100,
+              'studentName': Random.cname(),
+              'groupId': config.query.groupId,
+              'groupName': Random.cname(),
+              'classId|+1': 20,
+              'className': Random.cname(),
+              'datetime': Random.date(),
+              'histories|4-15': [
+                {
+                  'generation|1': [1, 2, 3, 4, 5, 6, 7],
+                  'groupId|1': mockGroups,
+                  'description': Random.cname(),
+                  'rankDescriptors|1-3': [
+                    { key: Random.cname(), value: Random.integer(1, 300), description: Random.cname() }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  },
+  {
+    url: '/mock/front/report/getGenerationOptionalMajorsBatch',
+    type: 'get',
+    response: config => {
+      let studentIds = config.query.studentIds
+      if (typeof studentIds === 'string') studentIds = studentIds.split(',')
+      const majorsMap = {}
+      studentIds.forEach(id => {
+        majorsMap[id] = {
+          'bestMatchedGroupId|1': mockGroups,
+          'majors|0-6': [{
+            'collegeId|+1': 5000,
+            'collegeName': Random.cname(),
+            'majorCategoryCode': Random.cname(),
+            'majorCategoryName': Random.cname(),
+            'majors': {},
+            'limitationA': '', // 选科限制1
+            'limitationB': '', // 选科限制2
+            'matchedGroupIds|0-3': mockGroups // 匹配的组合
+          }]
+        }
+      })
+      return {
+        code: 200,
+        msg: 'success',
+        data: majorsMap
+      }
+    }
   }
 ]

+ 16 - 0
src/api/webApi/elective/generation.js

@@ -15,3 +15,19 @@ export function getElectiveSummary(params) {
     params
   })
 }
+
+export function getElectiveGenerationDetails(params) {
+  return request({
+    url: '/mock/front/report/getElectiveGenerationDetails',
+    method: 'get',
+    params
+  })
+}
+
+export function getGenerationOptionalMajorsBatch(params) {
+  return request({
+    url: '/mock/front/report/getGenerationOptionalMajorsBatch',
+    method: 'get',
+    params
+  })
+}

+ 0 - 1
src/assets/styles/common.scss

@@ -618,7 +618,6 @@
   width: 100%;
 }
 
-
 .f12 {
   font-size: 12px;
 }

+ 16 - 4
src/common/mx-extension.js

@@ -1,11 +1,23 @@
 export default {
   install(Vue) {
     // Array ext.
-    Array.prototype.first = function() {
-      return this.length ? this[0] : null
+    Array.prototype.first = function(predicate) {
+      if (predicate == null || typeof predicate !== 'function') {
+        return this.length ? this[0] : null
+      }
+      for (let i = 0; i < this.length; i++) {
+        const item = this[i]
+        if (predicate(item)) return item
+      }
     }
-    Array.prototype.last = function() {
-      return this.length ? this[this.length - 1] : null
+    Array.prototype.last = function(predicate) {
+      if (predicate == null || typeof predicate !== 'function') {
+        return this.length ? this[this.length - 1] : null
+      }
+      for (let i = this.length - 1; i >= 0; i--) {
+        const item = this[i]
+        if (predicate(item)) return item
+      }
     }
     Array.prototype.groupBy = function(propGetter, groupName = 'label', listName = 'options') {
       const results = []

+ 9 - 3
src/components/MxTable/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-table ref="table" :border="border"  :data="rows" :span-method="mergeRowsColumns"
+  <el-table ref="table" :border="border" :data="rows" :span-method="mergeRowsColumns"
             @selection-change="$emit('selection-change', $event)" tooltip-effect="dark">
     <template v-for="(prop, key) in propDefines">
       <template v-if="prop.hidden"></template>
@@ -66,7 +66,13 @@ import MxTableColumn from '@/components/MxTable/mx-table-column'
 
 export default {
   components: { MxTableColumn },
-  inject: ['mergeTable'],
+  inject: {
+    mergeTable: {
+      default: () => {
+        // do nothing.
+      }
+    }
+  },
   props: {
     border: {
       type: Boolean,
@@ -80,7 +86,7 @@ export default {
       type: Object,
       default: () => ({
         id: {
-          label: 'id', // label
+          label: '', // label
           slot: '', // define slot if need custom body
           slotHeader: '', // define slot if need custom header
           slotFooter: '' // define slot if need custm footer

+ 23 - 0
src/views/elective/generation/components/elective-flow-major.vue

@@ -0,0 +1,23 @@
+<template>
+  <el-popover trigger="hover" class="mx-generation-major">
+    <div class="fx-row fx-cen-cen f12 bold i">专业意向符合</div>
+    <div v-for="(group,idx) in matchedMajors" :key="idx">
+      <div class="bold f-666 mt10">{{ group.label }}</div>
+      <div v-for="(opts, index) in group.options" :key="index" class="pl20 f-999">
+        {{ opts.majorCategoryName }}
+      </div>
+    </div>
+    <i slot="reference" class="el-icon-check icon24 bold" :class="iconClasses"></i>
+  </el-popover>
+</template>
+
+<script>
+export default {
+  name: 'elective-flow-major',
+  props: ['matchedMajors', 'iconClasses']
+}
+</script>
+
+<style scoped>
+
+</style>

+ 21 - 0
src/views/elective/generation/components/elective-flow-rank-descriptor.vue

@@ -0,0 +1,21 @@
+<template>
+  <el-popover trigger="hover" class="mx-generation-rank">
+    <div v-for="(desc,idx) in rankDescriptors" :key="idx">
+      <span>{{ desc.description }}</span> <span class="bold">{{ desc.value }}</span>
+    </div>
+    <el-tag slot="reference" size="mini" class="round-y ml3" effect="dark">
+      {{ rankDescriptors.map(d => d.value).join('/') }}
+    </el-tag>
+  </el-popover>
+</template>
+
+<script>
+export default {
+  name: 'elective-flow-rank-descriptor',
+  props: ['rankDescriptors']
+}
+</script>
+
+<style scoped>
+
+</style>

+ 30 - 0
src/views/elective/generation/components/elective-flow-table-style.css

@@ -0,0 +1,30 @@
+.elective-flow-table.el-table td {
+  position: relative;
+}
+
+.elective-flow-table.el-table th > .cell {
+  position: unset;
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+
+.elective-flow-table.el-table tbody .cell {
+  overflow: visible;
+  margin-top: 10px;
+}
+
+.elective-flow-table .round-y {
+  border-radius: 12px;
+}
+
+.elective-flow-table .mx-generation-rank {
+  position: absolute;
+  right: 3px;
+  top: 2px;
+}
+
+.elective-flow-table .mx-generation-major {
+  position: absolute;
+  left: 3px;
+  top: 2px;
+}

+ 72 - 0
src/views/elective/generation/components/elective-generation-flow-log.vue

@@ -0,0 +1,72 @@
+<template>
+  <mx-table :rows="logTable.rows" :prop-defines="logTable.columns" border class="elective-flow-table">
+    <template #group-header="{key, label}">
+      <elective-flow-major v-if="logTable.majors[key]" :icon-classes="['f-fff']"
+                           :matched-majors="logTable.majors[key]"></elective-flow-major>
+      {{ label }}
+    </template>
+    <template #group-flow="{value}">
+      <span v-if="value.text">{{ value.text }}</span>
+      <span v-else v-html="'&#12288'"></span>
+      <elective-flow-rank-descriptor v-if="value.rankDescriptors"
+                                     :rank-descriptors="value.rankDescriptors"></elective-flow-rank-descriptor>
+    </template>
+  </mx-table>
+</template>
+
+<script>
+import config from '@/common/mx-config'
+import ElectiveFlowMajor from '@/views/elective/generation/components/elective-flow-major'
+import ElectiveFlowRankDescriptor from '@/views/elective/generation/components/elective-flow-rank-descriptor'
+
+export default {
+  name: 'elective-generation-flow-log',
+  components: { ElectiveFlowRankDescriptor, ElectiveFlowMajor },
+  props: ['groups', 'matchedMajors', 'histories'],
+  computed: {
+    logTable() {
+      if (!this.histories.length) return {}
+      const maxGeneration = this.histories.last().generation
+      const options = Object.values(config.electiveGenerationOptions)
+      // columns & rows
+      const rows = []
+      for (let g = 1; g <= maxGeneration; g++) {
+        const opt = options.find(opt => opt.value == g)
+        rows.push({ opt, generation: opt.title })
+      }
+      const columns = { generation: { label: '进程' } }
+      const majors = {}
+      this.groups.forEach(group => {
+        const keyPrefix = 'group_'
+        const key = keyPrefix + group.groupId
+        columns[key] = { label: group.groupName, minWidth: '160px', slot: 'group-flow', slotHeader: 'group-header' }
+        // match major
+        const groupMajors = (this.matchedMajors?.majors
+          ?.filter(m => m['matchedGroupIds'].some(id => id == group.groupId)) || [])
+          .groupBy(m => m.collegeName)
+        if (groupMajors.length) majors[key] = groupMajors
+        // fill rows
+        rows.forEach(row => {
+          const g = row.opt.value
+          const gHistories = this.histories.filter(h => h.generation == g)
+          row[key] = {
+            text: gHistories.map(h => h.description).join('/'),
+            histories: gHistories,
+            rankDescriptors: gHistories.last(i => !!i.rankDescriptors?.length)?.rankDescriptors
+          }
+        })
+      })
+
+      return {
+        columns,
+        rows,
+        majors
+      }
+    }
+  }
+}
+</script>
+
+<style>
+@import url('./elective-flow-table-style.css');
+</style>

+ 1 - 1
src/views/elective/generation/components/elective-generation-table.vue

@@ -102,7 +102,7 @@ export default {
       if (data.queryCode) {
         let nextName = data.displayName
         if (overrideQueryName) nextName = overrideQueryName + '/' + nextName
-        ext.queryableCategories.push({ id: data.queryCode, name: nextName })
+        ext.queryableCategories.push({ id: data.queryCode, name: nextName, detailName: data.detailName })
         if (shouldMerge) ext.ignoreGroupCategories.push(data.queryCode)
       }
 

+ 159 - 7
src/views/elective/generation/detail.vue

@@ -1,10 +1,41 @@
 <template>
-  <div class="app-container">
+  <div class="app-container" v-loading="loading">
     <evaluation-title :title="title" :sub-title="subTitle" nav-back-button></evaluation-title>
     <el-card>
       <mx-condition :query-params="queryParams" :require-fields="requireFields" :local-data="localData"
                     @query="handleQuery"></mx-condition>
     </el-card>
+    <mx-table :rows="detailTable.rows" :prop-defines="detailTable.columns" border class="mt20 elective-flow-table">
+      <template #group-header="{label, key}">
+        <div class="fx-row jc-cen">
+          <span>{{ label }}</span>
+          <el-popover trigger="hover" :disabled="!!!detailTable.groupHeaders[key].text">
+            <div v-for="(desc,idx) in detailTable.groupHeaders[key].descriptions" :key="idx">
+              <span>{{ desc.description }}</span> <span class="bold">{{ desc.value }}</span></div>
+            <el-tag slot="reference" size="mini" class="round-y ml3">{{ detailTable.groupHeaders[key].text }}</el-tag>
+          </el-popover>
+        </div>
+      </template>
+      <template #group-flow="{value}">
+        <elective-flow-major v-if="value.matchedMajors.length" :icon-classes="['f-primary']"
+                             :matched-majors="value.matchedMajors"></elective-flow-major>
+        <div class="fx-row fx-cen-cen">
+          <span v-if="value.text">{{ value.text }}</span>
+          <span v-else v-html="'&#12288'"></span>
+        </div>
+        <elective-flow-rank-descriptor v-if="value.rankDescriptors"
+                                       :rank-descriptors="value.rankDescriptors"></elective-flow-rank-descriptor>
+      </template>
+      <template #flow-log="{row}">
+        <el-link @click="handleFlowLog(row)" :underline="false">查看</el-link>
+      </template>
+    </mx-table>
+    <pagination :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="loadGenerationDetails"></pagination>
+    <el-dialog title="选科历史记录" v-if="logVisible" :visible.sync="logVisible" :width="logDialogWidth">
+      <elective-generation-flow-log :groups="prevData.groups" :histories="logRow.histories"
+                                    :matched-majors="this.majorsMap[logRow['studentId']]"/>
+    </el-dialog>
   </div>
 </template>
 
@@ -13,9 +44,13 @@ import config from '@/common/mx-config'
 import transferMixin from '@/components/mx-transfer-mixin'
 import { mapGetters } from 'vuex'
 import MxCondition from '@/components/MxCondition/mx-condition'
+import { getElectiveGenerationDetails, getGenerationOptionalMajorsBatch } from '@/api/webApi/elective/generation'
+import ElectiveGenerationFlowLog from '@/views/elective/generation/components/elective-generation-flow-log'
+import ElectiveFlowMajor from '@/views/elective/generation/components/elective-flow-major'
+import ElectiveFlowRankDescriptor from '@/views/elective/generation/components/elective-flow-rank-descriptor'
 
 export default {
-  components: { MxCondition },
+  components: { ElectiveFlowRankDescriptor, ElectiveFlowMajor, ElectiveGenerationFlowLog, MxCondition },
   mixins: [transferMixin],
   name: 'generation-detail',
   computed: {
@@ -32,7 +67,6 @@ export default {
       return hideGeneration ? '' : g?.title || ''
     },
     localData() {
-      console.log('exec localData')
       this.queryParams.generation = this.prevData.queryGeneration
       this.queryParams.generationQueryCode = this.prevData.queryCode
       this.queryParams.generationGroupId = this.prevData.queryGroupId
@@ -41,26 +75,144 @@ export default {
         categories: this.prevData.queryableCategories,
         groups: this.prevData.groups
       }
+    },
+    detailTable() {
+      if (!this.majorsMap) return {}
+      if (!this.prevData.queryableCategories.length) return {}
+      if (!this.detailWrapper?.groupDescriptors?.length) return {}
+      // fixed columns
+      const queryCategory = this.prevData.queryableCategories.find(i => i.id == this.queryParams.generationQueryCode)
+      const ignoreGroups = this.prevData.ignoreGroupCategories.includes(this.queryParams.generationQueryCode)
+      const columns = {
+        className: { label: '班级' },
+        studentName: { label: '姓名' }
+      }
+      if (!ignoreGroups) {
+        columns.groupName = { label: queryCategory.detailName || '组合' }
+        columns.datetime = { label: '时间', minWidth: '110px' }
+      }
+      // extension generation columns & custom group header
+      const rows = this.detailWrapper.details // todo: need clone?
+      const groupHeaders = {}
+      const groupKeyPrefix = 'group_'
+      if (!ignoreGroups) {
+        this.prevData.groups.forEach(group => {
+          const groupKey = groupKeyPrefix + group.groupId
+          columns[groupKey] = {
+            label: group.groupName,
+            slotHeader: 'group-header',
+            slot: 'group-flow',
+            minWidth: '180px'
+          }
+          groupHeaders[groupKey] = this.getGroupHeaderDescription(group.groupId)
+          rows.forEach(row => {
+            row[groupKey] = this.getGroupDescription(row, group.groupId, this.majorsMap)
+          })
+        })
+      }
+      columns.actions = { label: '查看明细', slot: 'flow-log' }
+      return {
+        rows,
+        columns,
+        groupHeaders
+      }
+    },
+    logDialogWidth() {
+      const expectedWidth = (this.prevData.groups.length + 2) * 160 // 假定elective-generation-flow-log 单格宽160
+      const finalWidth = Math.min(expectedWidth, window.innerWidth * 0.8)
+      return finalWidth + 'px'
     }
   },
   data() {
     return {
+      loading: false,
       requireFields: ['generationQueryCode'],
       queryParams: {
+        pageNo: 1,
+        pageSize: 20,
         generation: '',
         generationQueryCode: '',
         generationGroupId: ''
-      }
+      },
+      // query data
+      total: 0,
+      detailWrapper: null,
+      majorsMap: null,
+      // log
+      logVisible: false,
+      logRow: {}
     }
   },
   methods: {
     handleQuery() {
-      console.log('handle query', this.queryParams)
+      this.queryParams.pageNo = 1
+      this.loadGenerationDetails()
+    },
+    loadGenerationDetails() {
+      const params = {
+        pageNo: this.queryParams.pageNo,
+        pageSize: this.queryParams.pageSize,
+        roundId: this.prevData.roundId,
+        generation: this.queryParams.generation,
+        groupId: this.queryParams.generationGroupId,
+        queryCode: this.queryParams.generationQueryCode
+      }
+      this.loading = true
+      this.majorsMap = null
+      getElectiveGenerationDetails(params).then(res => {
+        this.total = res['total']
+        this.detailWrapper = res.data
+
+        const studentIds = res.data.details?.map(d => d['studentId']).toString()
+        return getGenerationOptionalMajorsBatch({ studentIds })
+      }).then(res => {
+        this.majorsMap = res.data
+      }).finally(() => this.loading = false)
+    },
+    getGroupHeaderDescription(groupId) {
+      const statistic = this.detailWrapper.groupDescriptors.find(d => d.groupId == groupId)
+      if (!statistic?.descriptors?.length) return {}
+      const descriptors = statistic.descriptors.reverse()
+      return {
+        text: descriptors.map(d => d.value).join('/'),
+        descriptions: descriptors
+      }
+    },
+    getGroupDescription(row, groupId) {
+      const matchedMajors = (this.majorsMap[row['studentId']]
+        ?.majors?.filter(m => m['matchedGroupIds'].some(id => id == groupId)) || [])
+        .groupBy(m => m.collegeName)
+      const current = this.prevData.queryGeneration
+      const options = Object.values(config.electiveGenerationOptions)
+      const histories = row['histories'] || []
+      const filterHistories = []
+      for (let g = current; g > 0; g--) {
+        const opt = options.find(opt => opt.value == g)
+        const groupHistories = histories.filter(h => h.generation == g && h.groupId == groupId)
+        if (groupHistories.length) filterHistories.push(groupHistories)
+        if (g < current && opt.decisionMaking) break // TODO: 仅迭代至最近的决策代(可能需要调整)
+      }
+      if (!filterHistories.length) return { matchedMajors }
+      filterHistories.reverse() // 还原顺序
+      const mergedHistories = filterHistories.reduce((prev, cur) => {
+        prev.push(...cur)
+        return prev
+      }, [])
+      return {
+        matchedMajors,
+        text: mergedHistories.map(h => h.description).join('/'),
+        histories: mergedHistories,
+        rankDescriptors: mergedHistories.last(i => !!i.rankDescriptors?.length)?.rankDescriptors
+      }
+    },
+    handleFlowLog(row) {
+      this.logRow = row
+      this.logVisible = true
     }
   }
 }
 </script>
 
-<style scoped>
-
+<style>
+@import url('./components/elective-flow-table-style.css');
 </style>