onepsycho@163.com 1 year ago
parent
commit
1b5c6ed1b2

+ 31 - 5
TEAMModelBI/ClientApp/src/router/index.js

@@ -1,7 +1,8 @@
-import { createWebHashHistory, createRouter } from "vue-router";
+import { createWebHashHistory, createRouter  } from "vue-router";
 import jwt_decode from 'jwt-decode'
 // import store from '@/store/index.js'
-const routes = [{
+const routes = [
+    {
         path: "/",
         redirect: "/login"
     },
@@ -17,11 +18,32 @@ const routes = [{
         roles: "all",
         component: () => require.ensure([], (require) => require(`@/view/index/test.vue`))
     },
+    // 統購平台
+    {
+        path: "/adminpanel",
+        name: "adminpanel",
+        roles: ['admin'],                
+        component: () => require.ensure([], (require) => require(`@/view/htcommunity/adminpanel.vue`))
+    },
     // {
     //     path: "/dashboard",
     //     name: "dashboard",
     //     component: () => require.ensure([], (require) => require(`@/view/index/dashboard.vue`))
     // },
+    // {
+    //     path: "/htcommunity",
+    //     name: "htcommunity",
+    //     component: () => require.ensure([], (require) => require(`@/view/htcommunity/adminpanel.vue`)),
+    //     children: [{
+    //             name: "adminpanel",
+    //             path: "adminpanel",
+    //             permission: "teacher-read|teacher-upd",
+    //             roles: ['all'],
+    //             isShow: true,
+    //             component: () => require.ensure([], (require) => require(`@/view/htcommunity/adminpanel.vue`))
+    //         },            
+    //     ]
+    // },
     {
         path: "/home",
         name: "home",
@@ -276,11 +298,15 @@ const routes = [{
     // },
 ];
 const router = createRouter({
-    history: createWebHashHistory(),
+    history: createWebHashHistory(),    
     routes,
 });
-router.beforeEach((to, from, next) => {
-    console.log(to, 'router')
+router.beforeEach((to, from, next) => {    
+    console.log(to, 'router')      
+    if (to.query.htype === 'adminlogin')
+    {        
+        localStorage.setItem("htype",'adminlogin');
+    }
     if (to.path === '/login' || to.path === '/resultPage') return next()
     if (to.path === '/login-thirdparty') return next()
     let thirdUser = localStorage.getItem('thirdUser')

+ 231 - 0
TEAMModelBI/ClientApp/src/view/htcommunity/adminpanel.vue

@@ -0,0 +1,231 @@
+<template>
+    <div class="backgorundbox">
+        <div class="logsbox">
+           <div ><span>TEAMModel·BI 数据监控系统</span></div>           
+        </div>
+        <!-- <el-button type="primary" plain @click="quit = true">{{$t(`header.quit`)}}</el-button>       -->
+        <el-space direction="vertical" style="width: 100%;margin-left: auto;margin-right: auto;margin-top: 3%;"
+            shadow="Always">
+
+            <el-card style="width: 60%;" shadow="Always">
+                <span
+                    style="font-size: 20px;border-bottom:1px #000 solid; padding-bottom:5px; margin-bottom: 20px;">統購平台 - 產品管理</span>
+                <!-- <div style="float: right;">
+                    <el-button type="primary" plain @click="routerskip('/AdminLogin')">返回</el-button>
+                </div> -->
+                <div style="float: right;">
+                    <el-button type="primary" plain @click="quit">{{$t(`header.quit`)}}</el-button>
+                </div>
+                <br /><br /><br />
+
+                <el-button type="primary" plain @click="dialogFormVisible = true">新增</el-button>
+                <el-divider />
+                <el-table :data="tableData" style="width: 100%; margin-bottom: 50px;">
+                    <el-table-column prop="region" label="縣市" width="200" />
+                    <el-table-column prop="name" label="產品名稱" width="200" />
+                    <el-table-column prop="extensions" label="擴充項" width="400" />
+                    <el-table-column prop="space" label="容量(GB)" width="100" />
+                    <el-table-column prop="qwen" label="名額" width="100" />
+                    <el-table-column prop="time" label="使用期限" width="400" />
+                </el-table>
+            </el-card>
+        </el-space>
+        <el-dialog v-model="dialogFormVisible" title="購買產品" width="700">
+            <el-form :model="form">
+                <el-form-item label="縣市" :label-width="formLabelWidth">
+                    <el-select v-model="form.region" placeholder="請選擇縣市">
+                        <el-option label="臺北市" value="臺北市" />
+                        <el-option label="新北市" value="新北市" />
+                        <el-option label="桃園市" value="桃園市" />
+                        <el-option label="新竹市" value="新竹市" />
+                        <el-option label="苗栗市" value="苗栗市" />
+                    </el-select>
+                </el-form-item>
+
+                <el-form-item label="產品名稱" :label-width="formLabelWidth">
+                    <el-select v-model="form.name" placeholder="請選擇產品" @change="handleChange">
+                        <el-option label="Hiteach" value="Hiteach" />
+                        <el-option label="IES5個人空間" value="IES5個人空間" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="空間" :label-width="formLabelWidth" v-show="isShowSpace">
+                    <el-input v-model="form.space" style="width: 240px" placeholder="請輸入容量" /> &nbsp;&nbsp;(單位GB)
+                </el-form-item>
+                <el-form-item label="擴充項" :label-width="formLabelWidth" v-show="isShowExtensions">
+                    <el-checkbox v-model="form.extensions" v-for="extension in extensions" :key="extension" :label="extension" :value="extension">
+                      {{ extension }}
+                    </el-checkbox>                  
+                </el-form-item>
+                <el-form-item label="使用期限" :label-width="formLabelWidth">
+                    <el-date-picker v-model="form.time" type="daterange" range-separator="To" start-placeholder="開始時間"
+                        end-placeholder="結束時間" :size="size" format="YYYY/MM/DD" value-format="YYYY-MM-DD" />
+                    <!-- <el-select v-model="form.time" placeholder="請選擇使用期限">
+            <el-option label="1個月" value="1個月" />
+            <el-option label="2個月" value="2個月" />
+            <el-option label="3個月" value="3個月" />
+          </el-select> -->
+                </el-form-item>
+                <el-form-item label="名額" :label-width="formLabelWidth">
+                    <el-input v-model="form.qwen" style="width: 240px" placeholder="請輸入名額" />
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <div class="dialog-footer">
+                    <el-button type="primary" @click="insert">
+                        確定
+                    </el-button>
+                    <el-button @click="dialogFormVisible = false">取消</el-button>
+
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+  
+  <script setup>
+  //import { reactive } from 'vue'
+  import { useRouter } from 'vue-router'
+  import { ref, reactive, onMounted } from 'vue'
+  import { ElMessage } from 'element-plus'
+  //路由
+  const routers = useRouter()
+
+  // function quit() {
+  //   debugger
+  //   localStorage.clear();
+  //   routers.push('/QrLogin') 
+  // }
+  const dialogFormVisible = ref(false)
+  const formLabelWidth = '140px'
+  const form = reactive({
+    region: '',
+    name: '',  
+    qwen: '',
+    time:'',
+    space:'',
+    extensions:[]
+  })
+  let tableData = reactive([])  
+  let isShowSpace = ref(false)
+  let isShowExtensions = ref(false)
+  let extensions = reactive(['AI智能終端',
+                      '禁用硬體IRS',
+                      'USB錄影支援',
+                      'AI文句分析',
+                      '分組數',
+                      '議課人數',
+                      'IRS連線授權數',
+                      '蘇格拉底議課APP',
+                      '蘇格拉底影片',
+                      '學校簡碼',
+                      '蘇格拉底桌面',
+                      '電子學生證',
+                      '智慧評分系統',
+                      '蘇格拉底小數據',
+                      '蘇格拉底報告',
+                      '雲端診斷分析系統',
+                      '蘇格拉底語音轉寫',
+                      '協作',
+                      'AI GPT服務',
+                       ]);
+  onMounted(() => {
+    //routers.push({ name: 'adminpanel'})  
+    //routers.forward()  
+
+  })
+  function insert(){  
+    dialogFormVisible.value = false 
+    //let htime = form.time;
+    let arr = form.time.toString().split(',');
+    var item = {
+      region: form.region,
+      name: form.name,
+      qwen: form.qwen,
+      time: arr[0] + "~" + arr[1],
+      space: form.space,
+      extensions:form.extensions
+    }
+    tableData.push(item);  
+    ElMessage({
+      message: '新增成功!',
+      type: 'success',
+    })
+
+    
+    form.region= ''
+    form.name= ''  
+    form.qwen= ''
+    form.time=''
+    form.space=''
+    form.extensions=[]
+    isShowSpace.value = false;
+    isShowExtensions.value = false;
+  
+    
+  }
+  function handleChange(){
+    //debugger
+    if(form.name==='Hiteach'){
+        isShowSpace.value = false;
+        isShowExtensions.value = true;
+    }
+    if(form.name==='IES5個人空間'){
+        isShowSpace.value = true;
+        isShowExtensions.value = false;
+    }
+  }
+function quit() {
+    routers.push('/login?htype=adminlogin')
+    localStorage.removeItem('userData')
+    localStorage.removeItem('id_token')
+    localStorage.removeItem('blobInfo')
+    localStorage.removeItem('management')
+    localStorage.removeItem('organization')
+    // localStorage.clear()
+}
+  
+  </script>
+  
+  <style lang="less">
+  
+  </style>
+<style scoped>
+.backgorundbox {
+  background: url("../../assets/img/background1.png") no-repeat;
+  background-size: cover;
+  width: 100vw;
+  height: 100vh;
+  position: relative;
+  /* filter: blur(2px); */
+}
+
+.logsbox {
+    color:#fff;
+    font-size: 34px;
+    text-align: center;
+    width: 95%;
+    line-height: 50px;
+    ;
+    position: relative;
+}
+
+.logsbox .el-icon-s-order {
+    font-size: 16px;
+}
+.header-icon {
+  width: 1.2em;
+  height: 1.2em;
+  vertical-align: -0.3em;
+  fill: currentColor;
+  overflow: hidden;
+  margin-right: 5px;
+}
+
+:deep(.my-label) {
+    background: var(--el-color-success-light-9) !important;
+}
+
+:deep(.my-content) {
+    background: var(--el-color-danger-light-9);
+}
+</style>

+ 13 - 1
TEAMModelBI/ClientApp/src/view/login.vue

@@ -117,7 +117,19 @@ export default {
         getOrganization()
         Allpermission()
         if (res.roles.includes('admin') || res.roles.includes('leader')) {
-          router.push('/home/index')
+          // 檢查是否為統構平台串接過來的用戶
+          let htype = localStorage.getItem('htype');
+          localStorage.removeItem("htype");
+          if(htype==="adminlogin"){            
+            //router.push({ name: 'adminpanel'}) 
+            // 因為目前網站的router模式為Hash 所以必須用window.location.href的方式轉跳網頁 不然會多代一些前頁的文字
+            window.location.href = window.location.origin+'/#/adminpanel'; 
+                
+            //router.push({ redirect: window.location.href = window.location.origin+'/#/adminpanel' });
+          }else{            
+            router.push('/home/index')
+          }
+          
         } else if (res.roles.includes('assist') || res.roles.includes('sales')) {
           router.push('/home/campus')
         }

+ 38 - 8
TEAMModelContest/contest.client/src/utils/blobTool.js

@@ -1,8 +1,9 @@
 import API from '@/api/index.js'
 import { GLOBAL } from '@/utils/Global.js'
 import { getCurrentInstance } from "vue"
+import SparkMD5 from "spark-md5"
 
-let proxy = getCurrentInstance()?.proxy
+// let {proxy} = getCurrentInstance()
 
 const { BlobServiceClient, BlobClient } = require("@azure/storage-blob")
 const blobPath = ['activity']
@@ -123,6 +124,33 @@ export default class BlobTool {
         })
     }
 
+    
+    /* 获取文件的MD5 */
+    getFileMD5(file) {
+        return new Promise((r, j) => {
+            const CHUNK_SIZE = 4194304; // 大文件获取数前4M大小
+            const fileReader = new FileReader();
+            const chunkFile = file.slice(0, CHUNK_SIZE);
+            fileReader.readAsBinaryString(chunkFile);
+            let spark = new SparkMD5();
+            console.time('getMd5')
+            fileReader.onload = e => {
+                spark.appendBinary(e.target.result);
+                var md5 = spark.end(true);
+                r(this.stringToUint8Array(md5))
+            };
+        })
+    }
+	/* 将二进制流字符串转换成Uint8Array */
+	stringToUint8Array(str) {
+		var arr = [];
+		for (var i = 0, j = str.length; i < j; ++i) {
+			arr.push(str.charCodeAt(i));
+		}
+		var tmpUint8Array = new Uint8Array(arr);
+		return tmpUint8Array
+	}
+
     /**
      * 上传文件方法,带回调上传进度
      * @param {any} file 文件对象
@@ -155,19 +183,21 @@ export default class BlobTool {
                 }
             }
             const blockBlobClient = this.containerClient.getBlockBlobClient(path + '/' + file.name)
-            blockBlobClient.uploadBrowserData(file, option).then(res => {
+            blockBlobClient.uploadBrowserData(file, option).then(async res => {
                 // 设置blob MD5 (解决大文件分块上传没有MD5的问题)
-                if(!res.contentMD5) {
-                    proxy.$tools.getFileMD5(file).then(res5 => {
-                        let option = {getBlockMD5: res5}
+                let md5value = res.contentMD5
+                if(!md5value) {
+                    try {
+                        md5value = await this.getFileMD5(file)
+                        let option = {getBlockMD5: md5value}
                         this.setFileProperties(path + '/' + file.name, option).then(resSet => {
                             console.log('MD5设置成功', resSet);
                         }, err => {
                             console.error('MD5设置失败', err);
                         })
-                    }, err => {
+                    } catch (error) {
                         console.error('前端获取MD5失败', err);
-                    })
+                    }
                 }
                 let url = decodeURIComponent(res._response.request.url)
                 url = url.substring(0, url.lastIndexOf('?'))
@@ -180,7 +210,7 @@ export default class BlobTool {
                     extension: info.ex,
                     type: info.type,
                     blob: '/' + path + '/' + file.name,
-                    md5: res.contentMD5,
+                    md5: md5value,
                 })
             }, err => {
                 reject(err)

+ 51 - 44
TEAMModelContest/contest.client/src/view/myactivity/MyActivity.vue

@@ -718,52 +718,59 @@ function handleChange(file, files) {
 
 function uploadBlob() {
     return new Promise(async (resolve, reject) => {
-        const scope = actInfo.value.scope === 'school' ? 'school' : 'area'
-        let sasUrl = await proxy.$api.blobSasRCW({name: actInfo.value.owner, role: 'teacher' })
-        // activity/活动id/upload/醍摩豆ID
-        let path = `activity/${actInfo.value.id}/upload/${store.userInfo.sub}`
-        let statusCode = 0
-        if(uploadList.value.length) {
-            statusCode = await deleteBlobPrefix(actInfo.value.owner, path)
-        } else {
-            statusCode = 200
-        }
-        if(statusCode === 200) {
-            let Blobs = new BlobTool(sasUrl.url, actInfo.value.owner, '?' + sasUrl.sas, scope)
-            let promiseAll = []
-            fileList.value.map(item => {
-                let info = toRaw(item)
-                promiseAll.push(new Promise((r, j) => {
-                    Blobs.upload(info.raw, {path, checkSize: false}, {
+        try {
+            const scope = actInfo.value.scope === 'school' ? 'school' : 'area'
+            let sasUrl = await proxy.$api.blobSasRCW({name: actInfo.value.owner, role: 'teacher' })
+            // activity/活动id/upload/醍摩豆ID
+            let path = `activity/${actInfo.value.id}/upload/${store.userInfo.sub}`
+            let statusCode = 200
+            /* if(uploadList.value.length) {
+                statusCode = await deleteBlobPrefix(actInfo.value.owner, path)
+            } else {
+                statusCode = 200
+            } */
+            if(statusCode === 200) {
+                let Blobs = new BlobTool(sasUrl.url, actInfo.value.owner, '?' + sasUrl.sas, scope)
+                let promiseAll = []
+                fileList.value.map(item => {
+                    let info = toRaw(item)
+                    promiseAll.push(new Promise((r, j) => {
+                        Blobs.upload(info.raw, {path, checkSize: false}, {
                             onProgress: (e) => {
                                 item.progress = parseInt(e.loadedBytes * 100 / item.size)
                             }
                         }).then(async res => {
-                        let datas = res
-                        // let fileMD5 = await proxy.$tools.getFileMD5(info.raw)
-                        datas.hash = await proxy.$tools.convertFileMD5ToString(res.md5)
-                        if(res.type === 'video') {
-                            datas.duration = await proxy.$tools.getVideoDuration(info.raw)
-                        } else {
-                            datas.duration = 0
-                        }
-                        datas.url = res.blob
-                        datas.extension = res.extension.toLowerCase()
-                        datas.cnt = actInfo.value.owner
-                        datas.tmdid = store.userInfo.sub
-                        datas.tag = []
-                        r(datas)
-                    }).catch(e => {
-                        j(e)
-                    })
-                }))
-            })
-            Promise.all(promiseAll).then(res => {
-                resolve(res)
-            }).catch(e => {
-                reject(undefined)
-            })
-        } else {
+                            let datas = res
+                            // let fileMD5 = await proxy.$tools.getFileMD5(info.raw)
+                            datas.hash = await proxy.$tools.convertFileMD5ToString(res.md5)
+                            if(res.type === 'video') {
+                                datas.duration = await proxy.$tools.getVideoDuration(info.raw)
+                            } else {
+                                datas.duration = 0
+                            }
+                            datas.url = res.blob
+                            datas.extension = res.extension.toLowerCase()
+                            datas.cnt = actInfo.value.owner
+                            datas.tmdid = store.userInfo.sub
+                            datas.tag = []
+                            r(datas)
+                        }).catch(e => {
+                            console.log('55555555555', e);
+                            j(e)
+                        })
+                    }))
+                })
+                Promise.all(promiseAll).then(res => {
+                    console.log('22222222', res);
+                    resolve(res)
+                }).catch(e => {
+                    reject(undefined)
+                })
+            } else {
+                resolve(undefined)
+            }
+        } catch (error) {
+            console.log('777777777777', error);
             resolve(undefined)
         }
     })
@@ -984,8 +991,8 @@ function lookSokrates(info) {
     if(info.type === 'sokrates') {
         // 查看苏格拉底单点登录页面所需字段,在此处重新拼接
         // https://account.teammodel.cn/?callback=https%3A%2F%2Fsokrates.teammodel.cn%2Fauth%2Flogin%2Fcallback-habook%3Fto%3DaHR0cHM6Ly9zb2tyYXRlcy50ZWFtbW9kZWwuY24vZXhoaWJpdGlvbi90YmF2aWRlbyMvY29udGVudC81MDk1Mz9ncm91cElkcz0xNDImY2hhbm5lbElkPTEzMQ%3D%3D
-        let url = "https://account.teammodel.cn/?callback=" + 'https://sokrates.teammodel.cn/auth/login/callback-habook' + '?to=' + btoa(info.lessonSokrates.url)
-        window.open(url, '_blank')
+        // let url = "https://account.teammodel.cn/?callback=" + 'https://sokrates.teammodel.cn/auth/login/callback-habook' + '?to=' + btoa(info.lessonSokrates.url)
+        window.open(info.lessonSokrates.url, '_blank')
     } else {
         // 课例未商定怎么呈现
     }

+ 3 - 2
TEAMModelContest/contest.client/src/view/myactivity/MyReview.vue

@@ -524,8 +524,9 @@ function startWork() {
 function lookSokrates(info) {
     // 查看苏格拉底单点登录页面所需字段,在此处重新拼接
     // https://account.teammodel.cn/?callback=https%3A%2F%2Fsokrates.teammodel.cn%2Fauth%2Flogin%2Fcallback-habook%3Fto%3DaHR0cHM6Ly9zb2tyYXRlcy50ZWFtbW9kZWwuY24vZXhoaWJpdGlvbi90YmF2aWRlbyMvY29udGVudC81MDk1Mz9ncm91cElkcz0xNDImY2hhbm5lbElkPTEzMQ%3D%3D
-    let url = "https://account.teammodel.cn/?callback=" + 'https://sokrates.teammodel.cn/auth/login/callback-habook' + '?to=' + btoa(info.url)
-    window.open(url, '_blank')
+    // let url = "https://account.teammodel.cn/?callback=" + 'https://sokrates.teammodel.cn/auth/login/callback-habook' + '?to=' + btoa(info.url)
+    // 取消跳转到苏格拉底登录页面,直接跳转到对应地址
+    window.open(info.url, '_blank')
 }
 </script>
 

+ 1 - 0
TEAMModelOS.SDK/Context/Constant/Constant.cs

@@ -9,6 +9,7 @@ namespace TEAMModelOS.SDK.DI
         public static readonly List<string> BlobPrefix = new List<string> { "exam", "vote", "survey", "item", "paper", "syllabus", "records", "doc", "image", "res", "video", "audio", "other", "thum", "train", "temp", "jyzx" };
         public static readonly List<string> ContentPrefix = new List<string> { "doc", "image", "res", "video", "audio", "other" };
         public static readonly string TEAMModelOS = "TEAMModelOS";
+        public static readonly string teammodelos_blob = "teammodelos";
         public static readonly string ScopeTeacher = "teacher";
         public static readonly string ScopeTmdUser = "tmduser";
         public static readonly string ScopeStudent = "student";

+ 8 - 0
TEAMModelOS.SDK/Models/Cosmos/School/Knowledge.cs

@@ -46,6 +46,14 @@ namespace TEAMModelOS.SDK.Models
         public string id { get; set; }
         public string name { get; set; }
         public string pid { get; set; }
+        /// <summary>
+        /// 使用次数
+        /// </summary>
+        public long used { get; set; }
+        /// <summary>
+        /// 关联的资源数量
+        /// </summary>
+        public long link {  get; set; }
     }
     public class PointTree 
     {

+ 5 - 5
TEAMModelOS/ClientApp/public/lang/en-US.js

@@ -193,7 +193,7 @@ const LANG_EN_US = {
             scoreDown: '分以下',
             scoreUp: '分以上',
             self: 'Self-evaluation Result',
-            tip6: '溫馨提示',
+            tip6: 'Note',
             tip7: '您未確認“提交評審結果”,無法保存修改內容,是否確認離開?',
             tip8: '等',
             tip9: '個文件已更新!',
@@ -1613,9 +1613,9 @@ const LANG_EN_US = {
             tip7: 'Switching will clear the current question data, do you confirm to switch?',
             tip8: 'Maximum 20 questions!',
             tip9: 'Enter the speaking prompt',
-            tip10: '试题数量不可为空!',
-            tip11: '单选题答案数与题目数不一致,请检查后重试!',
-            tip12: '多选题答案数与题目数不一致,请检查后重试!',
+            tip10: 'The number of questions cannot be 0!',
+            tip11: 'The number of answers for single answer questions does not match the number of questions, please check and retry!',
+            tip12: 'The number of answers for multiple answers questions does not match the number of questions, please check and retry!',
         },
         cpTip1: 'Pick from the question bank',
         cpTip2: 'Pick from the exam paper',
@@ -3441,7 +3441,7 @@ const LANG_EN_US = {
             ac7: 'Homework',
             ac8: 'Self-directed Learning',
             online: 'Online',
-            offline: 'Offline',
+            offline: 'OMR',
             mid: 'In-class',
             noGroup: 'Not grouped',
             noType: 'Not categorized',

+ 3 - 3
TEAMModelOS/ClientApp/public/lang/zh-CN.js

@@ -425,10 +425,10 @@ const LANG_ZH_CN = {
             point: '微能力点',
             artData: '艺术评测',
             student: '素质',
-            studentAll: '学生综合素质评价',
+            studentAll: '学生综合素质评价系统',
             sportData: '体育评测',
             study: '学业',
-            studyAll: '学生学业质量监测',
+            studyAll: '学生学业质量',
             studyMenu: '学情分析',
             art1: '艺术看板',
             art2: '评测设置',
@@ -6011,7 +6011,7 @@ const LANG_ZH_CN = {
             acRecord: '活动记录',
             cusMgt: '课程管理',
             taskList: '我的任务',
-            train: '培训中心',
+            train: '研修中心',
             trainData: '研修数据',
             abilityPoint: '微能力点',
             scTrain: '校本研修',

+ 9 - 9
TEAMModelOS/ClientApp/public/lang/zh-TW.js

@@ -218,7 +218,7 @@ const LANG_ZH_TW = {
         paper: {
             noData: '當前增能項目下暫無檢測試題數據',
             single: '單選',
-            multiple: '選',
+            multiple: '選',
             judge: '是非',
             edit: '編輯試題',
             delete: '刪除試題',
@@ -244,7 +244,7 @@ const LANG_ZH_TW = {
             paper: '自我檢測試卷',
             add: '新增試題',
             single: '單選題',
-            multiple: '選題',
+            multiple: '選題',
             judge: '是非題',
             import: '匯入試題',
             resource: '關聯資源',
@@ -1615,9 +1615,9 @@ const LANG_ZH_TW = {
             tip7: '切換模式將會清除目前試題資料,是否確認切換? ',
             tip8: '最多增加20個試題',
             tip9: '輸入錄音題文字提示',
-            tip10: '试题数量不可为空!',
-            tip11: '单选题答案数与题目数不一致,请检查后重试!',
-            tip12: '多选题答案数与题目数不一致,请检查后重试!',
+            tip10: '試題數量不可為0!',
+            tip11: '單選題答案數與題目數不一致,請檢查後重試!',
+            tip12: '複選題答案數與題目數不一致,請檢查後重試!',
         },
         cpTip1: '從題庫挑選',
         cpTip2: '從試卷挑選',
@@ -3443,7 +3443,7 @@ const LANG_ZH_TW = {
             ac7: '作業活動',
             ac8: '自主學習',
             online: '線上',
-            offline: '線下',
+            offline: '閱卷',
             mid: '課中',
             noGroup: '未分組',
             noType: '未分類',
@@ -5341,7 +5341,7 @@ const LANG_ZH_TW = {
                 itemIndex: '題號',
                 standardIndex: '標準答案/配分',
                 stuAnsIndex: '學生作答/得分',
-                notice: '【-】--答對,【#】--未作答,【?】--選題',
+                notice: '【-】--答對,【#】--未作答,【?】--選題',
                 gradeErr: '成績數據錯誤! '
             },
             correctAnswer: '答對',
@@ -5652,7 +5652,7 @@ const LANG_ZH_TW = {
     },
     // 问卷调查
     survey: {
-        pickTip: '為符合問卷活動題目模板,我們只保留試卷內的單選、選、是非和問答題,確認繼續嗎?',
+        pickTip: '為符合問卷活動題目模板,我們只保留試卷內的單選、選、是非和問答題,確認繼續嗎?',
         pickTip2: '請先選擇試卷',
         pickPaper: '從試卷庫挑選題目',
         noItemTip: '問卷內容不能為空',
@@ -7000,7 +7000,7 @@ const LANG_ZH_TW = {
             examErr: '評量儲存失敗',
             fullInfo: '請完善資訊! ',
             quType1: '單選題',
-            quType2: '選題',
+            quType2: '選題',
             quType3: '是非題',
             quType4: '問答題',
             trainType1: '資訊化教學案例展示與分享',

+ 7 - 3
TEAMModelOS/ClientApp/src/view/signupActivity/infoComponent/skContent.vue

@@ -65,7 +65,7 @@
                         <template #action="{row, index}">
                             <Button type="info" size="small" @click="getTeaInfo(row)" v-show="row.inviteStatus != -2">{{ $t('learnActivity.score.view') }}</Button>
                             <template v-if="contestInfo.modules.includes('review') && viewPermission">
-                                <Button type="success" size="small" style="margin-left: 10px;" @click="manualAssignChange(index)" v-show="row.inviteStatus && row.signContestStatus === 1 && row.reviewContestAssignCount != ruleInfo.taskCount">{{ $t('activity.allocation') }}</Button>
+                                <Button type="success" size="small" style="margin-left: 10px;" @click="manualAssignChange(index)" v-show="row.inviteStatus && row.signContestStatus === 1 && row.reviewContestAssignCount < ruleInfo.taskCount">{{ $t('activity.allocation') }}</Button>
                             </template>
                             <!-- <Button type="error" size="small" @click="deleteApplica(row, index, 'join')">删除</Button> -->
                         </template>
@@ -864,6 +864,10 @@ export default {
             })
         },
         saveAllocationResults() {
+            if(!this.taskKey) {
+                this.isAllocation = false
+                return
+            }
             let params = {
                 grant_type: 'allocation-task-auto-save',
                 activityId: this.actInfo.id,
@@ -1038,8 +1042,8 @@ export default {
             if(item.uploadType === 'file') {
                 this.onPreview(item)
             } else {
-                let url = "https://account.teammodel.cn/?callback=" + 'https://sokrates.teammodel.cn/auth/login/callback-habook' + '?to=' + btoa(item.url)
-                window.open(url, '_blank')
+                // let url = "https://account.teammodel.cn/?callback=" + 'https://sokrates.teammodel.cn/auth/login/callback-habook' + '?to=' + btoa(item.url)
+                window.open(item.url, '_blank')
             }
         },
 		// 分页操作

+ 1 - 1
TEAMModelOS/Controllers/OpenApi/Business/BizExamController.cs

@@ -103,7 +103,7 @@ namespace TEAMModelOS.Controllers
                 List<StuAnswer> stuAnswers = new();
                 await foreach (var item in cosmosClient.GetContainer(Constant.TEAMModelOS, "Student").GetItemQueryIterator<StuAnswer>(
                    queryText: $"select value(c) from c where c.examId = '{id}'",
-                   requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ExamClassResult-{school}") }))
+                   requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Answer-{school}") }))
                 {
                     stuAnswers.Add(item);
                 }

+ 2 - 0
TEAMModelOS/Controllers/System/ImportController.cs

@@ -62,6 +62,8 @@ namespace TEAMModelOS.Controllers
             _option = option?.Value;
 
         }
+     
+
         private static string ReplaceLast(string input, string oldValue, string newValue)
         {
             int index = input.LastIndexOf(oldValue);

+ 241 - 1
TEAMModelOS/Controllers/Third/Moofen/MoofenController.cs

@@ -26,6 +26,14 @@ using TEAMModelOS.SDK.DI;
 using HTEXLib.Helpers.ShapeHelpers;
 using HtmlAgilityPack;
 using Top.Api.Util;
+using DocumentFormat.OpenXml.Drawing.Charts;
+using DocumentFormat.OpenXml.Spreadsheet;
+using TEAMModelOS.SDK.Models.Cosmos.School;
+using OfficeOpenXml;
+using System.IO.Packaging;
+using Microsoft.Azure.Amqp.Framing;
+using DocumentFormat.OpenXml.Office2016.Drawing.ChartDrawing;
+using DocumentFormat.OpenXml.Office2010.Excel;
 namespace TEAMModelOS.Controllers.Third.Moofen
 {
     [ProducesResponseType(StatusCodes.Status200OK)]
@@ -38,13 +46,206 @@ namespace TEAMModelOS.Controllers.Third.Moofen
         private readonly IHttpClientFactory _httpClient;
         private readonly DingDing _dingDing;
         private readonly AzureCosmosFactory _azureCosmos;
-        public MoofenController(AzureCosmosFactory  azureCosmos,CoreAPIHttpService coreAPIHttpService, IConfiguration configuration, IHttpClientFactory httpClient, DingDing  dingDing) 
+        private readonly AzureStorageFactory _azureStorage;
+        public MoofenController(AzureCosmosFactory  azureCosmos,CoreAPIHttpService coreAPIHttpService, IConfiguration configuration, IHttpClientFactory httpClient, DingDing  dingDing,AzureStorageFactory azureStorage) 
         {
             _coreAPIHttpService=coreAPIHttpService;
             _configuration=configuration;
             _httpClient=httpClient;
             _dingDing=dingDing;
             _azureCosmos=azureCosmos;
+            _azureStorage=azureStorage;
+        }
+
+        [HttpPost("moofen/read-excel-knowledge-point")]
+        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
+        public async Task<IActionResult> ReadExcelKnowledgePoint([FromForm] IFormFile[] file)
+        {
+          
+            ExcelPackage.LicenseContext = OfficeOpenXml.LicenseContext.NonCommercial;
+            List<dynamic> period_subject = new List<dynamic>();
+            foreach (var f in file)
+            {
+                List<MoofenKnowledgePointDto> pointDtos = new List<MoofenKnowledgePointDto>();
+                string[] parts = f.FileName.Split(']');
+                string[] periodp = parts[0].Split('[');
+                string[] subjectp = parts[1].Split('[');
+               
+                string period = periodp[1].Trim();
+                string subject = subjectp[1].Trim();
+
+                List<string> titles = new List<string>();
+                List<List<string>> datas = new List<List<string>>();
+                using (ExcelPackage package = new ExcelPackage(f.OpenReadStream()))
+                {
+                    ExcelWorksheets sheets = package.Workbook.Worksheets;
+                    var sheet = sheets.FirstOrDefault();
+                    if (sheet!= null)
+                    {
+                      
+                        var rows = sheet.Dimension.Rows;
+                        var columns = sheet.Dimension.Columns;
+                        for (int r = 1; r <= rows; r++)
+                        {
+                            List<string> data = new List<string>();
+                            for (int c = 1; c <= columns; c++)
+                            {
+                                var value = sheet .GetValue(r, c);
+                                if (r == 1)
+                                {
+                                    if (!string.IsNullOrWhiteSpace($"{value}"))
+                                    {
+                                        titles.Add($"{value}");
+                                    }
+                                    else
+                                    {
+                                        break;
+                                    }
+                                }
+                                else
+                                {
+                                    if (c > titles.Count)
+                                    {
+                                        break;
+                                    }
+                                    else
+                                    {
+                                        data.Add($"{value}");
+                                    }
+                                }
+                            }
+                            if (data.Any())
+                            {
+                                datas.Add(data);
+                            }
+                        }
+                    }
+                }
+            
+                foreach ( var data in datas )
+                {
+                    MoofenKnowledgePointDto knowledgePointDto = new MoofenKnowledgePointDto {  kp1 = data[0] , kp2= data[1], kp3 = data[2], kp4=  data[3], id= data[4]  };
+                    if (!string.IsNullOrWhiteSpace(knowledgePointDto.kp1)) {
+                        knowledgePointDto.level=1;
+                        knowledgePointDto.kp=knowledgePointDto.kp1;
+                    }
+                    if (!string.IsNullOrWhiteSpace(knowledgePointDto.kp2))
+                    {
+                        knowledgePointDto.level=2;
+                        knowledgePointDto.kp=knowledgePointDto.kp2;
+                    }
+                    if (!string.IsNullOrWhiteSpace(knowledgePointDto.kp3))
+                    {
+                        knowledgePointDto.level=3;
+                        knowledgePointDto.kp=knowledgePointDto.kp3;
+                    }
+                    if (!string.IsNullOrWhiteSpace(knowledgePointDto.kp4))
+                    {
+                        knowledgePointDto.level=4;
+                        knowledgePointDto.kp=knowledgePointDto.kp4;
+                    }
+                    pointDtos.Add( knowledgePointDto );
+                }
+                // 构建树形结构
+                var tree = BuildTree(pointDtos);
+                tree.kp=$"{period}-{subject}";
+                var json= tree.children.ToJsonString();
+                string subjectId = string.Empty;
+                string periodId = string.Empty;
+                switch (period) 
+                {
+                    case "小学": periodId="primary"; break;
+                    case "初中": periodId="junior"; break;
+                    case "高中": periodId="senior"; break;
+                }
+                switch (subject)
+                {
+                    case "语文":
+                        subjectId="subject_chinese";
+                        break;
+                    case "数学":
+                        subjectId="subject_math";
+                        break;
+                    case "英语":
+                        subjectId="subject_english";
+                        break;
+                    case "物理":
+                        subjectId="subject_physics";
+                        break;
+                    case "化学":
+                        subjectId="subject_chemistry";
+                        break;
+                    case "生物":
+                        subjectId="subject_biology";
+                        break;
+                    case "政治":
+                        subjectId="subject_politics";
+                        break;
+                    case "历史":
+                        subjectId="subject_history";
+                        break;
+                    case "地理":
+                        subjectId="subject_geography";
+                        break;
+                }
+                period_subject.Add(new { period,subject, periodId,subjectId }) ;
+                await _azureStorage.GetBlobContainerClient(Constant.teammodelos_blob).UploadFileByContainer(json,"third",$"moofen/kp-{periodId}-{subjectId}.json",true);
+            }
+            return Ok(new { period_subject });
+        }
+
+        private   TreeNode BuildTree(List<MoofenKnowledgePointDto> pointDtos)
+        {
+            var root = new TreeNode();
+            var stack = new Stack<TreeNode>();
+            stack.Push(root);
+            foreach (var pointDto in pointDtos)
+            {
+                var node = new TreeNode
+                {
+                    id = pointDto.id,
+                    tid = pointDto.id,
+                    kp = pointDto.kp,
+                    level = pointDto.level
+                };
+                while (stack.Count > 0 && stack.Peek().level >= node.level)
+                {
+                    stack.Pop();
+                }
+                if (stack.Count > 0)
+                {
+                    stack.Peek().children.Add(node);
+                }
+                stack.Push(node);
+            }
+
+            if (root.level==0  && root.children.IsNotEmpty()) {
+               
+                foreach (var node in root.children)
+                {
+                    node.subcids=node.children.Select(x => x.tid).ToList();
+                    var cids = GetChildIds(node);
+                    node.allcids=cids.Count>100 ? new List<string>():cids ;
+                }
+            }
+            return root;
+        }
+
+        static List<string> GetChildIds(TreeNode node)
+        {
+            List<string> childIds = new List<string>();
+            if (node.children != null)
+            {
+                foreach (TreeNode child in node.children)
+                {
+                    childIds.Add(child.tid);
+                    var cids = GetChildIds(child);
+                    childIds.AddRange(cids);
+                    child.allcids=cids.Count>100 ? new List<string>() : cids;
+                    child.subcids=child.children.Select(x => x.tid).ToList();
+                }
+            }
+            return childIds;
         }
 
         [HttpPost("moofen/question")]
@@ -70,6 +271,11 @@ namespace TEAMModelOS.Controllers.Third.Moofen
                 {
                     dict.Add("subject", $"{subject}");
                 }
+                if (json.TryGetProperty("kpIds", out JsonElement kpIds) && kpIds.ValueKind.Equals(JsonValueKind.Array))
+                {
+                    //对方接口数据参数是"kpIds": "[\"221206\"]",
+                    dict.Add("kpIds", $"{kpIds.ToJsonString()}");
+                }
                 if (json.TryGetProperty("grades", out JsonElement grades) && !string.IsNullOrWhiteSpace($"{grades}"))
                 {
                     dict.Add("grades", $"{grades}");
@@ -406,7 +612,41 @@ namespace TEAMModelOS.Controllers.Third.Moofen
             return Ok(new { code = 400 });
         }
     }
+    public class TreeNode
+    {
+       
 
+        public string kp {  get; set; }
+        public string id { get; set; }
+        /// <summary>
+        ///  第三方id
+        /// </summary>
+        public string tid { get; set; }
+        public int level {  get; set; }
+        /// <summary>
+        /// 直接子节点
+        /// </summary>
+        public List<string> subcids { get; set; } = new List<string>();
+        /// <summary>
+        /// 所有子节点
+        /// </summary>
+        public List<string> allcids { get; set; }= new List<string>();
+        public List<TreeNode> children { get; set; } = new List<TreeNode>();
+    }
+
+    public class MoofenKnowledgePointDto
+    {
+        public string id { get; set; }
+        public string kp { get; set; }
+        public int level { get; set; }
+        public string kp1 { get; set; }
+        public string kp2 { get; set; }
+        public string kp3 { get; set; }
+        public string kp4 { get; set; }
+      
+     
+      
+    }
     public class MoofenQuestion
     {
         public int difficulty { get; set; }