Просмотр исходного кода

Merge branch 'beta' of https://git.cocorobo.cn/jack/PPT into beta

lsc 1 день назад
Родитель
Сommit
5bffb807c3

+ 102 - 100
src/hooks/useImport.ts

@@ -1982,119 +1982,121 @@ export default () => {
             // ---------- 形状 ----------
             else if (el.type === 'shape') {
               if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
-                // 线条元素(单独处理)
-                const lineElement = parseLineElement(el, ratio)
-                slide.elements.push(lineElement)
+                //el.isFlipH = el.isFlipV = false;
+                //el.rotate = 0;
+                //   // 线条元素(单独处理)
+                //   const lineElement = parseLineElement(el, ratio)
+                //   slide.elements.push(lineElement)
               }
-              else {
-                const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
+              // else {
+              const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
 
-                const vAlignMap: { [key: string]: ShapeTextAlign } = {
-                  mid: 'middle',
-                  down: 'bottom',
-                  up: 'top',
+              const vAlignMap: { [key: string]: ShapeTextAlign } = {
+                mid: 'middle',
+                down: 'bottom',
+                up: 'top',
+              }
+
+              const gradient: Gradient | undefined = el.fill?.type === 'gradient'
+                ? {
+                  type: el.fill.value.path === 'line' ? 'linear' : 'radial',
+                  colors: el.fill.value.colors.map(item => ({
+                    ...item,
+                    pos: parseInt(item.pos),
+                  })),
+                  rotate: el.fill.value.rot,
                 }
+                : undefined
 
-                const gradient: Gradient | undefined = el.fill?.type === 'gradient'
-                  ? {
-                    type: el.fill.value.path === 'line' ? 'linear' : 'radial',
-                    colors: el.fill.value.colors.map(item => ({
-                      ...item,
-                      pos: parseInt(item.pos),
-                    })),
-                    rotate: el.fill.value.rot,
-                  }
-                  : undefined
+              const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
+              const fill = el.fill?.type === 'color' ? el.fill.value : 'none'
+              const style = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)) + (el.pathBBox?.pWidth ? ';width:' + (el.pathBBox?.pWidth * ratio) + 'px;height:' + (el.pathBBox?.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
+              const element: PPTShapeElement = {
+                type: 'shape',
+                id: nanoid(10),
+                width: el.width,
+                height: el.height,
+                left: el.left,
+                top: el.top,
+                viewBox: [200, 200],
+                path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
+                fill,
+                gradient,
+                pattern,
+                fixedRatio: false,
+                rotate: el.rotate,
+                pathBBox: el.pathBBox,
+                outline: {
+                  color: el.borderColor,
+                  width: +(el.borderWidth * ratio).toFixed(2),
+                  style: el.borderType,
+                },
+                text: {
+                  content: convertFontSizePtToPx(el.content, ratio, el.autoFit),
+                  style: style,
+                  defaultFontName: theme.value.fontName,
+                  defaultColor: theme.value.fontColor,
+                  align: vAlignMap[el.vAlign] || 'middle',
+                },
+                flipH: el.isFlipH,
+                flipV: el.isFlipV,
+              }
 
-                const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
-                const fill = el.fill?.type === 'color' ? el.fill.value : 'none'
-                const style = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)) + (el.pathBBox?.pWidth ? ';width:' + (el.pathBBox?.pWidth * ratio) + 'px;height:' + (el.pathBBox?.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
-                const element: PPTShapeElement = {
-                  type: 'shape',
-                  id: nanoid(10),
-                  width: el.width,
-                  height: el.height,
-                  left: el.left,
-                  top: el.top,
-                  viewBox: [200, 200],
-                  path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
-                  fill,
-                  gradient,
-                  pattern,
-                  fixedRatio: false,
-                  rotate: el.rotate,
-                  pathBBox: el.pathBBox,
-                  outline: {
-                    color: el.borderColor,
-                    width: +(el.borderWidth * ratio).toFixed(2),
-                    style: el.borderType,
-                  },
-                  text: {
-                    content: convertFontSizePtToPx(el.content, ratio, el.autoFit),
-                    style: style,
-                    defaultFontName: theme.value.fontName,
-                    defaultColor: theme.value.fontColor,
-                    align: vAlignMap[el.vAlign] || 'middle',
-                  },
-                  flipH: el.isFlipH,
-                  flipV: el.isFlipV,
+              if (el.shadow) {
+                element.shadow = {
+                  h: el.shadow.h * ratio,
+                  v: el.shadow.v * ratio,
+                  blur: el.shadow.blur * ratio,
+                  color: el.shadow.color,
                 }
+              }
 
-                if (el.shadow) {
-                  element.shadow = {
-                    h: el.shadow.h * ratio,
-                    v: el.shadow.v * ratio,
-                    blur: el.shadow.blur * ratio,
-                    color: el.shadow.color,
+              if (shape) {
+                const { maxX, maxY } = getSvgPathRange(el.path)
+                element.path = el.path
+                element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
+                /*
+                if (shape.pathFormula) {
+                  element.pathFormula = shape.pathFormula
+                  element.viewBox = [el.width, el.height]
+                  // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
+                  const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
+                  if ('editable' in pathFormula && pathFormula.editable) {
+                    element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
+                    element.keypoints = pathFormula.defaultValue
                   }
-                }
-
-                if (shape) {
-                  const { maxX, maxY } = getSvgPathRange(el.path)
-                  element.path = el.path
-                  element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
-                  /*
-                  if (shape.pathFormula) {
-                    element.pathFormula = shape.pathFormula
-                    element.viewBox = [el.width, el.height]
-                    // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
-                    const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
-                    if ('editable' in pathFormula && pathFormula.editable) {
-                      element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
-                      element.keypoints = pathFormula.defaultValue
-                    }
-                    else {
-                      element.path = pathFormula.formula(el.width, el.height)
-                    }
+                  else {
+                    element.path = pathFormula.formula(el.width, el.height)
                   }
-                  */
-                }
-                else if (el.path && el.path.indexOf('NaN') === -1) {
-                  const { maxX, maxY } = getSvgPathRange(el.path)
-                  element.path = el.path
-                  element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
-                  // element.viewBox = [originWidth || maxX, originHeight || maxY];  
-                  // element.viewBox = originWidth? [(originWidth/(poriginWidth||1)), (originHeight/(poriginHeight||1))] : [maxX, maxY];
-                  // element.viewBox = [poriginWidth || maxX, poriginHeight || maxY];
                 }
+                */
+              }
+              else if (el.path && el.path.indexOf('NaN') === -1) {
+                const { maxX, maxY } = getSvgPathRange(el.path)
+                element.path = el.path
+                element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
+                // element.viewBox = [originWidth || maxX, originHeight || maxY];  
+                // element.viewBox = originWidth? [(originWidth/(poriginWidth||1)), (originHeight/(poriginHeight||1))] : [maxX, maxY];
+                // element.viewBox = [poriginWidth || maxX, poriginHeight || maxY];
+              }
 
-                if (el.shapType === 'custom') {
-                  if (el.path!.indexOf('NaN') !== -1) {
-                    if (element.width === 0) element.width = 0.1
-                    if (element.height === 0) element.height = 0.1
-                    element.path = el.path!.replace(/NaN/g, '0')
-                  }
-                  const { maxX, maxY } = getSvgPathRange(element.path)
-                  element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
-                  // element.viewBox = [originWidth || maxX, originHeight || maxY];  
-                  // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
-                  // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
-                  // element.viewBox = [Math.max(maxX, originWidth), Math.max(maxY, originHeight)];
-                  // element.viewBox = [originWidth, originHeight];
+              if (el.shapType === 'custom') {
+                if (el.path!.indexOf('NaN') !== -1) {
+                  if (element.width === 0) element.width = 0.1
+                  if (element.height === 0) element.height = 0.1
+                  element.path = el.path!.replace(/NaN/g, '0')
                 }
-
-                if (element.path) slide.elements.push(element)
+                const { maxX, maxY } = getSvgPathRange(element.path)
+                element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
+                // element.viewBox = [originWidth || maxX, originHeight || maxY];  
+                // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
+                // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
+                // element.viewBox = [Math.max(maxX, originWidth), Math.max(maxY, originHeight)];
+                // element.viewBox = [originWidth, originHeight];
               }
+
+              if (element.path) slide.elements.push(element)
+              //}
             }
 
             // ---------- 表格 ----------

+ 15 - 9
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -316,6 +316,7 @@ const props = defineProps<{
   courseDetail: any;
   userId: string;
   workId: string;
+  cid: string;
 }>()
 
 const emit = defineEmits<{
@@ -731,7 +732,7 @@ const getAnalysis = () => {
   }
   console.log('props.workId', props.workId)
   const params = {
-    pid: props.workId,
+    pid: props.workId+(props.cid?','+props.cid:''),
   }
   axios.get('https://pbl.cocorobo.cn/api/pbl/select_pptAnalysisByPid?pid=' + params.pid).then(res => {
     const data = res[0]
@@ -1063,12 +1064,17 @@ const openEchatsDialog = () => {
 const getWordCloud15 = () => {
 
   return new Promise((resolve,) => {
-    const msg = `## 任务 请基于以下文本,提炼出10 - 30个关键词,用于绘制词云图。请给出相应的关键词,以及关键词出现的频次。请确保输出的关键字准确反映该段文本的主要内容和主题。
-## 要求
-1. ** 提取关键词 **:从提供的文本中提取出10 - 30个最具代表性的关键字。关键词应该涵盖该文本的主要概念、重要术语和核心主题。尽量选择多样化的关键词,避免过于集中在某一个主题或概念上。
-    2. ** 词频统计 **:计算每个关键字在文本中出现的频率。
-    3. ** 词汇大小 **:根据词频数量,确定每个关键字在词云图中的大小。词频越高,词汇大小数值越大,数值范围1 - 100。
-    4. ** 输出格式 **:输出结果应包含输出相应的关键词或元话语、对应的词频数量以及词汇大小数值,请以json格式输出,严格按照输出示例输出。
+    const msg = `## 任务
+请针对文本中学生提交的问答题回答内容进行深度分析,提炼出 10 - 30 个核心关键词,用于绘制词云图。
+## 提取准则
+1. 聚焦内容主体:仅从学生的具体回答文本中提取关键词。
+2. 严格排除杂质:禁止提取任何属于系统元数据或固定格式的词汇,包括但不限于:“课程数据”、“学生姓名”、“回答结果”、“课程标题”、“提交时间”、“作业名称”、“分数”等。
+3. 语义去重:将意思相近的词进行合并(例如“高效”与“效率高”),保留最具代表性的词条。
+4. 涵盖核心:关键词应准确反映学生回答中的核心观点、关键知识点、高频论据或情感倾向。
+## 任务要求
+1. 词频统计:计算每个有效关键字在回答内容中出现的频率。
+2. 词汇大小:根据词频,确定每个关键字在词云中的权重。词频越高,数值越大,范围 1 - 100。
+3. 输出格式:请严格按照 JSON 格式输出,包含关键词、词频及对应的词汇大小。
 
 ## 文本
 课程数据:
@@ -1076,7 +1082,7 @@ const getWordCloud15 = () => {
 - 课程学科:${props.courseDetail.name}
 当前页面答题数据(问答题):【分析重点】
 - 问答题题目:${props.showData.workDetail.json.answerQ}
-- 回答数据:${JSON.stringify(processedWorkArray.value.map((i) => ({ user: i.name, answer: i.content.answer })))}
+- 回答数据:${JSON.stringify(processedWorkArray.value.map((i) => ({ answer: i.content.answer })))}
 
 ## 输出示例
 {"tooltip":{"show":false},"series":[{"type":"wordCloud","sizeRange":[14,38],"rotationRange":[0,0],"keepAspect":false,"shape":"circle","left":"center","top":"center","right":null,"bottom":null,"width":"100%","height":"100%","rotationStep":20,"data":[{"value":"词汇大小,数值范围1-100","name":"词汇","textStyle":{"color":"词汇颜色(16进制)"}},{"value":"词汇大小,数值范围1-100","name":"词汇","textStyle":{"color":"(16进制)"}}]}]}`
@@ -1216,7 +1222,7 @@ const saveAnalysis = () => {
     return
   }
   const params = [{
-    pid: props.workId,
+    pid: props.workId+(props.cid?','+props.cid:''),
     idx: props.showData.workIndex,
     json: JSON.stringify(currentAnalysis.value.json),
   }]

+ 1 - 1
src/views/Student/index.vue

@@ -106,7 +106,7 @@
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
 
-          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)" :workId="workId"  :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
+          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)" :cid="props.cid" :workId="workId"  :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
 
 
           <div class="slide-bottom" v-if="!isFullscreen">

+ 35 - 11
src/views/components/element/ShapeElement/index.vue

@@ -12,16 +12,13 @@
       height: elementInfo.height + 'px',
     }"
   >
-    <div
-      class="rotate-wrapper"
-      :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
-    >
+    <div class="rotate-wrapper">
       <div
         class="element-content"
         :style="{
           opacity: elementInfo.opacity,
           filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
-          //transform: flipStyle,
+          transform: flipStyle,
           color: text.defaultColor,
           fontFamily: text.defaultFontName,
         }"
@@ -50,11 +47,7 @@
               :rotate="elementInfo.gradient.rotate"
             />
           </defs>
-          <g
-            :transform="`scale(${elementInfo.width / elementInfo.viewBox[0]}, ${
-              elementInfo.height / elementInfo.viewBox[1]
-            }) translate(0,0) matrix(1,0,0,1,0,0)`"
-          >
+          <g :transform="shapeTransform">
             <path
               class="shape-path"
               vector-effect="non-scaling-stroke"
@@ -148,6 +141,34 @@ const execFormatPainter = () => {
   if (!keep) mainStore.setShapeFormatPainter(null);
 };
 
+const shapeTransform = computed(() => {
+  {
+    const info = props.elementInfo;
+    const w = info.width;
+    const h = info.height;
+    const vb = info.viewBox;        // 假设为 [vbW, vbH]
+    const vbW = vb[0], vbH = vb[1];
+    
+    const scaleX = w / vbW;
+    const scaleY = h / vbH;
+    
+    const cx = vbW / 2, cy = vbH / 2;  // 旋转中心
+    const rot = info.rotate || 0;
+    
+    // 当前已证实可用的补偿翻转(基于镜像路径)
+    const flipX = info.flipH ? -1 : 1;
+    const flipY = info.flipV ? -1 : 1;  // 补偿后的取值
+    
+    return `
+      scale(${scaleX}, ${scaleY})
+      translate(${cx}, ${cy})
+      rotate(${rot})
+      scale(${flipX}, ${flipY})
+      translate(${-cx}, ${-cy})
+    `;
+  }
+});
+
 const element = computed(() => props.elementInfo);
 const { fill } = useElementFill(element, "editable");
 
@@ -236,7 +257,9 @@ const startEdit = () => {
   height: 100%;
 }
 .element-content {
-  font-family: Kaiti, "Kaiti SC", "Kaiti TC", Roboto, "Noto Sans SC", "Noto Sans TC", "Noto Sans KR", "Noto Sans JP", "Roboto", Roboto, "Noto Sans SC", "Noto Sans TC", "Noto Sans KR", "Noto Sans JP";
+  font-family: Kaiti, "Kaiti SC", "Kaiti TC", Roboto, "Noto Sans SC",
+    "Noto Sans TC", "Noto Sans KR", "Noto Sans JP", "Roboto", Roboto,
+    "Noto Sans SC", "Noto Sans TC", "Noto Sans KR", "Noto Sans JP";
   width: 100%;
   height: 100%;
   position: relative;
@@ -267,6 +290,7 @@ const startEdit = () => {
   word-break: break-word;
   pointer-events: none;
   white-space: break-spaces;
+  text-align: left;
 
   &.editable {
     pointer-events: all;

+ 2 - 0
src/views/components/element/TextElement/index.vue

@@ -234,6 +234,8 @@ watch(isHandleElement, () => {
   word-break: break-word;
   pointer-events: none;
   white-space: break-spaces;
+  text-align: left;
+  
   &.editable {
     pointer-events: all;
   }

+ 9 - 10
src/views/components/element/hooks/useElementFlip.ts

@@ -1,18 +1,17 @@
 import { computed, type Ref } from 'vue'
 
-// 计算元素的翻转样式
-export default (flipH: Ref<boolean | undefined>, flipV: Ref<boolean | undefined>) => {
-  const flipStyle = computed(() => {
-    let style = ''
-    
-    if (flipH.value && flipV.value) style = 'rotateX(180deg) rotateY(180deg)'
-    else if (flipV.value) style = 'rotateX(180deg)'
-    else if (flipH.value) style = 'rotateY(180deg)'
+export default (flipH: Ref<boolean | undefined>, flipV: Ref<boolean | undefined>, rotateDeg: Ref<number>) => {
+  const transform = computed(() => {
+    const scaleX = flipH.value ? -1 : 1
+    const scaleY = flipV.value ? -1 : 1
+    const scale = (scaleX !== 1 || scaleY !== 1) ? `scale(${scaleX}, ${scaleY})` : ''
+    const rotate = rotateDeg.value ? `rotate(${rotateDeg.value}deg)` : ''
 
-    return style
+    // 变换顺序:先缩放(翻转),再旋转(PPT 惯用顺序)
+    return [scale, rotate].filter(Boolean).join(' ')
   })
 
   return {
-    flipStyle,
+    transform,
   }
 }