Prechádzať zdrojové kódy

[v5.0.230512.2] Merge branch 'louise' into ChatGPT_backendAPI

# Conflicts:
#	HiTeachCC/ClientApp/src/locale/lang/en-US/index.js
#	HiTeachCC/ClientApp/src/locale/lang/zh-CN/index.js
#	HiTeachCC/ClientApp/src/locale/lang/zh-TW/index.js
#	HiTeachCC/ClientApp/src/store/module/preferences.js
#	HiTeachCC/ClientApp/src/views/Board.vue
#	package-lock.json
Louise lin 2 rokov pred
rodič
commit
a18e8a7907

+ 167 - 61
HiTeachCC/ClientApp/src/assets/css/Board.less

@@ -1,4 +1,4 @@
-@import 'color.less';
+@import "color.less";
 .screen-note {
   display: none;
 }
@@ -124,50 +124,48 @@
     border-bottom: 0.5px solid rgb(201, 201, 201);
   }
 }
-.stickerbg-setting{
-    padding:0 8px;
-    .menu-title {
-      font-weight: bolder;
-      text-align: left;
-      
-    }
-    .set-nocolor{
-      color:rgb(26, 26, 26) !important;
-    }
-    .nocolor-icon{
-      font-size: 32px;
-      line-height: 40px;
-      color:gray;
-      
-    }
-    .transparent-btn{
-      height: 40px !important;
-      width: 40px !important;
-      border: 4px solid white;
-      margin: 8px;
-      top:-14px !important;
-      display: inline-block;
-      position: relative;
-      box-shadow: 0px 0px 2px gray;
-      border-radius: 50%;
-      cursor: pointer;  
-    }
-    .color-btn {
-      height: 36px !important;
-      width: 36px !important;
-      border: 2px solid white;
-      margin: 8px;
-      margin-top:24px;
-      display: inline-block;
-      box-shadow: 0px 0px 2px gray;
-      border-radius: 50%;
-      cursor: pointer;
-    }
-    .select-color {
-      border: 0px solid rgba(223, 223, 223, 0);
-      box-shadow: 0px 0px 2px gray;
-    }
+.stickerbg-setting {
+  padding: 0 8px;
+  .menu-title {
+    font-weight: bolder;
+    text-align: left;
+  }
+  .set-nocolor {
+    color: rgb(26, 26, 26) !important;
+  }
+  .nocolor-icon {
+    font-size: 32px;
+    line-height: 40px;
+    color: gray;
   }
+  .transparent-btn {
+    height: 40px !important;
+    width: 40px !important;
+    border: 4px solid white;
+    margin: 8px;
+    top: -14px !important;
+    display: inline-block;
+    position: relative;
+    box-shadow: 0px 0px 2px gray;
+    border-radius: 50%;
+    cursor: pointer;
+  }
+  .color-btn {
+    height: 36px !important;
+    width: 36px !important;
+    border: 2px solid white;
+    margin: 8px;
+    margin-top: 24px;
+    display: inline-block;
+    box-shadow: 0px 0px 2px gray;
+    border-radius: 50%;
+    cursor: pointer;
+  }
+  .select-color {
+    border: 0px solid rgba(223, 223, 223, 0);
+    box-shadow: 0px 0px 2px gray;
+  }
+}
 .limit-text {
   text-align: center;
   font-size: 12px;
@@ -183,6 +181,106 @@
     line-height: 24px;
   }
 }
+.page-btn{
+  cursor: pointer;
+  position: relative;
+  display: inline-block;
+  white-space: nowrap;
+  padding: 5px 10px;
+  border-radius: 5px;
+  margin-top: 20px;
+  font-size: 16px;
+  background-color: @btn-color;
+  color: white;
+ }
+ .cancel-btn{
+  color: black;
+  background-color: #d8d8d8;
+  margin-bottom: 10px;
+ }
+//學生名單
+.memberlist-page {
+  padding: 10px;
+  position: relative;
+  .memberfreebtn-light{
+    color: blue;
+    border: 1px solid blue !important;
+  }
+  .memberfreebtn {
+    width: 100%;
+    cursor: pointer;
+    margin: 10px 0px;
+    display: inline-block !important;
+    border: 1px solid #ccc ;
+    background-color: #fcfcfc;
+    border-radius: 4px;
+    padding: 10px;
+    font-size: 20px;
+    &:hover {
+      // background-color: darken(#fcfcfc, 10%);
+      color: blue;
+      border: 1px solid blue !important;
+    }
+  }
+
+  .memberlist-wrap {
+    display: flex;
+    background-color: #fcfcfc;
+    border-width: 2px;
+    border-radius: 4px;
+    border: 1px solid #ccc !important;
+    
+    .listitems {
+      flex:1;
+      .listitem-light{
+        color: blue;
+        border: 1px solid blue !important;
+        border-bottom: 1px solid blue !important;
+      }
+      .listitem {
+        width: 100%;
+        cursor: pointer;
+        display: inline-block !important;
+        background-color: #fcfcfc;
+        border-width: 2px;
+        border: 1px solid transparent ;
+        border-bottom: 1px solid #ccc ;
+        padding: 5px;
+        font-size: 16px;
+        &:hover {
+          // background-color: darken(#fcfcfc, 10%);
+          color: blue;
+          border: 1px solid blue !important;
+        }
+      }
+    }
+    .listDetail {
+      background-color: #fcfcfc;
+      border-width: 2px;
+      border-radius: 4px;
+      border-left: 1px solid #ccc !important;
+      flex: 1;
+      padding: 10px;
+      text-align: left;
+      background-color: #fcfcfc;
+      height:180px;
+      overflow: auto;
+      .list-stu{
+        font-size: 14px;
+      }
+      .list-info{
+        text-align: left;
+        margin-top: 10%;
+        color: gray;
+        font-size: 14px;
+        p{
+          margin-top:5px;
+        }
+      }
+    }
+   
+  }
+}
 .defaultpdf-title {
   .attach-icon {
     position: relative;
@@ -273,6 +371,9 @@
     line-height: 0px;
     z-index: -1;
   }
+  .memberlist-icon{
+    stroke: #000;
+  }
 
   .section {
     display: flex;
@@ -294,19 +395,22 @@
         //background-color: darken(#fcfcfc, 10%);
         color: blue;
         border: 1px solid blue;
+        .memberlist-icon{
+          stroke: blue !important;
+        }
+       
       }
       p {
         font-weight: bold;
       }
     }
     label.hicard-btn {
-      input[type='file'] {
+      input[type="file"] {
         display: none;
       }
     }
   }
 }
-
 .closebtn {
   cursor: pointer;
   position: absolute;
@@ -465,10 +569,10 @@ svg {
     .pdfexport-page {
       font-size: 20px;
       margin-top: 10px;
-      color:gray;
+      color: gray;
     }
     .render-bar {
-      margin: 10px ;
+      margin: 10px;
     }
     .pdfexport-group {
       display: flex;
@@ -524,7 +628,7 @@ svg {
       margin: 20px -20px;
       overflow: auto;
       @media screen and (max-height: 500px) {
-         height: 40vh !important;
+        height: 40vh !important;
       }
     }
     .textinput {
@@ -566,14 +670,17 @@ svg {
     }
   }
 }
+
+
+
 .askPasteWhichPage-view {
   position: absolute;
   display: block;
-  width: calc(~'100% - 17px');
+  width: calc(~"100% - 17px");
   background-color: rgba(0, 0, 0, 0.4);
   border-radius: 5px;
   color: black;
-  height: calc(~'100% - 17px');
+  height: calc(~"100% - 17px");
   z-index: 100;
   .confirm-card {
     top: 20%;
@@ -610,7 +717,7 @@ svg {
 }
 .workCollect-full {
   position: fixed;
-  width: calc(~'100% + 17px');
+  width: calc(~"100% + 17px");
   height: auto;
   /* border:1px solid #ccc;*/
   top: 0px !important;
@@ -1190,7 +1297,7 @@ svg {
 }
 @media screen and (max-height: 768px) {
   .student-List {
-    .qrcode-note-wrap{
+    .qrcode-note-wrap {
       overflow-y: auto;
     }
     .student-group {
@@ -1232,10 +1339,9 @@ svg {
   }
 }
 @media screen and (max-height: 600px) {
-  
   .student-List {
     max-height: 520px;
-    .qrcode-note-wrap{
+    .qrcode-note-wrap {
       max-height: 10%;
       overflow-y: auto;
     }
@@ -1501,7 +1607,7 @@ svg {
     }
 
     .pick-default-img {
-      background: url('../../assets/img/profile-user.svg') no-repeat;
+      background: url("../../assets/img/profile-user.svg") no-repeat;
       background-size: contain;
       z-index: 100;
       width: 160px;
@@ -1541,20 +1647,20 @@ svg {
   box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
   // transition: 0.1s;
 }
-.obj-menu{
+.obj-menu {
   background-color: #fff;
   padding: 5px 0px;
   box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
-  color:black;
+  color: black;
   position: absolute;
   z-index: 11;
   border-radius: 4px;
   cursor: pointer;
   font-weight: bold;
-  & p{
+  & p {
     padding: 5px 10px;
   }
-  & p:hover{
+  & p:hover {
     background-color: darken(rgb(243, 243, 243), 10%);
   }
-}
+}

+ 32 - 0
HiTeachCC/ClientApp/src/assets/icons/svg/memberlist.svg

@@ -0,0 +1,32 @@
+<?xml version="1.0" ?>
+
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+
+<title/>
+
+<g id="Complete">
+
+<g id="F-File">
+
+<g id="Text">
+
+<g>
+
+<path d="M18,22H6a2,2,0,0,1-2-2V4A2,2,0,0,1,6,2h7.1a2,2,0,0,1,1.5.6l4.9,5.2A2,2,0,0,1,20,9.2V20A2,2,0,0,1,18,22Z" fill="none" id="File" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+
+<line fill="none"  stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="7.9" x2="16.1" y1="17.5" y2="17.5"/>
+
+<line fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="7.9" x2="16.1" y1="13.5" y2="13.5"/>
+
+<line fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="8" x2="13" y1="9.5" y2="9.5"/>
+
+</g>
+
+</g>
+
+</g>
+
+</g>
+
+</svg>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 6 - 0
HiTeachCC/ClientApp/src/assets/icons/svg/question.svg


+ 3 - 0
HiTeachCC/ClientApp/src/assets/icons/svg/trash-alt-solid.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) -->
+    <path d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"/>
+</svg>

+ 120 - 99
HiTeachCC/ClientApp/src/components/AddImgBox.vue

@@ -57,8 +57,6 @@
 </template>
 
 <script>
-const de = require('@/utils/lib1.js')
-import enc from '@/utils/enc.js'
 import { mapGetters } from 'vuex'
 
 export default {
@@ -91,6 +89,18 @@ export default {
   mounted() {
     this.importAll(require.context('../assets/HT_resource/Openclipart', true, /\.png$/), this.images)
     this.importAll(require.context('../assets/HT_resource/Backgrounds', true, /\.jpg$/), this.bgImages)
+    let that=this
+    document.addEventListener('paste',()=> {
+      const isShowTextAnsPop=this?.$optionView?.showTextAnspop
+      if(!this.$store.state.showtextInuptCard&&!isShowTextAnsPop){
+      this.$store.state.mode = 'check'
+      that.getClipboardData()
+      that.$nextTick(()=>{
+        setTimeout(()=>{
+          that.pastContent()
+        },500)
+      })}
+    })
   },
   watch: {
     freecolor(value) {
@@ -219,6 +229,7 @@ export default {
               that.$q.loading.hide()
               console.log(res, '上传Resource且變更檔名成功后的返回')
               let fileUrl = res.url
+              let currentIndex=layer?.children ? JSON.parse(JSON.stringify(layer?.children)).length : 1
               // console.log(fileUrl)
               // console.log(stage)
               Konva.Image.fromURL(fileUrl + '?' + blobUrl.sas_read, function(image) {
@@ -230,7 +241,9 @@ export default {
                   width: image.width() * scaleRatio,
                   height: image.height() * scaleRatio,
                   draggable: true,
-                  src: fileUrl
+                  src: fileUrl,
+                  uuid:that.$jsFn.getUUID(),
+                  index: currentIndex
                 })
 
                 // stage.find('Transformer').destroy()
@@ -246,14 +259,11 @@ export default {
                   padding: 10,
                   name: 'default'
                 })
-                stage.find('Transformer').forEach(function(ele, i) {
-                  if (ele.attrs.name == 'default') {
-                    ele.destroy()
-                  }
-                })
+                that.$toolbox.removeTransformer()
                 
 
                 layer.add(image)
+                that.$toolbox.saveUndoHistory('add',image)//儲存undo記錄
                 layer.add(tr)
                 tr.nodes([image])
                 that.$parent.addMenuBtnToTr(tr, image) 
@@ -309,6 +319,7 @@ export default {
           this.$q.loading.hide()
           console.log(res, '上传Resource且變更檔名成功后的返回')
           let fileUrl = res.url
+          let currentIndex=layer?.children ? JSON.parse(JSON.stringify(layer?.children)).length : 1
           // console.log(fileUrl)
           // console.log(stage)
           Konva.Image.fromURL(fileUrl + '?' + blobUrl.sas_read, function(image) {
@@ -320,7 +331,9 @@ export default {
               width: image.width() * scaleRatio,
               height: image.height() * scaleRatio,
               draggable: true,
-              src: fileUrl
+              src: fileUrl,
+              uuid:that.$jsFn.getUUID(),
+              index:currentIndex
             })
 
             // stage.find('Transformer').destroy()
@@ -336,12 +349,9 @@ export default {
               padding: 10,
               name: 'default'
             })
-            stage.find('Transformer').forEach(function(ele, i) {
-              if (ele.attrs.name == 'default') {
-                ele.destroy()
-              }
-            })
+            that.$toolbox.removeTransformer()
             layer.add(image)
+            that.$toolbox.saveUndoHistory('add',image)//儲存undo記錄
             layer.add(tr)
             tr.nodes([image])
             that.$parent.addMenuBtnToTr(tr, image) 
@@ -424,6 +434,7 @@ export default {
     //剪貼簿文字
     addTextBox(target) {
       let that=this
+      let currentIndex=layer?.children ? JSON.parse(JSON.stringify(layer?.children)).length : 1
       this.$store.state.showtextInuptCard = false
       let layer = this.$store.state.layer
       document.body.classList.remove('cursor-fdj')
@@ -438,7 +449,9 @@ export default {
         width: window.innerWidth / 2,
         lineHeight: 1,
         height: 'auto',
-        name: 'text'
+        name: 'text',
+        uuid:that.$jsFn.getUUID(),
+        index:currentIndex
       })
 
       this.$store.state.mode = 'check'
@@ -455,6 +468,7 @@ export default {
         name: 'default'
       })
       layer.add(textNode)
+      that.$toolbox.saveUndoHistory('add',textNode)//儲存undo記錄
       layer.add(tr)
       tr.nodes([textNode])
       that.$parent.addMenuBtnToTr(tr, textNode) 
@@ -504,97 +518,103 @@ export default {
       let that = this
       this.clipboardmixImgs = []
       this.clipboardmixText = ''
-      if (navigator.clipboard) {
-        if (Object.getPrototypeOf(navigator.clipboard).read) {
-          let items = await navigator.clipboard.read()
-          for (let item of items) {
-            // console.log(item)
-            //情境1:貼上html
-            if (item.types.includes('text/html')) {
-              let reader = new FileReader()
-              reader.addEventListener('load', loadEvent => {
-                console.log(reader.result)
-                that.clipboardmixText = reader.result
-                  .replace(/\u00a0/g, '')
-                  .replace(/<[^>]*>/g, '')
-                  .replace(/&nbsp;/g, '')
+      try{
+        if (navigator.clipboard) {
+          if (Object.getPrototypeOf(navigator.clipboard).read) {
+            let items = await navigator.clipboard.read()
+            for (let item of items) {
+              // console.log(item)
+              //情境1:貼上html
+              if (item.types.includes('text/html')) {
+                let reader = new FileReader()
+                reader.addEventListener('load', loadEvent => {
+                  console.log(reader.result)
+                  that.clipboardmixText = reader.result
+                    .replace(/\u00a0/g, '')
+                    .replace(/<[^>]*>/g, '')
+                    .replace(/&nbsp;/g, '')
 
-                that.clipboardData = `<p>` + that.clipboardmixText + '</p>'
-                that.clipboardImg = ''
+                  that.clipboardData = `<p>` + that.clipboardmixText + '</p>'
+                  that.clipboardImg = ''
 
-                //取得圖片連結
-                let tmp = document.createElement('div')
-                tmp.innerHTML = reader.result
-                let ImgItems = tmp.querySelectorAll('img')
+                  //取得圖片連結
+                  let tmp = document.createElement('div')
+                  tmp.innerHTML = reader.result
+                  let ImgItems = tmp.querySelectorAll('img')
 
-                for (let ImgItem of ImgItems) {
-                  // console.log(ImgItem.getAttribute('src').slice(0, 16) == 'data:image/jpeg;')
-                  if (ImgItem.getAttribute('src').slice(0, 15) == 'data:image/png;' || ImgItem.getAttribute('src').slice(0, 16) == 'data:image/jpeg;') {
-                    if (that.clipboardmixImgs.length == 1) {
-                      that.clipboardmixImgs[0] = ImgItem.getAttribute('src')
+                  for (let ImgItem of ImgItems) {
+                    // console.log(ImgItem.getAttribute('src').slice(0, 16) == 'data:image/jpeg;')
+                    if (ImgItem.getAttribute('src').slice(0, 15) == 'data:image/png;' || ImgItem.getAttribute('src').slice(0, 16) == 'data:image/jpeg;') {
+                      if (that.clipboardmixImgs.length == 1) {
+                        that.clipboardmixImgs[0] = ImgItem.getAttribute('src')
+                      } else {
+                        that.clipboardmixImgs.push(ImgItem.getAttribute('src'))
+                      }
+                      that.clipboardData = that.clipboardData + ImgItem.outerHTML
                     } else {
-                      that.clipboardmixImgs.push(ImgItem.getAttribute('src'))
+                      that.clipboardData = that.clipboardmixText + '\t' + ImgItem.getAttribute('src') + '\n'
+                      that.clipboardmixText = that.clipboardmixText + '\t' + ImgItem.getAttribute('src') + '\n'
                     }
-                    that.clipboardData = that.clipboardData + ImgItem.outerHTML
-                  } else {
-                    that.clipboardData = that.clipboardmixText + '\t' + ImgItem.getAttribute('src') + '\n'
-                    that.clipboardmixText = that.clipboardmixText + '\t' + ImgItem.getAttribute('src') + '\n'
+                    console.log(ImgItem)
+                  }
+                  if (that.clipboardmixText.length > 500) {
+                    that.$Message.warning(that.$t("board.addImgBox['字串長度提示']"))
+                    that.clipboardData = that.clipboardmixText.slice(0, 499) + '...'
+                    that.clipboardmixText = that.clipboardmixText.slice(0, 499) + '...'
                   }
-                  console.log(ImgItem)
-                }
-                if (that.clipboardmixText.length > 500) {
-                  that.$Message.warning(that.$t("board.addImgBox['字串長度提示']"))
-                  that.clipboardData = that.clipboardmixText.slice(0, 499) + '...'
-                  that.clipboardmixText = that.clipboardmixText.slice(0, 499) + '...'
-                }
-                //   let tmp = document.createElement('div')
-                //   tmp.innerHTML = reader.result
-                //   let ImgItems = tmp.querySelectorAll('img')
-                //   for (let ImgItem of ImgItems) {
-                //     that.clipboardmixImgs.push(ImgItem.getAttribute('src'))
-                //     that.clipboardData = that.clipboardData + ImgItem.outerHTML
+                  //   let tmp = document.createElement('div')
+                  //   tmp.innerHTML = reader.result
+                  //   let ImgItems = tmp.querySelectorAll('img')
+                  //   for (let ImgItem of ImgItems) {
+                  //     that.clipboardmixImgs.push(ImgItem.getAttribute('src'))
+                  //     that.clipboardData = that.clipboardData + ImgItem.outerHTML
 
-                //     console.log(ImgItem)
-                //   }
-              })
-              reader.readAsText(await item.getType('text/html'))
-              that.currentClipBoardDataType = 'html'
-            }
-            //情境2:貼上純文字
-            else if (item.types.includes('text/plain')) {
-              let reader = new FileReader()
-              reader.addEventListener('load', loadEvent => {
-                that.clipboardData = ''
-                that.clipboardImg = ''
-                that.clipboardData = reader.result
+                  //     console.log(ImgItem)
+                  //   }
+                })
+                reader.readAsText(await item.getType('text/html'))
+                that.currentClipBoardDataType = 'html'
+              }
+              //情境2:貼上純文字
+              else if (item.types.includes('text/plain')) {
+                let reader = new FileReader()
+                reader.addEventListener('load', loadEvent => {
+                  that.clipboardData = ''
+                  that.clipboardImg = ''
+                  that.clipboardData = reader.result
 
-                if (that.clipboardmixText.length > 500) {
-                  that.$Message.warning(that.$t("board.addImgBox['字串長度提示']"))
-                  that.clipboardData = that.clipboardmixText.slice(0, 499) + '...'
-                }
-              })
-              reader.readAsText(await item.getType('text/plain'))
-              that.currentClipBoardDataType = 'text'
-            }
+                  if (that.clipboardmixText.length > 500) {
+                    that.$Message.warning(that.$t("board.addImgBox['字串長度提示']"))
+                    that.clipboardData = that.clipboardmixText.slice(0, 499) + '...'
+                  }
+                })
+                reader.readAsText(await item.getType('text/plain'))
+                that.currentClipBoardDataType = 'text'
+              }
 
-            //情境3:貼上圖片
-            else {
-              const IMAGE_MIME_REGEX = /^image\/(p?jpeg|gif|png)$/i
-              item.types.forEach(type => {
-                if (IMAGE_MIME_REGEX.test(type)) {
-                  console.log(type)
-                  that.loadImage(item, type)
-                  that.currentClipBoardDataType = 'Image'
-                  return
-                }
-              })
+              //情境3:貼上圖片
+              else {
+                const IMAGE_MIME_REGEX = /^image\/(p?jpeg|gif|png)$/i
+                item.types.forEach(type => {
+                  if (IMAGE_MIME_REGEX.test(type)) {
+                    console.log(type)
+                    that.loadImage(item, type)
+                    that.currentClipBoardDataType = 'Image'
+                    return
+                  }
+                })
+              }
             }
+          } else {
+            this.$Message.warning(this.$t("board.addImgBox['瀏覽器版本不支援']"))
           }
         } else {
           this.$Message.warning(this.$t("board.addImgBox['瀏覽器版本不支援']"))
         }
-      } else {
-        this.$Message.warning(this.$t("board.addImgBox['瀏覽器版本不支援']"))
+      }
+      catch(err){
+       if(err.toString()=='NotAllowedError: Read permission denied.')
+       this.$Message.warning(this.$t("board.addImgBox['瀏覽器不允許讀剪貼簿']"))
       }
     },
     //插入素材庫的圖片
@@ -604,6 +624,7 @@ export default {
       let layer = this.$store.state.layer
       let imageObj = new Image()
       let finallink = Imgurl.slice(0, 14) != 'data:image/png' ? window.location.origin + Imgurl : Imgurl
+      let currentIndex=layer?.children ? JSON.parse(JSON.stringify(layer?.children)).length : 1
 
       imageObj.onload = function() {
         // console.log(imageObj)
@@ -614,7 +635,9 @@ export default {
           width: imageObj.width * 0.4,
           height: imageObj.height * 0.4,
           draggable: true,
-          src: finallink
+          src: finallink,
+          uuid:that.$jsFn.getUUID(),
+          index:currentIndex
         })
         // stage.find('Transformer').destroy()
         let tr = new Konva.Transformer({
@@ -629,13 +652,10 @@ export default {
           padding: 10,
           name: 'default'
         })
-        stage.find('Transformer').forEach(function(ele, i) {
-          if (ele.attrs.name == 'default') {
-            ele.destroy()
-          }
-        })
+        that.$toolbox.removeTransformer()
        
         layer.add(image)
+        that.$toolbox.saveUndoHistory('add',image)//儲存undo記錄
         layer.add(tr)
         tr.nodes([image])
         that.$parent.addMenuBtnToTr(tr, image) 
@@ -754,7 +774,8 @@ export default {
           height: image.height() * ratio,
           // image: imageObj,
           src: finallink,
-          name: 'boardPdfBg'
+          name: 'boardPdfBg',
+          index:0,
         })
 
         layer.add(image)

+ 6 - 6
HiTeachCC/ClientApp/src/components/Chart/OptionView.vue

@@ -61,7 +61,7 @@
         <div :title="$t('board.optionView.停止作答')" class="listicon AnswerShow" v-show="this.$store.state.IRSswitch == true && this.$store.state.currentState != '' && this.$store.state.currentState != 'irsImmediatelyEnd' && this.$store.state.currentState != 'irsTextEnd'" @click="stopIRS()"><svg-icon icon-class="stop" class="stop-icon" /></div>
 
         <!-- <div class="resetAns listicon" @click="againanswer" v-show="current != 'bar' && current != 'pie'"><svg-icon icon-class="refresh" /></div> -->
-
+        <!--文字題設定答案-->
         <div class="textans-pop" v-show="currentIRS() == 'irsText' && showTextAnspop == true">
           <textarea v-model="currentAns" maxlength="200" />
           <button class="textans-btn" @click="showTextAns()">OK</button>
@@ -391,6 +391,7 @@ import Result from './../Chart/result.vue'
 import draggable from 'vuedraggable'
 import html2canvas from 'html2canvas'
 import { mapGetters } from 'vuex'
+import Vue from 'vue'
 
 export default {
   components: {
@@ -532,6 +533,8 @@ export default {
     this.studentss()
   },
   mounted() {
+    Vue.prototype.$optionView=this
+    
     let that = this
     this.hiteachccorigin = window.location.origin
     this.$nextTick(() => {
@@ -1011,6 +1014,7 @@ export default {
       } else {
         this.current = type
         this.status = type
+        if(type=='text'&&this.currentAns.trim()!='')this.showTextAns() //文字題二次作答翻牌對答案
       }
     },
     //处理学生排行榜数据
@@ -1475,11 +1479,7 @@ export default {
               padding: 10,
               name: 'default'
             })
-            stage.find('Transformer').forEach(function (ele, i) {
-              if (ele.attrs.name == 'default') {
-                ele.destroy()
-              }
-            })
+            that.$toolbox.removeTransformer()
             layer.add(image)
             layer.add(tr)
             tr.nodes([image])

+ 797 - 0
HiTeachCC/ClientApp/src/components/EditMemberList.vue

@@ -0,0 +1,797 @@
+<template>
+  <div class="editmemberlist-view" v-show="showEditMemberList">
+    <!--編輯單個學生-->
+    <div class="editmemberlist-card editstudent-card" v-if="showEditStudent">
+      <p class="editstudent-title">{{ editStudentMode == "edit" ? $t("memberlist['編輯學生']") : $t("memberlist['新增學生']") }}</p>
+      <div class="input-wrap">
+        <label> {{ $t("memberlist['姓名']") }}</label
+        ><input type="text" v-model="tempEditName" maxlength="50" />
+      </div>
+      <div class="input-wrap">
+        <label> {{ $t("memberlist['座號']") }}</label
+        ><input type="number" v-model="tempEditSeatID" oninput="javascript: if (this.value.length > this.maxLength) this.value = this.value.slice(0, this.maxLength);" @keydown="checkUserKeyboardInputSeatID($event)" min="1" max="999" maxlength="3" />
+      </div>
+      <div class="errmsg" v-show="showStudentErrMsgNum != 0">
+        <p v-show="showStudentErrMsgNum == 1">{{ $t("memberlist['座號重複']") }}</p>
+        <p v-show="showStudentErrMsgNum == 2">{{ $t("memberlist['座號不可為空或輸入無法辨認的值']") }}</p>
+        <p v-show="showStudentErrMsgNum == 3">{{ $t("memberlist['姓名不可為空']") }}</p>
+      </div>
+      <div class="editmemberlistbtn-group">
+        <div class="editmemberlist-btn" @click="updateStudent()">{{ $t("memberlist['確定']") }}</div>
+        <div class="editmemberlist-btn editmemberlist-cancelbtn" @click="cancelEditStudent()">{{ $t("memberlist['取消']") }}</div>
+      </div>
+    </div>
+    <!--修改名單名稱-->
+    <div class="editmemberlist-card editstudent-card" v-show="showEditListName">
+      <p class="editstudent-title">{{ $t("memberlist['修改名單名稱']") }}</p>
+      <div class="input-wrap edit-listname"><input type="text" v-model="tempEditListName" maxlength="50" /></div>
+      <div class="errmsg" v-show="showEditListNameMsg">{{ $t("memberlist['表單名稱不可為空']") }}</div>
+      <div class="editmemberlistbtn-group">
+        <div class="editmemberlist-btn" @click="updateListName()">{{ $t("memberlist['確定']") }}</div>
+        <div class="editmemberlist-btn editmemberlist-cancelbtn" @click="cancelEditListName()">{{ $t("memberlist['取消']") }}</div>
+      </div>
+    </div>
+
+    <!--確認刪除名單-->
+    <div class="editmemberlist-card editstudent-card" v-show="showCorfirmDelete">
+      <p class="editstudent-title">{{ $t("memberlist['刪除']") }}{{ $parent.currentMemberList.listName }}</p>
+      <p class="warndeletemsg">{{ $t("memberlist['確定刪除後這個名單將無法復原']") }}</p>
+      <div class="editmemberlistbtn-group">
+        <div class="editmemberlist-btn" @click="deleteMemberList()">{{ $t("memberlist['確定']") }}</div>
+        <div class="editmemberlist-btn editmemberlist-cancelbtn" @click="showCorfirmDelete = false">{{ $t("memberlist['取消']") }}</div>
+      </div>
+    </div>
+
+    <!--自訂學生名單總覽-->
+    <div class="editmemberlist-card" v-show="!showAddMemberList && !showEditStudent && !showCorfirmDelete && !showEditListName">
+      <div class="closebtn" @click="closeEditMemberList()"><img src="@/statics/iconsvg/interaction/Close.svg" class="q-icon" /></div>
+      <p class="editmemberlist-title">{{ $t("memberlist['自訂學生名單']") }}</p>
+      <!--選單控制按鈕外部-->
+      <div class="editmemberlist-rightbtns" v-show="!isCustomMemberlistEmpty()">
+        <div class="edit-btn" @click="openAddMemberList()"><svg-icon icon-class="new-page" /></div>
+        <div class="edit-btn" @click="openEditListName()" v-show="Object.keys($parent.currentMemberList).length > 0"><font-awesome-icon icon="pencil-alt" /></div>
+        <div class="delete-btn" @click="showCorfirmDelete = true"><svg-icon icon-class="trash-alt-solid" /></div>
+      </div>
+      <div class="editmemberlist-empty" v-show="isCustomMemberlistEmpty()">
+        <div @click="openAddMemberList()"><svg-icon icon-class="new-page" class="add-icon" /><br />{{ $t("memberlist['創建名單']") }}</div>
+      </div>
+      <div class="memberlist-wrap" v-if="!isCustomMemberlistEmpty()">
+        <div class="listitems">
+          <div class="listitem" v-for="(list, index) in $parent.customMemberlist" :key="list.listName + index" @click="setCurrentMemberList(list)" :class="{ 'listitem-light': $parent.currentMemberList.listID == list.listID }">
+            {{ list.listName }}
+          </div>
+        </div>
+        <div class="listDetail" :style="{ 'min-height': tableHeight + 'px' }">
+          <Table :fixed-header="true" @on-row-click="setEditStudent" :row-class-name="hintSelectedRow" v-show="Object.keys($parent.currentMemberList).length >= 0" :height="tableHeight - 41" :no-data-text="`<p>${$t('memberlist.暫無數據')}</p>`" :columns="columnsTitle" :data="$parent.currentMemberList.list"></Table>
+          <!--選單控制按鈕內部-->
+          <div class="editmemberlist-bottombtns" v-show="!isCustomMemberlistEmpty()">
+            <div class="edit-btn" @click="addStudent()"><svg-icon icon-class="new-page" /></div>
+            <div class="edit-btn" v-show="Object.keys(currentEditStudent).length !== 0" @click="editStudent()"><font-awesome-icon icon="pencil-alt" /></div>
+            <div class="delete-btn" v-show="Object.keys(currentEditStudent).length !== 0" @click="deleteStudent()"><svg-icon icon-class="trash-alt-solid" /></div>
+          </div>
+        </div>
+      </div>
+      <div class="errmsg" v-if="showAddListReachMax">{{ $t("memberlist['自訂名單已達上限提示字']") }}</div>
+      <div class="errmsg" v-if="showAddStudentReachMax">{{ $t("memberlist['新增單筆名單人數達上限提示字']") }}</div>
+    </div>
+    <!--新增學生名單-->
+    <div class="editmemberlist-card addmember-card" v-show="showAddMemberList && !showEditStudent">
+      <div v-show="showEditHint">
+        <div class="edit-tooltip-triangle"></div>
+        <div class="edit-tooltip">
+          <div class="close-btn" @click="showEditHint = false"><svg-icon icon-class="Close" /></div>
+          <p v-html="hintInfo"></p>
+          <div class="tooltip-btn" @click="useSampleList()">{{ $t("memberlist.複製範例") }}</div>
+        </div>
+      </div>
+
+      <input type="text" class="add-title" v-model="currentEditMemberListName" :placeholder="$t('memberlist.新建的學生名單')" maxlength="50" />
+      <div class="example-btn" @click="showEditHint = !showEditHint"><svg-icon icon-class="question" /></div>
+      <div class="addmember-area">
+        <textarea class="left-part" v-model="addmemberlisttext" name="addmember" id="" cols="30" rows="10" :placeholder="$t('memberlist.輸入名單資料')"></textarea>
+        <div class="right-part">
+          <Table :fixed-header="true" :height="tableHeight" :no-data-text="`<p>${$t('memberlist.暫無數據')}</p>`" :columns="columnsTitle" :data="tempAddMemberList" :row-class-name="hintBugRow"></Table>
+        </div>
+      </div>
+      <div v-show="addMemberListErrMsg.length > 0" class="errmsg">
+        <div v-for="text in addMemberListErrMsg" :key="text">
+          {{ text }}
+        </div>
+      </div>
+      <div></div>
+      <div v-show="showAddMemberAutoSeatIDMsg" class="hintmsg">
+        {{ $t("memberlist.自動編號提示字") }}
+      </div>
+      <div class="editmemberlistbtn-group">
+        <div class="editmemberlist-btn" @click="checkAddMemberList()">{{ $t("memberlist.預覽") }}</div>
+        <div class="editmemberlist-btn" @click="savecustomMemberlist()" :class="{ 'editmemberlist-disabledBtn': addMemberListErrMsg.length > 0 || currentEditMemberListName == '' || addmemberlisttext == '' || tempAddMemberList.length == 0 }">{{ $t("memberlist.儲存預覽表格") }}</div>
+        <div class="editmemberlist-btn" @click="showAddMemberList = false">{{ $t("memberlist['取消']") }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "EditMemberList",
+  data() {
+    return {
+      hintInfo: this.$t("memberlist['名單範例模板']"),
+      showEditMemberList: false,
+      showAddMemberList: false,
+      showEditStudent: false,
+      currentEditMemberListName: "",
+      addmemberlisttext: "",
+      columnsTitle: [
+        {
+          title: this.$t("memberlist['姓名']"),
+          key: "memberName",
+          sortable: true,
+          ellipsis: true,
+          tooltip: true,
+        },
+        {
+          title: this.$t("memberlist['座號']"),
+          key: "seatID",
+          sortable: true,
+        },
+      ],
+      tempAddMemberList: [],
+      addMemberListErrMsg: [],
+      showAddMemberAutoSeatIDMsg: false,
+      addMemberListErrRow: [], // 存放預覽有問題的行數
+      editMemberListMode: "Init", //Init or Menu
+      showEditHint: false,
+      currentEditStudent: {},
+      tempEditName: "",
+      tempEditSeatID: -1,
+      tempEditListName: "",
+      editStudentMode: "edit",
+      showStudentErrMsgNum: 0,
+      showCorfirmDelete: false,
+      showEditListName: false,
+      showEditListNameMsg: false,
+      showAddListReachMax: false,
+      showAddStudentReachMax: false,
+    };
+  },
+  computed: {
+    tableHeight() {
+      return window.innerHeight * 0.5 < 250 ? 250 : window.innerHeight * 0.5;
+    },
+  },
+  methods: {
+    isCustomMemberlistEmpty() {
+      if (this.$parent.customMemberlist) {
+        return this.$parent.customMemberlist?.length == 0;
+      } else return true;
+    },
+    checkUserKeyboardInputSeatID(e) {
+      if (e.keyCode == 69 || (this.hasNonNumeric(e.key) && e.keyCode !== 8)) e.preventDefault();
+    },
+    setCurrentMemberList(list) {
+      this.$parent.currentMemberList = list;
+      this.$parent.memberList = Object.keys(this.$parent.currentMemberList).length == 0 ? [] : this.$parent.currentMemberList.list;
+      this.$parent.verifyMode = Object.keys(this.$parent.currentMemberList).length == 0 ? "Dynamic" : "Fixed";
+      this.currentEditStudent = {};
+    },
+    openEditMemberListAtInit() {
+      this.editMemberListMode = "Init";
+      this.showEditMemberList = true;
+    },
+    openEditMemberList() {
+      this.editMemberListMode = "Menu";
+      this.showEditMemberList = true;
+      this.Hishow = false;
+    },
+    openAddMemberList() {
+      this.showAddListReachMax = false;
+      if (this.$parent.customMemberlist.length < 3) {
+        let autolength = this.$parent.customMemberlist ? this.$parent.customMemberlist.length : 0;
+        this.showAddMemberList = true;
+        this.tempAddMemberList = [];
+        this.addMemberListErrRow = [];
+        this.addMemberListErrMsg = [];
+        this.currentEditMemberListName = this.$t("memberlist['自訂名單']") + (autolength + 1);
+        this.currentEditMemberList = {};
+        this.addmemberlisttext = "";
+        this.showAddMemberAutoSeatIDMsg = false;
+      } else {
+        this.showAddListReachMax = true;
+      }
+    },
+    async closeEditMemberList() {
+      this.showEditMemberList = false;
+      this.showAddMemberList = false;
+      let blob = new Blob([JSON.stringify(this.$parent.customMemberlist)], { type: "application/json" });
+      blob.lastModifiedDate = new Date();
+      blob.name = "CCroster.json";
+      await this.$parent.uploadResourceByPath(blob, "local/CCroster");
+    },
+    fetchCCroster() {
+      return new Promise(async (r, j) => {
+        const blobinfo = this.$parent.classInfo.blob;
+        const CCrosterURL = blobinfo.url + "/local/CCroster/CCroster.json" + "?" + blobinfo.sas_read;
+        try {
+          await fetch(CCrosterURL)
+            .then((res) => {
+              return res.json();
+            })
+            .then((data) => {
+              r(data);
+            });
+        } catch (err) {
+          j(err, "fetchCCroster err");
+          console.log(err, "fetchCCroster err");
+        }
+      });
+    },
+    hasDuplicates(arr) {
+      return arr.some((x) => arr.indexOf(x) !== arr.lastIndexOf(x));
+    },
+    setAddMemberErrMsg(Msg) {
+      if (this.addMemberListErrMsg.indexOf(Msg) == -1) {
+        this.addMemberListErrMsg.push(Msg);
+      }
+    },
+    hasNonNumeric(str) {
+      return /[^0-9]/.test(str); //匹配除了 0 到 9 的数字以外的任何字符
+    },
+    checkAddMemberList() {
+      this.tempAddMemberList = [];
+      this.addMemberListErrRow = [];
+      this.addMemberListErrMsg = [];
+      let data = this.addmemberlisttext
+        .replaceAll("\n", ",")
+        .split(",")
+        .filter((item) => item != "");
+      data = data.map((item) => item.trim());
+      let isAutoSeatID = false;
+      this.showAddMemberAutoSeatIDMsg = false;
+
+      //檢測座號是否皆為數字,部分有部分沒有就直接自動編號
+      if (this.currentEditMemberListName == "") this.setAddMemberErrMsg(this.$t("memberlist['表單名稱不可為空']"));
+      if (data.length % 2 == 1) {
+        isAutoSeatID = true;
+      } else {
+        data.forEach((item, index) => {
+          const pointIndex = item.indexOf(".");
+          const zeroIndex = item.indexOf("0");
+          if (index % 2 == 1 && (zeroIndex == 0 || pointIndex != -1 || this.hasNonNumeric(item))) {
+            //座號第一位數字非0、不可帶有小數點、須為數字
+            isAutoSeatID = true;
+          }
+        });
+      }
+      // console.log(isAutoSeatID,'isAutoSeatID')
+      if (!isAutoSeatID) {
+        //檢測座號是否重複
+        const seatIDs = data.filter((item, index) => index % 2 == 1);
+        // console.log(seatIDs)
+        if (this.hasDuplicates(seatIDs)) {
+          this.setAddMemberErrMsg(this.$t("memberlist['帶有重複座號']"));
+
+          let firstAppearID = [];
+          seatIDs.forEach((id, index) => {
+            const itemIndex = firstAppearID.findIndex((item) => item.id == id);
+            if (itemIndex == -1) firstAppearID.push({ id: id, count: 1, index: index });
+            else {
+              seatIDs.forEach((subId) => {
+                if (id == subId) {
+                  this.addMemberListErrRow.push(index);
+                }
+              });
+            }
+          });
+          // console.log(firstAppearID)
+        }
+        data.forEach((item, index) => {
+          //檢測座號是否<=999
+          // console.log(index%2)
+          if (index % 2 == 1 && parseInt(item) > 999) {
+            this.setAddMemberErrMsg(this.$t("memberlist['座號範圍提示字']"));
+            this.addMemberListErrRow.push(parseInt(index / 2));
+          }
+          if (index % 2 == 1 && !isNaN(parseInt(item))) {
+            this.tempAddMemberList.push({
+              seatID: item,
+              memberID: "member" + new Date().getTime() + index,
+              memberName: data[index - 1],
+            });
+          }
+        });
+      } else {
+        this.showAddMemberAutoSeatIDMsg = true;
+        data.forEach((item, index) => {
+          if (item) {
+            this.tempAddMemberList.push({
+              seatID: index + 1,
+              memberID: "member" + new Date().getTime() + index,
+              memberName: data[index],
+            });
+          }
+        });
+      }
+
+      if (this.tempAddMemberList.length > 100) {
+        this.setAddMemberErrMsg(this.$t("memberlist['名單人數上限提示字']"));
+      }
+    },
+    savecustomMemberlist() {
+      this.$parent.customMemberlist = this.$parent.customMemberlist ? this.$parent.customMemberlist : [];
+      if (this.addMemberListErrMsg.length === 0 && this.addmemberlisttext != "" && this.tempAddMemberList.length != 0 && this.currentEditMemberListName != "") {
+        this.$parent.customMemberlist.push({
+          listName: this.currentEditMemberListName,
+          list: this.tempAddMemberList,
+          listID: "list" + new Date().getTime(),
+        });
+        this.editMemberListMode == "Init" ? this.closeEditMemberList() : (this.showAddMemberList = false);
+        this.$parent.currentMemberList = this.$parent.customMemberlist[this.$parent.customMemberlist.length - 1];
+      }
+    },
+    useSampleList() {
+      this.addmemberlisttext = this.$t("memberlist['範例名單']");
+    },
+    deleteMemberList() {
+      const deleteIndex = this.$parent.customMemberlist.findIndex((list) => list.listID == this.$parent.currentMemberList.listID);
+      this.$parent.customMemberlist.splice(deleteIndex, 1);
+      this.$parent.currentMemberList = {};
+      this.showCorfirmDelete = false;
+      this.showAddListReachMax = false;
+    },
+    setEditStudent(data) {
+      this.currentEditStudent = data;
+    },
+    updateStudent() {
+      const parentIndex = this.$parent.customMemberlist.findIndex((list) => list.listID == this.$parent.currentMemberList.listID);
+      if (this.tempEditName.trim() == "") {
+        this.showStudentErrMsgNum = 3;
+        return;
+      }
+      if (this.tempEditSeatID == "") {
+        this.showStudentErrMsgNum = 2;
+        return;
+      }
+      if (this.editStudentMode == "edit") {
+        if ((this.isSeatIDduplicate() && this.currentEditStudent.seatID == this.tempEditSeatID) || (!this.isSeatIDduplicate() && this.currentEditStudent.seatID != this.tempEditSeatID)) {
+          //   console.log("純改");
+          const targetId = this.$parent.customMemberlist[parentIndex].list.findIndex((stu) => stu.memberID == this.currentEditStudent.memberID);
+          this.showStudentErrMsgNum = 0;
+          this.$parent.customMemberlist[parentIndex].list[targetId].seatID = this.tempEditSeatID;
+          this.$parent.customMemberlist[parentIndex].list[targetId].memberName = this.tempEditName;
+          this.currentEditStudent = this.$parent.customMemberlist[parentIndex].list[targetId];
+          this.showEditStudent = false;
+        } else {
+          this.showStudentErrMsgNum = 1;
+        }
+      } else if (this.editStudentMode == "add") {
+        if (!this.isSeatIDduplicate()) {
+          this.$parent.customMemberlist[parentIndex].list.push({
+            seatID: this.tempEditSeatID,
+            memberID: "member" + new Date().getTime(),
+            memberName: this.tempEditName,
+          });
+          this.currentEditStudent = this.$parent.customMemberlist[parentIndex].list[this.$parent.customMemberlist[parentIndex].list.length - 1];
+          this.showEditStudent = false;
+        }
+        this.showStudentErrMsgNum = this.isSeatIDduplicate() ? 1 : 0;
+      }
+    },
+    isSeatIDduplicate() {
+      const parentIndex = this.$parent.customMemberlist.findIndex((list) => list.listID == this.$parent.currentMemberList.listID);
+      const targetId = this.$parent.customMemberlist[parentIndex].list.findIndex((stu) => stu.seatID == this.tempEditSeatID);
+      //   console.log(this.$parent.customMemberlist[parentIndex].list, targetId);
+      return targetId !== -1;
+    },
+    cancelEditStudent() {
+      this.showEditStudent = false;
+    },
+    deleteStudent() {
+      const parentIndex = this.$parent.customMemberlist.findIndex((list) => list.listID == this.$parent.currentMemberList.listID);
+      const targetId = this.$parent.customMemberlist[parentIndex].list.findIndex((stu) => stu.memberID === this.currentEditStudent.memberID);
+      this.$parent.customMemberlist[parentIndex].list.splice(targetId, 1);
+      this.currentEditStudent = {};
+      this.showAddStudentReachMax = false;
+    },
+    editStudent() {
+      this.showStudentErrMsgNum = 0;
+      this.showEditStudent = true;
+      this.editStudentMode = "edit";
+      this.tempEditSeatID = this.currentEditStudent.seatID;
+      this.tempEditName = this.currentEditStudent.memberName;
+    },
+    addStudent() {
+      if (this.$parent.currentMemberList.list.length < 100) {
+        this.showStudentErrMsgNum = 0;
+        this.showEditStudent = true;
+        this.editStudentMode = "add";
+        this.tempEditName = "";
+        this.tempEditSeatID = "";
+      } else {
+        this.showAddStudentReachMax = true;
+      }
+    },
+    hintBugRow(row, index) {
+      return this.addMemberListErrRow.indexOf(index) != -1 ? "bug-row" : "";
+    },
+    hintSelectedRow(row) {
+      return row.memberID === this.currentEditStudent.memberID ? "selected-row" : "";
+    },
+    openEditListName() {
+      this.showEditListNameMsg = false;
+      this.showEditListName = true;
+      this.tempEditListName = this.$parent.currentMemberList.listName;
+      this.showAddListReachMax = false;
+    },
+    updateListName() {
+      if (this.tempEditListName != "") {
+        const parentIndex = this.$parent.customMemberlist.findIndex((list) => list.listID == this.$parent.currentMemberList.listID);
+        this.$parent.customMemberlist[parentIndex].listName = this.tempEditListName;
+        this.showEditListName = false;
+        this.showEditListNameMsg = false;
+      } else {
+        this.showEditListNameMsg = true;
+      }
+    },
+    cancelEditListName() {
+      this.showEditListNameMsg = false;
+      this.tempEditListName = "";
+      this.showEditListName = false;
+    },
+  },
+};
+</script>
+
+<style lang="less">
+@import "../assets/css/color.less";
+.ivu-table .bug-row td {
+  background-color: pink;
+}
+.ivu-table tr {
+  cursor: pointer;
+}
+.ivu-table .selected-row td {
+  background-color: @btn-color;
+  color: white;
+}
+
+.editmemberlist-view {
+  position: relative;
+  position: fixed;
+  display: block;
+  width: 100%;
+  background-color: rgba(0, 0, 0, 0.8);
+  // border-radius: 5px;
+  color: black;
+  height: 100%;
+  z-index: 1016;
+  .close-btn {
+    cursor: pointer;
+    font-size: 16px;
+    color: #3d3d3d;
+    position: absolute;
+    right: 10px;
+    top: 5px;
+  }
+  .example-btn {
+    position: absolute;
+    right: 20px;
+    top: 15px;
+    cursor: pointer;
+    font-size: 30px;
+    color: #3d3d3d;
+  }
+  .tooltip-btn {
+    display: inline-block;
+    padding: 10px;
+    color: white;
+    background-color: @btn-color;
+    cursor: pointer;
+    white-space: nowrap;
+    padding: 5px 10px;
+    margin: 10px 5px;
+    border-radius: 5px;
+    text-align: center;
+  }
+  .edit-tooltip {
+    position: absolute;
+    display: inline-block;
+    background-color: rgb(254, 255, 183);
+    color: black;
+    width: 65%;
+    top: 65px;
+    right: -14%;
+    text-align: left;
+    padding: 10px;
+    border-radius: 4px;
+    box-shadow: 0 2px 10px rgba(112, 112, 112, 0.5);
+    z-index: 999;
+  }
+  .edit-tooltip-triangle {
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-width: 0 15px 15px 15px;
+    border-color: transparent transparent rgb(254, 255, 183) transparent;
+    line-height: 0px;
+    position: absolute;
+    top: 50px;
+    right: 5%;
+    z-index: 1000;
+  }
+
+  .addmember-card {
+    text-align: left !important;
+  }
+  .closebtn {
+    cursor: pointer;
+    position: absolute;
+    top: -12px;
+    right: -10px;
+    width: 30px;
+    height: 30px;
+    line-height: 27px;
+    background-color: #d8d8d8;
+    text-align: center;
+    border-radius: 50%;
+    z-index: 1017;
+  }
+
+  .closebtn img {
+    width: 80%;
+  }
+  .editstudent-card {
+    max-width: 300px !important;
+    .editstudent-title {
+      font-size: 20px;
+      margin-bottom: 15px;
+    }
+    .errmsg {
+      text-align: left;
+    }
+    .warndeletemsg {
+      color: red;
+      margin-bottom: 10px;
+    }
+  }
+  .editmemberlist-card {
+    top: 8%;
+    max-width: 500px;
+    transition: 0.5s;
+    position: relative;
+    left: 50%;
+    transform: translateX(-50%);
+    background-color: #ededed;
+    font-weight: bolder;
+    padding: 20px 20px 10px 20px;
+    border-radius: 5px;
+    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+    z-index: 1002;
+    text-align: center;
+    .edit-listname {
+      text-align: center !important;
+    }
+    .input-wrap {
+      text-align: left;
+      padding: 5px 10px 10px 5px;
+      line-height: 30px;
+      label {
+        margin: 10px 0px;
+      }
+      input {
+        font-size: 16px;
+        padding: 5px;
+        border-radius: 4px;
+        border: 2px solid gray;
+        width: 100%;
+        text-align: center;
+        &:focus {
+          border: 2px solid blue;
+        }
+      }
+    }
+    .errmsg {
+      margin-left: 6px;
+      text-align: left;
+      color: red;
+      padding-bottom: 4px;
+    }
+    .hintmsg {
+      margin-left: 6px;
+      color: blue;
+    }
+    .editmemberlist-rightbtns {
+      position: absolute;
+      top: 10px;
+      right: 15px;
+      text-align: right;
+
+      * {
+        display: inline-block;
+        &:hover {
+          fill: blue !important;
+          stroke: blue !important;
+        }
+      }
+      .delete-btn {
+        padding: 10px;
+        font-size: 20px;
+        fill: black;
+        cursor: pointer;
+      }
+      .edit-btn {
+        padding: 10px;
+        font-size: 20px;
+        stroke: black;
+        cursor: pointer;
+      }
+    }
+    .memberlist-wrap {
+      display: flex;
+      background-color: #fcfcfc;
+      border-width: 2px;
+      border-radius: 4px;
+      margin: 10px 5px;
+
+      .editmemberlist-bottombtns {
+        position: absolute;
+        width: 100%;
+        bottom: 0;
+        text-align: center;
+        border-top: 1px solid #ccc !important;
+        * {
+          display: inline-block;
+          &:hover {
+            fill: blue !important;
+            stroke: blue !important;
+          }
+        }
+        .delete-btn {
+          padding: 10px;
+          font-size: 14px;
+          fill: black;
+          cursor: pointer;
+        }
+        .edit-btn {
+          padding: 10px;
+          font-size: 14px;
+          stroke: black;
+          cursor: pointer;
+        }
+      }
+      .listitems {
+        flex: 2;
+        border: 1px solid #ccc !important;
+        position: relative;
+        width: 40%;
+        .listitem-light {
+          color: blue;
+          border: 1px solid blue !important;
+          border-bottom: 1px solid blue !important;
+        }
+        .listitem {
+          width: 100%;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          cursor: pointer;
+          background-color: #fcfcfc;
+          border-width: 2px;
+          border: 1px solid transparent;
+          border-bottom: 1px solid #ccc;
+          padding: 15px;
+          font-size: 14px;
+          &:hover {
+            // background-color: darken(#fcfcfc, 10%);
+            color: blue;
+            border: 1px solid blue !important;
+          }
+        }
+      }
+      .listDetail {
+        .editmemberlist-bottombtns {
+          border: 1px solid #ccc !important;
+          border-top: 1px solid transparent !important;
+        }
+        background-color: #fcfcfc;
+        border-width: 2px;
+        border-radius: 4px;
+        position: relative;
+        flex: 3;
+        text-align: left;
+        background-color: #fcfcfc;
+        overflow: auto;
+        .list-stu {
+          font-size: 14px;
+        }
+        .list-info {
+          text-align: center;
+          margin-top: 10%;
+          color: gray;
+          font-size: 14px;
+        }
+      }
+    }
+    .editmemberlist-title {
+      font-size: 20px;
+      text-align: left;
+      margin-left: 5px;
+    }
+    .add-title {
+      font-size: 16px;
+      text-align: left;
+      margin-left: 5px;
+      padding: 5px;
+      border-radius: 4px;
+      border: 1px solid #ccc;
+      &:focus {
+        border: 2px solid blue;
+      }
+    }
+    .addmember-area {
+      background-color: #fff;
+      margin: 10px 5px;
+      display: flex;
+      .left-part {
+        flex: 2;
+      }
+      .right-part {
+        flex: 3;
+        width: 100%;
+      }
+      textarea {
+        font-size: 14px;
+        border: 1px solid #ccc;
+        padding: 5px;
+        &:focus {
+          border: 2px solid blue;
+        }
+      }
+    }
+    .editmemberlist-empty {
+      background-color: #fff;
+      color: #3d3d3d;
+      border-radius: 5px;
+      padding: 20px;
+      margin: 10px 5px;
+      cursor: pointer;
+      .add-icon {
+        stroke: #3d3d3d;
+        font-size: 40px;
+        margin: 10px;
+      }
+    }
+    .editmemberlistbtn-group {
+      display: flex;
+      align-items: center;
+      .editmemberlist-disabledBtn {
+        cursor: not-allowed !important;
+        color: rgba(128, 128, 128, 0.3) !important;
+        background-color: #d8d8d8 !important;
+        &:hover {
+          color: rgba(128, 128, 128, 0.3);
+        }
+      }
+      .editmemberlist-cancelbtn {
+        color: #3d3d3d !important;
+        background-color: #d8d8d8 !important;
+      }
+      .editmemberlist-btn {
+        cursor: pointer;
+        flex: 1;
+        white-space: nowrap;
+        padding: 5px 10px;
+        margin: 10px 5px;
+        border-radius: 5px;
+        text-align: center;
+        background-color: #d8d8d8;
+        &:first-child,
+        &:nth-child(2) {
+          color: white;
+          background-color: @btn-color;
+        }
+      }
+    }
+  }
+}
+</style>

+ 238 - 26
HiTeachCC/ClientApp/src/components/ToolBox.vue

@@ -260,6 +260,9 @@
 </template>
 
 <script>
+import Vue from 'vue'
+import { mapGetters } from 'vuex'
+import SvgIcon from '../views/SvgIcon.vue'
 import Card from './Card.vue'
 import ColorPicker from './ColorPop/ColorPicker.vue'
 import LaserPicker from './ColorPop/LaserPicker.vue'
@@ -269,11 +272,6 @@ import Zoom from './ColorPop/Zoom.vue'
 import Eraser from './ColorPop/Eraser.vue'
 import Ques from './ColorPop/Ques.vue'
 import CheckBox from './ColorPop/Checkbox.vue'
-import SvgIcon from '../views/SvgIcon.vue'
-import { mapGetters } from 'vuex'
-
-import enc from '@/utils/enc.js'
-const de = require('@/utils/lib1.js')
 
 export default {
   name: 'toolBox',
@@ -323,20 +321,244 @@ export default {
       zoomPopVisible: false,
       penPopVisible: false,
       shapePopVisible: false,
-      isSendIRS: false //避免doubled click,
+      isSendIRS: false, //避免doubled click,
+      //存放Undo動作
+      undoHistory:[],
+      doUndoIndex:0,
+      currentSelectElement: '',
     }
   },
 
   methods: {
+    saveUndoHistory(mode,element){
+      console.log(mode,element)
+      //加入物件的undo
+      switch(mode){
+        case 'add': {
+          this.undoHistory.push({ action: 'delete', item: JSON.parse(JSON.stringify(element)) })
+          //新增前一動應提供該物件的初始化Attr
+          this.undoHistory.push({ action: 'update', item: JSON.parse(JSON.stringify(element)) })
+          break;
+        }
+        case 'update': {
+          this.undoHistory.push({ action: 'update', item: JSON.parse(JSON.stringify(element)) })
+          break;
+        }
+        case 'delete': {
+          if(element.attrs.name!='selectionRectangle') //針對圈選刪除,不存圈選框
+          this.undoHistory.push({ action: 'add', item: JSON.parse(JSON.stringify(element)) })
+          break;
+        }
+      }
+    },
     undo() {
-      this.setMode('undo')
-      // console.log(this.$store.state.layer)
+      // this.setMode('undo')
+      if (this.undoHistory == '') {
+        alert(this.$t('toolbox["目前沒有動作可復原"]'))
+        return
+      }
+      this.removeTransformer()
+      // this.removeTextTransformer()
+      this.doUndoIndex = this.undoHistory.length - 1
+      if (this.undoHistory != '') this.updateStageObj(this.undoHistory[this.undoHistory.length - 1])
+      this.undoHistory.pop()
+    },
+
+     removeTransformer() {
+      const stage= this.$store.state.stage
+      stage.find('Transformer').forEach(ele => {
+        if (ele.attrs.name == 'default') {
+          ele.destroy()
+        }
+      })
+    },
+
+    //接收到單一物件針對uuid與動作去更新
+    async updateStageObj(targetAction) {
+      console.log('單一物件渲染!', targetAction)
+      let that = this
       let layer = this.$store.state.layer
-      if (layer.children.length > 1) {
-        layer.children.pop()
-        layer.batchDraw()
+      let objectIndex
+      if (targetAction.action == 'add') {
+        
+        let normalClassName = ['Line', 'Circle', 'Rect', 'Star', 'Text', 'Path', 'Ellipse', 'RegularPolygon']
+        if (normalClassName.includes(JSON.parse(targetAction.item).className)) {
+          let KonvaItem = new Konva[JSON.parse(targetAction.item).className](JSON.parse(targetAction.item).attrs)
+          layer.add(KonvaItem)
+          if (JSON.parse(targetAction.item).className == 'Rect' && KonvaItem.attrs.name == 'bgRect') {
+            KonvaItem.moveToBottom()
+          }
+          objectIndex=KonvaItem.attrs?.index?KonvaItem.attrs?.index:KonvaItem.index
+          KonvaItem.zIndex(objectIndex)
+        }else if (JSON.parse(targetAction.item).className == 'Label') {
+          //文字便利貼
+          let label = new Konva.Label(JSON.parse(targetAction.item).attrs)
+          JSON.parse(targetAction.item).children.forEach(item => {
+            if (item.className == 'Tag') {
+              let tag = new Konva.Tag(item.attrs)
+              label.add(tag)
+            }
+            if (item.className == 'Text') {
+              let text = new Konva.Text(item.attrs)
+              label.add(text)
+            }
+          })
+          layer.add(label)
+          objectIndex=JSON.parse(targetAction.item).attrs?.index?JSON.parse(targetAction.item).attrs?.index:JSON.parse(targetAction.item)?.index
+          label.zIndex(objectIndex)
+        } else if (JSON.parse(targetAction.item).className == 'Image') {
+          await this.addUndoSingleImg(JSON.parse(targetAction.item), layer)
+        } else if (JSON.parse(targetAction.item).className == 'Group') {
+          let KonvaGroup = new Konva[JSON.parse(targetAction.item).className](JSON.parse(targetAction.item).attrs)
+
+          JSON.parse(targetAction.item).children.forEach(async child => {
+            let normalClassName = ['Line', 'Circle', 'Rect', 'Star', 'Text', 'Path', 'Ellipse', 'RegularPolygon']
+            if (normalClassName.includes(child.className)) {
+              KonvaGroup.add(
+                new Konva[child.className]({
+                  parent: KonvaGroup,
+                  ...child.attrs
+                })
+              )
+            }
+            if (child.className == 'Image') {
+             await this.addUndoSingleImg(child, KonvaGroup)
+            }
+
+            layer.add(KonvaGroup)
+            objectIndex=KonvaGroup.attrs?.index?KonvaGroup.attrs?.index:KonvaGroup.index
+            KonvaGroup.zIndex(objectIndex)
+            
+          })
+        }
+         layer.children.forEach(child => {
+            if (child.attrs.name == 'boardPdfBg') child.moveToBottom()
+          })
+
+         layer.batchDraw()
+
+        console.log(layer)
+      } else if (targetAction.action == 'update') {
+        // console.log('更新物件', targetAction)
+        let targetItem = typeof targetAction.item === 'object' ? targetAction.item : JSON.parse(targetAction.item)
+
+        let childrenIndex = layer.children.map(x => x.attrs.uuid).indexOf(targetItem.attrs.uuid)
+        //  console.log('更新物件', targetItem, childrenIndex)
+
+        if (childrenIndex != -1) {
+
+          layer.children[childrenIndex].setAttrs(targetItem.attrs)
+          if (!targetItem.attrs.hasOwnProperty('x')) layer.children[childrenIndex].setAttrs({ x: 0 })
+          if (!targetItem.attrs.hasOwnProperty('y')) layer.children[childrenIndex].setAttrs({ y: 0 })
+          if (!targetItem.attrs.hasOwnProperty('rotation')) layer.children[childrenIndex].setAttrs({ rotation: 0 })
+          if (!targetItem.attrs.hasOwnProperty('scaleX')) layer.children[childrenIndex].setAttrs({ scaleX: 1 })
+          if (!targetItem.attrs.hasOwnProperty('scaleY')) layer.children[childrenIndex].setAttrs({ scaleY: 1 })
+          if (layer.children[childrenIndex].className == 'Label' || layer.children[childrenIndex].className == 'Group') {
+            layer.children[childrenIndex].children.forEach((item, i) => {
+              item.setAttrs(targetItem.children[i].attrs)
+            })
+          }
+          this.removeTransformer()
+          // this.removeTextTransformer()
+          let tr = new Konva.Transformer({
+            anchorStroke: '#00a6ff',
+            anchorFill: '#fff',
+            anchorSize: 12,
+            anchorCornerRadius: 5,
+            anchorStrokeWidth: 2,
+            borderStroke: '#6ac9fc',
+            borderStrokeWidth: 2,
+            borderDash: [3, 3],
+            padding: 10,
+            name: 'default'
+          })
+          layer.add(tr)
+          tr.nodes([layer.children[childrenIndex]])
+          this.$parent.addMenuBtnToTr(tr, layer.children[childrenIndex])
+          layer.batchDraw()
+        }
+        //複合物件點擊目標待修
+      } else if (targetAction.action == 'delete') {
+        let childrenIndex = layer.children.findIndex(x => x.attrs.uuid == JSON.parse(targetAction.item).attrs.uuid)
+        if (childrenIndex != -1) {
+          layer.children[childrenIndex].destroy()
+          layer.draw()
+        }
+      }
+    },
+
+     addUndoSingleImg(target, parent) {
+      return new Promise(async(r,j)=>{
+       let layer = this.$store.state.layer
+      let blobUrl = this.classInfo.blob
+      let finallink = target.attrs.src.includes(blobUrl.sas_read) ? target.attrs.src : target.attrs.src + '?' + blobUrl.sas_read
+
+      let imageObj = new Image()
+      imageObj.setAttribute('crossOrigin', 'Anonymous')
+      imageObj.src = finallink
+      imageObj.onload = await function() {
+        Konva.Image.fromURL(finallink, function(image) {
+          image.setAttrs({
+            ...target.attrs,
+            parent: parent
+          })
+          parent.add(image)
+          image.zIndex(target.attrs.index >= parent.children.length ? parent.children.length - 1 : target.attrs.index)
+          if (target.attrs.name == 'boardPdfBg') {
+            image.moveToBottom()
+          } else {
+            if (image.zIndex() == 0) image.zIndex(1)
+          }
+          layer.batchDraw()
+          r(200)
+        })
+      }
+      imageObj.onerror =function(){
+        j('err')
       }
+      })
+      
     },
+
+    //紀錄Undo位移行為
+    saveUndoUpdate() {
+    //儲存undo歷史
+    if (this.currentSelectElement !== '') {
+      // console.log('偵測滑鼠點按物件放開', this.currentSelectElement.attrs.x, this.currentSelectElement.attrs.y)
+      //選中物件調整細節判斷
+      if (this.currentSelectElement.className == 'Rect' && this.currentSelectElement?.parent?.className == 'Transformer') {
+        this.currentSelectElement = this.currentSelectElement.parent._nodes[0]
+        // console.log(this.currentSelectElement, '調整縮放時選中的元素')
+      }
+      //便利貼, 挑人貼回名牌, 作品收集圖
+      if ((this.currentSelectElement.className == 'Text' && this.currentSelectElement?.parent?.className == 'Label')|| (this.currentSelectElement?.attrs?.name=='pastTextPickName')|| (this.currentSelectElement?.attrs?.name=='pastImgContent')) {
+        this.currentSelectElement = this.currentSelectElement.parent
+      }
+      console.log(this.currentSelectElement, '調整縮放時選中的元素')
+      
+      let finalhistoryItem = this.undoHistory[this.undoHistory.length - 1]
+      if (!this.currentSelectElement.attrs.hasOwnProperty('x')) this.currentSelectElement.attrs.x = 0
+      if (!this.currentSelectElement.attrs.hasOwnProperty('y')) this.currentSelectElement.attrs.y = 0
+      if (!this.currentSelectElement.attrs.hasOwnProperty('rotation')) this.currentSelectElement.attrs.rotation = 0
+      if (!this.currentSelectElement.attrs.hasOwnProperty('scaleX')) this.currentSelectElement.attrs.scaleX = 1
+      if (!this.currentSelectElement.attrs.hasOwnProperty('scaleY')) this.currentSelectElement.attrs.scaleY = 1
+
+     
+        if (finalhistoryItem?.action != 'update') {
+          this.saveUndoHistory('update',this.currentSelectElement)
+        } else {
+          let finalhistoryItemAttr = JSON.parse(this.undoHistory[this.undoHistory.length - 1].item)
+          if (this.currentSelectElement.attrs.x != 0 && this.currentSelectElement.attrs.y != 0) {
+            //判斷位移是否相同
+            if (this.currentSelectElement.attrs.x != finalhistoryItemAttr.attrs.x || this.currentSelectElement.attrs.y != finalhistoryItemAttr.attrs.y) {
+             this.saveUndoHistory('update',this.currentSelectElement)
+            }
+          }
+        }
+      
+    }
+  },
+
     async pushStageImg() {
       // this.$Message.info('推送圖片功能')
 
@@ -820,7 +1042,9 @@ export default {
     }
   },
   created() {},
-  mounted() {},
+  mounted() {
+     Vue.prototype.$toolbox = this
+  },
   computed: {
    ...mapGetters({
       classInfo: 'classInfo/getInfo', // 取得課堂設定
@@ -942,9 +1166,6 @@ export default {
   border: 2px solid #fff;
 }
 
-.q-color-picker__palette-rows--editable .q-color-picker__cube {
-}
-
 .q-color-picker {
   box-shadow: none !important;
 }
@@ -952,18 +1173,9 @@ export default {
 .shapes_icon {
   cursor: pointer;
 }
-/*.q-card {
-    max-width: 1920px !important;
-    max-height: 1080px !important;
-  }*/
-.ivu-slider-button {
-}
+
 .ivu-slider-button-wrap {
   top: -1px !important;
 }
-// /*.q-gutter-y-sm > *, .q-gutter-sm > *{margin-left:1px !important;}*/
-// .ivu-poptip-popper {
-//   left: -260px !important;
-//   min-height: 0;
-// }
+
 </style>

+ 14 - 15
HiTeachCC/ClientApp/src/components/Tools/turnTable.vue

@@ -126,11 +126,7 @@ export default {
               padding: 10,
               name: 'default'
             })
-            stage.find('Transformer').forEach(function(ele, i) {
-              if (ele.attrs.name == 'default') {
-                ele.destroy()
-              }
-            })
+            that.$toolbox.removeTransformer()
             layer.add(tr)
             tr.nodes([image])
             that.$parent.addMenuBtnToTr(tr, image) 
@@ -148,16 +144,18 @@ export default {
     pasteTargetByKonvaObj() {
       let stage = this.$store.state.stage
       let layer = this.$store.state.layer
+      let currentIndex=layer?.children ? JSON.parse(JSON.stringify(layer?.children)).length : 1
       this.$store.state.textColor = 'black'
       let text = this.pickTarget.sort + '\t\t\t' + this.pickTarget.name
-
+      let uuid= this.$jsFn.getUUID()
       var circle = new Konva.Circle({
         radius: 16,
         x: 33,
         y: 33,
         fill: '#ffb929',
         draggable: false,
-        listening: false
+        listening: false,
+        uuid:uuid
       })
       var rect = new Konva.Rect({
         radius: 20,
@@ -168,7 +166,8 @@ export default {
         y: 10,
         fill: '#d8d8d8',
         draggable: false,
-        listening: false
+        listening: false,
+        uuid:uuid
       })
 
       var group = new Konva.Group({
@@ -176,7 +175,9 @@ export default {
         y: stage.height() * 0.2 + Math.floor(Math.random() * 200),
         draggable: true,
         listening: true,
-        text: text
+        text: text,
+        uuid:uuid,
+        index:currentIndex,
       })
 
       group.add(rect)
@@ -197,11 +198,13 @@ export default {
           height: 'auto',
           name: 'pastTextPickName',
           draggable: false,
-          listening: true
+          listening: true,
+          uuid:uuid
         })
       )
 
       layer.add(group)
+      this.$toolbox.saveUndoHistory('add',group)//儲存undo記錄
 
       // stage.find('Transformer').destroy()
       let tr = new Konva.Transformer({
@@ -216,11 +219,7 @@ export default {
         padding: 10,
         name: 'default'
       })
-      stage.find('Transformer').forEach(function(ele, i) {
-        if (ele.attrs.name == 'default') {
-          ele.destroy()
-        }
-      })
+      this.$toolbox.removeTransformer()
       layer.add(tr)
       tr.nodes([group])
       this.$parent.addMenuBtnToTr(tr, group) 

+ 90 - 0
HiTeachCC/ClientApp/src/components/saveIES.vue

@@ -142,9 +142,97 @@ export default {
       Vue.prototype.$saveIES = this
   },
   methods: {
+    resetInitData(){
+        this.path= ''
+        this.Base= {
+            'summary': {
+                'activityName': 'HiTeachCC',
+                'hostName': 'Noname',
+                'meterialName': 'Noname',
+                'date': 'Nodate',
+                'startTime': 'Nostart',
+                'endTime': 'Noend',
+                'attendCount': 0,
+                'clientCount': 0,
+                'attendRate': 0,
+                'groupCount': 0,
+                'totalPoint': 0,
+                'totalInteractPoint': 0,
+                'collateTaskCount': 0,
+                'collateCount': 0,
+                'pushCount': 0,
+                'examCount': 0,
+                'examQuizCount': 0,
+                'examPointRate': 0,
+                'interactionCount': 0,
+                'clientInteractionCount': 0,
+                'clientInteractionAverge': 0,
+                'diffPushCount': 0,
+                'learningCategory': {
+                    'cooperation': 0,
+                    'interaction': 0,
+                    'task': 0,
+                    'exam': 0,
+                    'diffential': 0
+                }
+            },
+            'report': {
+                'activityName': 'HiTeachCC',
+                'hostName': 'Noname',
+                'meterialName': 'Noname',
+                'date': 'Nodate',
+                'startTime': 'Nostart',
+                'endTime': 'Noend',
+                'attendCount': 0,
+                'clientCount': 0,
+                'attendRate': 0,
+                'groupCount': 0,
+                'totalPoint': 0,
+                'totalInteractPoint': 0,
+                'collateTaskCount': 0,
+                'collateCount': 0,
+                'pushCount': 0,
+                'examCount': 0,
+                'examQuizCount': 0,
+                'examPointRate': 0,
+                'interactionCount': 0,
+                'clientInteractionCount': 0,
+                'clientInteractionAverge': 0,
+                'diffPushCount': 0,
+                'learningCategory': {
+                    'cooperation': 0,
+                    'interaction': 0,
+                    'task': 0,
+                    'exam': 0,
+                    'diffential': 0
+                },
+                'quizSummaryList': [],
+                'clientSummaryList': [],
+            },
+            'teacheract': [],
+            'student': [
+                //    {
+                //        'id': '1605192585',
+                //        'seatID': 1,
+                //        'name': '林筱茹',
+                //        'type': 1
+                //    },
+            ]
+        }
+        this.Task= [],
+        this.Push= []
+        this.TimeLine= {
+            'events': [],
+            'PgIdList': []
+        },
+        this.IRS= [],
+        this.aoa= [],
+        this.totalOptions= ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
+    },
     print() {
         console.log(this.path)
         this.path = this.savePath
+        this.resetInitData() //每次生成應清空
         this.$store.state.msgBody.sort(function(x,y) { return(x.page-y.page) })
         this.generateBaseJSON()
         this.generateTaskJSON()
@@ -196,9 +284,11 @@ export default {
         for (var i=0; i < this.$store.state.msgBody.length; i++) {
             let tempscore = 0
             for(var j=0; j < this.$store.state.msgBody[i].totalirs.length; j++) {
+                if(this.$store.state.msgBody[i].irsmodel=='irsImmediately'||this.$store.state.msgBody[i].irsmodel=='irsText'){
                 tempscore += this.$store.state.msgBody[i].totalirs[j].score
                 totalInteractPoint += this.$store.state.msgBody[i].totalirs[j].score
                 that.Base.report.clientSummaryList[j].interactScore += this.$store.state.msgBody[i].totalirs[j].score
+               }
             }
 
             collateCount += this.$store.state.msgBody[i].workData.length

+ 52 - 1
HiTeachCC/ClientApp/src/locale/lang/en-US/index.js

@@ -33,7 +33,8 @@ export default {
     IRS搶權: 'Buzz-in',
     挑人: 'Pick-out',
     倒數計時: 'Timer',
-    重設為一般挑人模式:'Reset to normal Pick-out'
+    重設為一般挑人模式:'Reset to normal Pick-out',
+    目前沒有動作可復原: 'No more undo'
   },
   board: {
     網路斷線:'Detected that the network has been disconnected, please check the network!',
@@ -67,7 +68,9 @@ export default {
     重啟課堂: 'Restart Class',
     結束登出: 'Logout',
     匯出PDF: 'Export PDF',
+    匯出課堂記錄: 'Export Class Records',
     插入PDF:'Insert PDF',
+    自訂學生名單:'Custom student list',
     PDFInit:{
       開啟PDF檔案: 'Open PDF file',
       從:'From',
@@ -215,6 +218,7 @@ export default {
       取得複製到剪貼簿的資料:'Get clipboard content',
       轉換物件貼回:'Paste',
       瀏覽器版本不支援:'Your browser does not support this feature yet!',
+      瀏覽器不允許讀剪貼簿:'Clipboard read permission denied!',
       素材庫:'Cliparts',
       背景庫:'Backgrounds',
       發送文字:'Send Text',
@@ -345,6 +349,53 @@ export default {
     interactScore: 'IRS Score',
     answerList: 'Answer',
   },
+  memberlist:{
+    自由加入:'Ad hoc dynamic join',
+    請選擇學生名單:'Please select the name list for this lesson',
+    創建自訂學生名單:'Create a pre-store name list',
+    點選左側指定可加入的學生名單:'Click on the left to select a name list',
+    點選Hi進行編輯:'If you need to edit the pre-stored name list, please operate from the Hi menu after clicking "Next"',
+    下一步:'Next',
+    上一步:'Previous',
+    編輯學生:'Edit student',
+    新增學生:'New student',
+    姓名:'Name',
+    座號:'Seat number',
+    座號重複:'Duplicate seat number',
+    座號不可為空或輸入無法辨認的值:'Seat number cannot be empty or unrecognized value',
+    姓名不可為空:'Name cannot be empty',
+    確定:'OK',
+    取消:'Cancel',
+    修改名單名稱:'Modify list name',
+    表單名稱不可為空:'The list name cannot be empty',
+    確定刪除後這個名單將無法復原:'This list will not be recoverable after deletion. Are you sure?',
+    刪除:'Delete\t',
+    自訂學生名單:'Edit pre-stored name list',
+    創建名單:'Create a list',
+    暫無數據:'No data',
+    新建的學生名單:'New name list...',
+    輸入名單資料:'Enter list information...',
+    自動編號提示字:'Unable to recognize seat number rules, system automatically assigns numbers',
+    預覽:'Conversion',
+    儲存預覽表格:'Save list',
+    名單範例模板:`Input list tips<br><br>The format of the list is:
+    Name, Seat number <br>(Seat number can be omitted, but must be filled in every row if provided)<br><br>
+    Restrictions<br>
+    Name: Within 50 characters<br>
+    Seat number: Non-repeating digits, should be less than 1000<br><br>
+    Example <br>
+    John, 1,<br>
+    Louise, 2<br>
+    George, 3<br>`,
+    複製範例:'Copy example',
+    自訂名單:'My list',
+    帶有重複座號:'Contains duplicate seat numbers',
+    座號範圍提示字:'Seat number should be set between 1-999',
+    名單人數上限提示字:'The number of people on the list exceeds the limit of 100, please reduce the number',
+    新增單筆名單人數達上限提示字:'The number of people on the list has reached the upper limit of 100 and cannot be added',
+    自訂名單已達上限提示字:'The maximum of 3 pre-stored lists have been reached',
+    範例名單:`John, 1\nLouise, 2\nGeorge, 3\n`
+  },
   gptBox:{
     Gpt工具:'Gpt tool',
     管理指令:'Management command',

+ 55 - 3
HiTeachCC/ClientApp/src/locale/lang/zh-CN/index.js

@@ -33,7 +33,8 @@ export default {
     IRS搶權: '抢权',
     挑人: '挑人',
     倒數計時: '倒数计时',
-    重設為一般挑人模式:'重设为一般挑人模式'
+    重設為一般挑人模式:'重设为一般挑人模式',
+    目前沒有動作可復原: '目前没有动作可复原'
   },
   board: {
     網路斷線:'侦测到网路已经断线,请检查网路!',
@@ -67,7 +68,9 @@ export default {
     重啟課堂: '重启课堂',
     結束登出: '结束登出',
     匯出PDF: '导出PDF',
+    匯出課堂記錄: '导出课堂记录',
     插入PDF:'插入PDF',
+    自訂學生名單:'自订学生名单',
     PDFInit:{
       開啟PDF檔案: '开启PDF档案',
       從:'从',
@@ -178,7 +181,7 @@ export default {
       三角形:'三角形',
       星形:'星形',
       刪除元素:'删除元素',
-      清空畫布:'清空画布'
+      清空畫布:'清空画布',
     },
     optionView: {
       統計翻牌自動結束: '统计翻牌后自动停止接收',
@@ -216,6 +219,7 @@ export default {
       取得複製到剪貼簿的資料:'取得剪贴板的内容',
       轉換物件貼回:'贴回画板',
       瀏覽器版本不支援:'您所使用的浏览器版本不支援此功能',
+      瀏覽器不允許讀剪貼簿:'您所使用的浏览器不允许读剪贴簿,请调整设定',
       素材庫:'素材库',
       背景庫:'背景库',
       發送文字:'推送文字',
@@ -228,7 +232,7 @@ export default {
     'Uploading': '上传中',
     'ssoError': '您的帐户已在另一台设备上登录。如果不是您操作,请及时修改密码。',
     objMenu:{
-      刪除物件:'删除物件',
+      刪除物件:'删除',
       複製:'复制',
       移到最上層:'移到最上层',
       移到最下層:'移到最下层',
@@ -317,6 +321,54 @@ export default {
     interactScore: '互动分',
     answerList: '学生作答',
   },
+  memberlist:{
+    自由加入:'临时參加动态生成',
+    請選擇學生名單:'请选择上课名单',
+    創建自訂學生名單:'创建预存的学生名单',
+    點選左側指定可加入的學生名單: '点击左侧清单选择预存的名单',
+    點選Hi進行編輯:'如需编辑预存名单,请按下一步进入系统后从Hi菜单操作',
+    下一步:'下一步',
+    上一步:'上一步',
+    編輯學生:'编辑学生',
+    新增學生:'添加学生',
+    姓名:'姓名',
+    座號:'座位号',
+    座號重複:'座位号重复',
+    座號不可為空或輸入無法辨認的值:'座位号不可为空或输入无法辨认的值',
+    姓名不可為空:'姓名不可为空',
+    確定:'确定',
+    取消:'取消',
+    修改名單名稱:'修改名单名称',
+    表單名稱不可為空:'名称不可为空',
+    確定刪除後這個名單將無法復原:'确定删除后这个名单将无法复原!',
+    刪除:'删除',
+    自訂學生名單:'编辑预存名单',
+    創建名單:'创建名单',
+    暫無數據:'暂无数据',
+    新建的學生名單:'创建的学生名单...',
+    輸入名單資料:'输入名单数据...',
+    自動編號提示字:'无法辨识座位号规则,系统自动编号',
+    預覽:'预览转换',
+    儲存預覽表格:'保存名单',
+    名單範例模板:`输入名单小技巧<br><br>名单的格式为:
+    姓名, 座位号 <br>(座位号可省略,如有填写则须每列都填写)<br><br>
+    限制<br>
+    姓名:字数长度50以内<br>
+    座号:不能重复数字,应小于1000
+     <br><br>
+    范例 <br>
+    王大明, 1<br>
+    张小凡, 2<br>
+    陈士轩, 3<br>`,
+    複製範例:'复制范例',
+    自訂名單:'我的名单',
+    帶有重複座號:'带有重复座位号',
+    座號範圍提示字:'座位号应设置于1-999',
+    名單人數上限提示字:'名单人数超过上限100人,请删减人数',
+    新增單筆名單人數達上限提示字:'名单人数达上限100人,无法添加',
+    自訂名單已達上限提示字:'预存名单已达3个上限',
+    範例名單: `王大明, 1\n张小凡, 2\n陈士轩, 3\n`
+   },
   gptBox:{
     Gpt工具:'Gpt工具',
     管理指令:'管理指令',

+ 55 - 3
HiTeachCC/ClientApp/src/locale/lang/zh-TW/index.js

@@ -33,7 +33,8 @@ export default {
     IRS搶權: '搶權',
     挑人: '挑人',
     倒數計時: '倒數計時',
-    重設為一般挑人模式:'重設為一般挑人模式'
+    重設為一般挑人模式:'重設為一般挑人模式',
+    目前沒有動作可復原: '目前沒有動作可復原',
   },
   board: {
     網路斷線:'偵測到網路已經斷線,請檢查網路!',
@@ -67,7 +68,9 @@ export default {
     重啟課堂: '重啟課堂',
     結束登出: '結束登出',
     匯出PDF: '匯出PDF',
+    匯出課堂記錄: '匯出課堂記錄',
     插入PDF:'插入PDF',
+    自訂學生名單:'自訂學生名單',
     PDFInit:{
       開啟PDF檔案: '使用PDF簡報',
       從:'從',
@@ -178,7 +181,7 @@ export default {
       三角形:'三角形',
       星形:'星形',
       刪除元素:'刪除元素',
-      清空畫布:'清空畫布'
+      清空畫布:'清空畫布',
     },
     optionView: {
       統計翻牌自動結束: '統計翻牌後自動停止作答',
@@ -215,6 +218,7 @@ export default {
       取得複製到剪貼簿的資料:'取得剪貼簿的內容',
       轉換物件貼回:'貼回畫板',
       瀏覽器版本不支援:'您所使用的瀏覽器版本不支援此功能',
+      瀏覽器不允許讀剪貼簿:'您所使用的瀏覽器不允許讀剪貼簿,請調整設定',
       素材庫:'素材庫',
       背景庫:'背景庫',
       發送文字:'推送文字',
@@ -227,7 +231,7 @@ export default {
     'Uploading': '上傳中',
     'ssoError': '您的帳號在其他設備登入,如果不是您的操作,請及時修改您的密碼。',
     objMenu:{
-      刪除物件:'刪除物件',
+      刪除物件:'刪除',
       複製:'複製',
       移到最上層:'移到最上層',
       移到最下層:'移到最下層',
@@ -345,6 +349,54 @@ export default {
     interactScore: '互動分',
     answerList: '學生作答',
   },
+  memberlist:{
+    自由加入:'臨時加入動態生成',
+    請選擇學生名單:'請選擇上課名單',
+    創建自訂學生名單:'新建立預存的學生名單',
+    點選左側指定可加入的學生名單:'點選左側列表選擇預存的名單',
+    點選Hi進行編輯:'如需編修預存名單,請按下一步進入系統後從Hi選單操作',
+    下一步:'下一步',
+    上一步:'上一步',
+    編輯學生:'編輯學生',
+    新增學生:'新增學生',
+    姓名:'姓名',
+    座號:'座號',
+    座號重複:'座號重複',
+    座號不可為空或輸入無法辨認的值:'座號不可為空或輸入無法辨認的值',
+    姓名不可為空:'姓名不可為空',
+    確定:'確定',
+    取消:'取消',
+    修改名單名稱:'修改名單名稱',
+    表單名稱不可為空:'表單名稱不可為空',
+    確定刪除後這個名單將無法復原:'確定刪除後這個名單將無法復原!',
+    刪除:'刪除',
+    自訂學生名單:'編輯預存名單',
+    創建名單:'新建名單',
+    暫無數據:'暫無數據',
+    新建的學生名單:'新建的學生名單...',
+    輸入名單資料:'輸入名單資料...',
+    自動編號提示字:'無法辨識座號規則,系統自動編號',
+    預覽:'預覽轉換',
+    儲存預覽表格:'儲存名單',
+    名單範例模板: `輸入名單小技巧<br><br>名單的格式為:
+    姓名, 座號 <br>(座號可省略,如有填寫則須每列都填寫)<br><br>
+    限制<br>
+    姓名:字數長度50以內<br>
+    座號:不能重複數字,應小於1000
+     <br><br>
+    範例 <br>
+    王大明, 1<br>
+    張小凡, 2<br>
+    陳士軒, 3<br>`,
+    複製範例:'複製範例',
+    自訂名單:'我的名單',
+    帶有重複座號:'帶有重複座號',
+    座號範圍提示字:'座號應設置於1-999',
+    名單人數上限提示字:'名單人數超過上限100人,請刪減人數',
+    新增單筆名單人數達上限提示字:'名單人數達上限100人,無法新加',
+    自訂名單已達上限提示字:'預存名單已達3個上限',
+    範例名單:`王大明, 1\n張小凡, 2\n陳士軒, 3\n`
+  },
   gptBox:{
     Gpt工具:'Gpt工具',
     管理指令:'管理指令',

+ 1 - 1
HiTeachCC/ClientApp/src/store/index.js

@@ -201,7 +201,7 @@ export default new Vuex.Store({
     startTime: 0,
     elapsedTime: 0,
     timeLineEvents: [],
-    version: 'v5.0.230313.1'
+    version: 'v5.0.230512.2'
   },
   mutations: {},
   actions: {

+ 1 - 0
HiTeachCC/ClientApp/src/store/module/mqtt.js

@@ -47,6 +47,7 @@ export default {
             interactionCount:0, //20:題數
             clientInteractionCount:0, //21:互動總次數
             external_ip:'',//22.external_ip
+            version:'',//23.version (ex. 5.0.21.0000 -> 500210000)
         }
     },
     getters: {

+ 2 - 0
HiTeachCC/ClientApp/src/store/module/preferences.js

@@ -26,6 +26,7 @@ export default {
                     size: '3',
                 },
             ],
+            memberListIDs:[],
             gptActionList: [{ value: i18n.t('gptBox["總結大意"]') }, { value: i18n.t('gptBox["整理同義字"]') }, { value: '' }, { value: '' }, { value: '' }],
         }
     },
@@ -35,6 +36,7 @@ export default {
        getPenState: state => (type) => {
         return state.pref.pens.find(item => item.type === type)
        },
+       getPREFMemberList:state=>state.pref.memberListIDs, // 取得自訂名單,
        getGptActionList:state => state.pref.gptActionList
     },
     mutations: {

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3880 - 3628
HiTeachCC/ClientApp/src/views/Board.vue


+ 5 - 0
package-lock.json

@@ -2,6 +2,11 @@
   "requires": true,
   "lockfileVersion": 1,
   "dependencies": {
+    "file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
+    },
     "expiry-map": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/expiry-map/-/expiry-map-2.0.0.tgz",