Browse Source

拉取主分支,完善分班完成列表

shilipojs 3 years ago
parent
commit
6c24fb5921
36 changed files with 3525 additions and 213 deletions
  1. 229 0
      doc/Mind/ElectiveEngine.cs
  2. 490 0
      doc/Mind/ElectiveGeneration.cs
  3. 102 0
      doc/Mind/FacultyEstimate.cs
  4. 136 0
      doc/Mind/PrimaryElective.cs
  5. 140 0
      doc/Mind/StudentClassDispatch.cs
  6. 428 6
      mock/modules/elective-generation.js
  7. 1 0
      package.json
  8. 31 0
      public/ar/hd.html
  9. 45 0
      public/ar/index.html
  10. 31 0
      public/ar/qh.html
  11. 51 0
      public/usign/css/index.css
  12. 64 0
      public/usign/index.html
  13. 307 0
      public/usign/js/usign.js
  14. 32 3
      src/common/mx-config.js
  15. 0 11
      src/common/mx-const.js
  16. 3 0
      src/common/mx-extension.js
  17. 77 77
      src/components/EvaluationTitle/index.vue
  18. 1 1
      src/components/MxChart/index.vue
  19. 10 7
      src/components/MxCondition/condition-object/condition-dispatch-class.js
  20. 15 0
      src/components/MxCondition/condition-object/condition-dispatch-gender.js
  21. 53 7
      src/components/MxTable/index.vue
  22. 48 0
      src/components/MxTable/mx-table-column.vue
  23. 1 2
      src/filters/index.js
  24. 11 5
      src/router/index.js
  25. 307 0
      src/utils/usign.js
  26. 3 19
      src/views/elective/dispatch/components/class-adjust.vue
  27. 36 36
      src/views/elective/dispatch/components/class-table.vue
  28. 25 5
      src/views/elective/dispatch/components/dispatch-table.vue
  29. 49 21
      src/views/elective/dispatch/components/set-classcount.vue
  30. 27 5
      src/views/elective/dispatch/detail.vue
  31. 244 0
      src/views/elective/generation/components/elective-generation-charts.vue
  32. 90 0
      src/views/elective/generation/components/elective-generation-master.vue
  33. 156 0
      src/views/elective/generation/components/elective-generation-steps.vue
  34. 149 0
      src/views/elective/generation/components/elective-generation-table.vue
  35. 36 0
      src/views/elective/generation/detail.vue
  36. 97 8
      src/views/elective/generation/index.vue

+ 229 - 0
doc/Mind/ElectiveEngine.cs

@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace mxdemo.Mind
+{
+    #region models
+
+    /// <summary>
+    /// 选科组合
+    /// </summary>
+    public class SubjectGroup
+    {
+        public int Limitation { get; set; } // 人数限制
+        public string Name { get; set; }
+        public string Code { get; set; }
+        public long Id { get; set; }
+    }
+
+    /// <summary>
+    /// 志愿
+    /// </summary>
+    public class Preference
+    {
+        public int Rank { get; set; } // 第1、2、3志原 越小优先级越高
+        public long GroupId {get;set;}
+        public long StudentId { get; set; }
+        public long Id { get; set; }
+    }
+
+    /// <summary>
+    /// 得分情况
+    /// </summary>
+    public class GroupScore
+    {
+        public decimal Score { get; set; }
+        public long StudentId { get; set; }
+        public long GroupId { get; set; }
+    }
+
+    /// <summary>
+    /// 学生信息
+    /// </summary>
+    public class Student
+    {
+        public long Id { get; set; }
+        public string No { get; set; }
+        public string Name { get; set; }
+    }
+
+    public class ElectiveElement
+    {
+        public Student Student { get; set; }
+        public Preference Preference { get; set; }
+        public SubjectGroup Group { get; set; }
+
+        // 计算属性
+        public int PreferenceRank { get; set; } // 志愿排名
+        public int GroupRank { get; set; }  // 组合排名,即成绩排名(组合全校排名),同样越小优先级越高
+    }
+
+    #endregion
+
+    public class ElectiveResult
+    {
+        public ElectiveResult(List<Student> students)
+        {
+            this.UnmatchedStudents = students;
+        }
+
+        public List<Student> MatchedStudents { get; set; }
+            = new List<Student>();
+        public Dictionary<SubjectGroup, List<ElectiveElement>> MatchedDetails { get; set; }
+            = new Dictionary<SubjectGroup, List<ElectiveElement>>();
+        public List<Student> UnmatchedStudents { get; set; }
+
+        public void AppendMatched(ElectiveElement element)
+        {
+            // 应该还有更多细节需要考虑
+            var appendStudent = element.Student;
+            if (MatchedStudents.Contains(appendStudent)) return;
+
+            if (UnmatchedStudents.Contains(appendStudent))
+                RemoveUnmatched(appendStudent);
+
+            var appendGroup = MatchedDetails.ContainsKey(element.Group)
+                ? MatchedDetails[element.Group]
+                : new List<ElectiveElement>();
+            if (appendGroup.Count < element.Group.Limitation)
+            {
+                MatchedDetails[element.Group] = appendGroup;
+                AddMatchedAndDetails(element);
+            }
+            else
+            {
+                AddUnmatched(appendStudent);
+            }
+        }
+
+        private void AddMatchedAndDetails(ElectiveElement element)
+        {
+            // 应该还有更多细节需要考虑
+            MatchedDetails[element.Group].Add(element);
+            MatchedStudents.Add(element.Student);
+        }
+
+        private void AddUnmatched(Student student)
+        {
+            // 应该还有更多细节需要考虑
+            UnmatchedStudents.Add(student);
+        }
+
+        private void RemoveUnmatched(Student student)
+        {
+            // 应该还有更多细节需要考虑
+            UnmatchedStudents.Remove(student);
+        }
+    }
+
+    public interface IElectiveStrategy
+    {
+        /// <summary>
+        /// 从开放选科中取出按优先级分组的选科
+        /// </summary>
+        /// <returns></returns>
+        List<List<SubjectGroup>> GetAllPrioritizedGroups();
+
+        /// <summary>
+        /// 参与这次运算的志愿优先级集合
+        /// </summary>
+        /// <returns></returns>
+        List<List<int>> GetAllPrioritizedPreferenceRanks();
+
+        /// <summary>
+        /// 参与这次运算的所有学生
+        /// </summary>
+        /// <returns></returns>
+        List<Student> GetAllStudents();
+    }
+
+    public abstract class ElectiveEngine: IComparer<ElectiveElement>
+    { 
+        protected IElectiveStrategy ElectiveStrategy { get; set; }
+
+
+        protected ElectiveEngine(IElectiveStrategy electiveStrategy)
+        {
+            ElectiveStrategy = electiveStrategy;
+        }
+
+        public ElectiveResult Execute()
+        {
+            var prioritizedGroups = ElectiveStrategy.GetAllPrioritizedGroups();
+            var prioritizedPreferenceRanks = ElectiveStrategy.GetAllPrioritizedPreferenceRanks();
+            var allStudents = ElectiveStrategy.GetAllStudents();
+            var electiveResult = new ElectiveResult(allStudents);
+
+            foreach(var groups in prioritizedGroups)
+            {
+                foreach(var ranks in prioritizedPreferenceRanks)
+                {
+                    var students = electiveResult.UnmatchedStudents;
+                    var elements = BuildElectiveElements(students, groups, ranks);
+                    ExecuteCore(elements, electiveResult);
+                }
+            }
+            return electiveResult;
+        }
+
+        private void ExecuteCore(List<ElectiveElement> elements, ElectiveResult context)
+        {
+            // 核心方法优先级排序, 已经按期望排好了录取顺序
+            elements.Sort(this);  // this.IComparer<ElectiveElement>.Compare
+            // 接下来考虑每个组合容量的问题
+            foreach(var element in elements)
+            {
+                context.AppendMatched(element);
+            }
+        }
+
+        protected virtual List<ElectiveElement> BuildElectiveElements(List<Student> students, List<SubjectGroup> groups, List<int> ranks)
+        {
+            // 根据组合与志愿 生成不同的选科元素
+            // 可以按计算需要,相互挂一些引用,方便计算过程中的删除操作
+            // TODO: 如果引入意向专业,也可以改写这个生成的逻辑
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// 优先级比较
+        /// </summary>
+        /// <param name="x"></param>
+        /// <param name="y"></param>
+        /// <returns></returns>
+        public abstract int Compare(ElectiveElement x, ElectiveElement y);
+    }
+
+    public class PreferenceAheadElectiveEngine : ElectiveEngine
+    {
+        public PreferenceAheadElectiveEngine(IElectiveStrategy electiveStrategy)
+            :base(electiveStrategy)
+        {
+
+        }
+
+        public override int Compare(ElectiveElement x, ElectiveElement y)
+        {
+            // TODO: 容错处理
+            if (x.PreferenceRank == y.PreferenceRank) return y.GroupRank - x.GroupRank;
+            return y.PreferenceRank - x.PreferenceRank;
+        }
+    }
+
+    public class ScoreAheadElectiveEngine : ElectiveEngine
+    {
+        public ScoreAheadElectiveEngine(IElectiveStrategy electiveStrategy)
+            : base(electiveStrategy)
+        {
+
+        }
+
+        public override int Compare(ElectiveElement x, ElectiveElement y)
+        {
+            // TODO: 容错处理
+            if (x.GroupRank == y.GroupRank) return y.PreferenceRank - x.PreferenceRank;
+            return y.GroupRank - x.GroupRank;
+        }
+    }
+}

+ 490 - 0
doc/Mind/ElectiveGeneration.cs

@@ -0,0 +1,490 @@
+using System;
+using System.Collections.Generic;
+using static mxdemo.Mind.IStudentClassDispatchService;
+
+namespace mxdemo.Mind
+{
+    // 选科数据与决策会有很多重合的地方
+    public enum EnumElectiveGeneration
+    {
+        Init = 0, // 初始化态,选科正式开启前的状态,开启之后往下一代推进
+        Primary = 1, // 初选(正常报名)阶段,初选时间结束时往下一代推进 (先暂定进入PrimaryDM之前选科设置还可以修改,之后禁止,防止扰乱数据)
+        PrimaryDM, // (PrimaryDecisionMaking)初选录取阶段,应用决策之后,开启补录报名(时间段)并往下一代推进
+        BackTracking, // 补录报名时间结束后往下一代推进
+        BackTrackingDM, // 调剂OR提醒,如果为直接调剂,则跳过FinalAdjust,如果为提醒(设时间段)并往下一代推进
+        FinalAdjust, // 调剂报名期间
+        FinalAdjustDM, // 调剂决策,这个阶段会最终定版
+        RankBalance, // 可选步骤,行为类似强制调剂
+        // Dispatch, // 分班?分班是否要进入此结构?貌似结构不太统一!应该还是要分出去做
+        Terminate // 最终代,选科流程数据全部封存
+    }
+
+    public enum EnumElectiveOperation
+    {
+        None = 0,
+        Canceled,
+        Disabled,
+        Optional,
+        Rejected,
+        Approved,
+        Forced
+    }
+
+    // 待思考问题:
+    /*
+    1 哪个结点开始不允许修改选科设置
+    2 未操作志愿、未选完志愿、未响应决策的学生是否需要区分显示
+    3 补录决策时,是否存在部分调剂、部分提醒的情况?
+    4 调剂行为定义:直接修改学生最终录取组合,触发发签名
+     */
+
+    #region models
+
+    /// <summary>
+    /// Generation init时生成的缓存数据,方便后续计算
+    /// 生成开放组合的数据即可
+    /// 每次修改选科设置(在允许范围内修改)需要刷新
+    /// </summary>
+    public class ElectiveStudentScore
+    {
+        long id;
+        long roundId;
+
+        long studentId;
+        long groupId;
+
+        decimal scoreTotal; // 9科成绩,看是否有必要存
+        decimal scoreGroup; // 6科成绩
+        int rankTotal; // 9科全校排名,看是否有必要存
+        int rankGroup; // 6科全校排名
+    }
+
+    /// <summary>
+    /// 每一代都为体现学生的选科数据变化,任何操作都可以被记录下来,只是最终哪条生效的问题
+    /// 当然也可以根据实际情况减少部分记录,类下面的注解将讨论这些问题
+    /// </summary>
+    public class ElectiveGenerationFlowData
+    {
+        long id;
+        long roundId;
+
+        long studentId;
+        long groupId;
+
+        EnumElectiveGeneration generation;
+        EnumElectiveOperation operation;
+        long operateBy;
+        DateTime operateTime;
+        int rank; // Primary中可能存在多志愿,用来控制此阶段的志愿优先级,如第1志愿=0,第2志愿=1,第三志愿=2
+
+        string remark;
+        // 以下2个字段用来挂载生命周期关键引用对象,具体每个环节挂什么,需要再斟酌。
+        string refType;
+        long refId;
+    }
+    public class ElectiveGenerationFlowLog : ElectiveGenerationFlowData
+    {
+        // + 区分ElectiveGenerationFlowData的字段
+    }
+
+    public class RoundForFlow : mxdemo.Mind.IStudentClassDispatchService.Round // Round到时候合并在一起
+    {
+        bool AllMatched; // 所有学生匹配完毕,可以开始推进排名均衡或者分班
+        EnumElectiveGeneration currentGeneration; // 内部字段
+    }
+
+    // 1 初选决策时,需要进行设置
+    // 2 补录决策时,需要进行设置,此时要根据具体数据来定
+    //   如果所有学生都已经匹配完毕(Approved,Forced都属性已匹配),则可以推进排名均衡或者分班
+    //   如果还有未匹配的,则推进到FinalAdjust
+    public class FlowPushSetting
+    {
+        long id;
+        long roundId;
+        DateTime begin;
+        DateTime end;
+        bool onlyRecommand; // 仅允许学生选择推荐组合, 否则允许学生填报有空缺的组合
+        bool onlyAgree; // 调剂阶段,只允许学生选择同意, 否则允许学生拒绝
+    }
+
+    public class ElectiveEsign
+    {
+        long id;
+        long roundId;
+        long flowDataId; // 对应哪条匹配数据(录取或者调剂)【匹配算法结果表也可如此挂载flowDataId,这样可以追溯匹配原因】
+
+        string esignPath; // 签字保存图片
+        bool actived; // 有效标记,可以考虑:如果学生需要签名且未签名时,如果再遇到新的调剂时,旧的签名还要继续么?我觉得应该是不需要的
+        // 可能还有其它字段
+    }
+
+    /*  
+     总则:ElectiveGenerationFlowData记录的是每一代数据的最终结果,如果想要更精细的记录,可以结合ElectiveGenerationFlowLog
+    1 Generation.Init 可有可无,不需要生成初始数据?
+
+    2 Generation.Primary
+        报名时:
+        ADD operation=Approved
+
+        取消时:
+        UPDATE operation=Cancel 
+
+        排名挤出时-被挤出人:
+        UPDATE operation=Disabled, remark = '排名挤出'
+        refType = 'ElectiveGenerationFlowData'
+        refId = ElectiveGenerationFlowData.id // 报名解发挤出那条记录的ElectiveGenerationFlowData.id
+
+        取消时恢复补挤出的人(只有当被影响的人还有剩余报名次数的时候才自动恢复,因为期间他可能自己又补填了一个志愿):
+        QueryBy refId
+        UPDATE operation=Approved WHEN COUNT(operation=Approved)<setting.PreferenceCount
+        remark = ''
+        refType = ''
+        refId = 0 // 取消对应记录的ID
+
+        Primary中学生的有效报名数据:operation=Approved
+
+    3 Generation.PrimaryDM
+        目标:启用算法,给每个学生生成一条决策数据,表示初选录取情况(算法见ElectiveEngine草稿)
+
+        录取:
+        ADD operation=Approved
+        remark = 录取说明?
+        refType = 'ElectiveEsign'
+        refId = ElectiveEsign.id // 生成一条待签记录
+        // 这样的话签名与选科的代数据就可分割开了,具体说是否一定要学生签名才能推进下一代?
+        // 我看未必,因为学校不可能因为个别钉子户就暂停教务
+        
+        未录取,但自选专业满足
+        ADD operation=Optional, groupId=推荐组合
+        remark = 未录取原因
+        未录取,也无自选专业满足
+        ADD operation=Rejected, groupId=推荐组合
+        remark = 未录取原因
+
+        PrimaryDM中 operation=[Optional,Rejected]的学生,即是一代补录流程的目标学生
+
+    4 Generation.BackTracking
+        操作逻辑重用Generation.Primary
+
+    5 Generation.BackTrackingDM
+        操作逻辑重用Generation.PrimaryDM,
+        计算差别:仅1个志愿,但还是可以引用自选专业,因为学生在此期间可能更改了自选专业
+
+        操作差别:校长在推动下一代流程时,有可能会开始手动调剂
+        UPDATE operation=Forced  // Forced操作等同于Approved录取状态,即告诉学生被调剂了,签字同意即可(即也挂ref钩子)
+
+    6 Generation.FinalAdjust
+        可选步骤,有可能跳过,如未跳过,则是调剂报名
+        同意:前端会强制签名,所以这条在保存时可以直接挂钩ElectiveEsign
+        ADD operation=Approved, refType='ElectiveEsign', refId=ElectiveEsign.id
+        拒绝:
+        ADD operation=Rejected, remark=不同意原因
+        附:没有记录则是未参与的
+
+    7 Generation.FinalAdjustDM
+        针对未参与的或operation=Rejected, 校长会在此环节强制调剂,以达到最终定版的目的
+        ADD operation=Forced, refType='ElectiveEsign', refId=0 // 待签名
+
+    8 Generation.RankBalance
+        可选步骤,也是产生类似Generation.FinalAdjustDM数据逻辑,只是学校出发的角度不一样
+        ADD operation=Forced, refType='ElectiveEsign', refId=0 // 待签名
+
+    9 Generation.Dispatch 待定应该要分出去单独做
+
+    10 Generation.Terminate
+        不产生数据,但需要记录此状态,表示选科数据封存(记录在round实例中)
+     */
+
+    #endregion
+
+    public interface IElectiveService
+    {
+        #region getElectiveSummary
+        /// <summary>
+        /// 选科过程中的统计数据值,除了要展示之外还需要做查询支持
+        /// </summary>
+        public class ElectiveSummaryValue
+        {
+            // 展示字段
+            int groupId; // 组合ID,没有组合为0比如未参与报名人数(实际只有两种统计数据,针对某个组合的统计和不针对组合的统计)
+            int value; // 数值
+            string color; // 支持'R','G','B','',或者'#FA1234'等hexString
+            bool bold; // 加粗,重点展示
+            bool star; // 带*,重点展示
+
+            // 反向查询代码 将统计逻辑的条件关键字放入此代码,回传后台能反向解析并恢复明细
+            // 想法:让统计全部基于ElectiveGenerationFlowData,形成一种或多种QueryDto{roundId:'',generation:'',groupId:''...}等, 序列化实际查询条件
+            string queryCode;
+            bool disabled; // 不支持查询
+        }
+
+        /// <summary>
+        /// 查询的某一列
+        /// </summary>
+        public class ElectiveSummaryCategory
+        {
+            string category; // 身份标识,类似字段名/列名,可能被前端用来定制渲染模板
+            string displayName; // 显示名称
+
+            ElectiveSummaryValue[] values;
+        }
+
+        public interface IElectiveGenerationSummary
+        {
+            int roundId { get; set; } // 选科轮次
+            EnumElectiveGeneration generation { get; set; } // 进程代
+        }
+
+        /// <summary>
+        /// 进程代的统计结果从primary开始出数据
+        /// </summary>
+        public class ElectivePrimarySummary : IElectiveGenerationSummary
+        {
+            ElectiveSummaryCategory[][] categories; // 是多维数组,以应对多志愿的情况
+
+            public int roundId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public EnumElectiveGeneration generation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+        }
+
+        /// <summary>
+        /// 非Primary非DM的某一代的统计结果
+        /// </summary>
+        public class ElectiveGenerationSummary: IElectiveGenerationSummary
+        {
+            /// <summary>
+            /// 对某一代的统计项目,比如在初选决策、补录报名的统计等,下面接口会细述
+            /// </summary>
+            ElectiveSummaryCategory[] categories;
+
+            int IElectiveGenerationSummary.roundId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            EnumElectiveGeneration IElectiveGenerationSummary.generation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+        }
+
+        public class ElectiveGenerationDMSummary: ElectiveGenerationSummary
+        {
+            /// <summary>
+            /// 迭加到某决策代的统计项目,比如当前选科的总录取人数、待签人数、已签人数等,下面接口会细述
+            /// </summary>
+            ElectiveSummaryCategory[] accumulates;
+        }
+
+        /// <summary>
+        /// 获取选科进程的统计数据,一直汇总到当前进程代
+        /// </summary>
+        /// <returns></returns>
+        IElectiveGenerationSummary[] getElectiveSummary();
+        // 下面开始按代来描述ElectiveGenerationSummary需要返回的具体内容
+        /*  一、ElectivePrimarySummary,所需ElectiveSummaryCategory->{category:'',displayName:''}定义如下:
+         *  // hht.22/4/10 这个属性不要了 {'expectedCount','设置人数'},  -- 选科设置的人数, 为方便输出与图表展示,可能后续会有重复输出
+         *  {'actualCount','实际人数'},   -- 填报第X志愿的人数
+         *  {'unfinishedCount','未完成人数'},  -- 未填报第X志愿的人数(根据ElectiveGenerationFlowData实际Approved&Rank情况统计即可,因为如果报名还在进行中,本身就是动态变化的东西)
+         *  按第一志愿、第二志愿、...的顺序输出数据
+         *  
+         *  二、ElectiveGenerationDMSummary.accumulates 
+         *  仅决策代需要统计此数据,决策代[PrimaryDM BackTrackingDM FinalAdjustDM RankBalance]
+         *  // hht.22/4/12 同上,这个属性也不要了 {'expectedCount', '设置人数'} -- 选科设置人数
+         *  {'approvedCount', '正常录取人数'} -- 到此阶段为止,正常录取人数
+         *  {'forcedCount', '调剂录取人数'} -- 到此阶段为止,调剂录取人数
+         *  {'esignedCount', '已签人数'} -- 到此阶段为止,触发的有效签名统计
+         *  {'esigningCount', '待签人数'} -- 到此阶段为止,阶段触发有效的应签未签统计
+         *  
+         *  三、ElectiveGenerationSummary.categories 只统计某代的数据
+         *  A PrimaryDM
+         *  {'indicateCount','指标'} -- 正负表示超过设置的人数,负数表示低于设置的人数
+         *  {'approvedCount, '录取人数'} -- 正常录取人数
+         *  {'adjustCount','调剂人数'} -- 未录取人数,理论上应该<=指标的绝对值
+         *  {'matchedCount','可调剂人数'} -- 未录取可被调剂的人(志愿不满足但自选专业满足)
+         *  {'nonmatchedCount','不可调剂人数'} -- 未录取不可被调剂的人(志愿与自选专业均不满足)
+         *  
+         *  B BackTracking FinalAdjust
+         *  {'matchedApproved','可调剂同意人数'} -- 统计有效补录报名,且报名与推荐相同
+         *  {'matchedNotOptional','可调剂改填人数'} -- 统计有效补录报名,且报名与推荐不同
+         *  {'matchedRejected','可调剂已拒绝人数'} -- 统计拒绝报名的人数
+         *  {'matchedNonaction','可调剂未完成人数'} -- 统计未参的人数(因为有可能被排名挤出)
+         *  {'matchedRankout','可调剂被挤出'} -- 统计参与但已经失效的报名人数(因为有可能被排名挤出)
+         *  {'nonmatchedApproved','不可调剂已填人数'}
+         *  {'nonmatchedNotOptional','不可调剂改填人数'}
+         *  {'nonmatchedRejected','不可调剂已拒绝人数'}
+         *  {'nonmatchedNonaction','不可调剂未完成人数'}
+         *  {'nonmatchedRankout','不可调剂已填人数'}
+         *  
+         *  C BackTrackingDM FinalAdjustDM RankBalance
+         *  {'approvedCount','补录录取人数'} -- 正常录取 [BackTrackingDM FinalAdjustDM]
+         *  {'forcedCount','调剂录取人数'} -- 强制调剂录取 [BackTrackingDM FinalAdjustDM RankBalance]
+         *  {'matchedCount','可调剂人数'} -- 正常录取 [BackTrackingDM]
+         *  {'nonmatchedCount','不可调剂人数'} -- 正常录取 [BackTrackingDM] 
+         *  
+        */
+        #endregion
+
+        #region getElectiveSummaryDetail 汇总明细清单
+
+        /// <summary>
+        /// 明细肯定是通过 某个组合某个学生 来呈现的
+        /// </summary>
+        public interface IElectiveGengerationDetail
+        {
+            long id { get; set; }
+            EnumElectiveGeneration generation { get; set; } 
+            string category { get; set; } // ElectiveSummaryCategory.category
+            int roundId { get; set; }
+            int studentId { get; set; }
+            string studentName { get; set; }
+            int classId { get; set; }
+            string className { get; set; }
+            int groupId { get; set; }
+            string groupName { get; set; }
+        }
+
+        /// <summary>
+        /// 此类相当于ElectiveGenerationFlowData的DTO,将字段转义为UI需要的内容
+        /// 明细的学生组合信息, 主要包含报名信息;
+        /// 还有一部分自选专业的信息,需要借助其它接口
+        /// </summary>
+        public class ElectiveGenerationDetail: IElectiveGengerationDetail
+        {
+            int rankInTime { get; set; } // 组合即时排名
+            int rankInGroup { get; set; } // 组合全校排名
+            int rankInGrade { get; set; } // 总分全校排名
+            int rankPreference { get; set; } // 志愿RANK 第几志愿
+            string OperateTime { get; set; } // 报名时间
+
+            public long id { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string category { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public EnumElectiveGeneration generation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int roundId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int studentId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string studentName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int classId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string className { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int groupId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string groupName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+        }
+
+        /// <summary>
+        /// 录取明细(包含approved正常录取;forced强制调剂录取),当前组合为录取项
+        /// </summary>
+        public class ElectiveGenerationEnrollDetail: ElectiveGenerationDetail
+        {
+            ElectiveGenerationDetail[] otherEnlist; // 非录取报名清单
+            Dictionary<long, string> failedReasons; // 如果有未录取原因,填充在此MAP{key: detailId, value: failed reason}中
+            bool forced; // 是否属于强制调剂
+            bool esigned; // 是否已经签名
+        }
+
+        /// <summary>
+        /// 未录取明细,当前组合是推荐项
+        /// </summary>
+        public class ElectiveGenerationDisenrollDetail: ElectiveGenerationDetail
+        {
+            ElectiveGenerationDetail[] enlist; // 之前的报名清单(肯定都没有录取)
+            Dictionary<long, string> failedReasons; // 如果有未录取原因,填充在此MAP中
+            bool agreed; // 同意此组合
+            bool disagreed; // 不同意此组合
+            bool disagreeReason; // 不同意的原因
+            ElectiveGenerationDetail other; // 即没有同意,也没有不同意,选了其它组合
+            bool esigned; // 是否已经签名
+        }
+
+        /// <summary>
+        /// 初选报名搭配使用,展示其它可报组合情况
+        /// </summary>
+        public class ElectiveGenerationOptionalMajor : ElectiveOptionalMajor
+        {
+            int bestMatchedGroupId; // 最佳匹配
+        }
+
+        /// <summary>
+        /// Map: {key: studentid, value: ElectiveGenerationOptionalMajor}
+        /// </summary>
+        /// <param name="studentIds"></param>
+        /// <returns>批量取学生自选专业匹配情况</returns>
+        Dictionary<int, ElectiveGenerationOptionalMajor> getGenerationOptionalMajorsBatch(int[] studentIds);
+
+        /// <summary>
+        /// 通过summary中输出的queryCode反查明细
+        /// </summary>
+        /// <param name="queryCode"></param>
+        /// <returns></returns>
+        List<IElectiveGengerationDetail> getElectiveGengerationDetails(string queryCode);
+
+        #endregion
+
+        #region decision-making
+
+        /// <summary>
+        /// 匹配算法,算法名称代表的具体逻辑请看注解(不要顾名思义)
+        /// </summary>
+        public enum EnumElectiveDMAlgorithm
+        {
+            /// <summary>
+            /// 按成绩:
+            /// ElectiveEngine中,相当于[未录学生]/[组合*ALL]/[志愿*ALL]一并运行,排序时排名优先;
+            /// 以上逻辑循环迭代,每次只补空缺组合,直至未录学生=0。
+            /// 第1次迭代之后运算的学生均属于可调剂名单,不会有不可调剂名单
+            /// </summary>
+            RankFirst,
+
+            /// <summary>
+            /// 按专业:
+            /// ElectiveEngine中,相当于[未录学生]/[组合*ALL]/[志愿1][志愿2,志愿3][自选专业伪志愿]/三波运行,排序时排名优先;
+            /// 以上逻辑循环迭代,每次只补空缺组合,直至伪志愿运算结束。
+            /// [志愿1][志愿2,志愿3]迭代的录取学生为正常录取;自选专业伪志愿为可调剂名单;运行结束后的未录学生为不可调剂名单。
+            /// </summary>
+            MajorFirst, // 
+        }
+
+        /// <summary>
+        /// 执行选科匹配算法。为减少错误,对DM代数据全删全加可能好点。
+        /// </summary>
+        /// <param name="algorithm"></param>
+        void applyElectiveDMAlgorithm(EnumElectiveDMAlgorithm algorithm);
+
+        #endregion
+
+        #region forced
+
+        /// <summary>
+        /// 决策阶段,强制调剂录取
+        /// </summary>
+        /// <param name="studentId"></param>
+        /// <param name="groupId"></param>
+        void enrollByForce(long studentId, int groupId);
+
+        /// <summary>
+        /// 取消强制调剂操作
+        /// </summary>
+        /// <param name="id">ElectiveGenerationFlowData.id</param>
+        void cancelEnrollByForce(long id);       
+
+        #endregion
+
+        #region generation推进
+
+        /// <summary>
+        /// 决策完毕时,推进下一代进行
+        /// </summary>
+        /// <param name="setting"></param>
+        void pushGenerationSetting(FlowPushSetting setting);
+
+        /// <summary>
+        /// 如果在所有学生全部录取的情况,可以在任意决策结点跳转至排名均衡
+        /// </summary>
+        void jumpGenerationRankBalance();
+
+        /// <summary>
+        /// 如果在所有学生全部录取的情况,可以在任意决策结点跳转至终止态,封存数据
+        /// </summary>
+        void terminateGeneration();
+
+        /// <summary>
+        /// 在任意报名阶段,如果校长发现数据已经完全OK,则可以强制推进进程,提前进入决策
+        /// </summary>
+        void flushIntoGenerationDM();
+
+        #endregion
+
+        #region 排名均衡(略)先完成前面的
+        #endregion
+    }
+}

+ 102 - 0
doc/Mind/FacultyEstimate.cs

@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+
+namespace mxdemo.Mind
+{
+    #region model
+
+    public class FacultyForm
+    {
+        public int subjectId;   // 这个想想「班会、体育、自习」怎么处理?
+        public string subjectName; // 名称
+        public bool isFixed; // 固定师资,用以区分6门合格考与等级考学科,非固定科目需要填写合格考与等级考
+        public bool isOffical; // 正式科目,9科为正式科目,非正式科目提供的校验不一样
+        public int classesCountPrev; // 周课时数-分班前-仅参考用
+        public int teacherCount; // 单科老师数量
+        public int teacherClassesCount; // 单科老师周课时数
+        public int classesCount; // 单科总课时数,前端会提供辅助计算
+        public int levelClassesCount; // 等级考课时数-分班后
+        public int qualifiedClassesCount; // 合格考课时数-分班后
+    }
+
+    public class FacultyResult : FacultyForm
+    {
+        public int levelClassCount; // 等级考班级数
+        public int qualifiedClassCount; // 合格考班级数
+        public int requiredClassesCount; // 所需课时
+        public int actualClassesCount; // 实际课时
+        public int missingClassesCount; // 缺少课时,正数表示缺的,负数表示超的
+        public int missingTeacherCount; // 缺少老师,正负同上
+
+        public bool completed; // 是否计算结束
+    }
+
+    public class GroupSetting
+    {
+        // 略,延用之前的结构内容
+    }
+
+    public class GroupModel
+    {
+        // 后台自己设置标识,前端会原样返回,前端展示与操作只会用到索引
+        public string groupIds;
+        public List<GroupSetting> roundGroups;
+        public List<FacultyResult> faculties;
+    }
+
+    public class ScoreImportConfig // 需要调整结构
+    {
+        #region 不变部分
+        //      {
+        //"params": {},
+        //"roundId": 14,
+        //"schoolId": 31,
+        //"year": 2021,
+        //"name": "第1次选科",
+        //"beginTime": "2022-03-05",
+        //"endTime": "2022-03-20",
+        //"state": "1",
+        //"importWeight":[]
+        #endregion
+
+        #region 变更部分
+        // 新增模型列表
+        public List<GroupModel> groupModels;
+        // 选择其中1个模型
+        public int activeGroupModelIndex; // 构想的是传索引,因为前端会做删除等调整,而且新建时也可能同时创建多个引用,此时无ID能够做出区分
+        // 选志愿的数量,默认为1
+        public int preferenceCount = 1;
+        #endregion
+
+        #region 新增控制配置
+        public bool rankInvisible; // 排名不可见,默认false: 排名可见
+        public bool scoreInvisible; // 分数不可见,默认false: 分数可见
+        public bool enableEsign;  // 开启电子签名,默认false: 不需要
+        public bool enablePaperSign; // 开启电子签名的情况下,是否需要纸质签名(促进学生使用班牌),默认false: 不需要
+        #endregion
+    }
+
+    #endregion
+
+    public interface ISelectedScoreImport
+    {
+        // ScoreImportConfig的结构有变化见上面region
+        ScoreImportConfig getScoreImportConfig(int roundId);
+
+        /// <summary>
+        /// 生成空的师资表格,在校长全部重新填写的时候调用。如果是从已完成的模型复制,则不会调用,前端会自行处理
+        /// </summary>
+        /// <returns></returns>
+        List<FacultyResult> generateFaculties();
+
+        // 计算师资匹配结果
+        // 构想:全量传数据,但后台只挑需要的groupModels[targetIndex]进行计算,计算完毕标记为completed=true
+        // 计算结束,需要标记出groupModels中,completed=true的模型,最优解为isBest=true
+        // 最优解:faculties.sum(f => Math.Abs(f.missingTeacherCount)) 总和最小的GroupModel为最优
+        ScoreImportConfig calculateFaculty(ScoreImportConfig round, int groupModelIndex);
+
+        void saveScoreImportConfig({ round: ScoreImportConfig }); // config结构变了,保存也要注意
+
+        void queryScoreImportDetails({ round: ScoreImportConfig, groupModelIndex: int }); // config结构变了,查询时要指定哪个模型
+    }
+}

+ 136 - 0
doc/Mind/PrimaryElective.cs

@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+
+namespace mxdemo.Mind
+{
+    #region model
+
+    public class ElectiveSetting
+    {
+
+        //"selectResult": {
+        //  "params": {},
+        //  "roundId": 8,
+        //  "schoolId": 1,
+        //  "year": 2021,
+        //  "name": "第三次",
+        //  "beginTime": "2022-02-16",
+        //  "endTime": "2022-02-17",
+        //  "groupIds": "1,2,3,10,11,12",
+
+        public int preferenceCount; // 选填志愿数量
+
+  //  "groupList": [
+  //    {
+  //      "params": {},
+  //      "groupId": 1,
+  //      "mask": 448,
+  //      "name": "物化生",
+  //      "rank": 1
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 2,
+  //      "mask": 416,
+  //      "name": "物化政",
+  //      "rank": 2
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 3,
+  //      "mask": 400,
+  //      "name": "物化地",
+  //      "rank": 3
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 10,
+  //      "mask": 56,
+  //      "name": "历政地",
+  //      "rank": 10
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 11,
+  //      "mask": 104,
+  //      "name": "历政生",
+  //      "rank": 11
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 12,
+  //      "mask": 88,
+  //      "name": "历生地",
+  //      "rank": 12
+  //    }
+  //  ],
+  //  "state": "1"
+  //},
+  //"allowSelect": false
+
+    }
+
+    // 学生这里无论什么时间得到的都是最终结果,不需要中间快照,以此区分校长的选科数据。
+    // 校长的选科数据,初选时与学生相同;补录时也需要这份数据、但也需要初选与补录的差异部分;排名均衡同理
+    public class ElectiveSelectModel
+    {
+        public int groupId; // 组合
+        public string groupName; // 名称
+        public int classCount; // 班级数
+        public int personCount; // 限制人数
+
+        public int personInTime; // 实时人数
+        public int rankInGroup; // 选科实时排名
+        public int rankInGrade; // 选科全校排名
+
+        public bool allowSelect; // 是否可以报名
+        public string disabledReason; // 不可报名时的原因
+
+        public bool selected;  // 已报名
+        public int selectedRank; // 多志愿时的排序
+    }
+
+    // 自选专业
+    public class ElectiveOptionalMajor
+    {
+        public int collegeId;
+        public string collegeName;
+        // 可能还包含院校的一些其它属性
+
+        public string majorCategoryCode;
+        public string majorCategoryName;
+
+        public Dictionary<string, string> majors;
+
+        public string limitationA; // 选科限制1
+        public string limitationB; // 选科限制2
+
+        public List<int> matchedGroupIds; // 匹配的组合
+    }
+
+    #endregion
+
+    // 本期会涉及很多隐藏需求,前端会去back-ui按页面+按功能设置很多按钮功能,后续配置为权限组来操作
+    // 本期后台输出数据的时候不需要按权限屏蔽数据,全量输出即可,以减少后端开发的工作量
+    // 因为本期要控制隐藏的内容比功能入口、API入口要更精细的粒度,可能是列表的某几列,可能是某些按钮,如果严格API数据规范可能会增加海量的工作。
+    public interface IPrimaryElectiveService
+    {
+        // 旧接口getStudentSelected,返回内容+preferenceCount,志愿数
+        ElectiveSetting getStudentSelected();
+
+        // 学生获取选科状态数据,得到是每个组合最终状态和结果
+        List<ElectiveSelectModel> getPrimaryElectives();
+
+        // 学生获取自选专业及匹配情况
+        List<ElectiveOptionalMajor> getOptionalMajors();
+
+        // 初选报名。这里简化原型里的操作,直接提交,如需排序,调sortElectiveSelected接口,并刷新getPrimaryElectives
+        void submitPrimaryElective(ElectiveSelectModel model);
+
+        // 调整排序。这里简化原型里的操作,并刷新getPrimaryElectives
+        void sortPrimaryElective(ElectiveSelectModel model);
+
+        // 取消报名
+        void cancelPrimaryElective(ElectiveSelectModel model);
+    }
+}

+ 140 - 0
doc/Mind/StudentClassDispatch.cs

@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+
+namespace mxdemo.Mind
+{
+    public interface IStudentClassDispatchService
+    {
+        // 1 班级设置 参考校长的class-manage页面
+        //   a 从年级树缓存中查找当前选科学年对应的班级进行选择
+        //   b 允许用户输入不存在的班级,输入完毕后确认是否创建新班级
+        //       附:(创建后务必刷新缓存)(暂不考虑批量创建班级)(创建接口参考class-manage页面)
+
+        // 2 应用班级设置 设置人数及分配模式,得到初步分派的结果
+        // 3 手工微调 将查询组件化,允许加入多个对比查询,作选中操作,执行转移/或者交换操作
+
+        #region model
+
+        /// <summary>
+        /// 选科轮次,仅显示分班关键字段,后续可能还会加选科流程的其它字段
+        /// </summary>
+        public class Round
+        {
+            // 本身的字段
+            int roundId; // 选科轮次ID
+            string groupIds; // 开放组合
+            object[] roundGroups; // 这里包含各组合以及班级数设置
+            // ...
+
+            // 流程控制字段
+            bool allowDispatch; // 标记当前是否可以进行分班逻辑
+            Dictionary<int, int> enrollGroupCount; // 各组合录取人数,按组合汇总
+            bool dispatchCompleted; // 所有学生已经分配完毕
+        }
+
+        /// <summary>
+        /// 班级,仅显示分班关键字段
+        /// </summary>
+        public class Clazz
+        {
+            int classId;
+            string name;
+        }
+
+        public class Student
+        {
+            int studentId;
+            int sex;
+            int rankInGroup; // 组合内排名
+        }
+
+        public class DispatchSetting
+        {
+            int roundId; // 轮次ID
+            int groupId; // 组合ID
+
+            int classId; // 班级
+            int expectedCount; // 期望人数
+            int? actualCount; // 实际人数,应用设置才产生数量
+            int? actualCountInMale; // 实际男生,应用设置才产生数量
+            int? actualCountInFemale; // 实际女生,应用设置才产生数量
+        }
+
+        public class DispatchResult
+        {
+            int roundId; // 轮次ID
+            int groupId; // 组合ID
+
+            int classId; // 班级
+            Student[] students; // 分配的学生
+        }
+
+        public enum EnumDispatchMode
+        {
+            RankBalance, // 按成绩平均分配,默认按这种分配方式,可保证各个班成绩都差不多
+            RankPriority, // 按成绩优先,会导致成绩好的集中到一起
+            Random // 随机
+        }
+
+        /// <summary>
+        /// 查询参数
+        /// </summary>
+        public class DispatchQuery
+        {
+            int roundId;
+            int groupId;
+            int classId;
+            int? sex;
+        }
+        #endregion
+        /// <summary>
+        /// 按学年,次数查询
+        /// ROUND: 如果allowDispatch=false,分班页仅只读不能操作
+        /// </summary>
+        /// <returns></returns>
+        Round GetRoundStatus(int year, int round); // 类似/front/report/getStudentSelected
+
+        /// <summary>
+        /// 获取分班配置
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <returns></returns>
+        DispatchSetting[] GetDispatchSettings(int roundId);
+
+        /// <summary>
+        /// 保存配置
+        /// </summary>
+        /// <param name="settings"></param>
+        void SaveDispatchSettings(DispatchSetting[] settings);
+
+        /// <summary>
+        /// 应用配置,分派学生,按组合执行
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <param name="mode"></param>
+        /// <returns></returns>
+        DispatchResult ApplyDispatchSettings(int roundId, int groupId, EnumDispatchMode mode = EnumDispatchMode.RankBalance);
+
+        /// <summary>
+        /// 分派名单查询,不需要分页,按班展示,启动table排序功能
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <param name="groupIds"></param>
+        /// <returns></returns>
+        DispatchResult GetDispatchResult(DispatchQuery query);
+
+        /// <summary>
+        /// 分派转移,用这个接口实现转移学生或者交换学生(手工操作),后台在操作成功后记得刷新setting中的3个人数汇总字段
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <param name="students"></param>
+        /// <param name="toClassId"></param>
+        void ExchangeDispatch(int roundId, Student[] students, int toClassId);
+
+        /// <summary>
+        /// 分配完毕后发布,锁定分班流程
+        /// </summary>
+        /// <param name="roundId"></param>
+        void PublishDispatch(int roundId);
+    }
+}

+ 428 - 6
mock/modules/elective-generation.js

@@ -1,7 +1,9 @@
 const Mock = require('mockjs')
+const Random = Mock['Random']
 
-const mockGeneration = 1 // primary
-const mockGroups = [1, 2, 3, 9, 10, 11]
+const mockGeneration = 8 // primary
+const mockGroups = [1, 2, 3, 4, 5, 6]
+const mockPreferenceCount = 3 // 1 or 3 // 1志愿/3志愿
 
 module.exports = [
   {
@@ -15,8 +17,21 @@ module.exports = [
           year: 2021,
           roundId: 1,
           roundName: '第X次选科',
+          groupIds: mockGroups.toString(),
+          roundGroups: mockGroups.map(groupId => ({
+            groupId: groupId,
+            classCount: Random.integer(2, 6),
+            personCount: function() {
+              return this.classCount * 50
+            },
+            limitPerson: false,
+            rankOut: false
+          })),
           allMatched: false,
-          currentGeneration: mockGeneration
+          currentGeneration: mockGeneration,
+
+          // +
+          disenrollCount: Random.integer(20, 100)
         }
       }
     }
@@ -25,12 +40,419 @@ module.exports = [
     url: '/mock/front/report/getElectiveSummary',
     type: 'get',
     response: config => {
+      const results = []
+      for (let generation = 1; generation <= mockGeneration; generation++) {
+        const copyGeneration = generation
+        switch (generation) {
+          case 1:
+            // primary
+            const allPreferences = []
+            for (let pref = 0; pref < mockPreferenceCount; pref++) {
+              const preferences = []
+              const groupDefines = [
+                {
+                  category: 'actualCount',
+                  displayName: '已报名',
+                  values: mockGroups.map(groupId => ({
+                    groupId: groupId,
+                    value: Random.integer(120, 400),
+                    color: 'B',
+                    bold: false,
+                    star: false,
+                    queryCode: 'abc',
+                    disabled: false
+                  }))
+                }
+              ]
+              const nonGroupDefines = [
+                {
+                  category: 'unfinishedCount',
+                  displayName: '未报名',
+                  values: [{
+                    groupId: 0,
+                    value: Random.integer(0, 10),
+                    color: '',
+                    bold: false,
+                    star: false,
+                    queryCode: 'abc',
+                    disabled: false
+                  }]
+                }
+              ]
+              preferences.push(...groupDefines)
+              preferences.push(...nonGroupDefines)
+              allPreferences.push(preferences)
+            }
+            const primary = {
+              roundId: 1,
+              generation: copyGeneration,
+              categories: allPreferences
+            }
+            results.push(primary)
+            break
+          default:
+            const accumulateDefine = {
+              generations: [2, 4, 6, 7],
+              factory: () => [
+                {
+                  category: 'approvedCount',
+                  displayName: '正常录取',
+                  values: mockGroups.map(groupId => ({
+                    groupId: groupId,
+                    value: Random.integer(120, 300),
+                    color: '',
+                    bold: false,
+                    star: false,
+                    queryCode: 'abc',
+                    disabled: false
+                  }))
+                },
+                {
+                  category: 'forcedCount',
+                  displayName: '调剂录取',
+                  values: mockGroups.map(groupId => ({
+                    groupId: groupId,
+                    value: Random.integer(0, 10),
+                    color: '',
+                    bold: false,
+                    star: false,
+                    queryCode: 'abc',
+                    disabled: false
+                  }))
+                },
+                {
+                  category: 'esignedCount',
+                  displayName: '已签人数',
+                  values: mockGroups.map(groupId => ({
+                    groupId: groupId,
+                    value: Random.integer(120, 400),
+                    color: '',
+                    bold: false,
+                    star: false,
+                    queryCode: 'abc',
+                    disabled: false
+                  }))
+                },
+                {
+                  category: 'esigningCount',
+                  displayName: '待签人数',
+                  values: mockGroups.map(groupId => ({
+                    groupId: groupId,
+                    value: Random.integer(120, 400),
+                    color: 'R',
+                    bold: false,
+                    star: false,
+                    queryCode: 'abc',
+                    disabled: false
+                  }))
+                }
+              ]
+            }
+            const commonDefines = [
+              {
+                generations: [2],
+                factory: () => [
+                  {
+                    category: 'indicateCount',
+                    displayName: '指标',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: true,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'approvedCount',
+                    displayName: '录取人数',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'adjustCount',
+                    displayName: '调剂人数',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'matchedCount',
+                    displayName: '可调剂人数',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'nonmatchedCount',
+                    displayName: '不可调剂人数',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  }
+                ]
+              },
+              {
+                generations: [3, 5],
+                factory: () => [
+                  {
+                    category: 'matchedApproved',
+                    displayName: '可调剂同意',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'matchedNotOptional',
+                    displayName: '可调剂改选',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'matchedRejected',
+                    displayName: '可调剂拒绝',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'matchedNonaction',
+                    displayName: '可调剂未填',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'matchedRankout',
+                    displayName: '可调剂被挤出',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'nonmatchedApproved',
+                    displayName: '不可调剂已填',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'nonmatchedRejected',
+                    displayName: '不可调剂拒绝',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'nonmatchedNonaction',
+                    displayName: '不可调剂未填',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  }
+                ]
+              },
+              {
+                generations: [4],
+                factory: () => [
+                  {
+                    category: 'approvedCount',
+                    displayName: '补录录取',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'forcedCount',
+                    displayName: '调剂录取',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'matchedCount',
+                    displayName: '可调剂人数',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'nonmatchedCount',
+                    displayName: '不可调剂人数',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  }
+                ]
+              },
+              {
+                generations: [6],
+                factory: () => [
+                  {
+                    category: 'approvedCount',
+                    displayName: '补录录取',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  },
+                  {
+                    category: 'forcedCount',
+                    displayName: '调剂录取',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  }
+                ]
+              },
+              {
+                generations: [7],
+                factory: () => [
+                  {
+                    category: 'forcedCount',
+                    displayName: '调剂录取',
+                    values: mockGroups.map(groupId => ({
+                      groupId: groupId,
+                      value: Random.integer(120, 400),
+                      color: '',
+                      bold: false,
+                      star: false,
+                      queryCode: 'abc',
+                      disabled: false
+                    }))
+                  }
+                ]
+              }
+            ]
+            const defines = commonDefines.find(d => d.generations.includes(generation))
+            const inAccDef = accumulateDefine.generations.includes(generation)
+            if (defines) {
+              results.push({
+                roundId: 1,
+                generation: copyGeneration,
+                categories: defines.factory(),
+                accumulates: inAccDef ? accumulateDefine.factory() : undefined
+              })
+            }
+            break
+        }
+      }
+
       return {
         code: 200,
         msg: 'success',
-        'data|6': [{
-          year: 2021,
-        }]
+        data: results
       }
     }
   }

+ 1 - 0
package.json

@@ -54,6 +54,7 @@
     "jszip": "^3.7.1",
     "jszip-utils": "^0.1.0",
     "mockjs": "^1.1.0",
+    "module": "^1.2.5",
     "nprogress": "0.2.0",
     "pizzip": "^3.1.1",
     "quill": "1.3.7",

+ 31 - 0
public/ar/hd.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>湖南大学</title>
+</head>
+<style>
+  * {
+    margin: 0;
+    padding: 0;
+  }
+  #app {
+    width: 100vw;
+    height: 100vh;
+  }
+
+  iframe {
+    border: none;
+  }
+</style>
+
+<body>
+  <div id="app">
+    <iframe allowfullscreen="true" width="100%" height="100%" src="http://720yun.com/pano/256zccfvk0s"></iframe>
+  </div>
+</body>
+
+</html>

+ 45 - 0
public/ar/index.html

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="renderer" content="webkit">
+  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+  <title>vr</title>
+
+  <script>
+
+  </script>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    #app {
+      padding: 30px;
+    }
+
+    ul {
+      list-style-type: disclosure-closed;
+    }
+
+    ul li+li {
+      margin-top: 20px;
+    }
+  </style>
+</head>
+
+<body>
+  <div id="app">
+    <ul>
+      <li>
+        <a href="./qh.html">清华大学</a>
+      </li>
+      <li><a href="./hd.html">湖南大学</a></li>
+    </ul>
+  </div>
+</body>
+
+</html>

+ 31 - 0
public/ar/qh.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>清华大学</title>
+</head>
+<style>
+  * {
+    margin: 0;
+    padding: 0;
+  }
+  #app {
+    width: 100vw;
+    height: 100vh;
+  }
+
+  iframe {
+    border: none;
+  }
+</style>
+
+<body>
+  <div id="app">
+    <iframe allowfullscreen="true" width="100%" height="100%" src="http://720yun.com/t/a7727upvq4i"></iframe>
+  </div>
+</body>
+
+</html>

+ 51 - 0
public/usign/css/index.css

@@ -0,0 +1,51 @@
+* {
+  margin: 0;
+  padding: 0;
+}
+
+#app {
+  padding: 20px;
+  box-sizing: border-box;
+  display: flex;
+}
+
+form {
+  flex: 1;
+}
+
+.form-item {
+  display: flex;
+}
+
+textarea[type=text] {
+  width: 300px;
+  height: 80px;
+  outline: none;
+  padding: 3px;
+}
+
+#encrypt {
+  margin-top: 20px;
+  width: 60px;
+  height: 28px;
+  background-color: #1ecdcd;
+  border: none;
+  border-radius: 4px;
+  color: #ffffff;
+  cursor: pointer;
+}
+
+#copy {
+  margin-top: 20px;
+  width: 60px;
+  height: 28px;
+  background-color: #ae1ecd;
+  border: none;
+  border-radius: 4px;
+  color: #ffffff;
+  cursor: pointer;
+}
+
+#payload {
+  margin-left: 10px;
+}

+ 64 - 0
public/usign/index.html

@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Document</title>
+  <script src="./js/usign.js"></script>
+  <script type="module">
+    window.onload = () => {
+      const resultDom = document.querySelector('#result');
+      const btn = document.querySelector('#encrypt');
+      btn.addEventListener('click', () => {
+        const inputDom = document.querySelector('#params');
+        const payloadDom = document.querySelector('#payload');
+        const value = inputDom.value;
+        let payload = undefined;
+        try {
+          if (payloadDom.value.trim()) {
+            payload = JSON.parse(payloadDom.value);
+          }
+          resultDom.value = u_sign(value, payload);
+        } catch (error) {
+          alert('参数错误');
+        }
+      });
+      const copy = document.querySelector('#copy');
+      copy.addEventListener('click', () => {
+        const value = resultDom.value;
+        if (value) {
+          resultDom.select();
+          document.execCommand("Copy");
+          resultDom.blur();
+        }
+      });
+    }
+  </script>
+  <link rel="stylesheet" href="./css/index.css" />
+</head>
+
+<body>
+  <div id="app">
+    <form>
+      <div class="form-item">
+        <label>请求地址及参数:</label>
+        <textarea id="params" type="text"
+          placeholder="示例: /youzy.dms.basiclib.api.career.job.hits.inc?jobCode=02030502"></textarea>
+        <textarea id="payload" type="text"
+          placeholder='示例(json字符串): {"provinceCode":43,"score":0,"year":2021,"course":"物理","bzType":"本专"}'></textarea>
+      </div>
+      <input id="encrypt" type="button" value="加密">
+    </form>
+    <form>
+      <div class="form-item">
+        <label>结果:</label>
+        <textarea id="result" type="text"></textarea>
+      </div>
+      <input id="copy" type="button" value="复制">
+    </form>
+  </div>
+</body>
+
+</html>

+ 307 - 0
public/usign/js/usign.js

@@ -0,0 +1,307 @@
+function Rc(n) {
+  return null == n ? "" : pu(n)
+}
+function pu(n) {
+  if ("string" == typeof n)
+    return n;
+  if (yh(n))
+    return c(n, pu) + "";
+  if (yc(n))
+    return ps ? ps.call(n) : "";
+  var t = n + "";
+  return "0" == t && 1 / n == -Rn ? "-0" : t
+}
+function V(n) {
+  return B(n) ? H(n) : p(n)
+}
+function B(n) {
+  return undefined;
+}
+function p(n) {
+  return n.split("")
+}
+function W(n, t) {
+  for (var r = n.length; r-- && y(t, n[r], 0) > -1;)
+    ;
+  return r
+}
+function y(n, t, r) {
+  return t === t ? q(n, t, r) : g(n, b, r)
+}
+function q(n, t, r) {
+  for (var e = r - 1, u = n.length; ++e < u;)
+    if (n[e] === t)
+      return e;
+  return -1
+}
+function Au(n, t, r) {
+  var e = n.length;
+  return r = r === undefined ? e : r,
+    !t && r >= e ? n : fu(n, t, r)
+}
+function fu(n, t, r) {
+  var e = -1
+    , u = n.length;
+  t < 0 && (t = -t > u ? 0 : u + t),
+    r = r > u ? u : r,
+    r < 0 && (r += u),
+    u = t > r ? 0 : r - t >>> 0,
+    t >>>= 0;
+  for (var i = Array(u); ++e < u;)
+    i[e] = n[e + t];
+  return i
+}
+function xa(n, t, r) {
+  if (n = Rc(n),
+    n && (r || t === undefined))
+    return n.replace(Wt, "");
+  if (!n || !(t = pu(t)))
+    return n;
+  var e = V(n);
+  return Au(e, 0, W(e, V(t)) + 1).join("")
+}
+
+
+
+
+
+
+const rr = {
+  '00d8': function (e, t) {
+    var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+      , r = {
+        rotl: function (e, t) {
+          return e << t | e >>> 32 - t
+        },
+        rotr: function (e, t) {
+          return e << 32 - t | e >>> t
+        },
+        endian: function (e) {
+          if (e.constructor == Number)
+            return 16711935 & r.rotl(e, 8) | 4278255360 & r.rotl(e, 24);
+          for (var t = 0; t < e.length; t++)
+            e[t] = r.endian(e[t]);
+          return e
+        },
+        randomBytes: function (e) {
+          for (var t = []; e > 0; e--)
+            t.push(Math.floor(256 * Math.random()));
+          return t
+        },
+        bytesToWords: function (e) {
+          for (var t = [], r = 0, n = 0; r < e.length; r++,
+            n += 8)
+            t[n >>> 5] |= e[r] << 24 - n % 32;
+          return t
+        },
+        wordsToBytes: function (e) {
+          for (var t = [], r = 0; r < 32 * e.length; r += 8)
+            t.push(e[r >>> 5] >>> 24 - r % 32 & 255);
+          return t
+        },
+        bytesToHex: function (e) {
+          for (var t = [], r = 0; r < e.length; r++)
+            t.push((e[r] >>> 4).toString(16)),
+              t.push((15 & e[r]).toString(16));
+          return t.join("")
+        },
+        hexToBytes: function (e) {
+          for (var t = [], r = 0; r < e.length; r += 2)
+            t.push(parseInt(e.substr(r, 2), 16));
+          return t
+        },
+        bytesToBase64: function (e) {
+          for (var r = [], n = 0; n < e.length; n += 3)
+            for (var i = e[n] << 16 | e[n + 1] << 8 | e[n + 2], o = 0; o < 4; o++)
+              8 * n + 6 * o <= 8 * e.length ? r.push(t.charAt(i >>> 6 * (3 - o) & 63)) : r.push("=");
+          return r.join("")
+        },
+        base64ToBytes: function (e) {
+          e = e.replace(/[^A-Z0-9+\/]/gi, "");
+          for (var r = [], n = 0, i = 0; n < e.length; i = ++n % 4)
+            0 != i && r.push((t.indexOf(e.charAt(n - 1)) & Math.pow(2, -2 * i + 8) - 1) << 2 * i | t.indexOf(e.charAt(n)) >>> 6 - 2 * i);
+          return r
+        }
+      };
+    return r;
+  },
+  "9a63": function (e, t) {
+    var r = {
+      utf8: {
+        stringToBytes: function (e) {
+          return r.bin.stringToBytes(unescape(encodeURIComponent(e)))
+        },
+        bytesToString: function (e) {
+          return decodeURIComponent(escape(r.bin.bytesToString(e)))
+        }
+      },
+      bin: {
+        stringToBytes: function (e) {
+          for (var t = [], r = 0; r < e.length; r++)
+            t.push(255 & e.charCodeAt(r));
+          return t
+        },
+        bytesToString: function (e) {
+          for (var t = [], r = 0; r < e.length; r++)
+            t.push(String.fromCharCode(e[r]));
+          return t.join("")
+        }
+      }
+    };
+    return r
+  },
+  "8349": function (e, t) {
+    function r(e) {
+      return !!e.constructor && "function" == typeof e.constructor.isBuffer && e.constructor.isBuffer(e)
+    }
+    return function (e) {
+      return null != e && (r(e) || function (e) {
+        return "function" == typeof e.readFloatLE && "function" == typeof e.slice && r(e.slice(0, 0))
+      }(e) || !!e._isBuffer)
+    }
+  },
+}
+
+
+function a(e, r) {
+  var t = rr["00d8"]()
+    , n = rr["9a63"]().utf8
+    , i = rr["8349"]
+    , o = rr["9a63"]().bin
+  e.constructor == String ? e = r && "binary" === r.encoding ? o.stringToBytes(e) : n.stringToBytes(e) : i(e) ? e = Array.prototype.slice.call(e, 0) : Array.isArray(e) || e.constructor === Uint8Array || (e = e.toString());
+  for (var s = t.bytesToWords(e), u = 8 * e.length, c = 1732584193, f = -271733879, d = -1732584194, l = 271733878, h = 0; h < s.length; h++)
+    s[h] = 16711935 & (s[h] << 8 | s[h] >>> 24) | 4278255360 & (s[h] << 24 | s[h] >>> 8);
+  s[u >>> 5] |= 128 << u % 32,
+    s[14 + (u + 64 >>> 9 << 4)] = u;
+  var p = a._ff
+    , b = a._gg
+    , y = a._hh
+    , m = a._ii;
+  for (h = 0; h < s.length; h += 16) {
+    var g = c
+      , v = f
+      , _ = d
+      , w = l;
+    c = p(c, f, d, l, s[h + 0], 7, -680876936),
+      l = p(l, c, f, d, s[h + 1], 12, -389564586),
+      d = p(d, l, c, f, s[h + 2], 17, 606105819),
+      f = p(f, d, l, c, s[h + 3], 22, -1044525330),
+      c = p(c, f, d, l, s[h + 4], 7, -176418897),
+      l = p(l, c, f, d, s[h + 5], 12, 1200080426),
+      d = p(d, l, c, f, s[h + 6], 17, -1473231341),
+      f = p(f, d, l, c, s[h + 7], 22, -45705983),
+      c = p(c, f, d, l, s[h + 8], 7, 1770035416),
+      l = p(l, c, f, d, s[h + 9], 12, -1958414417),
+      d = p(d, l, c, f, s[h + 10], 17, -42063),
+      f = p(f, d, l, c, s[h + 11], 22, -1990404162),
+      c = p(c, f, d, l, s[h + 12], 7, 1804603682),
+      l = p(l, c, f, d, s[h + 13], 12, -40341101),
+      d = p(d, l, c, f, s[h + 14], 17, -1502002290),
+      c = b(c, f = p(f, d, l, c, s[h + 15], 22, 1236535329), d, l, s[h + 1], 5, -165796510),
+      l = b(l, c, f, d, s[h + 6], 9, -1069501632),
+      d = b(d, l, c, f, s[h + 11], 14, 643717713),
+      f = b(f, d, l, c, s[h + 0], 20, -373897302),
+      c = b(c, f, d, l, s[h + 5], 5, -701558691),
+      l = b(l, c, f, d, s[h + 10], 9, 38016083),
+      d = b(d, l, c, f, s[h + 15], 14, -660478335),
+      f = b(f, d, l, c, s[h + 4], 20, -405537848),
+      c = b(c, f, d, l, s[h + 9], 5, 568446438),
+      l = b(l, c, f, d, s[h + 14], 9, -1019803690),
+      d = b(d, l, c, f, s[h + 3], 14, -187363961),
+      f = b(f, d, l, c, s[h + 8], 20, 1163531501),
+      c = b(c, f, d, l, s[h + 13], 5, -1444681467),
+      l = b(l, c, f, d, s[h + 2], 9, -51403784),
+      d = b(d, l, c, f, s[h + 7], 14, 1735328473),
+      c = y(c, f = b(f, d, l, c, s[h + 12], 20, -1926607734), d, l, s[h + 5], 4, -378558),
+      l = y(l, c, f, d, s[h + 8], 11, -2022574463),
+      d = y(d, l, c, f, s[h + 11], 16, 1839030562),
+      f = y(f, d, l, c, s[h + 14], 23, -35309556),
+      c = y(c, f, d, l, s[h + 1], 4, -1530992060),
+      l = y(l, c, f, d, s[h + 4], 11, 1272893353),
+      d = y(d, l, c, f, s[h + 7], 16, -155497632),
+      f = y(f, d, l, c, s[h + 10], 23, -1094730640),
+      c = y(c, f, d, l, s[h + 13], 4, 681279174),
+      l = y(l, c, f, d, s[h + 0], 11, -358537222),
+      d = y(d, l, c, f, s[h + 3], 16, -722521979),
+      f = y(f, d, l, c, s[h + 6], 23, 76029189),
+      c = y(c, f, d, l, s[h + 9], 4, -640364487),
+      l = y(l, c, f, d, s[h + 12], 11, -421815835),
+      d = y(d, l, c, f, s[h + 15], 16, 530742520),
+      c = m(c, f = y(f, d, l, c, s[h + 2], 23, -995338651), d, l, s[h + 0], 6, -198630844),
+      l = m(l, c, f, d, s[h + 7], 10, 1126891415),
+      d = m(d, l, c, f, s[h + 14], 15, -1416354905),
+      f = m(f, d, l, c, s[h + 5], 21, -57434055),
+      c = m(c, f, d, l, s[h + 12], 6, 1700485571),
+      l = m(l, c, f, d, s[h + 3], 10, -1894986606),
+      d = m(d, l, c, f, s[h + 10], 15, -1051523),
+      f = m(f, d, l, c, s[h + 1], 21, -2054922799),
+      c = m(c, f, d, l, s[h + 8], 6, 1873313359),
+      l = m(l, c, f, d, s[h + 15], 10, -30611744),
+      d = m(d, l, c, f, s[h + 6], 15, -1560198380),
+      f = m(f, d, l, c, s[h + 13], 21, 1309151649),
+      c = m(c, f, d, l, s[h + 4], 6, -145523070),
+      l = m(l, c, f, d, s[h + 11], 10, -1120210379),
+      d = m(d, l, c, f, s[h + 2], 15, 718787259),
+      f = m(f, d, l, c, s[h + 9], 21, -343485551),
+      c = c + g >>> 0,
+      f = f + v >>> 0,
+      d = d + _ >>> 0,
+      l = l + w >>> 0
+  }
+  return t.endian([c, f, d, l])
+};
+
+
+a._ff = function (e, t, r, n, i, o, a) {
+  var s = e + (t & r | ~t & n) + (i >>> 0) + a;
+  return (s << o | s >>> 32 - o) + t
+}
+  ,
+  a._gg = function (e, t, r, n, i, o, a) {
+    var s = e + (t & n | r & ~n) + (i >>> 0) + a;
+    return (s << o | s >>> 32 - o) + t
+  }
+  ,
+  a._hh = function (e, t, r, n, i, o, a) {
+    var s = e + (t ^ r ^ n) + (i >>> 0) + a;
+    return (s << o | s >>> 32 - o) + t
+  }
+  ,
+  a._ii = function (e, t, r, n, i, o, a) {
+    var s = e + (r ^ (t | ~n)) + (i >>> 0) + a;
+    return (s << o | s >>> 32 - o) + t
+  }
+
+function n(e) {
+  var t = rr["00d8"]()
+    , n = rr["9a63"]().utf8
+    , i = rr["8349"]()
+    , o = rr["9a63"]().bin
+  if (null == e)
+    throw new Error("Illegal argument " + e);
+  var n = t.wordsToBytes(a(e));
+  return t.bytesToHex(n)
+}
+
+
+// 第一个参数是请求地址及path参数,第二个参数是payload参数
+function u_sign(e, t = undefined) {
+  var r, o = "9SASji5OWnG41iRKiSvTJHlXHmRySRp1", a = "", s = t || {}, u = (e = e || "").split("?");
+  if (u.length > 0 && (r = u[1]),
+    r) {
+    var c = r.split("&")
+      , f = "";
+    c.forEach((function (e) {
+      var t = e.split("=");
+      f += "".concat(t[0], "=").concat(encodeURI(t[1]), "&")
+    }
+    )),
+      a = "".concat(xa(f, "&"), "&").concat(o)
+  } else
+    a = Object.keys(s).length > 0 ? "".concat(JSON.stringify(s), "&").concat(o) : "&".concat(o);
+  return a = a.toLowerCase(),
+    n(a)
+}
+
+// 示例:
+// u_sign('/youzy.dms.basiclib.api.career.job.hits.inc?jobCode=02030502')

+ 32 - 3
src/common/mx-config.js

@@ -4,8 +4,10 @@ export default {
     primary: '#47C6A2',
     primary_up: '#51C9A7',
     primary_down: '#D2F1E8',
+    primary_report: '#2EC7C9',
     yellow_up: '#F5AB86',
     yellow_down: '#FDEAE1',
+    yellow_report: '#FFA400',
     blue_up: '#608EDF',
     blue_down: '#DBE8FF',
     audit_bg: {
@@ -64,7 +66,7 @@ export default {
       title: '人生价值观探索',
       description: '价值观是指一个人对各类事物的意义和重要性的评价与看法,是一套判断事物是否有价值的观念体系,是指导我们选择与行动的内在指南。它指引着我们的生活。不同价值观的人会在不同的职业和专业领域实现自己的价值。下面的探索活动将帮助你了解自己在生命中所看的东西,指引你去探索属于自己专业的未来。'
     },
-    occupation:{
+    occupation: {
       title: '职业兴趣探索',
       description: '与学业和职业相关的兴趣,隐藏在生活的方方面面。探索兴趣的方法非常多样,通常人们通过思考自己在学习、生活中的表现或者对某些职业、活动的感受来确定自己的兴趣。职业研究者根据兴趣与职业世界、学业世界的关联把人的兴趣分为六种类型。下面让我们一起来看看你的兴趣代码及与之相关的专业和学科。'
     },
@@ -75,56 +77,83 @@ export default {
   },
   electiveGenerationOptions: {
     init: {
+      key: 'init',
+      value: 0,
+      decisionMaking: false,
       stepsVisible: false,
-      title: '',
+      title: '未开启',
       description: '',
       icon: ''
     },
     primary: {
+      key: 'primary',
+      value: 1,
+      decisionMaking: false,
       stepsVisible: true,
       title: '初选报名',
       description: '',
       icon: ''
     },
     primaryDM: {
+      key: 'primaryDM',
+      value: 2,
+      decisionMaking: true,
       stepsVisible: true,
       title: '初选报名决策',
       description: '',
       icon: ''
     },
     backTracking: {
+      key: 'backTracking',
+      value: 3,
+      decisionMaking: false,
       stepsVisible: true,
       title: '补录报名',
       description: '',
       icon: ''
     },
     backTrackingDM: {
+      key: 'backTrackingDM',
+      value: 4,
+      decisionMaking: true,
       stepsVisible: true,
       title: '补录报名决策',
       description: '',
       icon: ''
     },
     finalAdjust: {
+      key: 'finalAdjust',
+      value: 5,
+      decisionMaking: false,
       stepsVisible: true,
       title: '补录调剂报名',
       description: '',
       icon: ''
     },
     finalAdjustDM: {
+      key: 'finalAdjustDM',
+      value: 6,
+      decisionMaking: true,
       stepsVisible: true,
       title: '补录调剂报名决策',
       description: '',
       icon: ''
     },
     rankBalance: {
+      key: 'rankBalance',
+      value: 7,
+      decisionMaking: true,
       stepsVisible: true,
       title: '排名均衡',
       description: '',
       icon: ''
     },
     terminate: {
+      key: 'terminate',
+      value: 8,
+      decisionMaking: false,
       stepsVisible: false,
-      title: '',
+      title: '选科结束',
       description: '',
       icon: ''
     }

+ 0 - 11
src/common/mx-const.js

@@ -69,17 +69,6 @@ export default {
       philosophy: 1,  // 人生价值观
       occupation: 2,  // 职业兴趣
       knowledge: 3    // 知识兴趣
-    },
-    electiveGeneration: {
-      init: 0,
-      primary: 1,
-      primaryDM: 2,
-      backTracking: 3,
-      backTrackingDM: 4,
-      finalAdjust: 5,
-      finalAdjustDM: 6,
-      rankBalance: 7,
-      terminate: 8
     }
   }
 }

+ 3 - 0
src/common/mx-extension.js

@@ -51,6 +51,9 @@ export default {
         return this
       }
     }
+    String.prototype.tailingFix = function(fix) {
+      return this.endsWith(fix) ? this : this + fix
+    }
 
     // Global func.
     Vue.prototype.deepClone = function(obj) {

+ 77 - 77
src/components/EvaluationTitle/index.vue

@@ -1,77 +1,77 @@
-<template>
-  <el-card :class="{ 'box-card': cardStyle, 'evaluation-margin': withMarginBottom }">
-    <el-row :gutter="24">
-      <el-col :span="navBackButton ? 22 - slotSpan : 24 - slotSpan">
-        <div class="evaluation-title-row">
-          {{ title }}
-          <span class="evaulation-title-subItem">{{ subTitle }}</span>
-        </div>
-      </el-col>
-      <el-col v-if="slotSpan" :span="slotSpan">
-        <solt></solt>
-      </el-col>
-      <el-col v-if="navBackButton" :span="2" style="text-align: right">
-        <el-button @click="navAction ? navAction() : $router.go(-1)" size="small" round>返回</el-button>
-      </el-col>
-    </el-row>
-  </el-card>
-</template>
-<script>
-export default {
-  props: {
-    title: {
-      type: String,
-      default: "",
-    },
-    subTitle: {
-      type: String,
-      default: "",
-    },
-    navBackButton: {
-      type: Boolean,
-      default: false,
-    },
-    slotSpan: {
-      type: Number,
-      default: 0,
-    },
-    cardStyle: {
-      type: Boolean,
-      default: true,
-    },
-    withMarginBottom: {
-      type: Boolean,
-      default: true,
-    },
-    navAction: {
-      type: Function,
-      default: null,
-    },
-  },
-};
-</script>
-<style scoped>
-.evaluation-margin {
-  margin-bottom: 15px;
-}
-
-.evaluation-title-row {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-
-  display: flex;
-  justify-content: flex-start;
-  align-items: baseline;
-}
-
-.evaulation-title-subItem {
-  color: #999;
-  font-size: 12px;
-  margin-left: 10px;
-}
-
-/deep/ .el-card__body {
-  padding: 15px;
-}
-</style>
+<template>
+  <el-card :class="{ 'box-card': cardStyle, 'evaluation-margin': withMarginBottom }">
+    <el-row :gutter="24">
+      <el-col :span="navBackButton ? 22 - slotSpan : 24 - slotSpan">
+        <div class="evaluation-title-row">
+          {{ title }}
+          <span class="evaulation-title-subItem">{{ subTitle }}</span>
+        </div>
+      </el-col>
+      <el-col v-if="slotSpan" :span="slotSpan">
+        <solt></solt>
+      </el-col>
+      <el-col v-if="navBackButton" :span="2" style="text-align: right">
+        <el-button @click="navAction ? navAction() : $router.go(-1)" size="small" round>返回</el-button>
+      </el-col>
+    </el-row>
+  </el-card>
+</template>
+<script>
+export default {
+  props: {
+    title: {
+      type: String,
+      default: "",
+    },
+    subTitle: {
+      type: String,
+      default: "",
+    },
+    navBackButton: {
+      type: Boolean,
+      default: false,
+    },
+    slotSpan: {
+      type: Number,
+      default: 0,
+    },
+    cardStyle: {
+      type: Boolean,
+      default: true,
+    },
+    withMarginBottom: {
+      type: Boolean,
+      default: true,
+    },
+    navAction: {
+      type: Function,
+      default: null,
+    },
+  },
+};
+</script>
+<style scoped>
+.evaluation-margin {
+  margin-bottom: 15px;
+}
+
+.evaluation-title-row {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+
+  display: flex;
+  justify-content: flex-start;
+  align-items: baseline;
+}
+
+.evaulation-title-subItem {
+  color: #999;
+  font-size: 12px;
+  margin-left: 10px;
+}
+
+/deep/ .el-card__body {
+  padding: 15px;
+}
+</style>

+ 1 - 1
src/components/MxChart/index.vue

@@ -39,7 +39,7 @@ export default {
     options: function(opt) {
       if (!this.chart || !opt) return
       console.log('mx-chart set option', this.options)
-      this.chart.setOption(opt)
+      this.chart.setOption(opt, true) // true: notMerge
     }
   },
   mounted() {

+ 10 - 7
src/components/MxCondition/condition-object/condition-dispatch-class.js

@@ -2,24 +2,27 @@ import conditionObjectBase from '../condition-object-base.js'
 import { getClass } from '@/api/webApi/elective/dispatch.js'
 export default {
   ...conditionObjectBase,
-  dependentKeys: ['groupId'],
-  key: 'class',
+  dependentKeys: ['localGroupId'],
+  key: 'dispatchClassId',
   title: '班级',
   isDependencyReady(params) {
-    return params.groupId
+    return params.localGroupId
   },
-  getList: function(param) {
+  getList: function(param, $vue) {
     return new Promise((resolve, reject) => {
-      getClass().then(res => {
+      getClass({
+        groupId: param.localGroupId,
+        roundId: $vue.model.dispatchRoundId
+      }).then(res => {
         resolve(res.data)
       })
       .catch(e => reject(e))
     })
   },
   getCode: function(item) {
-    return item.classId
+    return item.id
   },
   getLabel: function(item) {
-    return item.className
+    return item.name
   }
 }

+ 15 - 0
src/components/MxCondition/condition-object/condition-dispatch-gender.js

@@ -0,0 +1,15 @@
+import conditionObjectBase from '../condition-object-base.js'
+import MxConfig from '@/common/mx-config'
+export default {
+  ...conditionObjectBase,
+  key: 'dispatchGender',
+  title: '性别',
+  getList: function(param, $vue) {
+    return Promise.resolve(MxConfig.form.sexOptions.map(item => {
+      return {
+        id: item.value,
+        name: item.text
+      }
+    }))
+  },
+}

+ 53 - 7
src/components/MxTable/index.vue

@@ -1,9 +1,10 @@
 <template>
-  <el-table ref="table" :data="rows" v-loading="loading" @selection-change="$emit('selection-change', $event)">
+  <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>
-      <el-table-column v-else-if="!prop.type" :key="key" :label="prop.label" :width="prop.width"
-                       :min-width="prop.minWidth" :align="prop.align || 'center'">
+      <el-table-column v-else-if="!prop.type" :key="key" :label="prop.label" :prop="key" :width="prop.width"
+                       :min-width="prop.minWidth" :align="prop.align || 'center'" :fixed="prop.fixed">
         <template v-if="prop.slotHeader" #header="scopeHeader">
           <slot :name="prop.slotHeader" v-bind="{
             ...scopeHeader,
@@ -25,6 +26,35 @@
           </slot>
           <span v-else>{{ scope.row[key] }}</span>
         </template>
+        <!--    TODO: hht 22.4.11 未实现跨组件传递slot, 先支持固定级别    -->
+        <template v-if="prop.children">
+          <el-table-column v-for="(childProp, childKey) in prop.children" :key="childKey" :label="childProp.label"
+                           :prop="childKey"
+                           :width="childProp.width" :fixed="childProp.fixed"
+                           :min-width="childProp.minWidth" :align="childProp.align || 'center'">
+            <template v-if="childProp.slotHeader" #header="scopeHeader">
+              <slot :name="childProp.slotHeader" v-bind="{
+                  ...scopeHeader,
+                  key: childKey,
+                  label: childProp.label,
+                  prop: childProp
+                }">{{ childProp.label }}
+              </slot>
+            </template>
+            <template slot-scope="scope">
+              <slot v-if="childProp.slot" :name="childProp.slot" v-bind="{
+                  ...scope,
+                  key: childKey,
+                  label: childProp.label,
+                  value: scope.row[childKey],
+                  prop: childProp
+                }">
+                <span>{{ scope.row[childKey] }}</span>
+              </slot>
+              <span v-else>{{ scope.row[childKey] }}</span>
+            </template>
+          </el-table-column>
+        </template>
       </el-table-column>
       <el-table-column :key="key" v-else :type="prop.type" :label="prop.label" :width="prop.width"
                        :align="prop.align || 'center'"></el-table-column>
@@ -32,16 +62,20 @@
   </el-table>
 </template>
 <script>
+import MxTableColumn from '@/components/MxTable/mx-table-column'
+
 export default {
+  components: { MxTableColumn },
+  inject: ['mergeTable'],
   props: {
+    border: {
+      type: Boolean,
+      default: false
+    },
     rows: {
       type: Array,
       default: () => []
     },
-    loading: {
-      type: Boolean,
-      default: false,
-    },
     propDefines: {
       type: Object,
       default: () => ({
@@ -63,6 +97,18 @@ export default {
   methods: {
     getRuntimeTable() {
       return this.$refs.table
+    },
+    mergeRowsColumns(scope) {
+      if (typeof this['mergeTable'] === 'function') {
+        return this['mergeTable'](scope)
+      }
+    },
+    scrollToRight() {
+      // use `setTimeout` instead of `$nextTick`
+      // - because css style not ready in next-tick, flow maybe work well in `nextTick.nextTick`
+      setTimeout(() => {
+        this.$refs.table.bodyWrapper.scrollLeft = Number(this.$refs.table.bodyWidth.replace('px', ''))
+      }, 500)
     }
   }
 }

+ 48 - 0
src/components/MxTable/mx-table-column.vue

@@ -0,0 +1,48 @@
+<template>
+  <el-table-column v-bind="propAttrs">
+    <template v-if="propAttrs.slotHeader" #header="scopeHeader">
+      <slot :name="propAttrs.slotHeader" v-bind="{
+            ...scopeHeader,
+            key: propKey,
+            label: propAttrs.label,
+            prop: propAttrs
+          }">{{ propAttrs.label }}
+      </slot>
+    </template>
+    <template slot-scope="scope">
+      <slot v-if="propAttrs.slot" :name="propAttrs.slot" v-bind="{
+            ...scope,
+            key: propKey,
+            label: propAttrs.label,
+            value: scope.row[propKey],
+            prop: propAttrs
+          }">
+        <span>{{ scope.row[propKey] }}</span>
+      </slot>
+      <span v-else>{{ scope.row[propKey] }}</span>
+    </template>
+    <template v-if="propAttrs.children">
+      <mx-table-column v-for="(childProp, childKey) in propAttrs.children" :key="childKey" :prop="childProp">
+      </mx-table-column>
+    </template>
+  </el-table-column>
+</template>
+
+<script>
+export default {
+  // TODO: hht, 22.4.11 需要实现跨组件传递slot, 才能使用该组件
+  name: 'mx-table-column',
+  props: {
+    propKey: {
+      type: String
+    },
+    propAttrs: {
+      type: Object
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 1 - 2
src/filters/index.js

@@ -4,7 +4,6 @@
 export default {
   classTailing: function(name) {
     if (!name) return name
-    if (name.endsWith('班')) return name
-    return name + '班'
+    return  name.tailingFix('班')
   }
 }

+ 11 - 5
src/router/index.js

@@ -1,5 +1,7 @@
 import Vue from 'vue'
 import Router from 'vue-router'
+/* Layout */
+import Layout from '@/layout'
 
 // 解决冗余导航报错
 const originalPush = Router.prototype.push
@@ -10,10 +12,6 @@ Router.prototype.push = function push(location) {
 
 Vue.use(Router)
 
-/* Layout */
-import Layout from '@/layout'
-import ParentView from '@/components/ParentView'
-
 /**
  * Note: 路由配置项
  *
@@ -771,8 +769,16 @@ export const constantRoutes = [{
         meta: {
           title: '分班详情'
         }
+      },
+      {
+        path: '/elective/generation/detail',
+        component: (resolve) => require(['@/views/elective/generation/detail'], resolve),
+        name: 'ElectiveGenerationDetail',
+        meta: {
+          title: '选科名单详情'
+        }
       }
-      ]
+    ]
   }
 ]
 

+ 307 - 0
src/utils/usign.js

@@ -0,0 +1,307 @@
+function Rc(n) {
+  return null == n ? "" : pu(n)
+}
+function pu(n) {
+  if ("string" == typeof n)
+    return n;
+  if (yh(n))
+    return c(n, pu) + "";
+  if (yc(n))
+    return ps ? ps.call(n) : "";
+  var t = n + "";
+  return "0" == t && 1 / n == -Rn ? "-0" : t
+}
+function V(n) {
+  return B(n) ? H(n) : p(n)
+}
+function B(n) {
+  return undefined;
+}
+function p(n) {
+  return n.split("")
+}
+function W(n, t) {
+  for (var r = n.length; r-- && y(t, n[r], 0) > -1;)
+    ;
+  return r
+}
+function y(n, t, r) {
+  return t === t ? q(n, t, r) : g(n, b, r)
+}
+function q(n, t, r) {
+  for (var e = r - 1, u = n.length; ++e < u;)
+    if (n[e] === t)
+      return e;
+  return -1
+}
+function Au(n, t, r) {
+  var e = n.length;
+  return r = r === undefined ? e : r,
+    !t && r >= e ? n : fu(n, t, r)
+}
+function fu(n, t, r) {
+  var e = -1
+    , u = n.length;
+  t < 0 && (t = -t > u ? 0 : u + t),
+    r = r > u ? u : r,
+    r < 0 && (r += u),
+    u = t > r ? 0 : r - t >>> 0,
+    t >>>= 0;
+  for (var i = Array(u); ++e < u;)
+    i[e] = n[e + t];
+  return i
+}
+function xa(n, t, r) {
+  if (n = Rc(n),
+    n && (r || t === undefined))
+    return n.replace(Wt, "");
+  if (!n || !(t = pu(t)))
+    return n;
+  var e = V(n);
+  return Au(e, 0, W(e, V(t)) + 1).join("")
+}
+
+
+
+
+
+
+const rr = {
+  '00d8': function (e, t) {
+    var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+      , r = {
+        rotl: function (e, t) {
+          return e << t | e >>> 32 - t
+        },
+        rotr: function (e, t) {
+          return e << 32 - t | e >>> t
+        },
+        endian: function (e) {
+          if (e.constructor == Number)
+            return 16711935 & r.rotl(e, 8) | 4278255360 & r.rotl(e, 24);
+          for (var t = 0; t < e.length; t++)
+            e[t] = r.endian(e[t]);
+          return e
+        },
+        randomBytes: function (e) {
+          for (var t = []; e > 0; e--)
+            t.push(Math.floor(256 * Math.random()));
+          return t
+        },
+        bytesToWords: function (e) {
+          for (var t = [], r = 0, n = 0; r < e.length; r++,
+            n += 8)
+            t[n >>> 5] |= e[r] << 24 - n % 32;
+          return t
+        },
+        wordsToBytes: function (e) {
+          for (var t = [], r = 0; r < 32 * e.length; r += 8)
+            t.push(e[r >>> 5] >>> 24 - r % 32 & 255);
+          return t
+        },
+        bytesToHex: function (e) {
+          for (var t = [], r = 0; r < e.length; r++)
+            t.push((e[r] >>> 4).toString(16)),
+              t.push((15 & e[r]).toString(16));
+          return t.join("")
+        },
+        hexToBytes: function (e) {
+          for (var t = [], r = 0; r < e.length; r += 2)
+            t.push(parseInt(e.substr(r, 2), 16));
+          return t
+        },
+        bytesToBase64: function (e) {
+          for (var r = [], n = 0; n < e.length; n += 3)
+            for (var i = e[n] << 16 | e[n + 1] << 8 | e[n + 2], o = 0; o < 4; o++)
+              8 * n + 6 * o <= 8 * e.length ? r.push(t.charAt(i >>> 6 * (3 - o) & 63)) : r.push("=");
+          return r.join("")
+        },
+        base64ToBytes: function (e) {
+          e = e.replace(/[^A-Z0-9+\/]/gi, "");
+          for (var r = [], n = 0, i = 0; n < e.length; i = ++n % 4)
+            0 != i && r.push((t.indexOf(e.charAt(n - 1)) & Math.pow(2, -2 * i + 8) - 1) << 2 * i | t.indexOf(e.charAt(n)) >>> 6 - 2 * i);
+          return r
+        }
+      };
+    return r;
+  },
+  "9a63": function (e, t) {
+    var r = {
+      utf8: {
+        stringToBytes: function (e) {
+          return r.bin.stringToBytes(unescape(encodeURIComponent(e)))
+        },
+        bytesToString: function (e) {
+          return decodeURIComponent(escape(r.bin.bytesToString(e)))
+        }
+      },
+      bin: {
+        stringToBytes: function (e) {
+          for (var t = [], r = 0; r < e.length; r++)
+            t.push(255 & e.charCodeAt(r));
+          return t
+        },
+        bytesToString: function (e) {
+          for (var t = [], r = 0; r < e.length; r++)
+            t.push(String.fromCharCode(e[r]));
+          return t.join("")
+        }
+      }
+    };
+    return r
+  },
+  "8349": function (e, t) {
+    function r(e) {
+      return !!e.constructor && "function" == typeof e.constructor.isBuffer && e.constructor.isBuffer(e)
+    }
+    return function (e) {
+      return null != e && (r(e) || function (e) {
+        return "function" == typeof e.readFloatLE && "function" == typeof e.slice && r(e.slice(0, 0))
+      }(e) || !!e._isBuffer)
+    }
+  },
+}
+
+
+function a(e, r) {
+  var t = rr["00d8"]()
+    , n = rr["9a63"]().utf8
+    , i = rr["8349"]
+    , o = rr["9a63"]().bin
+  e.constructor == String ? e = r && "binary" === r.encoding ? o.stringToBytes(e) : n.stringToBytes(e) : i(e) ? e = Array.prototype.slice.call(e, 0) : Array.isArray(e) || e.constructor === Uint8Array || (e = e.toString());
+  for (var s = t.bytesToWords(e), u = 8 * e.length, c = 1732584193, f = -271733879, d = -1732584194, l = 271733878, h = 0; h < s.length; h++)
+    s[h] = 16711935 & (s[h] << 8 | s[h] >>> 24) | 4278255360 & (s[h] << 24 | s[h] >>> 8);
+  s[u >>> 5] |= 128 << u % 32,
+    s[14 + (u + 64 >>> 9 << 4)] = u;
+  var p = a._ff
+    , b = a._gg
+    , y = a._hh
+    , m = a._ii;
+  for (h = 0; h < s.length; h += 16) {
+    var g = c
+      , v = f
+      , _ = d
+      , w = l;
+    c = p(c, f, d, l, s[h + 0], 7, -680876936),
+      l = p(l, c, f, d, s[h + 1], 12, -389564586),
+      d = p(d, l, c, f, s[h + 2], 17, 606105819),
+      f = p(f, d, l, c, s[h + 3], 22, -1044525330),
+      c = p(c, f, d, l, s[h + 4], 7, -176418897),
+      l = p(l, c, f, d, s[h + 5], 12, 1200080426),
+      d = p(d, l, c, f, s[h + 6], 17, -1473231341),
+      f = p(f, d, l, c, s[h + 7], 22, -45705983),
+      c = p(c, f, d, l, s[h + 8], 7, 1770035416),
+      l = p(l, c, f, d, s[h + 9], 12, -1958414417),
+      d = p(d, l, c, f, s[h + 10], 17, -42063),
+      f = p(f, d, l, c, s[h + 11], 22, -1990404162),
+      c = p(c, f, d, l, s[h + 12], 7, 1804603682),
+      l = p(l, c, f, d, s[h + 13], 12, -40341101),
+      d = p(d, l, c, f, s[h + 14], 17, -1502002290),
+      c = b(c, f = p(f, d, l, c, s[h + 15], 22, 1236535329), d, l, s[h + 1], 5, -165796510),
+      l = b(l, c, f, d, s[h + 6], 9, -1069501632),
+      d = b(d, l, c, f, s[h + 11], 14, 643717713),
+      f = b(f, d, l, c, s[h + 0], 20, -373897302),
+      c = b(c, f, d, l, s[h + 5], 5, -701558691),
+      l = b(l, c, f, d, s[h + 10], 9, 38016083),
+      d = b(d, l, c, f, s[h + 15], 14, -660478335),
+      f = b(f, d, l, c, s[h + 4], 20, -405537848),
+      c = b(c, f, d, l, s[h + 9], 5, 568446438),
+      l = b(l, c, f, d, s[h + 14], 9, -1019803690),
+      d = b(d, l, c, f, s[h + 3], 14, -187363961),
+      f = b(f, d, l, c, s[h + 8], 20, 1163531501),
+      c = b(c, f, d, l, s[h + 13], 5, -1444681467),
+      l = b(l, c, f, d, s[h + 2], 9, -51403784),
+      d = b(d, l, c, f, s[h + 7], 14, 1735328473),
+      c = y(c, f = b(f, d, l, c, s[h + 12], 20, -1926607734), d, l, s[h + 5], 4, -378558),
+      l = y(l, c, f, d, s[h + 8], 11, -2022574463),
+      d = y(d, l, c, f, s[h + 11], 16, 1839030562),
+      f = y(f, d, l, c, s[h + 14], 23, -35309556),
+      c = y(c, f, d, l, s[h + 1], 4, -1530992060),
+      l = y(l, c, f, d, s[h + 4], 11, 1272893353),
+      d = y(d, l, c, f, s[h + 7], 16, -155497632),
+      f = y(f, d, l, c, s[h + 10], 23, -1094730640),
+      c = y(c, f, d, l, s[h + 13], 4, 681279174),
+      l = y(l, c, f, d, s[h + 0], 11, -358537222),
+      d = y(d, l, c, f, s[h + 3], 16, -722521979),
+      f = y(f, d, l, c, s[h + 6], 23, 76029189),
+      c = y(c, f, d, l, s[h + 9], 4, -640364487),
+      l = y(l, c, f, d, s[h + 12], 11, -421815835),
+      d = y(d, l, c, f, s[h + 15], 16, 530742520),
+      c = m(c, f = y(f, d, l, c, s[h + 2], 23, -995338651), d, l, s[h + 0], 6, -198630844),
+      l = m(l, c, f, d, s[h + 7], 10, 1126891415),
+      d = m(d, l, c, f, s[h + 14], 15, -1416354905),
+      f = m(f, d, l, c, s[h + 5], 21, -57434055),
+      c = m(c, f, d, l, s[h + 12], 6, 1700485571),
+      l = m(l, c, f, d, s[h + 3], 10, -1894986606),
+      d = m(d, l, c, f, s[h + 10], 15, -1051523),
+      f = m(f, d, l, c, s[h + 1], 21, -2054922799),
+      c = m(c, f, d, l, s[h + 8], 6, 1873313359),
+      l = m(l, c, f, d, s[h + 15], 10, -30611744),
+      d = m(d, l, c, f, s[h + 6], 15, -1560198380),
+      f = m(f, d, l, c, s[h + 13], 21, 1309151649),
+      c = m(c, f, d, l, s[h + 4], 6, -145523070),
+      l = m(l, c, f, d, s[h + 11], 10, -1120210379),
+      d = m(d, l, c, f, s[h + 2], 15, 718787259),
+      f = m(f, d, l, c, s[h + 9], 21, -343485551),
+      c = c + g >>> 0,
+      f = f + v >>> 0,
+      d = d + _ >>> 0,
+      l = l + w >>> 0
+  }
+  return t.endian([c, f, d, l])
+};
+
+
+a._ff = function (e, t, r, n, i, o, a) {
+  var s = e + (t & r | ~t & n) + (i >>> 0) + a;
+  return (s << o | s >>> 32 - o) + t
+}
+  ,
+  a._gg = function (e, t, r, n, i, o, a) {
+    var s = e + (t & n | r & ~n) + (i >>> 0) + a;
+    return (s << o | s >>> 32 - o) + t
+  }
+  ,
+  a._hh = function (e, t, r, n, i, o, a) {
+    var s = e + (t ^ r ^ n) + (i >>> 0) + a;
+    return (s << o | s >>> 32 - o) + t
+  }
+  ,
+  a._ii = function (e, t, r, n, i, o, a) {
+    var s = e + (r ^ (t | ~n)) + (i >>> 0) + a;
+    return (s << o | s >>> 32 - o) + t
+  }
+
+function n(e) {
+  var t = rr["00d8"]()
+    , n = rr["9a63"]().utf8
+    , i = rr["8349"]()
+    , o = rr["9a63"]().bin
+  if (null == e)
+    throw new Error("Illegal argument " + e);
+  var n = t.wordsToBytes(a(e));
+  return t.bytesToHex(n)
+}
+
+
+// 第一个参数是请求地址及path参数,第二个参数是payload参数
+export function u_sign(e, t = undefined) {
+  var r, o = "9SASji5OWnG41iRKiSvTJHlXHmRySRp1", a = "", s = t || {}, u = (e = e || "").split("?");
+  if (u.length > 0 && (r = u[1]),
+    r) {
+    var c = r.split("&")
+      , f = "";
+    c.forEach((function (e) {
+      var t = e.split("=");
+      f += "".concat(t[0], "=").concat(encodeURI(t[1]), "&")
+    }
+    )),
+      a = "".concat(xa(f, "&"), "&").concat(o)
+  } else
+    a = Object.keys(s).length > 0 ? "".concat(JSON.stringify(s), "&").concat(o) : "&".concat(o);
+  return a = a.toLowerCase(),
+    n(a)
+}
+
+// 示例:
+// u_sign('/youzy.dms.basiclib.api.career.job.hits.inc?jobCode=02030502')

+ 3 - 19
src/views/elective/dispatch/components/class-adjust.vue

@@ -85,6 +85,7 @@ export default {
     },
     parentFrame() {
       const selected = this.selectedRight.concat(this.selectedLeft)
+      console.log(selected)
       return selected.map(item => {
         return {
           key: item.studentId,
@@ -119,7 +120,6 @@ export default {
       this.selectedLeft = []
       this.selectedRight = []
       this.show =false
-      this.tranRight = []
     },
     getTranLeft() {
       // 获取左tran实际的数据
@@ -200,25 +200,9 @@ export default {
         classId: classId
       }).then(res => {
         if( type == 'from') {
-          if(!res.hasOwnProperty('data')){
-            this.studentsTableLeft = []
-            return
-          }
-          this.studentsTableLeft = res.data.students.map(item => {
-            item.classId = classId
-            item.className = this.getClassName(classId)
-            return item
-          })
+          this.studentsTableLeft = res.rows
         }else if( type == 'to') {
-          if(!res.hasOwnProperty('data')){
-            this.studentsTableRight = []
-            return
-          }
-          this.studentsTableRight = res.data.students.map(item => {
-            item.classId = classId
-            item.className = this.getClassName(classId)
-            return item
-          })
+          this.studentsTableRight = res.rows
         }
       })
     },

+ 36 - 36
src/views/elective/dispatch/components/class-table.vue

@@ -1,44 +1,11 @@
 <template>
   <div>
-    <el-table
-      :data="list"
-      @selection-change="handleSelectionChange"
-    >
-      <el-table-column
-        type="selection"
-        width="55"
-      >
-      </el-table-column>
-      <el-table-column
-        prop="className"
-        label="班级"
-      >
-      </el-table-column>
-      <el-table-column
-        prop="name"
-        label="姓名"
-      >
-      </el-table-column>
-      <el-table-column
-        prop="sex"
-        label="性别"
-      >
-        <template slot-scope="scope">
-          {{scope.$index % 2 == 0 ? '男' : '女'}}
-        </template>
-      </el-table-column>
-      <el-table-column
-        prop="rankInGroup"
-        label="排名"
-        type="index"
-        sortable
-      >
-      </el-table-column>
-    </el-table>
+    <mx-table :propDefines="propDefines" :rows="list" @selection-change="handleSelectionChange">
+    </mx-table>
     <!-- 操作 确认和分页-->
     <div class="tmp">
 
-<!--      <el-button v-if="list.length > 0 && selected.length > 0" type="primary" @click="confirm">确认选择</el-button>-->
+    <!-- <el-button v-if="list.length > 0 && selected.length > 0" type="primary" @click="confirm">确认选择</el-button>-->
       <el-pagination
         layout="prev, pager, next"
         :total="10">
@@ -50,6 +17,10 @@
 <script>
 export default {
   props: {
+    type: {
+      String: '',  //1 用于class-adjust 2 用于完成分班展示
+      default: '1'
+    },
     list: {
       required: true,
       type: Array,
@@ -58,9 +29,38 @@ export default {
   },
   data() {
     return {
+      propDefines: {
+        selection: {
+          label: '选择',
+          type:'selection',
+          hidden: this.type == '2'
+        },
+        groupName: {
+          label: '组合',
+          hidden: this.type == '1'
+        },
+        className: {
+          label: '班级'
+        },
+        name: {
+          label: '姓名',
+        },
+        sex: {
+          label: '性别',
+        },
+        rankInGroup: {
+          label: '班级排名'
+        },
+        rankInGrade: {
+          label: '年级排名',
+          hidden: this.type == '1'
+        }
+      },
       selected: [],
     }
   },
+  computed: {
+  },
   methods: {
     handleSelectionChange(row) {
       this.selected = row

+ 25 - 5
src/views/elective/dispatch/components/dispatch-table.vue

@@ -1,6 +1,6 @@
 <template>
   <el-row>
-    <mx-table v-loading="loading" :propDefines="propDefines" :rows="displayRows">
+    <mx-table  :propDefines="propDefines" :rows="displayRows">
       <template #classCount="{row,$index}">
         <el-input-number size="small" v-model="row.classCount" @change="classCountChange(row,$index)" :min="0"
                          :disabled="row.classCount != 0" label="label"
@@ -17,6 +17,14 @@
         >编辑
         </el-button>
       </template>
+      <template #className="{row}">
+        <div v-if="row.groupClass.length">
+          <span  class="btn-class" @click="toDetail(row,item.classId)" v-for="item in row.groupClass">{{item.className}}</span>
+        </div>
+        <div v-else>
+          <span ></span>
+        </div>
+      </template>
       <!-- 班级调整 -->
       <template #adjust="{row}">
         <el-button
@@ -92,7 +100,8 @@ export default {
           slot: 'edit'
         },
         groupClass: {
-          label: '班级名称'
+          label: '班级名称',
+          slot: 'className'
         },
         expectedCount: {
           label: '人数'
@@ -126,7 +135,12 @@ export default {
           expectedCount: this.settings.filter(item => item.groupId == rg.groupId).map(item => item.expectedCount).toString(),
           groupClass: this.settings
             .filter(item => item.groupId == rg.groupId)
-            .map(item => this.getClassName(item.classId)).toString()
+            .map(item => {
+              return {
+                classId: item.classId,
+                className: this.getClassName(item.classId)
+              }
+            })
         }))
       console.log('displayRows computed:', rows)
       return rows
@@ -153,8 +167,9 @@ export default {
       }
       this.$refs.adjustDialog.open(row,this.settings.filter(item => item.groupId == row.groupId))
     },
-    toDetail(row) {
-      const params = {group: row,groupIds: (this.round.groupIds &&this.round.groupIds.split(','))||[]}
+    toDetail(row,classId ='') {
+      console.log(classId)
+      const params = {group: row,classId:classId,groupIds: (this.round.groupIds &&this.round.groupIds.split(','))||[]}
       const path = '/elective/dispatch/detail'
       console.log('prev transfer', row, this.round)
       this.transferTo(path, params)
@@ -178,4 +193,9 @@ export default {
 }
 </script>
 <style scoped>
+.btn-class{
+  margin-right: 10px;
+  cursor: pointer;
+  border-bottom:1px solid #42b983;
+}
 </style>

+ 49 - 21
src/views/elective/dispatch/components/set-classcount.vue

@@ -32,28 +32,52 @@
               <el-input-number :min="1" :max="roundGroup.number?setPubMax(scope.$index):Infinity" v-model="scope.row.expectedCount" @change="countEdit(scope.row,scope.$index)" ></el-input-number>
           </template>
         </el-table-column>
-        <el-table-column
-          label="操作"
-        >
-          <template slot-scope="scope">
-            <el-popover
-              placement="bottom"
-              title="标题"
-              width="200"
-              trigger="click"
-              content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">
-              <el-switch
-                v-model="scope.row.active"
-                active-color=""
-                active-text="按排名"
-                inactive-text="随机"
-                inactive-color="#ff4949"
-                slot="reference">
-              </el-switch>
-            </el-popover>
-          </template>
-        </el-table-column>
+<!--        <el-table-column-->
+<!--          label="操作"-->
+<!--        >-->
+<!--          <template slot-scope="scope">-->
+<!--            <el-popover-->
+<!--              placement="bottom"-->
+<!--              title="标题"-->
+<!--              width="200"-->
+<!--              trigger="click"-->
+<!--              content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">-->
+<!--              <el-switch-->
+<!--                v-model="scope.row.active"-->
+<!--                active-color=""-->
+<!--                active-text="按排名"-->
+<!--                inactive-text="随机"-->
+<!--                inactive-color="#ff4949"-->
+<!--                slot="reference">-->
+<!--              </el-switch>-->
+<!--            </el-popover>-->
+<!--          </template>-->
+<!--        </el-table-column>-->
       </el-table>
+      <el-radio-group class="mt10" v-model="mode">
+        <el-popover
+
+          placement="bottom-start"
+          width="200"
+          trigger="hover"
+          content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">
+          <el-radio-button class="radio-btn" slot="reference">均衡</el-radio-button>
+        </el-popover>
+        <el-popover
+          placement="bottom-start"
+          width="200"
+          trigger="hover"
+          content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">
+          <el-radio-button class="radio-btn" slot="reference">排名</el-radio-button>
+        </el-popover>
+        <el-popover
+          placement="bottom-start"
+          width="200"
+          trigger="hover"
+          content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">
+          <el-radio-button class="radio-btn" slot="reference">随机</el-radio-button>
+        </el-popover>
+      </el-radio-group>
     </div>
 </template>
 
@@ -68,6 +92,7 @@ export default {
       setShow: false,
       roundId: '',
       roundGroup: {},
+      mode: '',
       settingContainer: []
     }
   },
@@ -154,5 +179,8 @@ export default {
 </script>
 
 <style scoped>
+.radio-btn{
 
+  margin-right: 20px;
+}
 </style>

+ 27 - 5
src/views/elective/dispatch/detail.vue

@@ -6,7 +6,7 @@
                     @query="handleGroupQuery" class="mb10"
       ></mx-condition>
     </el-card>
-
+    <class-table type="2" :list="studentList"></class-table>
   </div>
 </template>
 <!--:setting-model="settingModel" :group-model-index="groupModelIndex"-->
@@ -15,14 +15,17 @@
 import MxSelectTranslateMixin from '@/components/Cache/modules/mx-select-translate-mixin.js'
 import MxCondition from '@/components/MxCondition/mx-condition'
 import MxTransferMixin from '@/components/mx-transfer-mixin.js'
+import ClassTable from '@/views/elective/dispatch/components/class-table'
+import { classesResult } from '@/api/webApi/elective/dispatch'
 
 export default {
   mixins: [MxTransferMixin, MxSelectTranslateMixin],
-  components: { MxCondition },
+  components: { ClassTable, MxCondition },
   name: 'dispatch-detail',
   data() {
     return {
-      requireFields: ['localGroupId','class'],
+      requireFields: ['localGroupId'],
+      studentList: [],
       queryParams: null
     }
   },
@@ -39,13 +42,32 @@ export default {
           }
         })
       }
-      this.$nextTick(_ => this.queryParams = { localGroupId: this.prevData.group.groupId })
+      this.$nextTick(_ => this.queryParams = {
+        localGroupId: this.prevData.group.groupId,
+        dispatchClassId: this.prevData.classId || '',
+        dispatchGender: '',
+        dispatchRoundId: this.prevData.group.roundId
+      })
       return source
     }
   },
   methods: {
     handleGroupQuery() {
-
+       this.getDispatchResult()
+    },
+    getDispatchResult() {
+      classesResult({
+        classId: this.queryParams.dispatchClassId,
+        groupId: this.queryParams.localGroupId,
+        roundId: this.queryParams.dispatchRoundId,
+        sex: this.queryParams.dispatchGender || null,
+      }).then(res => {
+        console.log(res)
+        this.studentList = res.rows.map(item => {
+          item.groupName = this.translateGroup(this.queryParams.localGroupId)
+          return item
+        })
+      })
     }
   }
 }

+ 244 - 0
src/views/elective/generation/components/elective-generation-charts.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="fx-column">
+    <div class="fx-row fx-bet-cen">
+      <div class="fx-1">
+        <mx-chart ref="bar" :options="chartOptions.bar" :height="'450px'"></mx-chart>
+      </div>
+      <div v-if="chartOptions.pie" class="fx-row fx-cen-cen" style="width: 450px">
+        <mx-chart ref="pie" :options="chartOptions.pie" :height="'450px'"></mx-chart>
+      </div>
+    </div>
+    <template v-if="chartOptions.pies&&chartOptions.pies.length">
+      <el-divider></el-divider>
+      <div class="fx-row fx-bet-cen">
+        <div v-for="(pie,idx) in chartOptions.pies" :key="idx" class="fx-1 fx-row fx-cen-cen">
+          <mx-chart ref="pies" :options="pie" :height="'400px'"></mx-chart>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import MxChart from '@/components/MxChart/index'
+import config from '@/common/mx-config'
+
+export default {
+  name: 'elective-generation-charts',
+  components: { MxChart },
+  props: ['chartBinding'],
+  computed: {
+    chartOptions() {
+      const options = this.chartBinding.generation.options
+      let data = this.chartBinding.chartData?.accumulates
+      let generation = this.chartBinding.chartData?.generation
+      let desc = '录取超缺数量'
+      if (this.chartBinding.generation.active == options.primary.value) {
+        generation = options.primary.value // force override primary chart data
+        data = this.chartBinding.generation.summary.find(item => item.generation == generation)?.categories
+        desc = '报名超缺数量'
+      }
+      if (!data?.length) return {}
+      const currentOpt = Object.values(options).find(opt => opt.value == generation)
+
+      // xAxis data - round groups
+      const roundGroups = this.chartBinding.generation.roundGroups
+      const xAxis = roundGroups.map(rg => rg.groupName)
+
+      // yAxis data
+      let series = []
+      let valueNameGroup = null
+      let valueNameGroups = []
+      if (generation == options.primary.value) {
+        // 初选报名需要单独处理
+        const multiplePreference = data.length > 1
+        const preferenceSeries = data.map((single, idx) => {
+          const prefix = multiplePreference ? `第${idx + 1}志愿/` : ''
+          const stackName = 'preference_' + idx
+          const singleGroup = { dimension: !multiplePreference ? '报名人数' : `第${idx + 1}志愿`, values: [] }
+          const singleSeries = this.barSeriesMissingAndOverFactory(
+            roundGroups,
+            rg => rg.expectedCount * 1,
+            rg => {
+              let actualCount = single.find(item => item.category == 'actualCount')?.values
+                .find(item => item.groupId == rg.groupId)?.value || 0
+              actualCount = actualCount * 1
+              singleGroup.values.push({ value: actualCount, name: rg.groupName })
+              return actualCount
+            },
+            stackName,
+            [prefix + '报名人数', , prefix + '缺少人数', prefix + '超出人数']
+          )
+          let unfinishedCount = single.find(item => item.category == 'actualCount')?.values?.first()?.value || 0
+          unfinishedCount = unfinishedCount * 1
+          singleGroup.values.push({ value: unfinishedCount, name: '未报名' })
+          valueNameGroups.push(singleGroup)
+          return singleSeries
+        })
+        series = preferenceSeries.reduce((prev, cur) => prev.concat(cur), [])
+        if (valueNameGroups.length > 1) {
+          valueNameGroup = valueNameGroups.reduce((prev, cur) => {
+            const merge = this.deepClone(cur) // NOTE: DO NOT modify raw item
+            merge.dimension = '报名人次'
+            merge.values.forEach(val => {
+              const match = prev.values.find(i => i.name == val.name)
+              val.value = val.value * 1 + (match?.value || 0) * 1
+            })
+            return merge
+          }, { values: [] })
+        } else {
+          valueNameGroup = valueNameGroups?.first()
+          valueNameGroups = []
+        }
+      } else {
+        valueNameGroup = { dimension: '录取人数', values: [] }
+        series = this.barSeriesMissingAndOverFactory(
+          roundGroups,
+          rg => rg.expectedCount * 1,
+          rg => {
+            const approvedCount = data.find(item => item.category == 'approvedCount')?.values
+              .find(item => item.groupId == rg.groupId)?.value || 0
+            const forcedCount = data.find(item => item.category == 'forcedCount')?.values
+              .find(item => item.groupId == rg.groupId)?.value || 0
+            const enrollCount = approvedCount * 1 + forcedCount * 1
+            valueNameGroup.values.push({ value: enrollCount, name: rg.groupName })
+            return enrollCount
+          },
+          'Indicator',
+          []
+        )
+        valueNameGroup.values.push({ value: this.chartBinding.generation.status.disenrollCount, name: '未录取' })
+      }
+      setTimeout(() => {
+        this.$refs.bar?.resize()
+        this.$refs.pie?.resize()
+      }, 200)
+      return {
+        bar: this.barOptionFactory('组合统计/' + currentOpt.title, desc, xAxis, series),
+        pie: valueNameGroup && this.pieOptionFactory('组合统计/' + currentOpt.title, valueNameGroup.dimension, '', valueNameGroup.values),
+        pies: valueNameGroups.map(g => this.pieOptionFactory('组合统计/' + currentOpt.title, g.dimension, '', g.values))
+      }
+    }
+  },
+  methods: {
+    pieOptionFactory(title, subTitle, dimensionName, valueNamePairs) {
+      return {
+        title: {
+          text: title,
+          subtext: subTitle,
+          left: 'center'
+        },
+        tooltip: {
+          trigger: 'item'
+        },
+        legend: {
+          orient: 'vertical',
+          left: 'left'
+        },
+        series: [
+          {
+            name: dimensionName,
+            type: 'pie',
+            radius: '50%',
+            data: valueNamePairs,
+            emphasis: {
+              itemStyle: {
+                shadowBlur: 10,
+                shadowOffsetX: 0,
+                shadowColor: 'rgba(0, 0, 0, 0.5)'
+              }
+            }
+          }
+        ]
+      }
+    },
+    barOptionFactory(title, subTitle, xAxis, series) {
+      return {
+        title: {
+          text: title,
+          subtext: subTitle,
+          left: 'center'
+        },
+        xAxis: {
+          type: 'category',
+          data: xAxis
+        },
+        yAxis: {
+          type: 'value'
+        },
+        series: series,
+        tooltip: {
+          trigger: 'item'
+        }
+      }
+    },
+    barSeriesMissingAndOverFactory(sourceArr, baseGetter, actualGetter, stackName, lineNames) {
+      // 本来是创建3条线:基础线,缺少线,超出线,但图表的提示展示不方便
+      // 所以这里创建4条线:期望线,实际线,缺少线,超出线。(期望线,实际线保持相同颜色)
+      const series = []
+      const commonDefines = {
+        type: 'bar',
+        stack: stackName,
+        emphasis: {
+          focus: 'series',
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
+          }
+        }
+      }
+      const actualLine = {
+        ...commonDefines,
+        name: lineNames[0] || '录取人数',
+        data: []
+      }
+      const expectedLine = {
+        ...commonDefines,
+        name: lineNames[1] || '设置人数',
+        data: []
+      }
+      const missingLine = {
+        ...commonDefines,
+        name: lineNames[2] || '缺少人数',
+        data: []
+      }
+      const overLine = {
+        ...commonDefines,
+        name: lineNames[3] || '超出人数',
+        data: []
+      }
+      sourceArr.forEach(item => {
+        const settingCount = baseGetter(item)
+        const enrollCount = actualGetter(item)
+        if (settingCount <= 0 || settingCount == enrollCount) {
+          expectedLine.data.push(0)
+          actualLine.data.push({ value: enrollCount, itemStyle: { color: config.color.primary_report } })
+          missingLine.data.push(0)
+          overLine.data.push(0)
+        } else {
+          const subCount = Math.abs(settingCount - enrollCount)
+          const baseCount = Math.min(settingCount, enrollCount)
+          if (enrollCount > settingCount) {
+            expectedLine.data.push({ value: baseCount, itemStyle: { color: config.color.primary_report } })
+            actualLine.data.push(0)
+            missingLine.data.push(0)
+            overLine.data.push({ value: subCount, itemStyle: { color: config.color.error } })
+          } else {
+            expectedLine.data.push(0)
+            actualLine.data.push({ value: baseCount, itemStyle: { color: config.color.primary_report } })
+            missingLine.data.push({ value: subCount, itemStyle: { color: config.color.yellow_report } })
+            overLine.data.push(0)
+          }
+        }
+      })
+      series.push(actualLine, expectedLine, missingLine, overLine)
+      return series
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 90 - 0
src/views/elective/generation/components/elective-generation-master.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="fx-column">
+    <evaluation-empty v-if="isUnPassedStep" :shadow="false" :title="emptyTitle"></evaluation-empty>
+    <template v-else>
+      <div class="fx-row fx-bet-cen mb15">
+        <div>
+          <slot name="header-prefix"></slot>
+        </div>
+        <div class="fx-1">
+          <slot :name="activeKey+'-header'" v-bind="chartBinding"></slot>
+        </div>
+        <div>
+          <slot name="header-suffix"></slot>
+        </div>
+      </div>
+      <slot :name="activeKey" v-bind="chartBinding">
+        <elective-generation-table :chart-binding="chartBinding"></elective-generation-table>
+      </slot>
+      <slot name="footer-prefix"></slot>
+      <slot :name="activeKey+'-footer'" v-bind="chartBinding">
+        <elective-generation-bar-chart :chart-binding="chartBinding" class="mt40"></elective-generation-bar-chart>
+      </slot>
+      <slot name="footer-suffix"></slot>
+    </template>
+  </div>
+</template>
+
+<script>
+
+import ElectiveGenerationTable from '@/views/elective/generation/components/elective-generation-table'
+import ElectiveGenerationBarChart from '@/views/elective/generation/components/elective-generation-charts'
+
+export default {
+  name: 'elective-generation-master',
+  components: { ElectiveGenerationBarChart, ElectiveGenerationTable },
+  props: {
+    generation: {
+      type: Object
+    }
+  },
+  data() {
+    return {
+      emptyTitle: ''
+    }
+  },
+  computed: {
+    activeKey() {
+      return this.generation.activeOpt?.key || ''
+    },
+    isUnPassedStep() {
+      if (!this.generation.activeOpt) return false
+      this.emptyTitle = this.generation.currentOpt == this.generation.options.init
+        ? '选科还未开启'
+        : `正在进行${this.generation.currentOpt.title || '{未知进程}'}, 还未进行至${this.generation.activeOpt.title}`
+      return this.generation.active > this.generation.current
+    },
+    categories() {
+      // summary - categories for display in table
+      return this.generation.summary.filter(item => item.generation <= this.generation.active)
+    },
+    accumulates() {
+      // summary - accumulate for display in charts
+      let options = this.generation.options
+      let generation = this.generation.active
+      let generationData = this.generation.summary.find(item => item.generation == generation && item.accumulates?.length)
+      if (!generationData) {
+        generation = generation - 1
+        generationData = this.generation.summary.find(item => item.generation == generation)
+      }
+
+      return generationData?.accumulates?.length ? {
+        generation: generation, // 图表激活的进程代,除初选报名外,一般为决策代数据
+        accumulates: generationData.accumulates
+      } : null
+    },
+    chartBinding() {
+      return {
+        generation: this.generation,
+        tableData: this.categories,
+        chartData: this.accumulates
+      }
+    }
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 156 - 0
src/views/elective/generation/components/elective-generation-steps.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="fx-column pl20 pr20">
+    <div class="fx-row fx-cen-cen f18 f-333 bold">
+      {{ title }}
+    </div>
+    <el-steps :active="activeStep" class="mt20">
+      <el-step v-for="(step,idx) in steps" :key="step.key" :title="step.title" :status="step.status">
+        <template #title>
+          <div class="rel"
+               :class="{'bold':activeStep==idx+1, 'f-333':activeStep==idx+1&&step.value>generation.current}">
+            <i v-if="activeStep==idx+1" class="el-icon-thumb current-pointer current-animation abs"></i>
+            <el-popover v-else-if="step.value<generation.current&&step.value<activeStep" trigger="hover"
+                        class="abs current-view">
+              <el-checkbox :value="generation.hiddenGenerations.includes(step.value)"
+                           @input="toggleGenerationTableData($event,step)">
+                隐藏{{ step.title }}表格数据
+              </el-checkbox>
+              <i slot="reference" class="el-icon-view pointer"
+                 :class="{'current-disabled': generation.hiddenGenerations.includes(step.value)}"
+                 @click="toggleGenerationTableData(!generation.hiddenGenerations.includes(step.value), step)"></i>
+            </el-popover>
+            <span class="pointer" @click="handleStepChange(step)">
+              {{ step.title }}</span>
+          </div>
+        </template>
+      </el-step>
+    </el-steps>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+export default {
+  name: 'elective-generation-steps',
+  model: {
+    prop: 'modelValue',
+    event: 'change'
+  },
+  props: {
+    generation: {
+      type: Object,
+      default: () => ({})
+    },
+    modelValue: {
+      type: String,
+      default: '' // active step key.
+    }
+  },
+  data() {
+    return {
+      activeStep: 0
+    }
+  },
+  computed: {
+    ...mapGetters(['school']),
+    steps() {
+      const options = this.generation.options
+      const current = this.generation.current
+      const visibleSteps = Object.keys(options)
+        .filter(key => options[key].stepsVisible)
+      return visibleSteps.map(key => {
+        const opt = options[key]
+        return {
+          key: key,
+          value: opt.value,
+          title: opt.title,
+          status: opt.value < current
+            ? 'success'
+            : opt.value > current
+              ? 'wait'
+              : 'process'
+        }
+      })
+    },
+    title() {
+      const year = (this.generation.status.year + '').tailingFix('学年')
+      const schoolName = this.school.schoolName
+      const roundName = this.generation.status.roundName
+
+      const options = this.generation.options
+      let statusTip = this.generation.currentOpt?.title + '进行中...'
+      let defaultOpt = this.generation.currentOpt
+      if (this.generation.current < options.primary.value) {
+        statusTip = '选科未开启'
+        defaultOpt = options.primary
+      } else if (this.generation.current > options.rankBalance.value) {
+        statusTip = '选科结束'
+        defaultOpt = options.rankBalance
+      }
+
+      if (!this.modelValue) {
+        this.activeStep = this.steps.findIndex(item => item.value == defaultOpt.value) + 1
+        this.$emit('change', defaultOpt.key)
+      }
+      return `${year}${schoolName}${roundName}(${statusTip})`
+    }
+  },
+  methods: {
+    handleStepChange(step) {
+      const target = step.value
+      this.generation.hiddenGenerations.remove(target) // force display
+      if (target != this.activeStep) {
+        this.activeStep = target
+        this.$emit('change', step.key)
+      }
+    },
+    toggleGenerationTableData(e, step) {
+      if (e) {
+        if (!this.generation.hiddenGenerations.includes(step.value)) {
+          this.generation.hiddenGenerations.push(step.value)
+        }
+      } else {
+        this.generation.hiddenGenerations.remove(step.value)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.current-pointer {
+  left: -20px;
+  top: 10px;
+}
+
+.current-view {
+  left: -20px;
+}
+
+.current-disabled {
+  color: #999999;
+}
+
+.current-animation {
+  animation: finger infinite 2s;
+}
+
+@keyframes finger {
+  0% {
+    transform: translate(-5px) rotate(90deg)
+  }
+  25% {
+    transform: translate(5px) rotate(90deg)
+  }
+  50% {
+    transform: translate(-5px) rotate(90deg)
+  }
+  75% {
+    transform: translate(5px) rotate(90deg)
+  }
+  100% {
+    transform: translate(-5px) rotate(90deg)
+  }
+}
+</style>

+ 149 - 0
src/views/elective/generation/components/elective-generation-table.vue

@@ -0,0 +1,149 @@
+<template>
+  <mx-table ref="table" :prop-defines="resolvedTable.columns" :rows="resolvedTable.rows" border>
+    <template #elective-cell="{value, label}">
+      <el-popover trigger="hover" placement="right" :disabled="value&&value.disabled"
+                  popper-class="zero-padding-popover">
+        <div class="fx-column">
+          <el-button plain type="text" @click="goDetails(value, label)">查看名单</el-button>
+        </div>
+        <div slot="reference" :style="getCellStyles(value)">
+          <span v-if="value.star">*</span>
+          <span>{{ value && value.value }}</span>
+        </div>
+      </el-popover>
+    </template>
+  </mx-table>
+</template>
+
+<script>
+import config from '@/common/mx-config'
+import MxTransferMixin from '@/components/mx-transfer-mixin'
+
+export default {
+  mixins: [MxTransferMixin],
+  name: 'elective-generation-table',
+  props: ['chartBinding'],
+  computed: {
+    resolvedTable() {
+      // setting data
+      const columns = { groupName: { label: '组合', fixed: true }, expectedCount: { label: '设置人数', fixed: true } }
+      const rows = this.deepClone(this.chartBinding.generation.roundGroups) // TODO: need clone?
+      // generation data
+      const mergedColumns = []
+      this.chartBinding.tableData.forEach(item => {
+        if (this.chartBinding.generation.hiddenGenerations.includes(item.generation)) return
+        const ext = { roundId: item.roundId, generation: item.generation }
+        const isPreference = item.categories.every(item => Array.isArray(item))
+
+        const subColumns = {}
+        const mergedHeaderKey = 'generation_' + item.generation
+        const options = Object.values(this.chartBinding.generation.options)
+        const mergedHeaderOpt = options.find(opt => opt.value == item.generation)
+        const mergedHeaderDefine = { label: mergedHeaderOpt.title, children: subColumns }
+        if (isPreference) {
+          item.categories.forEach((subItem, idx) => {
+            const innerHeaderKey = mergedHeaderKey + '_' + idx
+            const innerHeaderDefine = item.categories.length > 1
+              ? { label: `第${idx + 1}志愿`, children: {} }
+              : mergedHeaderDefine
+            const prefix = item.categories.length > 1 ? `第${idx + 1}志愿` : ''
+            subItem.forEach(data => this.resolveTableGeneration(ext, data, innerHeaderDefine.children, rows, mergedColumns, innerHeaderKey))
+            columns[innerHeaderKey] = innerHeaderDefine
+          })
+        } else {
+          item.categories.forEach(data => this.resolveTableGeneration(ext, data, subColumns, rows, mergedColumns))
+          columns[mergedHeaderKey] = mergedHeaderDefine
+        }
+      })
+      // accumulate data
+      if (this.chartBinding.generation.active > this.chartBinding.generation.options.primaryDM.value
+        && this.chartBinding.chartData?.accumulates?.length) {
+        const prefix = 'accumulate_'
+        const ext = {
+          roundId: this.chartBinding.generation.status.roundId,
+          generation: -1 // for detail page special display
+        }
+        this.chartBinding.chartData.accumulates.forEach(acc => this.resolveTableGeneration(ext, acc, columns, rows, mergedColumns, prefix, true))
+      }
+      // completed
+      this.$refs.table?.scrollToRight()
+      return {
+        mergedColumns,
+        columns,
+        rows
+      }
+    }
+  },
+  mounted() {
+    window.tableVue = this
+  },
+  provide() {
+    return {
+      mergeTable: this.mergeTable
+    }
+  },
+  methods: {
+    resolveTableGeneration(ext, data, columnsRef, rowsRef, mergedColumnsRef, prefix = '', fixed = false) {
+      // resolve core
+      const prop = prefix + ext.generation + '_' + data.category
+      const name = data.displayName
+      const shouldMerge = data.values.length != rowsRef.length
+        || data.values.some(v => v.groupId == 0)
+
+      columnsRef[prop] = { label: name, slot: 'elective-cell', fixed: fixed }
+      if (!shouldMerge) {
+        data.values.forEach(val => {
+          const row = rowsRef.find(row => row.groupId == val.groupId)
+          if (row) {
+            row[prop] = { ...ext, ...val }
+          }
+        })
+      } else {
+        // mark for table rows merge
+        mergedColumnsRef.push(prop)
+
+        // keep value in display row
+        const val = data.values.first()
+        const row = rowsRef.first()
+        row[prop] = { ...ext, ...val }
+      }
+    },
+    mergeTable({ row, column, rowIndex }) {
+      if (this.resolvedTable.mergedColumns.includes(column.property)) {
+        return rowIndex === 0 ? [this.resolvedTable.rows.length, 1] : [0, 0]
+      }
+    },
+    getCellStyles(option) {
+      const styles = {}
+      if (option.color) {
+        const map = {
+          R: config.color.error,
+          r: config.color.error,
+          G: config.color.yellow_up,
+          g: config.color.yellow_up,
+          B: config.color.blue_up,
+          b: config.color.blue_up
+        }
+        styles.color = map[option.color] || option.color
+      }
+      return styles
+    },
+    goDetails(option, label) {
+      const path = '/elective/generation/detail'
+      const nextData = {
+        year: this.chartBinding.generation.status.year,
+        roundId: this.chartBinding.generation.status.roundId,
+        roundName: this.chartBinding.generation.status.roundName,
+        queryLabel: label,
+        queryGeneration: option.generation,
+        queryCode: option.queryCode
+      }
+      this.transferTo(path, nextData)
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 36 - 0
src/views/elective/generation/detail.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="app-container">
+    <evaluation-title :title="title" :sub-title="subTitle" nav-back-button></evaluation-title>
+    查询列表-未实现
+  </div>
+</template>
+
+<script>
+import config from '@/common/mx-config'
+import transferMixin from '@/components/mx-transfer-mixin'
+import { mapGetters } from 'vuex'
+
+export default {
+  mixins: [transferMixin],
+  name: 'generation-detail',
+  computed: {
+    ...mapGetters(['school']),
+    title() {
+      const y = (this.prevData.year + '').tailingFix('学年')
+      const s = this.school.schoolName
+      const n = this.prevData.roundName
+      return y + s + n
+    }, subTitle() {
+      const g = Object.values(config.electiveGenerationOptions).find(opt => opt.value == this.prevData.queryGeneration)
+      const hideGeneration = g == config.electiveGenerationOptions.init || g == config.electiveGenerationOptions.terminate
+      let gName = hideGeneration ? '' : g?.title || ''
+      gName = gName && gName + '/'
+      return gName + this.prevData.queryLabel
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 97 - 8
src/views/elective/generation/index.vue

@@ -1,20 +1,109 @@
 <template>
-
+  <div class="app-container" v-loading="loading">
+    <el-card>
+      <mx-condition :query-params="queryParams" :require-fields="requireFields"
+                    @query="handleQuery" @invalid="handleInvalid"></mx-condition>
+    </el-card>
+    <el-card v-if="electiveStatus" class="mt20">
+      <template #header>
+        <elective-generation-steps v-model="activeStep" :generation="generation"></elective-generation-steps>
+      </template>
+      <elective-generation-master :generation="generation">
+        <template #header-prefix>
+          <el-button circle icon="el-icon-refresh" @click="handleQuery"></el-button>
+        </template>
+      </elective-generation-master>
+    </el-card>
+    <evaluation-empty v-else class="mt20"></evaluation-empty>
+  </div>
 </template>
 
 <script>
-import { getElectiveSummary } from '@/api/webApi/elective/generation'
+import config from '@/common/mx-config'
+import { getElectiveStatus, getElectiveSummary } from '@/api/webApi/elective/generation'
+import MxCondition from '@/components/MxCondition/mx-condition'
+import ElectiveGenerationSteps from '@/views/elective/generation/components/elective-generation-steps'
+import ElectiveGenerationMaster from '@/views/elective/generation/components/elective-generation-master'
+import MxGroupTranslateMixin from '@/components/Cache/modules/mx-select-translate-mixin'
 
 export default {
+  mixins: [MxGroupTranslateMixin],
   name: 'generation-index',
-  mounted() {
-    this.loadElectiveSummary()
+  components: { ElectiveGenerationMaster, ElectiveGenerationSteps, MxCondition },
+  data() {
+    return {
+      // query
+      loading: false,
+      queryParams: {
+        year: '',
+        roundId: ''
+      },
+      requireFields: ['year', 'roundId'],
+      // api data
+      electiveStatus: null,
+      electiveSummary: [],
+      // local data
+      activeStep: '',
+      options: config.electiveGenerationOptions,
+      tableHiddenGenerations: []
+    }
+  },
+  computed: {
+    current() {
+      /// 当前进程代,可能没有值
+      return this.electiveStatus?.currentGeneration
+    },
+    currentOpt() {
+      /// 当前进程代对应的配置项,可能没有值
+      return Object.values(this.options).find(opt => opt.value == this.current)
+    },
+    activeOpt() {
+      /// 当前选中的进程代,可能没有值
+      return this.options[this.activeStep]
+    },
+    active() {
+      return this.activeOpt?.value
+    },
+    roundGroups() {
+      return this.electiveStatus?.roundGroups.map(rg => ({
+        groupId: rg.groupId,
+        groupName: this.translateGroup(rg.groupId),
+        expectedCount: rg.personCount
+      }))
+    },
+    generation() {
+      return {
+        // generation key value
+        options: this.options,
+        current: this.current,
+        currentOpt: this.currentOpt,
+        active: this.active,
+        activeOpt: this.activeOpt,
+        /// root data inject
+        summary: this.electiveSummary,
+        status: this.electiveStatus,
+        roundGroups: this.roundGroups,
+        /// local display setting
+        hiddenGenerations: this.tableHiddenGenerations
+      }
+    }
   },
   methods: {
-    loadElectiveSummary() {
-      getElectiveSummary().then(res => {
-        console.log('getElectiveSummary', res)
-      })
+    handleInvalid() {
+      this.activeStep = ''
+      this.electiveStatus = null
+      this.electiveSummary = []
+    },
+    async handleQuery() {
+      this.loading = true
+      try {
+        const resStatus = await getElectiveStatus(this.queryParams)
+        this.electiveStatus = resStatus.data
+        const resSummary = await getElectiveSummary(this.queryParams)
+        this.electiveSummary = resSummary.data
+      } finally {
+        this.loading = false
+      }
     }
   }
 }