Explorar o código

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

lsc hai 2 días
pai
achega
5bffb807c3

+ 102 - 100
src/hooks/useImport.ts

@@ -1982,119 +1982,121 @@ export default () => {
             // ---------- 形状 ----------
             // ---------- 形状 ----------
             else if (el.type === 'shape') {
             else if (el.type === 'shape') {
               if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
               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;
   courseDetail: any;
   userId: string;
   userId: string;
   workId: string;
   workId: string;
+  cid: string;
 }>()
 }>()
 
 
 const emit = defineEmits<{
 const emit = defineEmits<{
@@ -731,7 +732,7 @@ const getAnalysis = () => {
   }
   }
   console.log('props.workId', props.workId)
   console.log('props.workId', props.workId)
   const params = {
   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 => {
   axios.get('https://pbl.cocorobo.cn/api/pbl/select_pptAnalysisByPid?pid=' + params.pid).then(res => {
     const data = res[0]
     const data = res[0]
@@ -1063,12 +1064,17 @@ const openEchatsDialog = () => {
 const getWordCloud15 = () => {
 const getWordCloud15 = () => {
 
 
   return new Promise((resolve,) => {
   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.courseDetail.name}
 当前页面答题数据(问答题):【分析重点】
 当前页面答题数据(问答题):【分析重点】
 - 问答题题目:${props.showData.workDetail.json.answerQ}
 - 问答题题目:${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进制)"}}]}]}`
 {"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
     return
   }
   }
   const params = [{
   const params = [{
-    pid: props.workId,
+    pid: props.workId+(props.cid?','+props.cid:''),
     idx: props.showData.workIndex,
     idx: props.showData.workIndex,
     json: JSON.stringify(currentAnalysis.value.json),
     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"
           <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)"/>
             :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">
           <div class="slide-bottom" v-if="!isFullscreen">

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

@@ -12,16 +12,13 @@
       height: elementInfo.height + 'px',
       height: elementInfo.height + 'px',
     }"
     }"
   >
   >
-    <div
-      class="rotate-wrapper"
-      :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
-    >
+    <div class="rotate-wrapper">
       <div
       <div
         class="element-content"
         class="element-content"
         :style="{
         :style="{
           opacity: elementInfo.opacity,
           opacity: elementInfo.opacity,
           filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
           filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
-          //transform: flipStyle,
+          transform: flipStyle,
           color: text.defaultColor,
           color: text.defaultColor,
           fontFamily: text.defaultFontName,
           fontFamily: text.defaultFontName,
         }"
         }"
@@ -50,11 +47,7 @@
               :rotate="elementInfo.gradient.rotate"
               :rotate="elementInfo.gradient.rotate"
             />
             />
           </defs>
           </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
             <path
               class="shape-path"
               class="shape-path"
               vector-effect="non-scaling-stroke"
               vector-effect="non-scaling-stroke"
@@ -148,6 +141,34 @@ const execFormatPainter = () => {
   if (!keep) mainStore.setShapeFormatPainter(null);
   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 element = computed(() => props.elementInfo);
 const { fill } = useElementFill(element, "editable");
 const { fill } = useElementFill(element, "editable");
 
 
@@ -236,7 +257,9 @@ const startEdit = () => {
   height: 100%;
   height: 100%;
 }
 }
 .element-content {
 .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%;
   width: 100%;
   height: 100%;
   height: 100%;
   position: relative;
   position: relative;
@@ -267,6 +290,7 @@ const startEdit = () => {
   word-break: break-word;
   word-break: break-word;
   pointer-events: none;
   pointer-events: none;
   white-space: break-spaces;
   white-space: break-spaces;
+  text-align: left;
 
 
   &.editable {
   &.editable {
     pointer-events: all;
     pointer-events: all;

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

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

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

@@ -1,18 +1,17 @@
 import { computed, type Ref } from 'vue'
 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 {
   return {
-    flipStyle,
+    transform,
   }
   }
 }
 }