Browse Source

Merge branch 'develop5.0-tmd' of http://52.130.252.100:10000/TEAMMODEL/TEAMModelOS into develop5.0-tmd

zhouj1203@hotmail.com 3 years ago
parent
commit
62a4b10ecb

BIN
TEAMModeBI/ClientApp/src/assets/img/list-num.png


+ 2 - 1
TEAMModeBI/ClientApp/src/main.js

@@ -7,11 +7,12 @@ import 'element-plus/dist/index.css'
 import axios from '@/api/index.js'
 import store from '@/store'
 import powerfulTable from "el-plus-powerful-table-ts";
-
+import { ElIcon } from 'element-plus'
 const app = createApp(App)
 app.config.globalProperties.$api = axios
 app.use(router)
 app.use(ElementPlus)
 app.use(store)
 app.use(powerfulTable)
+app.use(ElIcon)
 app.mount('#app')

+ 0 - 1
TEAMModeBI/ClientApp/src/router/index.js

@@ -1,5 +1,4 @@
 import { createWebHistory, createRouter } from "vue-router";
-// import Login from "@/view/login.vue";
 import store from '@/store/index.js'
 const routes = [{
         path: "/login",

+ 253 - 75
TEAMModeBI/ClientApp/src/view/created/created.vue

@@ -10,7 +10,7 @@
     </div> -->
     <!--列表选单end-->
     <!--创区选单-->
-    <div>
+    <!-- <div>
         <div class="areabox">
             <p class="aera-title">创建学区</p>
             <el-form :label-position="labelPosition" label-width="100px" :model="formArea" :inline="true">
@@ -44,115 +44,225 @@
             <el-button type="success" @click="createdArea">创建</el-button>
             <el-button type="info">取消</el-button>
         </div>
-    </div>
+    </div> -->
     <!--创去选单end-->
+    <!--创校选单-->
+    <div class="schoolbox">
+        <p class="aera-title">创建学校</p>
+        <ul>
+            <li class="school-libox" v-for="(item,index) in schoolForm">
+                <div class="school-numlist">{{item.num}}</div>
+                <el-form ref="item" :model="item" label-width="120px">
+                    <el-form-item label="学校名称:" class="school-name">
+                        <el-input v-model="item.name"></el-input>
+                    </el-form-item>
+                    <el-form-item label="学段选择:">
+                        <el-radio v-model="item.radio1" label="1" border>小学</el-radio>
+                        <el-radio v-model="item.radio1" label="2" border>初中</el-radio>
+                        <el-radio v-model="item.radio1" label="3" border>高中</el-radio>
+                        <el-radio v-model="item.radio1" label="4" border>职高</el-radio>
+                        <el-radio v-model="item.radio1" label="5" border>大学</el-radio>
+                    </el-form-item>
+                    <el-form-item label="预设管理员:" class="scholl-admin">
+                        <el-autocomplete v-model="item.presupposeAdmin" :fetch-suggestions="querySearch" :trigger-on-focus="false" class="inline-input" placeholder="输入手机号码,查询账号" @select="handleSelect" prefix-icon="el-icon-search" :debounce="500" />
+                    </el-form-item>
+                    <el-form-item label="学校所属位置:" class="school-location">
+                        <elui-china-area-dht :leave="3" @change="onChange" placeholder="请选择地区"></elui-china-area-dht>
+                    </el-form-item>
+                    <el-form-item label="学校空间大小:">
+                        <el-select v-model="item.pitchSpace" placeholder="选择空间大小">
+                            <el-option v-for="items in item.schoolSpace" :key="items.value" :label="items.label" :value="items.value">
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-form>
+            </li>
+            <div class="add-schoolbtn" @click="addschool">
+                <el-button>添加学校</el-button>
+            </div>
+            <div>
+                <el-button type="primary">提交创建</el-button>
+                <el-button>取消</el-button>
+            </div>
+        </ul>
+    </div>
+    <!--创校选单end-->
 </template>
 <script>
-import { reactive, getCurrentInstance, toRefs } from "vue";
-import { EluiChinaAreaDht } from "elui-china-area-dht";
-import { ElMessage } from "element-plus";
-const chinaData = new EluiChinaAreaDht.ChinaArea().chinaAreaflat;
+import { reactive, getCurrentInstance, toRefs, ref, onMounted } from 'vue'
+import { EluiChinaAreaDht } from 'elui-china-area-dht'
+import { ElMessage } from 'element-plus'
+import { Search } from '@element-plus/icons'
+const chinaData = new EluiChinaAreaDht.ChinaArea().chinaAreaflat
 export default {
-    components: { EluiChinaAreaDht },
+    components: { EluiChinaAreaDht, Search },
     setup() {
-        const labelPosition = "left";
-        let { proxy } = getCurrentInstance();
+        const labelPosition = 'left'
+        let { proxy } = getCurrentInstance()
         let formLabelAlign = reactive({
-            name: "",
-            region: "",
-            type: "",
-        });
+            name: '',
+            region: '',
+            type: '',
+        })
         let formArea = reactive({
             areaselect: {
-                province: "",
-                provincevalue: "",
-                city: "",
-                cityvalue: "",
-                area: "",
-                areavalue: "",
+                province: '',
+                provincevalue: '',
+                city: '',
+                cityvalue: '',
+                area: '',
+                areavalue: '',
                 state: false,
             },
             areaname: {
-                value: "",
+                value: '',
                 state: false,
             },
-            capacityvalue: "",
+            capacityvalue: '',
             options: [],
-        });
+        })
+        let form = reactive({
+            name: '',
+            region: '',
+            date1: '',
+            date2: '',
+            delivery: false,
+            type: [],
+            resource: '',
+            desc: '',
+        })
+        let radio1 = ref('1')
+        const input2 = ref('')
+        let schoolForm = reactive([])
         //地区选择器
         function onChange(e) {
-            const one = chinaData[e[0]];
-            const two = chinaData[e[1]];
-            const three = chinaData[e[2]];
-            console.log(e, one, two, three);
-            formArea.areaname.value =
-                one.label + two.label + three.label + "学区";
-            formArea.areaselect.province = one.label;
-            formArea.areaselect.provincevalue = one.value;
-            formArea.areaselect.city = two.label;
-            formArea.areaselect.cityvalue = two.value;
-            formArea.areaselect.area = three.label;
-            formArea.areaselect.areavalue = three.value;
-            formArea.areaname.state = false;
+            const one = chinaData[e[0]]
+            const two = chinaData[e[1]]
+            const three = chinaData[e[2]]
+            console.log(e, one, two, three)
+            formArea.areaname.value = one.label + two.label + three.label + '学区'
+            formArea.areaselect.province = one.label
+            formArea.areaselect.provincevalue = one.value
+            formArea.areaselect.city = two.label
+            formArea.areaselect.cityvalue = two.value
+            formArea.areaselect.area = three.label
+            formArea.areaselect.areavalue = three.value
+            formArea.areaname.state = false
         }
         function changnone(val) {
-            val
-                ? (formArea.areaname.value = "全国区域学区")
-                : (formArea.areaname.value = "");
+            val ? (formArea.areaname.value = '全国区域学区') : (formArea.areaname.value = '')
         }
         //获取微能力点
         function getCapacitys() {
             proxy.$api.getCapacity({}).then((res) => {
-                console.log(res, "微能力点");
-                res.state === 200
-                    ? (formArea.options = res.areas)
-                    : ElMessage.error("获取微能力点方案失败,API异常");
-            });
+                console.log(res, '微能力点')
+                res.state === 200 ? (formArea.options = res.areas) : ElMessage.error('获取微能力点方案失败,API异常')
+            })
         }
         //创建区域
         function createdArea() {
             //获取最后一位的名字,生成名字
-            let newstandard = formArea.options[
-                formArea.options.length - 1
-            ].standard.substring(
-                formArea.options[formArea.options.length - 1].standard.indexOf(
-                    "standard"
-                ) + 8
-            );
-            let newname = "standard" + (Number(newstandard) + 1);
-            console.log(newstandard, newname, "截取的内容");
+            let newstandard = formArea.options[formArea.options.length - 1].standard.substring(formArea.options[formArea.options.length - 1].standard.indexOf('standard') + 8)
+            let newname = 'standard' + (Number(newstandard) + 1)
+            console.log(newstandard, newname, '截取的内容')
             //查找选中的名字
             let selectName = formArea.options.filter((item) => {
-                return item.id === formArea.capacityvalue;
-            });
-            console.log(selectName, "选中的名字");
-            let users = JSON.parse(localStorage.getItem("userData"));
-            console.log(users);
+                return item.id === formArea.capacityvalue
+            })
+            console.log(selectName, '选中的名字')
+            let users = JSON.parse(localStorage.getItem('userData'))
+            console.log(users)
             let createdParame = {
                 name: formArea.areaname.value,
-                provCode: formArea.areaselect.provincevalue,
-                provName: formArea.areaselect.province,
-                cityCode: formArea.areaselect.cityvalue,
-                cityName: formArea.areaselect.city,
+                provCode: !formArea.areaselect.state ? formArea.areaselect.provincevalue : '',
+                provName: !formArea.areaselect.state ? formArea.areaselect.province : '',
+                cityCode: !formArea.areaselect.state ? formArea.areaselect.cityvalue : '',
+                cityName: !formArea.areaselect.state ? formArea.areaselect.city : '',
                 standard: newname,
                 standardName: formArea.areaname.value,
-                institution: "醍摩豆(成都)信息技术有限公司",
+                institution: '醍摩豆(成都)信息技术有限公司',
                 oldId: formArea.capacityvalue,
                 oldStandard: selectName[0].standard,
                 tmdId: users.teacher.id,
                 tmdName: users.teacher.name,
-            };
-            proxy.$api
-                .createAreas(createdParame)
-                .then((res) =>
-                    console.log(
-                        res === 200
-                            ? ElMessage.success("学区创建成功")
-                            : ElMessage.error("学区创建失败")
-                    )
-                );
+            }
+            proxy.$api.createAreas(createdParame).then((res) => console.log(res.state === 200 ? ElMessage.success('学区创建成功') : ElMessage.error('学区创建失败')))
+        }
+        let state2 = ref('')
+        const restaurants = ref([])
+        const loadAll = () => {
+            return [
+                { value: 'vue', link: 'https://github.com/vuejs/vue' },
+                { value: 'element', link: 'https://github.com/ElemeFE/element' },
+                { value: 'cooking', link: 'https://github.com/ElemeFE/cooking' },
+                { value: 'mint-ui', link: 'https://github.com/ElemeFE/mint-ui' },
+                { value: 'vuex', link: 'https://github.com/vuejs/vuex' },
+                { value: 'vue-router', link: 'https://github.com/vuejs/vue-router' },
+                { value: 'babel', link: 'https://github.com/babel/babel' },
+                { value: '15218635716', link: 'https://github.com/babel/babel' },
+            ]
+        }
+        function querySearch(queryString, cb) {
+            console.log(queryString, '3333')
+            const results = queryString ? restaurants.value.filter(createFilter(queryString)) : restaurants.value
+            cb(results)
+        }
+        function createFilter(queryString) {
+            return (restaurant) => {
+                return restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
+            }
+        }
+        function handleSelect(item) {
+            console.log(item)
+        }
+        onMounted(() => {
+            restaurants.value = loadAll()
+            console.log(restaurants.value)
+            let schoolData = {
+                num: 1,
+                name: '',
+                radio1: '',
+                presupposeAdmin: '',
+                schoolLocation: {
+                    province: '',
+                    provincevalue: '',
+                    city: '',
+                    cityvalue: '',
+                    area: '',
+                    areavalue: '',
+                    state: false,
+                },
+                schoolSpace: [],
+                pitchSpace: '',
+            }
+            schoolForm.push(schoolData)
+            console.log(schoolForm, '学校内容')
+        })
+        function addschool() {
+            console.log(schoolForm.length)
+            let idnum = Number(schoolForm.length) + 1
+            let data = {
+                num: idnum,
+                name: '',
+                radio1: '',
+                presupposeAdmin: '',
+                schoolLocation: {
+                    province: '',
+                    provincevalue: '',
+                    city: '',
+                    cityvalue: '',
+                    area: '',
+                    areavalue: '',
+                    state: false,
+                },
+                schoolSpace: [],
+                pitchSpace: '',
+            }
+            console.log(schoolForm, '添加之前的')
+            schoolForm.push(data)
+            console.log(schoolForm, '添加后的学校')
         }
-        getCapacitys();
+        getCapacitys()
         return {
             labelPosition,
             formLabelAlign,
@@ -161,9 +271,18 @@ export default {
             changnone,
             getCapacitys,
             createdArea,
-        };
+            form,
+            radio1,
+            querySearch,
+            loadAll,
+            restaurants,
+            state2,
+            schoolForm,
+            handleSelect,
+            addschool,
+        }
     },
-};
+}
 </script>
 <style scoped>
 .selectbox {
@@ -213,6 +332,55 @@ export default {
 .capacity-box .el-select {
     width: 100%;
 }
+.school-box {
+    width: 70%;
+}
+/*创建学校样式*/
+.schoolbox {
+    width: 100%;
+    padding: 2%;
+    border: 1px solid #ccc;
+    line-height: 140px;
+    min-height: 895px;
+}
+.school-name {
+    width: 70%;
+}
+.school-libox {
+    position: relative;
+    border: 1px dashed #ccc;
+    padding: 1%;
+    margin-bottom: 2%;
+}
+.scholl-admin {
+    width: 65%;
+}
+.school-numlist {
+    position: absolute;
+    top: -3px;
+    left: -6px;
+    line-height: 1px;
+    width: 36px;
+    height: 36px;
+    line-height: 30px;
+    font-size: 16px;
+    font-weight: 900;
+    background: url('../../assets/img/list-num.png');
+}
+.add-schoolbtn {
+    width: 5.5%;
+    line-height: 0px;
+}
+.add-schoolbtn button {
+    font-weight: 800;
+}
+.demo-input-label {
+    display: inline-block;
+    width: 130px;
+}
+.demo-input-suffix {
+    margin-bottom: 16px;
+}
 </style>
 <style>
 .areabox .el-form {
@@ -228,4 +396,14 @@ export default {
 .areabox .el-form-item {
     min-width: 380px;
 }
+.schoolbox .el-form-item__content {
+    text-align: left;
+    margin-left: 1%;
+}
+.scholl-admin .el-autocomplete {
+    width: 70%;
+}
+.school-location .el-cascader {
+    width: 42%;
+}
 </style>

+ 7 - 13
TEAMModeBI/ClientApp/src/view/home.vue

@@ -15,26 +15,17 @@
     </div>
 </template>
 <script>
-import Header from "./common/header.vue";
-import Aside from "./common/aside.vue";
+import Header from './common/header.vue'
+import Aside from './common/aside.vue'
 export default {
     components: {
         Header,
         Aside,
     },
     setup() {
-        const handleOpen = (key, keyPath) => {
-            console.log(key, keyPath);
-        };
-        const handleClose = (key, keyPath) => {
-            console.log(key, keyPath);
-        };
-        return {
-            handleOpen,
-            handleClose,
-        };
+        return {}
     },
-};
+}
 </script>
 <style  scoped>
 .el-header,
@@ -77,6 +68,9 @@ body > .el-container {
 }
 </style>
 <style>
+li {
+    list-style-type: none;
+}
 .viewbox .el-radio-group {
     line-height: 5px !important;
     margin-bottom: 0px !important;

+ 5 - 4
TEAMModelFunction/MonitorCosmosDB.cs

@@ -22,9 +22,10 @@ namespace TEAMModelFunction
         private readonly AzureStorageFactory _azureStorage;
         private readonly DingDing _dingDing;
         private readonly AzureRedisFactory _azureRedis;
-       // private   IConfiguration _configuration { get; set; }
+         
+        private   IConfiguration _configuration { get; set; }
         public MonitorCosmosDB( AzureCosmosFactory azureCosmos, AzureServiceBusFactory azureServiceBus, AzureStorageFactory azureStorage, DingDing dingDing, AzureRedisFactory azureRedis
-           // , IConfiguration configuration
+            , IConfiguration configuration
             )
         {
             _azureCosmos = azureCosmos;
@@ -32,7 +33,7 @@ namespace TEAMModelFunction
             _azureStorage = azureStorage;
             _dingDing = dingDing;
             _azureRedis = azureRedis;
-          //  _configuration = configuration;
+            _configuration = configuration;
         }
 
         [FunctionName("Common")]
@@ -109,7 +110,7 @@ namespace TEAMModelFunction
                                     TriggerExamLite.Trigger(_serviceBus, _azureStorage, _dingDing, client, input, data, _azureRedis);
                                     break;
                                 case "Study":
-                                    TriggerStudy.Trigger(_serviceBus, _azureStorage, _dingDing, client, input, data, _azureRedis);
+                                    TriggerStudy.Trigger(_serviceBus, _azureStorage, _dingDing, client, input, data, _azureRedis, _configuration);
                                     break;
                                 case "Homework":
                                     TriggerHomework.Trigger(_serviceBus, _azureStorage, _dingDing, client, input, data, _azureRedis);

+ 1 - 0
TEAMModelFunction/MonitorServicesBus.cs

@@ -438,6 +438,7 @@ namespace TEAMModelFunction
                 {
                     teacherTrain = await StatisticsService.StatisticsTeacher(new TeacherTrain
                     {
+                        pk = "TeacherTrain",
                         id = change.tmdid,
                         code = $"TeacherTrain-{change.school}",
                         tmdid = change.tmdid,

+ 18 - 2
TEAMModelFunction/TriggerStudy.cs

@@ -2,9 +2,11 @@ using Azure.Cosmos;
 using Azure.Messaging.ServiceBus;
 using HTEXLib.COMM.Helpers;
 using Microsoft.Azure.Documents;
+using Microsoft.Extensions.Configuration;
 using System;
 using System.Collections.Generic;
 using System.Text;
+using System.Threading.Tasks;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
@@ -16,7 +18,7 @@ namespace TEAMModelFunction
     public static class TriggerStudy
     {
         public static async void Trigger(AzureServiceBusFactory _serviceBus, AzureStorageFactory _azureStorage, DingDing _dingDing,
-                    CosmosClient client, Document input, TriggerData tdata, AzureRedisFactory _azureRedis)
+                    CosmosClient client, Document input, TriggerData tdata, AzureRedisFactory _azureRedis, IConfiguration _configuration)
         {
             try
             {
@@ -84,9 +86,15 @@ namespace TEAMModelFunction
                             // (List<TmdInfo> tmdInfos, List<ClassListInfo> classInfos) = await TriggerStuActivity.GetTchList(client, _dingDing, ids, $"{school}");
                             //(List<TmdInfo> tchList, _) = await TriggerStuActivity.GetTchList(client, _dingDing, study.tchLists, study.school);
                             List<StuActivity> tchActivities = new List<StuActivity>();
-
+                            List<Task> tasks = new List<Task>();
                             if (tchList.IsNotEmpty())
                             {
+                                School school = null;
+                                if (string.IsNullOrEmpty(study.school))
+                                {
+                                    school=await client.GetContainer(Constant.TEAMModelOS, "School").ReadItemAsync<School>(study.school, new Azure.Cosmos.PartitionKey("Base"));
+                                }
+                                
                                 tchList.ForEach(x =>
                                 {
                                     tchActivities.Add(new StuActivity
@@ -109,6 +117,9 @@ namespace TEAMModelFunction
                                         taskStatus = -1,
                                         classIds = study.tchLists
                                     });
+                                    if (school != null) {
+                                        tasks.Add(StatisticsService.SendServiceBus($"{school.standard}", $"{x.id}", $"{study.school}", StatisticsService.OfflineRecord, 1, _configuration, _serviceBus));
+                                    }
                                 });
                             }
                             await ActivityService.SaveStuActivity(client, _dingDing, null, null, tchActivities);
@@ -133,6 +144,11 @@ namespace TEAMModelFunction
                                 };
                                 await _azureStorage.Save<ChangeRecord>(changeRecord);
                             }
+                            if (tasks.IsNotEmpty())
+                            {
+                                await Task.WhenAll(tasks);
+                            }
+                           
                             break;
                         case "finish":
                             break;

+ 6 - 2
TEAMModelOS.SDK/Models/Service/StatisticsService.cs

@@ -99,7 +99,8 @@ namespace TEAMModelOS.SDK
                 foreach (string x in unStatistics) {
                     var member = members.Find(y => y.id.Equals(x));
                     teacherTrains.Add(new TeacherTrain
-                    {   
+                    {
+                        pk = "TeacherTrain",
                         id = x,
                         code = $"TeacherTrain-{school}",
                         tmdid = x,
@@ -551,6 +552,7 @@ namespace TEAMModelOS.SDK
                         teacherAility.videoTime = view;
                         teacherAility.limitTime = ability.hour;
                         teacherAility.onlineTime = view / setting.lessonMinutes;
+                        
                     }
                     if (item.otherScore.IsNotEmpty())
                     {
@@ -602,7 +604,7 @@ namespace TEAMModelOS.SDK
                         }
                     }
                     if (currencyInt == 1)
-                    {
+                    {    
                         currency.subCount += 1;
                         currency.uploadTotal += ability.stds.FindAll(x => x.task.IsNotEmpty()).Select(y => y.task).Count();
                         currency.teacherAilities.Add(teacherAility);
@@ -614,6 +616,8 @@ namespace TEAMModelOS.SDK
             });
             train.currency = currency;
             train.currencyAll = currencyAll;
+            train.currency.videoTime = train.currency.teacherAilities.Select(x => x.videoTime).Sum();
+            train.currencyAll.videoTime= train.currencyAll.teacherAilities.Select(x => x.videoTime).Sum();
             //如果总分钟数超过20学时,则直接复制20学时。
             var videoTime = (int)train.currency.videoTime / setting.lessonMinutes;
             train.onlineTime = videoTime > setting.onlineTime ? setting.onlineTime:videoTime;

+ 6 - 0
TEAMModelOS/ClientApp/src/locale/lang/en-US/jyzx.js

@@ -232,5 +232,11 @@ export default{
         remarks1: "總學時:線上研修 + 校本研修 + 認證材料 + 課堂實錄",
         remarks2: "注:各指標達到要求後,多餘學時不再計入總學時",
         remarks3: "注:45分鐘 = 1學時",
+        summary1: "填寫總結",
+        summary2: "修改總結",
+        message1: "您的研修還未完成,不能填寫總結",
+        message2: "總結已提交",
+        message3: "總結提交失敗",
+        message4: "總結字數不足50字!",
     }
 }

+ 6 - 0
TEAMModelOS/ClientApp/src/locale/lang/zh-CN/jyzx.js

@@ -232,5 +232,11 @@ export default{
         remarks1: "总学时:线上研修 + 校本研修 + 认证材料 + 课堂实录",
         remarks2: "注:各指标达到要求后,多余学时不再计入总学时",
         remarks3: "注:45分钟 = 1学时",
+        summary1: "填写总结",
+        summary2: "修改总结",
+        message1: "您的研修还未完成,不能填写总结",
+        message2: "总结已提交",
+        message3: "总结提交失败",
+        message4: "总结字数不足50字!",
     }
 }

+ 6 - 0
TEAMModelOS/ClientApp/src/locale/lang/zh-TW/jyzx.js

@@ -232,5 +232,11 @@ export default{
         remarks1: "總學時:線上研修 + 校本研修 + 認證材料 + 課堂實錄",
         remarks2: "注:各指標達到要求後,多餘學時不再計入總學時",
         remarks3: "注:45分鐘 = 1學時",
+        summary1: "填寫總結",
+        summary2: "修改總結",
+        message1: "您的研修還未完成,不能填寫總結",
+        message2: "總結已提交",
+        message3: "總結提交失敗",
+        message4: "總結字數不足50字!",
     }
 }

+ 9 - 9
TEAMModelOS/ClientApp/src/view/jyzx/HomePage.vue

@@ -37,8 +37,8 @@
                         </i-circle>
                         <p class="summary" @click="writeSun">
                             <Icon type="md-clipboard" />
-                            <span v-show="!summaryNum.length">填写总结</span>
-                            <span v-show="summaryNum.length">修改总结</span>
+                            <span v-show="wirteOk != 2">{{ $t("jyzx.homePage.summary1") }}</span>
+                            <span v-show="wirteOk === 2">{{ $t("jyzx.homePage.summary2") }}</span>
                         </p>
                     </div>
                     <div class="box-border">
@@ -265,7 +265,7 @@ export default {
 
             setting: undefined,
             videoTime: 0,
-            wirteOk: false, //不能填写总结
+            wirteOk: 0, //0:不能填写总结,1:可以写,还没写,2:已经填过了
             summaryNum: "", //总结
             isModal: false,
         }
@@ -364,7 +364,7 @@ export default {
                     this.areaExamNum = res.teacherTrain.examAreaDone
                     this.areaExamTotal = res.teacherTrain.examAreaJoin
                     if(this.totalTime === res.setting.allTime) {
-                        this.wirteOk = true
+                        this.wirteOk = res.teacherTrain.summary ? 2 : 1
                     }
                     this.summaryNum = res.teacherTrain.summary ? res.teacherTrain.summary : ""
                 }
@@ -377,7 +377,7 @@ export default {
             if(this.wirteOk) {
                 this.isModal = true
             } else {
-                this.$Message.warning("您的研修还未完成,不能填写总结")
+                this.$Message.warning(this.$t("jyzx.homePage.message1"))
             }
         },
         // 发送总结
@@ -385,14 +385,14 @@ export default {
             if(this.summaryNum.length > 50) {
                 this.$api.jyzx.sendSummary({summary: this.summaryNum}).then(res => {
                     if(res.status === 200) {
-                        this.$Message.success("总结已提交")
-                        this.summaryNum = ""
+                        this.$Message.success(this.$t("jyzx.homePage.message2"))
+                        // this.summaryNum = ""
                     } else {
-                        this.$Message.error("总结提交失败")
+                        this.$Message.error(this.$t("jyzx.homePage.message3"))
                     }
                 })
             } else {
-                this.$Message.warning("总结字数不足50字!")
+                this.$Message.warning(this.$t("jyzx.homePage.message4"))
             }
         },
     },

+ 11 - 0
TEAMModelOS/ClientApp/src/view/jyzx/offline.less

@@ -99,6 +99,17 @@
 				display: inline-block;
 				margin-bottom: 5px;
 			}
+
+			.uploadHm{
+				padding: 5px 10px;
+				padding-top: 12px;
+				border-radius: 5px;
+				cursor: pointer;
+				color: #303030;
+				&:hover {
+					background-color: #E1EFF6;
+				}
+			}
 		}
 	}
 	.offline-content {

+ 4 - 4
TEAMModelOS/ClientApp/src/view/jyzx/offline.vue

@@ -126,11 +126,11 @@
                                     </Upload>
                                     <div>
                                         <div style="margin-top: 15px" v-if="uploadList.length">
-                                            <p style="margin-bottom: 15px">
+                                            <p style="margin-bottom: 10px">
                                                 {{ $t('jyzx.offline.tips1') }}
                                             </p>
-                                            <div :key="index" v-for="(item, index) in uploadList">
-                                                <div>
+                                            <div :key="index" v-for="(item, index) in uploadList" class="uploadHm">
+                                                <!-- <div> -->
                                                     <div @click="changeUp(index)">
                                                         <span>
                                                             <span v-show="uploadIndex === index" class="school-gade" style="display: inline-block; background: #a2b02e;">{{ $t('jyzx.common.area') }}</span>
@@ -141,7 +141,7 @@
                                                         </span>
                                                     </div>
                                                     <Progress :percent="0" />
-                                                </div>
+                                                <!-- </div> -->
                                             </div>
                                             <Button type="success" @click="submitHome" style="margin-top: 20px">
                                                 <svg-icon icon-class="doc" />

+ 1 - 1
TEAMModelOS/ClientApp/src/view/resource/Policy.less

@@ -21,7 +21,7 @@
     margin-bottom: 5px;
 }
 .policy-title{
-    width: ~"calc(100% - 200px)";
+    width: ~"calc(100% - 210px)";
     display: inline-block;
     text-overflow: ellipsis;
     overflow: hidden;

+ 86 - 2
TEAMModelOS/ClientApp/src/view/train/TrainDetail.less

@@ -44,12 +44,13 @@
         background-repeat: no-repeat;
     }
     .infos{
+        flex: 1;
         margin-left: 20px;
         padding: 10px;
         .title{
             font-size: 16px;
             font-weight: 600;
-            margin-bottom: 20px;
+            margin-bottom: 15px;
         }
         .hour{
             margin-left: 10px;
@@ -61,10 +62,24 @@
             border-radius: 4px;
         }
         .info-item{
-            margin-top: 10px;
+            margin-top: 5px;
         }
         .info-label{
             color: #a5a5a5;
+            white-space: nowrap; 
+        }
+        .upload-text{
+            color: #2d8cf0;
+            cursor: pointer;
+            user-select: none;
+            &:hover{
+                text-decoration: underline;
+            }
+        }
+        .text-info{
+            color: #aaaaaa;
+            margin-left: 5px;
+            user-select: none;
         }
     }
 }
@@ -169,4 +184,73 @@
 .tea-info-label{
     color: #a5a5a5;
     margin-top: 10px;
+}
+.file-list-box{
+    min-height: 180px;
+    padding: 50px 10px;
+    .file-prog-item{
+        margin-bottom: 20px;
+    }
+    .file-name{
+
+    }
+    .file-prog{
+        float: right;
+        font-size: 12px;
+        margin-right: 20px;
+        // color: #19be6b;
+        margin-left: 5px;
+    }
+}
+.image-viewer {
+    background-color: rgba(0, 0, 0, 0.8);
+    z-index: 9999;
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    text-align: center;
+    padding:50px;
+    padding-top:8%;
+    .close-icon {
+        position: absolute;
+        right: -16px;
+        top: -16px;
+        font-size: 24px;
+        color: black;
+        cursor: pointer;
+        border-radius:50px;
+        background:white;
+        padding:2px;
+        z-index: 9999;
+    }
+}
+.uploading-title{
+    text-align: center;
+    font-size: 16px;
+    color: #17233d;
+    font-weight: 600;
+}
+.train-file-item{
+    margin-right: 30px;
+    cursor: pointer;
+    white-space: nowrap; 
+    user-select: none;
+    position: relative;
+    &:hover{
+        text-decoration: underline;
+        color: #2d8cf0;
+    }
+    &:hover .delete-train-file{
+        opacity: 1;
+    }
+}
+.delete-train-file{
+    color: #ed4014;
+    opacity: 0;
+    position: absolute;
+    top: 3px;
 }

+ 253 - 6
TEAMModelOS/ClientApp/src/view/train/TrainDetail.vue

@@ -50,7 +50,51 @@
                                 </span>
                                 <span>
                                     <span>{{dateFormat(trainInfo.startTime) + ' - ' + dateFormat(trainInfo.endTime)}}</span>
-                                    <!-- {{trainInfo.time.join(' 至 ')}} -->
+                                </span>
+                            </div>
+                            <div class="info-item">
+                                <span class="info-label">
+                                    活动视频:
+                                </span>
+                                <span v-if="trainVideo">
+                                    <span class="upload-text" @click="handlePreviewFile('video')">
+                                        播放
+                                    </span>
+                                    <!-- <span class="upload-text" @click="downloadTrainFile(trainVideo.url, trainVideo.name)" style="margin-left:10px">
+                                        下载
+                                    </span> -->
+                                    <span class="upload-text" @click="deleteFile('video')" style="margin-left:10px">
+                                        删除
+                                    </span>
+                                    <Upload action="" :show-upload-list="false" :before-upload="handleUpload" style="display:inline-block;margin-left:10px">
+                                        <span class="upload-text" @click="setUpload('video')">重新上传</span>
+                                    </Upload>
+                                </span>
+                                <span v-else>
+                                    <Upload action="" :show-upload-list="false" :before-upload="handleUpload" style="display:inline-block">
+                                        <span class="upload-text" @click="setUpload('video')">上传</span>
+                                    </Upload>
+                                    <span class="text-info">(视频只能上传一个)</span>
+                                </span>
+                            </div>
+                            <div class="info-item" style="display:flex">
+                                <span class="info-label">
+                                    活动资料:
+                                </span>
+                                <span v-if="trainFiles.length">
+                                    <span class="train-file-item" v-for="(item,index) in trainFiles" :key="index" @click="handlePreviewFile('file',index)">
+                                        {{item.name}}
+                                        <Icon class="delete-train-file" type="md-close" @click="deleteFile('file',index)" />
+                                    </span>
+                                    <Upload action="" multiple :show-upload-list="false" :before-upload="handleUpload" style="display:inline-block">
+                                        <span class="upload-text" @click="setUpload('file')">继续上传</span>
+                                    </Upload>
+                                </span>
+                                <span v-else>
+                                    <Upload action="" multiple :show-upload-list="false" :before-upload="handleUpload" style="display:inline-block">
+                                        <span class="upload-text" @click="setUpload('file')">上传</span>
+                                    </Upload>
+                                    <span class="text-info">(资料可是上传多份)</span>
                                 </span>
                             </div>
                         </div>
@@ -388,7 +432,7 @@
                         {{$t('teachContent.notAudio')}}
                     </audio>
                     <img v-else-if="answer.type == 'image'" :src="answer.url" style="border-radius: 5px;max-height: 800px;max-width:870px;" />
-                    <iframe v-else-if="answer.extension == 'PDF'" :src="'/web/viewer.html?file=' + escapeBlobUrl(hanswer.url)" width='870' height='700' frameborder='1'></iframe>
+                    <iframe v-else-if="answer.extension == 'PDF'" :src="'/web/viewer.html?file=' + escapeBlobUrl(answer.url)" width='870' height='700' frameborder='1'></iframe>
                     <iframe v-else-if="answer.type == 'doc'" :src="'https://view.officeapps.live.com/op/view.aspx?src=' + escapeBlobUrl(answer.url)" width='870' height='700' frameborder='1'></iframe>
                     <div v-else>
                         <p>{{$t('train.detail.viewTips')}}</p>
@@ -405,9 +449,37 @@
         <Modal v-model="examStatus" :title="$t('train.detail.answerTitle')" width="800">
             <SurveyDetail v-if="examInfo && tableData[examIndex]" :survey="examInfo" :answer="tableData[examIndex].examAnswer || []"></SurveyDetail>
         </Modal>
+        <Modal v-model="progressstatus" footer-hide :mask-closable="false">
+            <p class="uploading-title">{{uploadStatus ? '上传中' : '上传完成'}}</p>
+            <div class="file-list-box">
+                <div class="file-prog-item" v-for="(item,index) in progList" :key="index">
+                    <p>
+                        <span class="file-name">{{item.name}}</span>
+                        <span class="file-prog">
+                            {{getSizeByBytes(item.loadedBytes)+'/'+getSizeByBytes(item.size)}}
+                        </span>
+                    </p>
+                    <Progress :percent="item.progress" stroke-color="#19be6b" />
+                </div>
+            </div>
+        </Modal>
+        <div v-if="previewStatus" class="image-viewer">
+            <div style="width:fit-content;position:relative;margin:auto;">
+                <Icon type="md-close" class="close-icon" @click="closePreview" />
+                <video v-if="previewFile.type == 'video'" id="previewVideo" autoplay :src="previewFile.url" width="870" controls="controls" style="max-height: 800px;">
+                    {{$t('teachContent.tips8')}}
+                </video>
+                <audio v-else-if="previewFile.type == 'audio'" controls>
+                    <source :src="previewFile.url">
+                    {{$t('teachContent.notAudio')}}
+                </audio>
+                <img v-else-if="previewFile.type == 'image'" :src="previewFile.url" style="border-radius: 5px;max-height: 800px;max-width:870px;" />
+            </div>
+        </div>
     </div>
 </template>
 <script>
+import BlobTool from "@/utils/blobTool.js";
 import QRCode from 'qrcodejs2'
 import { mapGetters } from 'vuex'
 import jwtDecode from 'jwt-decode'
@@ -418,6 +490,13 @@ export default {
     },
     data() {
         return {
+            previewFile: {},
+            previewStatus: false,
+            trainVideo: undefined,
+            trainFiles: [],
+            uploadType: '',
+            progressstatus: false,
+            progList: [],
             isLoading: false,
             selections: [],
             examIndex: 0,
@@ -544,6 +623,9 @@ export default {
         ...mapGetters({
             teachers: 'user/getSchoolUserJoined', // 取得已加入此學校的使用者
         }),
+        uploadStatus() {
+            return this.progList.some(item => item.progress < 100)
+        },
         signData() {
             let data = {
                 all: this.tableData.length,
@@ -633,6 +715,144 @@ export default {
         }
     },
     methods: {
+        downloadTrainFile(url, name) {
+            console.log(111)
+            const downloadRes = async () => {
+                let response = await fetch(url); // 内容转变成blob地址
+                let blob = await response.blob();  // 创建隐藏的可下载链接
+                let objectUrl = window.URL.createObjectURL(blob);
+                let a = document.createElement('a');
+                a.href = objectUrl;
+                a.download = name;
+                a.click()
+                a.remove()
+            }
+            downloadRes()
+        },
+        /**
+         * 预览/下载研修文件
+         * @param {string} type video 删除研修视频 file 删除资料
+         * @param {string} index type == file 删除文件在列表的index
+         */
+        handlePreviewFile(type, index) {
+            console.log(type, index)
+            if (type === 'video') {
+                this.previewFile = this.trainVideo
+                this.previewStatus = true
+            } else {
+                this.previewFile = this.trainFiles[index]
+                console.log(222)
+                if (this.previewFile.type === 'video' || this.previewFile.type === 'audio' || this.previewFile.type === 'image') {
+                    this.previewStatus = true
+                } else {
+                    console.log(33, this.previewFile.url)
+                    this.downloadTrainFile(this.previewFile.url, this.previewFile.name)
+                }
+            }
+        },
+        /**
+         * 删除研修文件
+         * @param {string} type video 删除研修视频 file 删除资料
+         * @param {string} index type == file 删除文件在列表的index
+         */
+        deleteFile(type, index) {
+            if (type) {
+                let fileName = type === 'video' ? '活动视频' : this.trainFiles[index].name
+                let blob = type === 'video' ? this.trainVideo.blob : this.trainFiles[index].blob
+                this.$Modal.confirm({
+                    title: '删除文件',
+                    content: `确认删除${fileName}吗?`,
+                    onOk: () => {
+                        let schoolSas = {
+                            sas: '?' + this.$store.state.user.schoolProfile.blob_sas,
+                            url: this.$store.state.user.schoolProfile.blob_uri.slice(0, this.$store.state.user.schoolProfile.blob_uri.lastIndexOf(this.$store.state.userInfo.schoolCode) - 1),
+                            name: this.$store.state.userInfo.schoolCode
+                        }
+                        let blobTool = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, 'school')
+                        blobTool.deleteBlob(blob).then(
+                            res => {
+                                this.$Message.success('删除成功')
+                                if (type === 'video') {
+                                    this.trainVideo = undefined
+                                } else {
+                                    this.trainFiles.splice(index, 1)
+                                }
+                            },
+                            err => {
+                                this.$Message.error('删除失败')
+                            }
+                        )
+                    }
+                })
+            } else {
+                this.$Message.error('删除失败')
+            }
+        },
+        closePreview() {
+            var myVideo = document.getElementById('previewVideo') // 获取视频video
+            if(myVideo) myVideo.pause()
+            this.previewStatus = false
+        },
+        getSizeByBytes(bytes) {
+            return bytes / 1024 < 1024 ? (bytes / 1024).toFixed(1) + 'KB' : bytes / 1024 / 1024 < 1024 ? (bytes / 1024 / 1024).toFixed(1) + 'M' : (bytes / 1024 / 1024 / 1024).toFixed(1) + 'G'
+        },
+        setUpload(type) {
+            this.uploadType = type
+            this.progList = []
+        },
+        handleUpload(file) {
+            let fileName = file.name
+            if (this.uploadType === 'video') {
+                let extension = file.name.substring(file.name.lastIndexOf('.') + 1, file.name.length)
+                if (extension.toLowerCase() != 'mp4') {
+                    this.$Message.error({
+                        content: '活动视频只支持mp4文件格式',
+                        duration: 3
+                    })
+                    return false
+                }
+                // file.name = 'activity.mp4' //研修活动只能上传一个视频,这里统一设置视频文件名。
+                file = new File([file], 'activity.mp4', {
+                    type: file.type
+                })
+            }
+
+            // 开始上传
+            this.progList.push({
+                name: fileName,
+                progress: 0,
+                size: file.size,
+                loadedBytes: 0
+            })
+            let schoolSas = {
+                sas: '?' + this.$store.state.user.schoolProfile.blob_sas,
+                url: this.$store.state.user.schoolProfile.blob_uri.slice(0, this.$store.state.user.schoolProfile.blob_uri.lastIndexOf(this.$store.state.userInfo.schoolCode) - 1),
+                name: this.$store.state.userInfo.schoolCode
+            }
+            let blobTool = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, 'school')
+            let path = `train/study/${this.trainInfo.id}/files`
+            let _this = this
+            let index = this.progList.length - 1
+            this.progressstatus = true
+            blobTool.upload(file, path, {
+                onProgress: function (e) {
+                    _this.progList[index].loadedBytes = e.loadedBytes
+                    _this.progList[index].progress = parseInt(e.loadedBytes * 100 / file.size)
+                }
+            }).then(
+                res => {
+                    res.url = res.url + schoolSas.sas
+                    if (this.uploadType === 'video') {
+                        this.trainVideo = res
+                    } else {
+                        this.trainFiles.push(res)
+                    }
+                },
+                err => {
+                    this.$Message.error('上传失败')
+                }
+            )
+        },
         delTrain() {
             this.$Modal.confirm({
                 title: this.$t('train.detail.delTitle'),
@@ -704,7 +924,7 @@ export default {
                 let params = {
                     // id: this.trainInfo.id,
                     // tId: ids,
-                    id:[obj],
+                    id: [obj],
                     type: status
                 }
                 this.$api.ability.AuditStudy(params).then(
@@ -719,7 +939,7 @@ export default {
                     err => {
                         this.$Message.error(this.$t('train.detail.saveErr'))
                     }
-                ).finally(()=>{
+                ).finally(() => {
                     this.isLoading = false
                 })
             } else {
@@ -772,11 +992,10 @@ export default {
                         let data = JSON.parse(JSON.stringify(this.tableData))
                         this.tableData = []
                         this.tableData = data
-                        console.log('ddd', this.tableData)
                     }
                 },
                 err => {
-
+                    this.$Message.error('问卷作答数据获取失败')
                 }
             )
         },
@@ -993,6 +1212,8 @@ export default {
                         if (res && res.study) {
                             this.trainInfo = res.study
                             this.checkData = res.records
+                            //获取上传的文件列表
+                            this.getTrainFiles()
                             if (this.trainInfo.settings.includes('sign')) {
                                 this.columns.push({
                                     title: this.$t('train.detail.signTime'),
@@ -1043,6 +1264,32 @@ export default {
                 })
             }
         },
+        getTrainFiles() {
+            let schoolSas = {
+                sas: '?' + this.$store.state.user.schoolProfile.blob_sas,
+                url: this.$store.state.user.schoolProfile.blob_uri.slice(0, this.$store.state.user.schoolProfile.blob_uri.lastIndexOf(this.$store.state.userInfo.schoolCode) - 1),
+                name: this.$store.state.userInfo.schoolCode
+            }
+            let blobTool = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, 'school')
+            blobTool.listBlob({
+                prefix: `train/study/${this.trainInfo.id}/files`
+            }).then(
+                res => {
+                    console.log('研修文件', res)
+                    if (res.blobList) {
+                        res.blobList.forEach(item => {
+                            item.url = item.url + schoolSas.sas
+                        })
+                        this.trainVideo = res.blobList.find(item => item.blob.includes('/files/activity.mp4'))
+
+                        this.trainFiles = res.blobList.filter(item => !item.blob.includes('/files/activity.mp4'))
+                    }
+                },
+                err => {
+
+                }
+            )
+        },
         // 获取老师列表
         getTeachers(ids) {
             if (this.trainInfo.tchLists) {

+ 3 - 1
TEAMModelOS/ClientApp/src/view/trainList/Join.vue

@@ -110,7 +110,9 @@ export default {
             selections: [],
             addStatus: false,
             researches: [],
-            trainList: {},
+            trainList: {
+                members:[]
+            },
             columns1: [
                 {
                     type: 'selection',

+ 14 - 2
TEAMModelOS/Controllers/Research/AbilityStatisticsController.cs

@@ -44,7 +44,7 @@ namespace TEAMModelOS.Controllers
         /// <returns></returns>
         [ProducesDefaultResponseType]
         [HttpPost("upsert-teacher-finalScore")]
-        [AuthToken(Roles = "teacher,admin,area")]
+        [AuthToken(Roles = "admin,teacher", Permissions = "train-appraise")]
         public async Task<IActionResult> UpsertFinalScore(JsonElement request)
         {
             var (userid, name, picture, school) = HttpContext.GetAuthTokenInfo();
@@ -331,6 +331,12 @@ namespace TEAMModelOS.Controllers
             var (userid, name, picture, school) = HttpContext.GetAuthTokenInfo();
             if (!HttpContext.Items.TryGetValue("Standard", out object standard)) return BadRequest();
             var client = _azureCosmos.GetCosmosClient();
+            request.TryGetProperty("update", out JsonElement _update);
+            HashSet<string> update = null;
+            if (_update.ValueKind.Equals(JsonValueKind.Array))
+            {
+                update = _update.ToObject<HashSet<string>>();
+            }
             Area area = null;
             string sql = $"select value(c) from c where c.standard='{standard}'";
             await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Normal").GetItemQueryIterator<Area>(queryText: sql,
@@ -378,10 +384,17 @@ namespace TEAMModelOS.Controllers
                     teacherTrain= await client.GetContainer(Constant.TEAMModelOS, "Teacher").ReadItemAsync<TeacherTrain>(userid, new PartitionKey($"TeacherTrain-{school}"));
                     teacherTrain.name = name;
                     teacherTrain.picture=picture;
+                    if (update!=null && update.Count > 0) {
+                        foreach (var up in update) {
+                            teacherTrain.updateProperty.Add(up);
+                        }
+                        await StatisticsService.StatisticsTeacher(teacherTrain, setting, area, client, null);
+                    }
                 }
                 catch (CosmosException) {
                      teacherTrain = await StatisticsService.StatisticsTeacher(new TeacherTrain
                     {
+                        pk= "TeacherTrain",
                         id = userid,
                         code = $"TeacherTrain-{school}",
                         tmdid = userid,
@@ -391,7 +404,6 @@ namespace TEAMModelOS.Controllers
                         updateProperty = new HashSet<string> {  StatisticsService.TeacherAility,
                         StatisticsService.TeacherClass, StatisticsService.OfflineRecord }
                     }, setting, area, client,null);
-                  
                 }
                 return Ok(new { teacherTrain , setting });
             } catch (Exception ex) { 

+ 45 - 2
TEAMModelOS/Controllers/Research/ClassVideoController.cs

@@ -190,6 +190,50 @@ namespace TEAMModelOS.Controllers.Research
             return Ok();
         }
 
+        /// <summary>
+        /// 更新教师最终学习分数。
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [ProducesDefaultResponseType]
+        [HttpPost("score-teacher-classvideo")]
+        [AuthToken(Roles = "admin,teacher", Permissions = "train-appraise")]
+        public async Task<IActionResult> UpsertFinalScore(JsonElement request)
+        {
+            if (!HttpContext.Items.TryGetValue("Standard", out object standard)) return BadRequest();
+            var (userid, name, picture, school) = HttpContext.GetAuthTokenInfo();
+            var client = _azureCosmos.GetCosmosClient();
+            if (!request.TryGetProperty("score", out JsonElement _score)) return BadRequest();
+            if (!request.TryGetProperty("tmdids", out JsonElement _tmdids)) return BadRequest();
+            int score = -999;
+            int.TryParse($"{_score}", out score);
+            List<string> tmdids = _tmdids.ToObject<List<string>>();
+            if (tmdids.IsNotEmpty() && score >= -1 && score <= 2)
+            {
+                List<ClassVideo> trains = new List<ClassVideo>();
+                string insql = $"select value(c) from c where c.id in ({string.Join(",", tmdids.Select(x => $"'{x}'"))})";
+                await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Teacher")
+                    .GetItemQueryIterator<ClassVideo>(queryText: insql, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ClassVideo-{school}") }))
+                {
+                    trains.Add(item);
+                }
+                List<Task<ItemResponse<ClassVideo>>> teacherTrains = new List<Task<ItemResponse<ClassVideo>>>();
+                List<Task> tasks = new List<Task>();
+                trains.ForEach(x => {
+                    x.files.Select(y => y.score = score);
+
+                    teacherTrains.Add(client.GetContainer(Constant.TEAMModelOS, "Teacher").ReplaceItemAsync(x, x.id, new PartitionKey(x.code)));
+                    tasks.Add(StatisticsService.SendServiceBus($"{standard}", $"{x.id}", $"{school}", StatisticsService.TeacherClass, 1, _configuration, _serviceBus));
+                });
+                await Task.WhenAll(teacherTrains);
+                await Task.WhenAll(tasks);
+                return Ok(new { status = 200 });
+            }
+            else
+            {
+                return Ok(new { });
+            }
+        }
         /// <summary>
         ///课堂实录操作接口
         /// </summary>
@@ -271,14 +315,13 @@ namespace TEAMModelOS.Controllers.Research
                                 }
                                 classVideo.files= abilities;
                                 await client.GetContainer("TEAMModelOS", "Teacher").ReplaceItemAsync<ClassVideo>(classVideo, $"{_tmdid}", new PartitionKey(code));
-                                return Ok(new { classVideo });
+                               
                             }
                             catch (CosmosException ex)
                             {
                                 if (ex.Status == 404) {
                                     classVideo = new ClassVideo { id = $"{_tmdid}", pk = "ClassVideo", code = code, school = $"{_school}", creatorId = $"{_tmdid}", files = files };
                                     await client.GetContainer("TEAMModelOS", "Teacher").CreateItemAsync<ClassVideo>(classVideo, new PartitionKey(code));
-                                    return Ok(new { classVideo });
                                 }
                             }
                             await StatisticsService.SendServiceBus($"{standard}", $"{_tmdid}", $"{school}", StatisticsService.TeacherClass, 1, _configuration, _serviceBus);

+ 220 - 1
TEAMModelOS/Controllers/XTest/DataMigrationController.cs

@@ -21,6 +21,7 @@ using TEAMModelOS.SDK.Models.Cosmos.Common;
 using TEAMModelOS.SDK.Models.Service;
 using TEAMModelOS.Services.Common;
 using HTEXLib.COMM.Helpers;
+using Azure;
 
 namespace TEAMModelOS.Controllers
 {
@@ -48,8 +49,226 @@ namespace TEAMModelOS.Controllers
             _azureStorage = azureStorage;
             _dingDing = dingDing;
         }
+
+        [ProducesDefaultResponseType]
+        [HttpPost("restore-video-record")]
+        public async Task<IActionResult> RestoreVideoRecord(JsonElement json) {
+            try
+            {
+                var client = _azureCosmos.GetCosmosClient();
+                List<School> schools = new List<School>(0);
+                await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIterator<School>(queryText: "SELECT  value(c) FROM c", requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("Base") }))
+                {
+                    schools.Add(item);
+                }
+                List<SchoolTeacher> tmdids = new List<SchoolTeacher>();
+                foreach (var school in schools)
+                {
+                    if (!string.IsNullOrEmpty(school.standard))
+                    {
+
+                        await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIterator<SchoolTeacher>(queryText: "SELECT  value(c) FROM c",
+                            requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Teacher-{school.id}") }))
+                        {
+                            tmdids.Add(item);
+                        }
+                    }
+                }
+                List<(SchoolTeacher Teacher, List<AbilitySub> abilitySubs)> ps = new List<(SchoolTeacher Teacher, List<AbilitySub> abilitySubs)>();
+                await foreach ((SchoolTeacher tmdid, List<AbilitySub> subs) a in GetSubsAsyn(tmdids, client))
+                {
+                    if (a.subs.IsNotEmpty())
+                    {
+                        ps.Add(a);
+                    }
+                }
+                List<dynamic> datas = new List<dynamic>();
+                List<TeacherFile> teacherFiles = new List<TeacherFile>();
+                foreach (var p in ps)
+                {
+                    Dictionary<string, FileRecord> dict = new Dictionary<string, FileRecord>();
+                    School school = schools.Find(x => p.Teacher.code.Replace("Teacher-", "").Equals(x.id));
+                    if (school != null && !string.IsNullOrEmpty(school.standard))
+                    {
+                        var rcds = p.abilitySubs.SelectMany(x => x.videoRcds);
+                        foreach (var c in rcds)
+                        {
+                            try {
+                                AbilityTask abilityTask = await client.GetContainer(Constant.TEAMModelOS, "Normal").ReadItemAsync<AbilityTask>(c.abilityTaskId, new PartitionKey($"AbilityTask-{school.standard}"));
+                                Rnode rnode = null;
+                                Tnode tnode = null;
+                                if (!string.IsNullOrEmpty(c.tnodeId))
+                                {
+                                    foreach (var node in abilityTask.children)
+                                    {
+                                        rnode = node.rnodes.Find(y => y.link.Equals(c.url));
+                                        if (rnode != null)
+                                        {
+                                            tnode = node;
+                                            break;
+                                        }
+                                    }
+                                }
+                                else
+                                {
+                                    tnode = abilityTask.children.Find(x => x.id.Equals(c.tnodeId));
+                                    if (tnode == null)
+                                    {
+                                        foreach (var node in abilityTask.children)
+                                        {
+                                            rnode = node.rnodes.Find(y => y.link.Equals(c.url));
+                                            if (rnode != null)
+                                            {
+                                                tnode = node;
+                                                break;
+                                            }
+                                        }
+                                    }
+                                    else
+                                    {
+                                        rnode = tnode.rnodes.Find(y => y.link.Equals(c.url));
+                                    }
+                                }
+                                if (rnode != null)
+                                {
+                                    if (dict.ContainsKey(rnode.hash))
+                                    {
+                                        FileRecord fileRecord = dict[rnode.hash];
+
+                                        if (!c.done)
+                                        {
+                                            if (fileRecord.type.Equals("video") && c.time * 60 > fileRecord.view)
+                                            {
+                                                fileRecord.view = (long)c.time * 60;
+                                            }
+                                            var file = fileRecord.files.Find(x => x.abilityId.Equals(abilityTask.abilityId)
+                                            && x.taskId.Equals(abilityTask.id) && x.nodeId.Equals(tnode.id) && x.url.Equals(rnode.link));
+                                            if (file == null)
+                                            {
+                                                fileRecord.files.Add(new FileAbility()
+                                                {
+                                                    abilityId = abilityTask.abilityId,
+                                                    taskId = abilityTask.id,
+                                                    nodeId = tnode.id,
+                                                    url = rnode.link
+                                                });
+                                            }
+                                        }
+                                        else
+                                        {
+                                            var file = fileRecord.files.Find(x => x.abilityId.Equals(abilityTask.abilityId)
+                                            && x.taskId.Equals(abilityTask.id) && x.nodeId.Equals(tnode.id) && x.url.Equals(rnode.link));
+                                            if (file == null)
+                                            {
+                                                fileRecord.files.Add(new FileAbility()
+                                                {
+                                                    abilityId = abilityTask.abilityId,
+                                                    taskId = abilityTask.id,
+                                                    nodeId = tnode.id,
+                                                    url = rnode.link
+                                                });
+                                            }
+                                            fileRecord.done = true;
+                                            fileRecord.view = (long)fileRecord.duration;
+                                        }
+                                    }
+                                    else
+                                    {
+                                        FileRecord teacherFileRcd = new FileRecord();
+                                        FileAbility fileAbility = new FileAbility()
+                                        {
+                                            abilityId = abilityTask.abilityId,
+                                            taskId = abilityTask.id,
+                                            nodeId = tnode.id,
+                                            url = rnode.link
+                                        };
+                                        teacherFileRcd.files.Add(fileAbility);
+                                        teacherFileRcd.hash = rnode.hash;
+                                        teacherFileRcd.duration = rnode.duration;
+                                        teacherFileRcd.type = rnode.type;
+                                        teacherFileRcd.size = rnode.size != null ? rnode.size.Value : 0;
+                                        teacherFileRcd.done = c.done;
+                                        if (teacherFileRcd.done)
+                                        {
+                                            teacherFileRcd.view = (long)teacherFileRcd.duration;
+                                        }
+                                        else
+                                        {
+                                            teacherFileRcd.view = (long)(c.time * 60);
+                                        }
+                                        dict.Add(rnode.hash, teacherFileRcd);
+                                    }
+                                }
+                            } catch (CosmosException ex) {
+                                Console.WriteLine(c.abilityTaskId);
+                            }
+                        }
+                        TeacherFile teacherFile = null;
+                        try 
+                        {
+                            teacherFile = await  client.GetContainer(Constant.TEAMModelOS, "Teacher").ReadItemAsync<TeacherFile>(p.Teacher.id, new PartitionKey($"TeacherFile-{school.id}"));
+                            foreach (var ds in dict) {
+                                var file= teacherFile.fileRecords.Find(x => x.hash.Equals(ds.Key));
+                                if (file != null)
+                                {
+                                    if (!file.done)
+                                    {
+                                        if (file.type.Equals("video") && file.view < ds.Value.view)
+                                        {
+                                            file.view = ds.Value.view;
+                                        }
+                                    }
+                                    var nofiles = ds.Value.files.FindAll(x =>
+                                        !file.files.Exists(y => x.abilityId.Equals(y.abilityId)
+                                        && x.taskId.Equals(y.taskId)
+                                        && x.nodeId.Equals(y.nodeId)
+                                        && x.url.Equals(y.url)
+                                      ));
+                                    if (nofiles.IsNotEmpty())
+                                    {
+                                        file.files.AddRange(ds.Value.files);
+                                    }
+                                }
+                                else {
+                                    teacherFile.fileRecords.Add(ds.Value);
+                                }
+                            }
+                            teacherFile = await client.GetContainer(Constant.TEAMModelOS, "Teacher").ReplaceItemAsync<TeacherFile>(teacherFile, teacherFile.id, new PartitionKey($"TeacherFile-{school.id}"));
+                            teacherFiles.Add(teacherFile);
+                        } catch (CosmosException ex) {
+                            teacherFile = new TeacherFile {
+                                id = p.Teacher.id,
+                                code = $"TeacherFile-{school.id}",
+                                fileRecords = dict.Values.ToList(),
+                                pk = "TeacherFile"
+                            };
+                            teacherFiles.Add(teacherFile);
+                            teacherFile = await client.GetContainer(Constant.TEAMModelOS, "Teacher").CreateItemAsync<TeacherFile>(teacherFile, new PartitionKey($"TeacherFile-{school.id}"));
+                        }
+                        datas.Add(new { teacher= p.Teacher , school= school, dict });
+                    }
+                }
+                return Ok( new { datas, teacherFiles } );
+            } catch (Exception ex) { 
+            }
+            return Ok();
+        }
+        private async IAsyncEnumerable<(SchoolTeacher, List<AbilitySub>)> GetSubsAsyn(List<SchoolTeacher> tmdids, CosmosClient client)
+        {
+            foreach (var tmdid in tmdids)
+            {
+                List<AbilitySub> abilitySubs = new List<AbilitySub>();
+                await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Teacher").GetItemQueryIterator<AbilitySub>(queryText: "SELECT  value(c) FROM c",
+                          requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"AbilitySub-{tmdid.code.Replace("Teacher-","")}-{tmdid.id}") }))
+                {
+                    abilitySubs.Add(item);
+                }
+                var abilitys=  abilitySubs.FindAll(x => x.videoRcds.IsNotEmpty());
+                yield return (tmdid, abilitys);
+            }
+        }
         /// <summary>
-        /// 迁移教师基础信息,并处理历史数据。
+        ///迁移Stulist数据至GroupList
         /// </summary>
         /// <param name="data"></param>
         /// <returns></returns>

+ 0 - 1
TEAMModelOS/Controllers/XTest/FixDataController.cs

@@ -116,7 +116,6 @@ namespace TEAMModelOS.Controllers
                     }
                    await CosmosClient.GetContainer(Constant.TEAMModelOS, "Normal").ReplaceItemAsync<AbilityTask>(item, item.id, new PartitionKey(item.code));
                 }
-
             }
             return Ok(abilityTasks);
         }