jack 1 тиждень тому
батько
коміт
7f3df24980

+ 8 - 8
src/assets/styles/font.scss

@@ -1,9 +1,9 @@
-$fonts: 'SourceHanSans', 'SourceHanSerif', 'FangZhengHeiTi', 'FangZhengKaiTi', 'FangZhengShuSong', 'FangZhengFangSong', 'AlibabaPuHuiTi', 'ZhuQueFangSong', 'LXGWWenKai', 'WenDingPLKaiTi', 'DeYiHei', 'MiSans', 'CangerXiaowanzi', 'YousheTitleBlack', 'FengguangMingrui', 'ShetuModernSquare', 'ZcoolHappy', 'ZizhiQuXiMai', 'SucaiJishiKangkang', 'SucaiJishiCoolSquare', 'TuniuRounded', 'RuiziZhenyan';
-
-@each $font in $fonts {
-  @font-face {
-    font-display: swap;
-    font-family: $font;
-    src: url('https://asset.pptist.cn/font/#{$font}.woff2') format('woff2');
-  }
+$fonts: 'SourceHanSans', 'SourceHanSerif', 'FangZhengHeiTi', 'FangZhengKaiTi', 'FangZhengShuSong', 'FangZhengFangSong', 'AlibabaPuHuiTi', 'ZhuQueFangSong', 'LXGWWenKai', 'WenDingPLKaiTi', 'DeYiHei', 'MiSans', 'CangerXiaowanzi', 'YousheTitleBlack', 'FengguangMingrui', 'ShetuModernSquare', 'ZcoolHappy', 'ZizhiQuXiMai', 'SucaiJishiKangkang', 'SucaiJishiCoolSquare', 'TuniuRounded', 'RuiziZhenyan';
+
+@each $font in $fonts {
+  @font-face {
+    font-display: swap;
+    font-family: $font;
+    src: url('https://asset.pptist.cn/font/#{$font}.woff2') format('woff2');
+  }
 }

+ 20 - 20
src/components/CheckboxButton.vue

@@ -1,21 +1,21 @@
-<template>
-  <Button 
-    :checked="checked"
-    :disabled="disabled"
-    type="checkbox"
-  >
-    <slot></slot>
-  </Button>
-</template>
-
-<script lang="ts" setup>
-import Button from './Button.vue'
-
-withDefaults(defineProps<{
-  checked?: boolean
-  disabled?: boolean
-}>(), {
-  checked: false,
-  disabled: false,
-})
+<template>
+  <Button 
+    :checked="checked"
+    :disabled="disabled"
+    type="checkbox"
+  >
+    <slot></slot>
+  </Button>
+</template>
+
+<script lang="ts" setup>
+import Button from './Button.vue'
+
+withDefaults(defineProps<{
+  checked?: boolean
+  disabled?: boolean
+}>(), {
+  checked: false,
+  disabled: false,
+})
 </script>

+ 233 - 233
src/configs/animation.ts

@@ -1,234 +1,234 @@
-import type { TurningMode } from '@/types/slides'
-
-export const ANIMATION_DEFAULT_DURATION = 1000
-export const ANIMATION_DEFAULT_TRIGGER = 'click'
-export const ANIMATION_CLASS_PREFIX = 'animate__'
-
-export const ENTER_ANIMATIONS = [
-  {
-    type: 'bounce',
-    name: '弹跳',
-    children: [
-      { name: '弹入', value: 'bounceIn' },
-      { name: '向右弹入', value: 'bounceInLeft' },
-      { name: '向左弹入', value: 'bounceInRight' },
-      { name: '向上弹入', value: 'bounceInUp' },
-      { name: '向下弹入', value: 'bounceInDown' },
-    ],
-  },
-  {
-    type: 'fade',
-    name: '浮现',
-    children: [
-      { name: '浮入', value: 'fadeIn' },
-      { name: '向下浮入', value: 'fadeInDown' },
-      { name: '向下长距浮入', value: 'fadeInDownBig' },
-      { name: '向右浮入', value: 'fadeInLeft' },
-      { name: '向右长距浮入', value: 'fadeInLeftBig' },
-      { name: '向左浮入', value: 'fadeInRight' },
-      { name: '向左长距浮入', value: 'fadeInRightBig' },
-      { name: '向上浮入', value: 'fadeInUp' },
-      { name: '向上长距浮入', value: 'fadeInUpBig' },
-      { name: '从左上浮入', value: 'fadeInTopLeft' },
-      { name: '从右上浮入', value: 'fadeInTopRight' },
-      { name: '从左下浮入', value: 'fadeInBottomLeft' },
-      { name: '从右下浮入', value: 'fadeInBottomRight' },
-    ],
-  },
-  {
-    type: 'rotate',
-    name: '旋转',
-    children: [
-      { name: '旋转进入', value: 'rotateIn' },
-      { name: '绕左下进入', value: 'rotateInDownLeft' },
-      { name: '绕右下进入', value: 'rotateInDownRight' },
-      { name: '绕左上进入', value: 'rotateInUpLeft' },
-      { name: '绕右上进入', value: 'rotateInUpRight' },
-    ],
-  },
-  {
-    type: 'zoom',
-    name: '缩放',
-    children: [
-      { name: '放大进入', value: 'zoomIn' },
-      { name: '向下放大进入', value: 'zoomInDown' },
-      { name: '从左放大进入', value: 'zoomInLeft' },
-      { name: '从右放大进入', value: 'zoomInRight' },
-      { name: '向上放大进入', value: 'zoomInUp' },
-    ],
-  },
-  {
-    type: 'slide',
-    name: '滑入',
-    children: [
-      { name: '向下滑入', value: 'slideInDown' },
-      { name: '从右滑入', value: 'slideInLeft' },
-      { name: '从左滑入', value: 'slideInRight' },
-      { name: '向上滑入', value: 'slideInUp' },
-    ],
-  },
-  {
-    type: 'flip',
-    name: '翻转',
-    children: [
-      { name: 'X轴翻转进入', value: 'flipInX' },
-      { name: 'Y轴翻转进入', value: 'flipInY' },
-    ],
-  },
-  {
-    type: 'back',
-    name: '放大滑入',
-    children: [
-      { name: '向下放大滑入', value: 'backInDown' },
-      { name: '从左放大滑入', value: 'backInLeft' },
-      { name: '从右放大滑入', value: 'backInRight' },
-      { name: '向上放大滑入', value: 'backInUp' },
-    ],
-  },
-  {
-    type: 'lightSpeed',
-    name: '飞入',
-    children: [
-      { name: '从右飞入', value: 'lightSpeedInRight' },
-      { name: '从左飞入', value: 'lightSpeedInLeft' },
-    ],
-  },
-]
-
-export const EXIT_ANIMATIONS = [
-  {
-    type: 'bounce',
-    name: '弹跳',
-    children: [
-      { name: '弹出', value: 'bounceOut' },
-      { name: '向左弹出', value: 'bounceOutLeft' },
-      { name: '向右弹出', value: 'bounceOutRight' },
-      { name: '向上弹出', value: 'bounceOutUp' },
-      { name: '向下弹出', value: 'bounceOutDown' },
-    ],
-  },
-  {
-    type: 'fade',
-    name: '浮现',
-    children: [
-      { name: '浮出', value: 'fadeOut' },
-      { name: '向下浮出', value: 'fadeOutDown' },
-      { name: '向下长距浮出', value: 'fadeOutDownBig' },
-      { name: '向左浮出', value: 'fadeOutLeft' },
-      { name: '向左长距浮出', value: 'fadeOutLeftBig' },
-      { name: '向右浮出', value: 'fadeOutRight' },
-      { name: '向右长距浮出', value: 'fadeOutRightBig' },
-      { name: '向上浮出', value: 'fadeOutUp' },
-      { name: '向上长距浮出', value: 'fadeOutUpBig' },
-      { name: '从左上浮出', value: 'fadeOutTopLeft' },
-      { name: '从右上浮出', value: 'fadeOutTopRight' },
-      { name: '从左下浮出', value: 'fadeOutBottomLeft' },
-      { name: '从右下浮出', value: 'fadeOutBottomRight' },
-    ],
-  },
-  {
-    type: 'rotate',
-    name: '旋转',
-    children: [
-      { name: '旋转退出', value: 'rotateOut' },
-      { name: '绕左下退出', value: 'rotateOutDownLeft' },
-      { name: '绕右下退出', value: 'rotateOutDownRight' },
-      { name: '绕左上退出', value: 'rotateOutUpLeft' },
-      { name: '绕右上退出', value: 'rotateOutUpRight' },
-    ],
-  },
-  {
-    type: 'zoom',
-    name: '缩放',
-    children: [
-      { name: '缩小退出', value: 'zoomOut' },
-      { name: '向下缩小退出', value: 'zoomOutDown' },
-      { name: '从左缩小退出', value: 'zoomOutLeft' },
-      { name: '从右缩小退出', value: 'zoomOutRight' },
-      { name: '向上缩小退出', value: 'zoomOutUp' },
-    ],
-  },
-  {
-    type: 'slide',
-    name: '滑出',
-    children: [
-      { name: '向下滑出', value: 'slideOutDown' },
-      { name: '从左滑出', value: 'slideOutLeft' },
-      { name: '从右滑出', value: 'slideOutRight' },
-      { name: '向上滑出', value: 'slideOutUp' },
-    ],
-  },
-  {
-    type: 'flip',
-    name: '翻转',
-    children: [
-      { name: 'X轴翻转退出', value: 'flipOutX' },
-      { name: 'Y轴翻转退出', value: 'flipOutY' },
-    ],
-  },
-  {
-    type: 'back',
-    name: '缩小滑出',
-    children: [
-      { name: '向下缩小滑出', value: 'backOutDown' },
-      { name: '从左缩小滑出', value: 'backOutLeft' },
-      { name: '从右缩小滑出', value: 'backOutRight' },
-      { name: '向上缩小滑出', value: 'backOutUp' },
-    ],
-  },
-  {
-    type: 'lightSpeed',
-    name: '飞出',
-    children: [
-      { name: '从右飞出', value: 'lightSpeedOutRight' },
-      { name: '从左飞出', value: 'lightSpeedOutLeft' },
-    ],
-  },
-]
-
-export const ATTENTION_ANIMATIONS = [
-  {
-    type: 'shake',
-    name: '晃动',
-    children: [
-      { name: '左右摇晃', value: 'shakeX' },
-      { name: '上下摇晃', value: 'shakeY' },
-      { name: '摇头', value: 'headShake' },
-      { name: '摆动', value: 'swing' },
-      { name: '晃动', value: 'wobble' },
-      { name: '惊恐', value: 'tada' },
-      { name: '果冻', value: 'jello' },
-    ],
-  },
-  {
-    type: 'other',
-    name: '其他',
-    children: [
-      { name: '弹跳', value: 'bounce' },
-      { name: '闪烁', value: 'flash' },
-      { name: '脉搏', value: 'pulse' },
-      { name: '橡皮筋', value: 'rubberBand' },
-      { name: '心跳(快)', value: 'heartBeat' },
-    ],
-  },
-]
-
-interface SlideAnimation {
-  label: string
-  value: TurningMode
-}
-
-export const SLIDE_ANIMATIONS: SlideAnimation[] = [
-  { label: '无', value: 'no' },
-  { label: '随机', value: 'random' },
-  { label: '左右推移', value: 'slideX' },
-  { label: '上下推移', value: 'slideY' },
-  { label: '左右推移(3D)', value: 'slideX3D' },
-  { label: '上下推移(3D)', value: 'slideY3D' },
-  { label: '淡入淡出', value: 'fade' },
-  { label: '旋转', value: 'rotate' },
-  { label: '上下展开', value: 'scaleY' },
-  { label: '左右展开', value: 'scaleX' },
-  { label: '放大', value: 'scale' },
-  { label: '缩小', value: 'scaleReverse' },
+import type { TurningMode } from '@/types/slides'
+
+export const ANIMATION_DEFAULT_DURATION = 1000
+export const ANIMATION_DEFAULT_TRIGGER = 'click'
+export const ANIMATION_CLASS_PREFIX = 'animate__'
+
+export const ENTER_ANIMATIONS = [
+  {
+    type: 'bounce',
+    name: '弹跳',
+    children: [
+      { name: '弹入', value: 'bounceIn' },
+      { name: '向右弹入', value: 'bounceInLeft' },
+      { name: '向左弹入', value: 'bounceInRight' },
+      { name: '向上弹入', value: 'bounceInUp' },
+      { name: '向下弹入', value: 'bounceInDown' },
+    ],
+  },
+  {
+    type: 'fade',
+    name: '浮现',
+    children: [
+      { name: '浮入', value: 'fadeIn' },
+      { name: '向下浮入', value: 'fadeInDown' },
+      { name: '向下长距浮入', value: 'fadeInDownBig' },
+      { name: '向右浮入', value: 'fadeInLeft' },
+      { name: '向右长距浮入', value: 'fadeInLeftBig' },
+      { name: '向左浮入', value: 'fadeInRight' },
+      { name: '向左长距浮入', value: 'fadeInRightBig' },
+      { name: '向上浮入', value: 'fadeInUp' },
+      { name: '向上长距浮入', value: 'fadeInUpBig' },
+      { name: '从左上浮入', value: 'fadeInTopLeft' },
+      { name: '从右上浮入', value: 'fadeInTopRight' },
+      { name: '从左下浮入', value: 'fadeInBottomLeft' },
+      { name: '从右下浮入', value: 'fadeInBottomRight' },
+    ],
+  },
+  {
+    type: 'rotate',
+    name: '旋转',
+    children: [
+      { name: '旋转进入', value: 'rotateIn' },
+      { name: '绕左下进入', value: 'rotateInDownLeft' },
+      { name: '绕右下进入', value: 'rotateInDownRight' },
+      { name: '绕左上进入', value: 'rotateInUpLeft' },
+      { name: '绕右上进入', value: 'rotateInUpRight' },
+    ],
+  },
+  {
+    type: 'zoom',
+    name: '缩放',
+    children: [
+      { name: '放大进入', value: 'zoomIn' },
+      { name: '向下放大进入', value: 'zoomInDown' },
+      { name: '从左放大进入', value: 'zoomInLeft' },
+      { name: '从右放大进入', value: 'zoomInRight' },
+      { name: '向上放大进入', value: 'zoomInUp' },
+    ],
+  },
+  {
+    type: 'slide',
+    name: '滑入',
+    children: [
+      { name: '向下滑入', value: 'slideInDown' },
+      { name: '从右滑入', value: 'slideInLeft' },
+      { name: '从左滑入', value: 'slideInRight' },
+      { name: '向上滑入', value: 'slideInUp' },
+    ],
+  },
+  {
+    type: 'flip',
+    name: '翻转',
+    children: [
+      { name: 'X轴翻转进入', value: 'flipInX' },
+      { name: 'Y轴翻转进入', value: 'flipInY' },
+    ],
+  },
+  {
+    type: 'back',
+    name: '放大滑入',
+    children: [
+      { name: '向下放大滑入', value: 'backInDown' },
+      { name: '从左放大滑入', value: 'backInLeft' },
+      { name: '从右放大滑入', value: 'backInRight' },
+      { name: '向上放大滑入', value: 'backInUp' },
+    ],
+  },
+  {
+    type: 'lightSpeed',
+    name: '飞入',
+    children: [
+      { name: '从右飞入', value: 'lightSpeedInRight' },
+      { name: '从左飞入', value: 'lightSpeedInLeft' },
+    ],
+  },
+]
+
+export const EXIT_ANIMATIONS = [
+  {
+    type: 'bounce',
+    name: '弹跳',
+    children: [
+      { name: '弹出', value: 'bounceOut' },
+      { name: '向左弹出', value: 'bounceOutLeft' },
+      { name: '向右弹出', value: 'bounceOutRight' },
+      { name: '向上弹出', value: 'bounceOutUp' },
+      { name: '向下弹出', value: 'bounceOutDown' },
+    ],
+  },
+  {
+    type: 'fade',
+    name: '浮现',
+    children: [
+      { name: '浮出', value: 'fadeOut' },
+      { name: '向下浮出', value: 'fadeOutDown' },
+      { name: '向下长距浮出', value: 'fadeOutDownBig' },
+      { name: '向左浮出', value: 'fadeOutLeft' },
+      { name: '向左长距浮出', value: 'fadeOutLeftBig' },
+      { name: '向右浮出', value: 'fadeOutRight' },
+      { name: '向右长距浮出', value: 'fadeOutRightBig' },
+      { name: '向上浮出', value: 'fadeOutUp' },
+      { name: '向上长距浮出', value: 'fadeOutUpBig' },
+      { name: '从左上浮出', value: 'fadeOutTopLeft' },
+      { name: '从右上浮出', value: 'fadeOutTopRight' },
+      { name: '从左下浮出', value: 'fadeOutBottomLeft' },
+      { name: '从右下浮出', value: 'fadeOutBottomRight' },
+    ],
+  },
+  {
+    type: 'rotate',
+    name: '旋转',
+    children: [
+      { name: '旋转退出', value: 'rotateOut' },
+      { name: '绕左下退出', value: 'rotateOutDownLeft' },
+      { name: '绕右下退出', value: 'rotateOutDownRight' },
+      { name: '绕左上退出', value: 'rotateOutUpLeft' },
+      { name: '绕右上退出', value: 'rotateOutUpRight' },
+    ],
+  },
+  {
+    type: 'zoom',
+    name: '缩放',
+    children: [
+      { name: '缩小退出', value: 'zoomOut' },
+      { name: '向下缩小退出', value: 'zoomOutDown' },
+      { name: '从左缩小退出', value: 'zoomOutLeft' },
+      { name: '从右缩小退出', value: 'zoomOutRight' },
+      { name: '向上缩小退出', value: 'zoomOutUp' },
+    ],
+  },
+  {
+    type: 'slide',
+    name: '滑出',
+    children: [
+      { name: '向下滑出', value: 'slideOutDown' },
+      { name: '从左滑出', value: 'slideOutLeft' },
+      { name: '从右滑出', value: 'slideOutRight' },
+      { name: '向上滑出', value: 'slideOutUp' },
+    ],
+  },
+  {
+    type: 'flip',
+    name: '翻转',
+    children: [
+      { name: 'X轴翻转退出', value: 'flipOutX' },
+      { name: 'Y轴翻转退出', value: 'flipOutY' },
+    ],
+  },
+  {
+    type: 'back',
+    name: '缩小滑出',
+    children: [
+      { name: '向下缩小滑出', value: 'backOutDown' },
+      { name: '从左缩小滑出', value: 'backOutLeft' },
+      { name: '从右缩小滑出', value: 'backOutRight' },
+      { name: '向上缩小滑出', value: 'backOutUp' },
+    ],
+  },
+  {
+    type: 'lightSpeed',
+    name: '飞出',
+    children: [
+      { name: '从右飞出', value: 'lightSpeedOutRight' },
+      { name: '从左飞出', value: 'lightSpeedOutLeft' },
+    ],
+  },
+]
+
+export const ATTENTION_ANIMATIONS = [
+  {
+    type: 'shake',
+    name: '晃动',
+    children: [
+      { name: '左右摇晃', value: 'shakeX' },
+      { name: '上下摇晃', value: 'shakeY' },
+      { name: '摇头', value: 'headShake' },
+      { name: '摆动', value: 'swing' },
+      { name: '晃动', value: 'wobble' },
+      { name: '惊恐', value: 'tada' },
+      { name: '果冻', value: 'jello' },
+    ],
+  },
+  {
+    type: 'other',
+    name: '其他',
+    children: [
+      { name: '弹跳', value: 'bounce' },
+      { name: '闪烁', value: 'flash' },
+      { name: '脉搏', value: 'pulse' },
+      { name: '橡皮筋', value: 'rubberBand' },
+      { name: '心跳(快)', value: 'heartBeat' },
+    ],
+  },
+]
+
+interface SlideAnimation {
+  label: string
+  value: TurningMode
+}
+
+export const SLIDE_ANIMATIONS: SlideAnimation[] = [
+  { label: '无', value: 'no' },
+  { label: '随机', value: 'random' },
+  { label: '左右推移', value: 'slideX' },
+  { label: '上下推移', value: 'slideY' },
+  { label: '左右推移(3D)', value: 'slideX3D' },
+  { label: '上下推移(3D)', value: 'slideY3D' },
+  { label: '淡入淡出', value: 'fade' },
+  { label: '旋转', value: 'rotate' },
+  { label: '上下展开', value: 'scaleY' },
+  { label: '左右展开', value: 'scaleX' },
+  { label: '放大', value: 'scale' },
+  { label: '缩小', value: 'scaleReverse' },
 ]

+ 125 - 125
src/configs/theme.ts

@@ -1,126 +1,126 @@
-import type { PPTElementOutline, PPTElementShadow } from '@/types/slides'
-
-export interface PresetTheme {
-  background: string
-  fontColor: string
-  fontname: string
-  colors: string[]
-  borderColor?: string
-  outline?: PPTElementOutline
-  shadow?: PPTElementShadow
-}
-
-export const PRESET_THEMES: PresetTheme[] = [
-  {
-    background: '#ffffff',
-    fontColor: '#333333',
-    borderColor: '#41719c',
-    fontname: '',
-    colors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4', '#70ad47'],
-  },
-  {
-    background: '#ffffff',
-    fontColor: '#333333',
-    borderColor: '#5f6f1c',
-    fontname: '',
-    colors: ['#83992a', '#3c9670', '#44709d', '#a23b32', '#d87728', '#deb340'],
-  },
-  {
-    background: '#ffffff',
-    fontColor: '#333333',
-    borderColor: '#a75f0a',
-    fontname: '',
-    colors: ['#e48312', '#bd582c', '#865640', '#9b8357', '#c2bc80', '#94a088'],
-  },
-  {
-    background: '#ffffff',
-    fontColor: '#333333',
-    borderColor: '#7c91a8',
-    fontname: '',
-    colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
-  },
-  {
-    background: '#ffffff',
-    fontColor: '#333333',
-    borderColor: '#688e19',
-    fontname: '',
-    colors: ['#90c225', '#54a121', '#e6b91e', '#e86618', '#c42f19', '#918756'],
-  },
-  {
-    background: '#ffffff',
-    fontColor: '#333333',
-    borderColor: '#4495b0',
-    fontname: '',
-    colors: ['#1cade4', '#2683c6', '#27ced7', '#42ba97', '#3e8853', '#62a39f'],
-  },
-  {
-    background: '#e9efd6',
-    fontColor: '#333333',
-    borderColor: '#782009',
-    fontname: '',
-    colors: ['#a5300f', '#de7e18', '#9f8351', '#728653', '#92aa4c', '#6aac91'],
-  },
-  {
-    background: '#17444e',
-    fontColor: '#ffffff',
-    borderColor: '#800c0b',
-    fontname: '',
-    colors: ['#b01513', '#ea6312', '#e6b729', '#6bab90', '#55839a', '#9e5d9d'],
-  },
-  {
-    background: '#36234d',
-    fontColor: '#ffffff',
-    borderColor: '#830949',
-    fontname: '',
-    colors: ['#b31166', '#e33d6f', '#e45f3c', '#e9943a', '#9b6bf2', '#d63cd0'],
-  },
-  {
-    background: '#247fad',
-    fontColor: '#ffffff',
-    borderColor: '#032e45',
-    fontname: '',
-    colors: ['#052f61', '#a50e82', '#14967c', '#6a9e1f', '#e87d37', '#c62324'],
-  },
-  {
-    background: '#103f55',
-    fontColor: '#ffffff',
-    borderColor: '#2d7f8a',
-    fontname: '',
-    colors: ['#40aebd', '#97e8d5', '#a1cf49', '#628f3e', '#f2df3a', '#fcb01c'],
-  },
-  {
-    background: '#242367',
-    fontColor: '#ffffff',
-    borderColor: '#7d2b8d',
-    fontname: '',
-    colors: ['#ac3ec1', '#477bd1', '#46b298', '#90ba4c', '#dd9d31', '#e25345'],
-  },
-  {
-    background: '#e4b75e',
-    fontColor: '#333333',
-    borderColor: '#b68317',
-    fontname: '',
-    colors: ['#a5644e', '#b58b80', '#c3986d', '#a19574', '#c17529', '#826277'],
-  },
-  {
-    background: '#333333',
-    fontColor: '#ffffff',
-    borderColor: '#7c91a8',
-    fontname: '',
-    colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
-  },
-  {
-    background: '#2b2b2d',
-    fontColor: '#ffffff',
-    borderColor: '#893011',
-    fontname: '',
-    colors: ['#bc451b', '#d3ba68', '#bb8640', '#ad9277', '#a55a43', '#ad9d7b'],
-  },
-  {
-    background: '#171b1e',
-    fontColor: '#ffffff',
-    borderColor: '#505050',
-    fontname: '',
-    colors: ['#6f6f6f', '#bfbfa5', '#dbd084', '#e7bf5f', '#e9a039', '#cf7133'],
-  },
+import type { PPTElementOutline, PPTElementShadow } from '@/types/slides'
+
+export interface PresetTheme {
+  background: string
+  fontColor: string
+  fontname: string
+  colors: string[]
+  borderColor?: string
+  outline?: PPTElementOutline
+  shadow?: PPTElementShadow
+}
+
+export const PRESET_THEMES: PresetTheme[] = [
+  {
+    background: '#ffffff',
+    fontColor: '#333333',
+    borderColor: '#41719c',
+    fontname: '',
+    colors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4', '#70ad47'],
+  },
+  {
+    background: '#ffffff',
+    fontColor: '#333333',
+    borderColor: '#5f6f1c',
+    fontname: '',
+    colors: ['#83992a', '#3c9670', '#44709d', '#a23b32', '#d87728', '#deb340'],
+  },
+  {
+    background: '#ffffff',
+    fontColor: '#333333',
+    borderColor: '#a75f0a',
+    fontname: '',
+    colors: ['#e48312', '#bd582c', '#865640', '#9b8357', '#c2bc80', '#94a088'],
+  },
+  {
+    background: '#ffffff',
+    fontColor: '#333333',
+    borderColor: '#7c91a8',
+    fontname: '',
+    colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
+  },
+  {
+    background: '#ffffff',
+    fontColor: '#333333',
+    borderColor: '#688e19',
+    fontname: '',
+    colors: ['#90c225', '#54a121', '#e6b91e', '#e86618', '#c42f19', '#918756'],
+  },
+  {
+    background: '#ffffff',
+    fontColor: '#333333',
+    borderColor: '#4495b0',
+    fontname: '',
+    colors: ['#1cade4', '#2683c6', '#27ced7', '#42ba97', '#3e8853', '#62a39f'],
+  },
+  {
+    background: '#e9efd6',
+    fontColor: '#333333',
+    borderColor: '#782009',
+    fontname: '',
+    colors: ['#a5300f', '#de7e18', '#9f8351', '#728653', '#92aa4c', '#6aac91'],
+  },
+  {
+    background: '#17444e',
+    fontColor: '#ffffff',
+    borderColor: '#800c0b',
+    fontname: '',
+    colors: ['#b01513', '#ea6312', '#e6b729', '#6bab90', '#55839a', '#9e5d9d'],
+  },
+  {
+    background: '#36234d',
+    fontColor: '#ffffff',
+    borderColor: '#830949',
+    fontname: '',
+    colors: ['#b31166', '#e33d6f', '#e45f3c', '#e9943a', '#9b6bf2', '#d63cd0'],
+  },
+  {
+    background: '#247fad',
+    fontColor: '#ffffff',
+    borderColor: '#032e45',
+    fontname: '',
+    colors: ['#052f61', '#a50e82', '#14967c', '#6a9e1f', '#e87d37', '#c62324'],
+  },
+  {
+    background: '#103f55',
+    fontColor: '#ffffff',
+    borderColor: '#2d7f8a',
+    fontname: '',
+    colors: ['#40aebd', '#97e8d5', '#a1cf49', '#628f3e', '#f2df3a', '#fcb01c'],
+  },
+  {
+    background: '#242367',
+    fontColor: '#ffffff',
+    borderColor: '#7d2b8d',
+    fontname: '',
+    colors: ['#ac3ec1', '#477bd1', '#46b298', '#90ba4c', '#dd9d31', '#e25345'],
+  },
+  {
+    background: '#e4b75e',
+    fontColor: '#333333',
+    borderColor: '#b68317',
+    fontname: '',
+    colors: ['#a5644e', '#b58b80', '#c3986d', '#a19574', '#c17529', '#826277'],
+  },
+  {
+    background: '#333333',
+    fontColor: '#ffffff',
+    borderColor: '#7c91a8',
+    fontname: '',
+    colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
+  },
+  {
+    background: '#2b2b2d',
+    fontColor: '#ffffff',
+    borderColor: '#893011',
+    fontname: '',
+    colors: ['#bc451b', '#d3ba68', '#bb8640', '#ad9277', '#a55a43', '#ad9d7b'],
+  },
+  {
+    background: '#171b1e',
+    fontColor: '#ffffff',
+    borderColor: '#505050',
+    fontname: '',
+    colors: ['#6f6f6f', '#bfbfa5', '#dbd084', '#e7bf5f', '#e9a039', '#cf7133'],
+  },
 ]

+ 176 - 176
src/hooks/useAlignActiveElement.ts

@@ -1,177 +1,177 @@
-import { storeToRefs } from 'pinia'
-import { useMainStore, useSlidesStore } from '@/store'
-import type { PPTElement } from '@/types/slides'
-import { ElementAlignCommands } from '@/types/edit'
-import { getElementListRange, getRectRotatedOffset } from '@/utils/element'
-import useHistorySnapshot from './useHistorySnapshot'
-
-interface RangeMap {
-  [id: string]: ReturnType<typeof getElementListRange> 
-}
-
-export default () => {
-  const slidesStore = useSlidesStore()
-  const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
-  const { currentSlide } = storeToRefs(slidesStore)
-
-  const { addHistorySnapshot } = useHistorySnapshot()
-
-  /**
-   * 对齐选中的元素
-   * @param command 对齐方向
-   */
-  const alignActiveElement = (command: ElementAlignCommands) => {
-    const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
-    const elementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
-
-    // 如果所选择的元素为组合元素的成员,需要计算该组合的整体范围
-    const groupElementRangeMap: RangeMap = {}
-    for (const activeElement of activeElementList.value) {
-      if (activeElement.groupId && !groupElementRangeMap[activeElement.groupId]) {
-        const groupElements = activeElementList.value.filter(item => item.groupId === activeElement.groupId)
-        groupElementRangeMap[activeElement.groupId] = getElementListRange(groupElements)
-      }
-    }
-
-    // 根据不同的命令,计算对齐的位置
-    if (command === ElementAlignCommands.LEFT) {
-      elementList.forEach(element => {
-        if (activeElementIdList.value.includes(element.id)) {
-          if (!element.groupId) {
-            if ('rotate' in element && element.rotate) {
-              const { offsetX } = getRectRotatedOffset({
-                left: element.left,
-                top: element.top,
-                width: element.width,
-                height: element.height,
-                rotate: element.rotate,
-              })
-              element.left = minX - offsetX
-            }
-            else element.left = minX
-          }
-          else {
-            const range = groupElementRangeMap[element.groupId]
-            const offset = range.minX - minX
-            element.left = element.left - offset
-          }
-        }
-      })
-    }
-    else if (command === ElementAlignCommands.RIGHT) {
-      elementList.forEach(element => {
-        if (activeElementIdList.value.includes(element.id)) {
-          if (!element.groupId) {
-            const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
-            if ('rotate' in element && element.rotate) {
-              const { offsetX } = getRectRotatedOffset({
-                left: element.left,
-                top: element.top,
-                width: element.width,
-                height: element.height,
-                rotate: element.rotate,
-              })
-              element.left = maxX - elWidth + offsetX
-            }
-            else element.left = maxX - elWidth
-          }
-          else {
-            const range = groupElementRangeMap[element.groupId]
-            const offset = range.maxX - maxX
-            element.left = element.left - offset
-          }
-        }
-      })
-    }
-    else if (command === ElementAlignCommands.TOP) {
-      elementList.forEach(element => {
-        if (activeElementIdList.value.includes(element.id)) {
-          if (!element.groupId) {
-            if ('rotate' in element && element.rotate) {
-              const { offsetY } = getRectRotatedOffset({
-                left: element.left,
-                top: element.top,
-                width: element.width,
-                height: element.height,
-                rotate: element.rotate,
-              })
-              element.top = minY - offsetY
-            }
-            else element.top = minY
-          }
-          else {
-            const range = groupElementRangeMap[element.groupId]
-            const offset = range.minY - minY
-            element.top = element.top - offset
-          }
-        }
-      })
-    }
-    else if (command === ElementAlignCommands.BOTTOM) {
-      elementList.forEach(element => {
-        if (activeElementIdList.value.includes(element.id)) {
-          if (!element.groupId) {
-            const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
-            if ('rotate' in element && element.rotate) {
-              const { offsetY } = getRectRotatedOffset({
-                left: element.left,
-                top: element.top,
-                width: element.width,
-                height: element.height,
-                rotate: element.rotate,
-              })
-              element.top = maxY - elHeight + offsetY
-            }
-            else element.top = maxY - elHeight
-          }
-          else {
-            const range = groupElementRangeMap[element.groupId]
-            const offset = range.maxY - maxY
-            element.top = element.top - offset
-          }
-        }
-      })
-    }
-    else if (command === ElementAlignCommands.HORIZONTAL) {
-      const horizontalCenter = (minX + maxX) / 2
-      elementList.forEach(element => {
-        if (activeElementIdList.value.includes(element.id)) {
-          if (!element.groupId) {
-            const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
-            element.left = horizontalCenter - elWidth / 2
-          }
-          else {
-            const range = groupElementRangeMap[element.groupId]
-            const center = (range.maxX + range.minX) / 2
-            const offset = center - horizontalCenter
-            element.left = element.left - offset
-          }
-        }
-      })
-    }
-    else if (command === ElementAlignCommands.VERTICAL) {
-      const verticalCenter = (minY + maxY) / 2
-      elementList.forEach(element => {
-        if (activeElementIdList.value.includes(element.id)) {
-          if (!element.groupId) {
-            const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
-            element.top = verticalCenter - elHeight / 2
-          }
-          else {
-            const range = groupElementRangeMap[element.groupId]
-            const center = (range.maxY + range.minY) / 2
-            const offset = center - verticalCenter
-            element.top = element.top - offset
-          }
-        }
-      })
-    }
-
-    slidesStore.updateSlide({ elements: elementList })
-    addHistorySnapshot()
-  }
-
-  return {
-    alignActiveElement,
-  }
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSlidesStore } from '@/store'
+import type { PPTElement } from '@/types/slides'
+import { ElementAlignCommands } from '@/types/edit'
+import { getElementListRange, getRectRotatedOffset } from '@/utils/element'
+import useHistorySnapshot from './useHistorySnapshot'
+
+interface RangeMap {
+  [id: string]: ReturnType<typeof getElementListRange> 
+}
+
+export default () => {
+  const slidesStore = useSlidesStore()
+  const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
+  const { currentSlide } = storeToRefs(slidesStore)
+
+  const { addHistorySnapshot } = useHistorySnapshot()
+
+  /**
+   * 对齐选中的元素
+   * @param command 对齐方向
+   */
+  const alignActiveElement = (command: ElementAlignCommands) => {
+    const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
+    const elementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
+
+    // 如果所选择的元素为组合元素的成员,需要计算该组合的整体范围
+    const groupElementRangeMap: RangeMap = {}
+    for (const activeElement of activeElementList.value) {
+      if (activeElement.groupId && !groupElementRangeMap[activeElement.groupId]) {
+        const groupElements = activeElementList.value.filter(item => item.groupId === activeElement.groupId)
+        groupElementRangeMap[activeElement.groupId] = getElementListRange(groupElements)
+      }
+    }
+
+    // 根据不同的命令,计算对齐的位置
+    if (command === ElementAlignCommands.LEFT) {
+      elementList.forEach(element => {
+        if (activeElementIdList.value.includes(element.id)) {
+          if (!element.groupId) {
+            if ('rotate' in element && element.rotate) {
+              const { offsetX } = getRectRotatedOffset({
+                left: element.left,
+                top: element.top,
+                width: element.width,
+                height: element.height,
+                rotate: element.rotate,
+              })
+              element.left = minX - offsetX
+            }
+            else element.left = minX
+          }
+          else {
+            const range = groupElementRangeMap[element.groupId]
+            const offset = range.minX - minX
+            element.left = element.left - offset
+          }
+        }
+      })
+    }
+    else if (command === ElementAlignCommands.RIGHT) {
+      elementList.forEach(element => {
+        if (activeElementIdList.value.includes(element.id)) {
+          if (!element.groupId) {
+            const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
+            if ('rotate' in element && element.rotate) {
+              const { offsetX } = getRectRotatedOffset({
+                left: element.left,
+                top: element.top,
+                width: element.width,
+                height: element.height,
+                rotate: element.rotate,
+              })
+              element.left = maxX - elWidth + offsetX
+            }
+            else element.left = maxX - elWidth
+          }
+          else {
+            const range = groupElementRangeMap[element.groupId]
+            const offset = range.maxX - maxX
+            element.left = element.left - offset
+          }
+        }
+      })
+    }
+    else if (command === ElementAlignCommands.TOP) {
+      elementList.forEach(element => {
+        if (activeElementIdList.value.includes(element.id)) {
+          if (!element.groupId) {
+            if ('rotate' in element && element.rotate) {
+              const { offsetY } = getRectRotatedOffset({
+                left: element.left,
+                top: element.top,
+                width: element.width,
+                height: element.height,
+                rotate: element.rotate,
+              })
+              element.top = minY - offsetY
+            }
+            else element.top = minY
+          }
+          else {
+            const range = groupElementRangeMap[element.groupId]
+            const offset = range.minY - minY
+            element.top = element.top - offset
+          }
+        }
+      })
+    }
+    else if (command === ElementAlignCommands.BOTTOM) {
+      elementList.forEach(element => {
+        if (activeElementIdList.value.includes(element.id)) {
+          if (!element.groupId) {
+            const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
+            if ('rotate' in element && element.rotate) {
+              const { offsetY } = getRectRotatedOffset({
+                left: element.left,
+                top: element.top,
+                width: element.width,
+                height: element.height,
+                rotate: element.rotate,
+              })
+              element.top = maxY - elHeight + offsetY
+            }
+            else element.top = maxY - elHeight
+          }
+          else {
+            const range = groupElementRangeMap[element.groupId]
+            const offset = range.maxY - maxY
+            element.top = element.top - offset
+          }
+        }
+      })
+    }
+    else if (command === ElementAlignCommands.HORIZONTAL) {
+      const horizontalCenter = (minX + maxX) / 2
+      elementList.forEach(element => {
+        if (activeElementIdList.value.includes(element.id)) {
+          if (!element.groupId) {
+            const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
+            element.left = horizontalCenter - elWidth / 2
+          }
+          else {
+            const range = groupElementRangeMap[element.groupId]
+            const center = (range.maxX + range.minX) / 2
+            const offset = center - horizontalCenter
+            element.left = element.left - offset
+          }
+        }
+      })
+    }
+    else if (command === ElementAlignCommands.VERTICAL) {
+      const verticalCenter = (minY + maxY) / 2
+      elementList.forEach(element => {
+        if (activeElementIdList.value.includes(element.id)) {
+          if (!element.groupId) {
+            const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
+            element.top = verticalCenter - elHeight / 2
+          }
+          else {
+            const range = groupElementRangeMap[element.groupId]
+            const center = (range.maxY + range.minY) / 2
+            const offset = center - verticalCenter
+            element.top = element.top - offset
+          }
+        }
+      })
+    }
+
+    slidesStore.updateSlide({ elements: elementList })
+    addHistorySnapshot()
+  }
+
+  return {
+    alignActiveElement,
+  }
 }

+ 28 - 28
src/hooks/useImport.ts

@@ -95,46 +95,46 @@ export default () => {
 
   const rotateLine = (line: PPTLineElement, angleDeg: number) => {
     const { start, end } = line
-
+      
     const angleRad = angleDeg * Math.PI / 180
-
+    
     const midX = (start[0] + end[0]) / 2
     const midY = (start[1] + end[1]) / 2
-
+    
     const startTransX = start[0] - midX
     const startTransY = start[1] - midY
     const endTransX = end[0] - midX
     const endTransY = end[1] - midY
-
+    
     const cosA = Math.cos(angleRad)
     const sinA = Math.sin(angleRad)
-
+    
     const startRotX = startTransX * cosA - startTransY * sinA
     const startRotY = startTransX * sinA + startTransY * cosA
-
+    
     const endRotX = endTransX * cosA - endTransY * sinA
     const endRotY = endTransX * sinA + endTransY * cosA
-
+    
     const startNewX = startRotX + midX
     const startNewY = startRotY + midY
     const endNewX = endRotX + midX
     const endNewY = endRotY + midY
-
+    
     const beforeMinX = Math.min(start[0], end[0])
     const beforeMinY = Math.min(start[1], end[1])
-
+    
     const afterMinX = Math.min(startNewX, endNewX)
     const afterMinY = Math.min(startNewY, endNewY)
-
+    
     const startAdjustedX = startNewX - afterMinX
     const startAdjustedY = startNewY - afterMinY
     const endAdjustedX = endNewX - afterMinX
     const endAdjustedY = endNewY - afterMinY
-
+    
     const startAdjusted: [number, number] = [startAdjustedX, startAdjustedY]
     const endAdjusted: [number, number] = [endAdjustedX, endAdjustedY]
     const offset = [afterMinX - beforeMinX, afterMinY - beforeMinY]
-
+    
     return {
       start: startAdjusted,
       end: endAdjusted,
@@ -214,7 +214,7 @@ export default () => {
 
       if (axis === 'y') newElement.left = 2 * centerX - element.left - element.width
       if (axis === 'x') newElement.top = 2 * centerY - element.top - element.height
-
+  
       return newElement
     })
   }
@@ -249,7 +249,7 @@ export default () => {
   const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean }) => {
     const defaultOptions = {
       cover: false,
-      fixedViewport: false,
+      fixedViewport: false, 
     }
     const { cover, fixedViewport } = { ...defaultOptions, ...options }
 
@@ -262,7 +262,7 @@ export default () => {
     for (const item of SHAPE_LIST) {
       shapeList.push(...item.children)
     }
-
+    
     const reader = new FileReader()
     reader.onload = async e => {
       let json = null
@@ -277,7 +277,7 @@ export default () => {
 
       let ratio = 96 / 72
       const width = json.size.width
-
+      
       if (fixedViewport) ratio = 1000 / width
       else slidesStore.setViewportSize(width * ratio)
 
@@ -336,7 +336,7 @@ export default () => {
             el.height = el.height * ratio
             el.left = el.left * ratio
             el.top = el.top * ratio
-
+  
             if (el.type === 'text') {
               const textEl: PPTTextElement = {
                 type: 'text',
@@ -481,7 +481,7 @@ export default () => {
                 const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
 
                 const fill = el.fill?.type === 'color' ? el.fill.value : ''
-
+                
                 const element: PPTShapeElement = {
                   type: 'shape',
                   id: nanoid(10),
@@ -518,15 +518,15 @@ export default () => {
                     color: el.shadow.color,
                   }
                 }
-
+    
                 if (shape) {
                   element.path = shape.path
                   element.viewBox = shape.viewBox
-
+    
                   if (shape.pathFormula) {
                     element.pathFormula = shape.pathFormula
                     element.viewBox = [el.width, el.height]
-
+    
                     const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
                     if ('editable' in pathFormula && pathFormula.editable) {
                       element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
@@ -553,14 +553,14 @@ export default () => {
                   const { maxX, maxY } = getSvgPathRange(element.path)
                   element.viewBox = [maxX || originWidth, maxY || originHeight]
                 }
-
+    
                 if (element.path) slide.elements.push(element)
               }
             }
             else if (el.type === 'table') {
               const row = el.data.length
               const col = el.data[0].length
-
+  
               const style: TableCellStyle = {
                 fontname: theme.value.fontName,
                 color: theme.value.fontColor,
@@ -600,7 +600,7 @@ export default () => {
                 }
                 data.push(rowCells)
               }
-
+  
               const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
               const colWidths: number[] = el.colWidths.map(item => item / allWidth)
 
@@ -616,7 +616,7 @@ export default () => {
               const borderWidth = border?.borderWidth || 0
               const borderStyle = border?.borderType || 'solid'
               const borderColor = border?.borderColor || '#eeece1'
-
+  
               slide.elements.push({
                 type: 'table',
                 id: nanoid(10),
@@ -639,7 +639,7 @@ export default () => {
               let labels: string[]
               let legends: string[]
               let series: number[][]
-
+  
               if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
                 labels = el.data[0].map((item, index) => `坐标${index + 1}`)
                 legends = ['X', 'Y']
@@ -653,7 +653,7 @@ export default () => {
               }
 
               const options: ChartOptions = {}
-
+  
               let chartType: ChartType = 'bar'
 
               switch (el.chartType) {
@@ -689,7 +689,7 @@ export default () => {
                   break
                 default:
               }
-
+  
               slide.elements.push({
                 type: 'chart',
                 id: nanoid(10),

+ 60 - 60
src/hooks/useMoveElement.ts

@@ -1,61 +1,61 @@
-import { storeToRefs } from 'pinia'
-import { useMainStore, useSlidesStore } from '@/store'
-import type { PPTElement } from '@/types/slides'
-import { KEYS } from '@/configs/hotkey'
-import useHistorySnapshot from '@/hooks/useHistorySnapshot'
-
-export default () => {
-  const slidesStore = useSlidesStore()
-  const { activeElementIdList, activeGroupElementId } = storeToRefs(useMainStore())
-  const { currentSlide } = storeToRefs(slidesStore)
-
-  const { addHistorySnapshot } = useHistorySnapshot()
-
-  /**
-   * 将元素向指定方向移动指定的距离
-   * 组合元素成员中,存在被选中可独立操作的元素时,优先移动该元素。否则默认移动所有被选中的元素
-   * @param command 移动方向
-   * @param step 移动距离
-   */
-  const moveElement = (command: string, step = 1) => {
-    let newElementList: PPTElement[] = []
-
-    const move = (el: PPTElement) => {
-      let { left, top } = el
-      switch (command) {
-        case KEYS.LEFT: 
-          left = left - step
-          break
-        case KEYS.RIGHT: 
-          left = left + step
-          break
-        case KEYS.UP: 
-          top = top - step
-          break
-        case KEYS.DOWN: 
-          top = top + step
-          break
-        default: break
-      }
-      return { ...el, left, top }
-    }
-
-    if (activeGroupElementId.value) {
-      newElementList = currentSlide.value.elements.map(el => {
-        return activeGroupElementId.value === el.id ? move(el) : el
-      })
-    }
-    else {
-      newElementList = currentSlide.value.elements.map(el => {
-        return activeElementIdList.value.includes(el.id) ? move(el) : el
-      })
-    }
-
-    slidesStore.updateSlide({ elements: newElementList })
-    addHistorySnapshot()
-  }
-
-  return {
-    moveElement,
-  }
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSlidesStore } from '@/store'
+import type { PPTElement } from '@/types/slides'
+import { KEYS } from '@/configs/hotkey'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+
+export default () => {
+  const slidesStore = useSlidesStore()
+  const { activeElementIdList, activeGroupElementId } = storeToRefs(useMainStore())
+  const { currentSlide } = storeToRefs(slidesStore)
+
+  const { addHistorySnapshot } = useHistorySnapshot()
+
+  /**
+   * 将元素向指定方向移动指定的距离
+   * 组合元素成员中,存在被选中可独立操作的元素时,优先移动该元素。否则默认移动所有被选中的元素
+   * @param command 移动方向
+   * @param step 移动距离
+   */
+  const moveElement = (command: string, step = 1) => {
+    let newElementList: PPTElement[] = []
+
+    const move = (el: PPTElement) => {
+      let { left, top } = el
+      switch (command) {
+        case KEYS.LEFT: 
+          left = left - step
+          break
+        case KEYS.RIGHT: 
+          left = left + step
+          break
+        case KEYS.UP: 
+          top = top - step
+          break
+        case KEYS.DOWN: 
+          top = top + step
+          break
+        default: break
+      }
+      return { ...el, left, top }
+    }
+
+    if (activeGroupElementId.value) {
+      newElementList = currentSlide.value.elements.map(el => {
+        return activeGroupElementId.value === el.id ? move(el) : el
+      })
+    }
+    else {
+      newElementList = currentSlide.value.elements.map(el => {
+        return activeElementIdList.value.includes(el.id) ? move(el) : el
+      })
+    }
+
+    slidesStore.updateSlide({ elements: newElementList })
+    addHistorySnapshot()
+  }
+
+  return {
+    moveElement,
+  }
 }

+ 260 - 260
src/hooks/useUniformDisplayElement.ts

@@ -1,261 +1,261 @@
-import { computed } from 'vue'
-import { storeToRefs } from 'pinia'
-import { useMainStore, useSlidesStore } from '@/store'
-import type { PPTElement } from '@/types/slides'
-import { getElementRange, getElementListRange, getRectRotatedOffset } from '@/utils/element'
-import useHistorySnapshot from './useHistorySnapshot'
-
-interface ElementItem {
-  min: number
-  max: number
-  el: PPTElement
-}
-
-interface GroupItem {
-  groupId: string
-  els: PPTElement[]
-}
-
-interface GroupElementsItem {
-  min: number
-  max: number
-  els: PPTElement[]
-}
-
-type Item = ElementItem | GroupElementsItem
-
-interface ElementWithPos {
-  pos: number
-  el: PPTElement
-}
-
-interface LastPos {
-  min: number
-  max: number
-}
-
-export default () => {
-  const slidesStore = useSlidesStore()
-  const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
-  const { currentSlide } = storeToRefs(slidesStore)
-
-  const { addHistorySnapshot } = useHistorySnapshot()
-
-  const displayItemCount = computed(() => {
-    let count = 0
-    const groupIdList: string[] = []
-    for (const el of activeElementList.value) {
-      if (!el.groupId) count += 1
-      else if (!groupIdList.includes(el.groupId)) {
-        groupIdList.push(el.groupId)
-        count += 1
-      }
-    }
-    return count
-  })
-  // 水平均匀排列
-  const uniformHorizontalDisplay = () => {
-    const { minX, maxX } = getElementListRange(activeElementList.value)
-    const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
-    const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
-
-    // 分别获取普通元素和组合元素集合,并记录下每一项的范围
-    const singleElemetList: ElementItem[] = []
-    let groupList: GroupItem[] = []
-    for (const el of copyOfActiveElementList) {
-      if (!el.groupId) {
-        const { minX, maxX } = getElementRange(el)
-        singleElemetList.push({ min: minX, max: maxX, el })
-      }
-      else {
-        const groupEl = groupList.find(item => item.groupId === el.groupId)
-        if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
-        else {
-          groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
-        }
-      }
-    }
-    const formatedGroupList: GroupElementsItem[] = []
-    for (const groupItem of groupList) {
-      const { minX, maxX } = getElementListRange(groupItem.els)
-      formatedGroupList.push({ min: minX, max: maxX, els: groupItem.els })
-    }
-
-    // 将普通元素和组合元素集合组合在一起,然后将每一项按位置(从左到右)排序
-    const list: Item[] = [...singleElemetList, ...formatedGroupList]
-    list.sort((itemA, itemB) => itemA.min - itemB.min)
-
-    // 计算元素均匀分布所需要的间隔:
-    // (所选元素整体范围 - 所有所选元素宽度和) / (所选元素数 - 1)
-    let totalWidth = 0
-    for (const item of list) {
-      const width = item.max - item.min
-      totalWidth += width
-    }
-    const span = ((maxX - minX) - totalWidth) / (list.length - 1)
-
-    // 按位置顺序依次计算每一个元素的目标位置
-    // 第一项中的元素即为起点,无需计算
-    // 从第二项开始,每一项的位置应该为:上一项位置 + 上一项宽度 + 间隔
-    // 注意此处计算的位置(pos)并非元素最终的left值,而是目标位置范围最小值(元素旋转后的left值 ≠ 范围最小值)
-    const sortedElementData: ElementWithPos[] = []
-
-    const firstItem = list[0]
-    let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
-
-    if ('el' in firstItem) {
-      sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
-    }
-    else {
-      for (const el of firstItem.els) {
-        const { minX: pos } = getElementRange(el)
-        sortedElementData.push({ pos, el })
-      }
-    }
-
-    for (let i = 1; i < list.length; i++) {
-      const item = list[i]
-      const lastWidth = lastPos.max - lastPos.min
-      const currentPos = lastPos.min + lastWidth + span
-      const currentWidth = item.max - item.min
-      lastPos = { min: currentPos, max: currentPos + currentWidth }
-
-      if ('el' in item) {
-        sortedElementData.push({ pos: currentPos, el: item.el })
-      }
-      else {
-        for (const el of item.els) {
-          const { minX } = getElementRange(el)
-          const offset = minX - item.min
-          sortedElementData.push({ pos: currentPos + offset, el })
-        }
-      }
-    }
-
-    // 根据目标位置计算元素最终目标left值
-    // 对于旋转后的元素,需要计算旋转前后left的偏移来做校正
-    for (const element of newElementList) {
-      if (!activeElementIdList.value.includes(element.id)) continue
-
-      for (const sortedItem of sortedElementData) {
-        if (sortedItem.el.id === element.id) {
-          if ('rotate' in element && element.rotate) {
-            const { offsetX } = getRectRotatedOffset({
-              left: element.left,
-              top: element.top,
-              width: element.width,
-              height: element.height,
-              rotate: element.rotate,
-            })
-            element.left = sortedItem.pos - offsetX
-          }
-          else element.left = sortedItem.pos
-        }
-      }
-    }
-
-    slidesStore.updateSlide({ elements: newElementList })
-    addHistorySnapshot()
-  }
-
-  // 垂直均匀排列(逻辑类似水平均匀排列方法)
-  const uniformVerticalDisplay = () => {
-    const { minY, maxY } = getElementListRange(activeElementList.value)
-    const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
-    const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
-
-    const singleElemetList: ElementItem[] = []
-    let groupList: GroupItem[] = []
-    for (const el of copyOfActiveElementList) {
-      if (!el.groupId) {
-        const { minY, maxY } = getElementRange(el)
-        singleElemetList.push({ min: minY, max: maxY, el })
-      }
-      else {
-        const groupEl = groupList.find(item => item.groupId === el.groupId)
-        if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
-        else {
-          groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
-        }
-      }
-    }
-    const formatedGroupList: GroupElementsItem[] = []
-    for (const groupItem of groupList) {
-      const { minY, maxY } = getElementListRange(groupItem.els)
-      formatedGroupList.push({ min: minY, max: maxY, els: groupItem.els })
-    }
-
-    const list: Item[] = [...singleElemetList, ...formatedGroupList]
-    list.sort((itemA, itemB) => itemA.min - itemB.min)
-
-    let totalHeight = 0
-    for (const item of list) {
-      const height = item.max - item.min
-      totalHeight += height
-    }
-    const span = ((maxY - minY) - totalHeight) / (list.length - 1)
-
-    const sortedElementData: ElementWithPos[] = []
-
-    const firstItem = list[0]
-    let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
-
-    if ('el' in firstItem) {
-      sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
-    }
-    else {
-      for (const el of firstItem.els) {
-        const { minY: pos } = getElementRange(el)
-        sortedElementData.push({ pos, el })
-      }
-    }
-
-    for (let i = 1; i < list.length; i++) {
-      const item = list[i]
-      const lastHeight = lastPos.max - lastPos.min
-      const currentPos = lastPos.min + lastHeight + span
-      const currentHeight = item.max - item.min
-      lastPos = { min: currentPos, max: currentPos + currentHeight }
-
-      if ('el' in item) {
-        sortedElementData.push({ pos: currentPos, el: item.el })
-      }
-      else {
-        for (const el of item.els) {
-          const { minY } = getElementRange(el)
-          const offset = minY - item.min
-          sortedElementData.push({ pos: currentPos + offset, el })
-        }
-      }
-    }
-
-    for (const element of newElementList) {
-      if (!activeElementIdList.value.includes(element.id)) continue
-
-      for (const sortedItem of sortedElementData) {
-        if (sortedItem.el.id === element.id) {
-          if ('rotate' in element && element.rotate) {
-            const { offsetY } = getRectRotatedOffset({
-              left: element.left,
-              top: element.top,
-              width: element.width,
-              height: element.height,
-              rotate: element.rotate,
-            })
-            element.top = sortedItem.pos - offsetY
-          }
-          else element.top = sortedItem.pos
-        }
-      }
-    }
-
-    slidesStore.updateSlide({ elements: newElementList })
-    addHistorySnapshot()
-  }
-
-  return {
-    displayItemCount,
-    uniformHorizontalDisplay,
-    uniformVerticalDisplay,
-  }
+import { computed } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSlidesStore } from '@/store'
+import type { PPTElement } from '@/types/slides'
+import { getElementRange, getElementListRange, getRectRotatedOffset } from '@/utils/element'
+import useHistorySnapshot from './useHistorySnapshot'
+
+interface ElementItem {
+  min: number
+  max: number
+  el: PPTElement
+}
+
+interface GroupItem {
+  groupId: string
+  els: PPTElement[]
+}
+
+interface GroupElementsItem {
+  min: number
+  max: number
+  els: PPTElement[]
+}
+
+type Item = ElementItem | GroupElementsItem
+
+interface ElementWithPos {
+  pos: number
+  el: PPTElement
+}
+
+interface LastPos {
+  min: number
+  max: number
+}
+
+export default () => {
+  const slidesStore = useSlidesStore()
+  const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
+  const { currentSlide } = storeToRefs(slidesStore)
+
+  const { addHistorySnapshot } = useHistorySnapshot()
+
+  const displayItemCount = computed(() => {
+    let count = 0
+    const groupIdList: string[] = []
+    for (const el of activeElementList.value) {
+      if (!el.groupId) count += 1
+      else if (!groupIdList.includes(el.groupId)) {
+        groupIdList.push(el.groupId)
+        count += 1
+      }
+    }
+    return count
+  })
+  // 水平均匀排列
+  const uniformHorizontalDisplay = () => {
+    const { minX, maxX } = getElementListRange(activeElementList.value)
+    const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
+    const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
+
+    // 分别获取普通元素和组合元素集合,并记录下每一项的范围
+    const singleElemetList: ElementItem[] = []
+    let groupList: GroupItem[] = []
+    for (const el of copyOfActiveElementList) {
+      if (!el.groupId) {
+        const { minX, maxX } = getElementRange(el)
+        singleElemetList.push({ min: minX, max: maxX, el })
+      }
+      else {
+        const groupEl = groupList.find(item => item.groupId === el.groupId)
+        if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
+        else {
+          groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
+        }
+      }
+    }
+    const formatedGroupList: GroupElementsItem[] = []
+    for (const groupItem of groupList) {
+      const { minX, maxX } = getElementListRange(groupItem.els)
+      formatedGroupList.push({ min: minX, max: maxX, els: groupItem.els })
+    }
+
+    // 将普通元素和组合元素集合组合在一起,然后将每一项按位置(从左到右)排序
+    const list: Item[] = [...singleElemetList, ...formatedGroupList]
+    list.sort((itemA, itemB) => itemA.min - itemB.min)
+
+    // 计算元素均匀分布所需要的间隔:
+    // (所选元素整体范围 - 所有所选元素宽度和) / (所选元素数 - 1)
+    let totalWidth = 0
+    for (const item of list) {
+      const width = item.max - item.min
+      totalWidth += width
+    }
+    const span = ((maxX - minX) - totalWidth) / (list.length - 1)
+
+    // 按位置顺序依次计算每一个元素的目标位置
+    // 第一项中的元素即为起点,无需计算
+    // 从第二项开始,每一项的位置应该为:上一项位置 + 上一项宽度 + 间隔
+    // 注意此处计算的位置(pos)并非元素最终的left值,而是目标位置范围最小值(元素旋转后的left值 ≠ 范围最小值)
+    const sortedElementData: ElementWithPos[] = []
+
+    const firstItem = list[0]
+    let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
+
+    if ('el' in firstItem) {
+      sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
+    }
+    else {
+      for (const el of firstItem.els) {
+        const { minX: pos } = getElementRange(el)
+        sortedElementData.push({ pos, el })
+      }
+    }
+
+    for (let i = 1; i < list.length; i++) {
+      const item = list[i]
+      const lastWidth = lastPos.max - lastPos.min
+      const currentPos = lastPos.min + lastWidth + span
+      const currentWidth = item.max - item.min
+      lastPos = { min: currentPos, max: currentPos + currentWidth }
+
+      if ('el' in item) {
+        sortedElementData.push({ pos: currentPos, el: item.el })
+      }
+      else {
+        for (const el of item.els) {
+          const { minX } = getElementRange(el)
+          const offset = minX - item.min
+          sortedElementData.push({ pos: currentPos + offset, el })
+        }
+      }
+    }
+
+    // 根据目标位置计算元素最终目标left值
+    // 对于旋转后的元素,需要计算旋转前后left的偏移来做校正
+    for (const element of newElementList) {
+      if (!activeElementIdList.value.includes(element.id)) continue
+
+      for (const sortedItem of sortedElementData) {
+        if (sortedItem.el.id === element.id) {
+          if ('rotate' in element && element.rotate) {
+            const { offsetX } = getRectRotatedOffset({
+              left: element.left,
+              top: element.top,
+              width: element.width,
+              height: element.height,
+              rotate: element.rotate,
+            })
+            element.left = sortedItem.pos - offsetX
+          }
+          else element.left = sortedItem.pos
+        }
+      }
+    }
+
+    slidesStore.updateSlide({ elements: newElementList })
+    addHistorySnapshot()
+  }
+
+  // 垂直均匀排列(逻辑类似水平均匀排列方法)
+  const uniformVerticalDisplay = () => {
+    const { minY, maxY } = getElementListRange(activeElementList.value)
+    const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
+    const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
+
+    const singleElemetList: ElementItem[] = []
+    let groupList: GroupItem[] = []
+    for (const el of copyOfActiveElementList) {
+      if (!el.groupId) {
+        const { minY, maxY } = getElementRange(el)
+        singleElemetList.push({ min: minY, max: maxY, el })
+      }
+      else {
+        const groupEl = groupList.find(item => item.groupId === el.groupId)
+        if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
+        else {
+          groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
+        }
+      }
+    }
+    const formatedGroupList: GroupElementsItem[] = []
+    for (const groupItem of groupList) {
+      const { minY, maxY } = getElementListRange(groupItem.els)
+      formatedGroupList.push({ min: minY, max: maxY, els: groupItem.els })
+    }
+
+    const list: Item[] = [...singleElemetList, ...formatedGroupList]
+    list.sort((itemA, itemB) => itemA.min - itemB.min)
+
+    let totalHeight = 0
+    for (const item of list) {
+      const height = item.max - item.min
+      totalHeight += height
+    }
+    const span = ((maxY - minY) - totalHeight) / (list.length - 1)
+
+    const sortedElementData: ElementWithPos[] = []
+
+    const firstItem = list[0]
+    let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
+
+    if ('el' in firstItem) {
+      sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
+    }
+    else {
+      for (const el of firstItem.els) {
+        const { minY: pos } = getElementRange(el)
+        sortedElementData.push({ pos, el })
+      }
+    }
+
+    for (let i = 1; i < list.length; i++) {
+      const item = list[i]
+      const lastHeight = lastPos.max - lastPos.min
+      const currentPos = lastPos.min + lastHeight + span
+      const currentHeight = item.max - item.min
+      lastPos = { min: currentPos, max: currentPos + currentHeight }
+
+      if ('el' in item) {
+        sortedElementData.push({ pos: currentPos, el: item.el })
+      }
+      else {
+        for (const el of item.els) {
+          const { minY } = getElementRange(el)
+          const offset = minY - item.min
+          sortedElementData.push({ pos: currentPos + offset, el })
+        }
+      }
+    }
+
+    for (const element of newElementList) {
+      if (!activeElementIdList.value.includes(element.id)) continue
+
+      for (const sortedItem of sortedElementData) {
+        if (sortedItem.el.id === element.id) {
+          if ('rotate' in element && element.rotate) {
+            const { offsetY } = getRectRotatedOffset({
+              left: element.left,
+              top: element.top,
+              width: element.width,
+              height: element.height,
+              rotate: element.rotate,
+            })
+            element.top = sortedItem.pos - offsetY
+          }
+          else element.top = sortedItem.pos
+        }
+      }
+    }
+
+    slidesStore.updateSlide({ elements: newElementList })
+    addHistorySnapshot()
+  }
+
+  return {
+    displayItemCount,
+    uniformHorizontalDisplay,
+    uniformVerticalDisplay,
+  }
 }

+ 3 - 2
src/services/index.ts

@@ -28,7 +28,8 @@ export default {
   },
 
   getFileData(filename: string): Promise<any> {
-    return axios.get(`${ASSET_URL}/data/${filename}.json`)
+	  //return axios.get(`${ASSET_URL}/data/${filename}.json`)
+	  return axios.get(`./mocks/${filename}.json`)
   },
 
   AIPPT_Outline({
@@ -87,4 +88,4 @@ export default {
       }),
     })
   },
-}
+}

+ 17 - 17
src/store/slides.ts

@@ -64,7 +64,7 @@ export const useSlidesStore = defineStore('slides', {
     currentSlide(state) {
       return state.slides[state.slideIndex]
     },
-
+  
     currentSlideAnimations(state) {
       const currentSlide = state.slides[state.slideIndex]
       if (!currentSlide?.animations) return []
@@ -116,23 +116,23 @@ export const useSlidesStore = defineStore('slides', {
     setTheme(themeProps: Partial<SlideTheme>) {
       this.theme = { ...this.theme, ...themeProps }
     },
-
+  
     setViewportSize(size: number) {
       this.viewportSize = size
     },
-
+  
     setViewportRatio(viewportRatio: number) {
       this.viewportRatio = viewportRatio
     },
-
+  
     setSlides(slides: Slide[]) {
       this.slides = slides
     },
-
+  
     setTemplates(templates: SlideTemplate[]) {
       this.templates = templates
     },
-
+  
     addSlide(slide: Slide | Slide[]) {
       const slides = Array.isArray(slide) ? slide : [slide]
       for (const slide of slides) {
@@ -143,12 +143,12 @@ export const useSlidesStore = defineStore('slides', {
       this.slides.splice(addIndex, 0, ...slides)
       this.slideIndex = addIndex
     },
-
+  
     updateSlide(props: Partial<Slide>, slideId?: string) {
       const slideIndex = slideId ? this.slides.findIndex(item => item.id === slideId) : this.slideIndex
       this.slides[slideIndex] = { ...this.slides[slideIndex], ...props }
     },
-
+  
     removeSlideProps(data: RemovePropData) {
       const { id, propName } = data
 
@@ -157,11 +157,11 @@ export const useSlidesStore = defineStore('slides', {
       }) as Slide[]
       this.slides = slides
     },
-
+  
     deleteSlide(slideId: string | string[]) {
       const slidesId = Array.isArray(slideId) ? slideId : [slideId]
       const slides: Slide[] = JSON.parse(JSON.stringify(this.slides))
-
+  
       const deleteSlidesIndex = []
       for (const deletedId of slidesId) {
         const index = slides.findIndex(item => item.id === deletedId)
@@ -179,18 +179,18 @@ export const useSlidesStore = defineStore('slides', {
         slides.splice(index, 1)
       }
       let newIndex = Math.min(...deleteSlidesIndex)
-
+  
       const maxIndex = slides.length - 1
       if (newIndex > maxIndex) newIndex = maxIndex
-
+  
       this.slideIndex = newIndex
       this.slides = slides
     },
-
+  
     updateSlideIndex(index: number) {
       this.slideIndex = index
     },
-
+  
     addElement(element: PPTElement | PPTElement[]) {
       const elements = Array.isArray(element) ? element : [element]
       const currentSlideEls = this.slides[this.slideIndex].elements
@@ -204,7 +204,7 @@ export const useSlidesStore = defineStore('slides', {
       const newEls = currentSlideEls.filter(item => !elementIdList.includes(item.id))
       this.slides[this.slideIndex].elements = newEls
     },
-
+  
     updateElement(data: UpdateElementData) {
       const { id, props, slideId } = data
       const elIdList = typeof id === 'string' ? [id] : id
@@ -216,11 +216,11 @@ export const useSlidesStore = defineStore('slides', {
       })
       this.slides[slideIndex].elements = (elements as PPTElement[])
     },
-
+  
     removeElementProps(data: RemovePropData) {
       const { id, propName } = data
       const propsNames = typeof propName === 'string' ? [propName] : propName
-
+  
       const slideIndex = this.slideIndex
       const slide = this.slides[slideIndex]
       const elements = slide.elements.map(el => {

+ 125 - 125
src/types/edit.ts

@@ -1,126 +1,126 @@
-import type { ShapePoolItem } from '@/configs/shapes'
-import type { LinePoolItem } from '@/configs/lines'
-import type { ImageClipDataRange, PPTElementOutline, PPTElementShadow, Gradient } from './slides'
-
-export enum ElementOrderCommands {
-  UP = 'up',
-  DOWN = 'down',
-  TOP = 'top',
-  BOTTOM = 'bottom',
-}
-
-export enum ElementAlignCommands {
-  TOP = 'top',
-  BOTTOM = 'bottom',
-  LEFT = 'left',
-  RIGHT = 'right',
-  VERTICAL = 'vertical',
-  HORIZONTAL = 'horizontal',
-  CENTER = 'center',
-}
-
-export const enum OperateBorderLines {
-  T = 'top',
-  B = 'bottom',
-  L = 'left',
-  R = 'right',
-}
-
-export const enum OperateResizeHandlers {
-  LEFT_TOP = 'left-top',
-  TOP = 'top',
-  RIGHT_TOP = 'right-top',
-  LEFT = 'left',
-  RIGHT = 'right',
-  LEFT_BOTTOM = 'left-bottom',
-  BOTTOM = 'bottom',
-  RIGHT_BOTTOM = 'right-bottom',
-}
-
-export const enum OperateLineHandlers {
-  START = 'start',
-  END = 'end',
-  C = 'ctrl',
-  C1 = 'ctrl1',
-  C2 = 'ctrl2',
-}
-
-export interface AlignmentLineAxis {
-  x: number
-  y: number
-}
-
-export interface AlignmentLineProps {
-  type: 'vertical' | 'horizontal'
-  axis: AlignmentLineAxis
-  length: number
-}
-
-export interface MultiSelectRange {
-  minX: number
-  maxX: number
-  minY: number
-  maxY: number
-}
-
-export interface ImageClipedEmitData {
-  range: ImageClipDataRange
-  position: {
-    left: number
-    top: number
-    width: number
-    height: number
-  }
-}
-
-export interface CreateElementSelectionData {
-  start: [number, number]
-  end: [number, number]
-}
-
-export interface CreateCustomShapeData {
-  start: [number, number]
-  end: [number, number]
-  path: string
-  viewBox: [number, number]
-  fill?: string
-  outline?: PPTElementOutline
-}
-
-export interface CreatingTextElement {
-  type: 'text'
-  vertical?: boolean
-}
-export interface CreatingShapeElement {
-  type: 'shape'
-  data: ShapePoolItem
-}
-export interface CreatingLineElement {
-  type: 'line'
-  data: LinePoolItem
-}
-export type CreatingElement = CreatingTextElement | CreatingShapeElement | CreatingLineElement
-
-export type TextFormatPainterKeys = 'bold' | 'em' | 'underline' | 'strikethrough' | 'color' | 'backcolor' | 'fontsize' | 'fontname' | 'align'
-
-export interface TextFormatPainter {
-  keep: boolean
-  bold?: boolean
-  em?: boolean
-  underline?: boolean
-  strikethrough?: boolean
-  color?: string
-  backcolor?: string
-  fontsize?: string
-  fontname?: string
-  align?: 'left' | 'right' | 'center'
-}
-
-export interface ShapeFormatPainter {
-  keep: boolean
-  fill?: string
-  gradient?: Gradient
-  outline?: PPTElementOutline
-  opacity?: number
-  shadow?: PPTElementShadow
+import type { ShapePoolItem } from '@/configs/shapes'
+import type { LinePoolItem } from '@/configs/lines'
+import type { ImageClipDataRange, PPTElementOutline, PPTElementShadow, Gradient } from './slides'
+
+export enum ElementOrderCommands {
+  UP = 'up',
+  DOWN = 'down',
+  TOP = 'top',
+  BOTTOM = 'bottom',
+}
+
+export enum ElementAlignCommands {
+  TOP = 'top',
+  BOTTOM = 'bottom',
+  LEFT = 'left',
+  RIGHT = 'right',
+  VERTICAL = 'vertical',
+  HORIZONTAL = 'horizontal',
+  CENTER = 'center',
+}
+
+export const enum OperateBorderLines {
+  T = 'top',
+  B = 'bottom',
+  L = 'left',
+  R = 'right',
+}
+
+export const enum OperateResizeHandlers {
+  LEFT_TOP = 'left-top',
+  TOP = 'top',
+  RIGHT_TOP = 'right-top',
+  LEFT = 'left',
+  RIGHT = 'right',
+  LEFT_BOTTOM = 'left-bottom',
+  BOTTOM = 'bottom',
+  RIGHT_BOTTOM = 'right-bottom',
+}
+
+export const enum OperateLineHandlers {
+  START = 'start',
+  END = 'end',
+  C = 'ctrl',
+  C1 = 'ctrl1',
+  C2 = 'ctrl2',
+}
+
+export interface AlignmentLineAxis {
+  x: number
+  y: number
+}
+
+export interface AlignmentLineProps {
+  type: 'vertical' | 'horizontal'
+  axis: AlignmentLineAxis
+  length: number
+}
+
+export interface MultiSelectRange {
+  minX: number
+  maxX: number
+  minY: number
+  maxY: number
+}
+
+export interface ImageClipedEmitData {
+  range: ImageClipDataRange
+  position: {
+    left: number
+    top: number
+    width: number
+    height: number
+  }
+}
+
+export interface CreateElementSelectionData {
+  start: [number, number]
+  end: [number, number]
+}
+
+export interface CreateCustomShapeData {
+  start: [number, number]
+  end: [number, number]
+  path: string
+  viewBox: [number, number]
+  fill?: string
+  outline?: PPTElementOutline
+}
+
+export interface CreatingTextElement {
+  type: 'text'
+  vertical?: boolean
+}
+export interface CreatingShapeElement {
+  type: 'shape'
+  data: ShapePoolItem
+}
+export interface CreatingLineElement {
+  type: 'line'
+  data: LinePoolItem
+}
+export type CreatingElement = CreatingTextElement | CreatingShapeElement | CreatingLineElement
+
+export type TextFormatPainterKeys = 'bold' | 'em' | 'underline' | 'strikethrough' | 'color' | 'backcolor' | 'fontsize' | 'fontname' | 'align'
+
+export interface TextFormatPainter {
+  keep: boolean
+  bold?: boolean
+  em?: boolean
+  underline?: boolean
+  strikethrough?: boolean
+  color?: string
+  backcolor?: string
+  fontsize?: string
+  fontname?: string
+  align?: 'left' | 'right' | 'center'
+}
+
+export interface ShapeFormatPainter {
+  keep: boolean
+  fill?: string
+  gradient?: Gradient
+  outline?: PPTElementOutline
+  opacity?: number
+  shadow?: PPTElementShadow
 }

+ 258 - 258
src/utils/element.ts

@@ -1,259 +1,259 @@
-import tinycolor from 'tinycolor2'
-import { nanoid } from 'nanoid'
-import type { PPTElement, PPTLineElement, Slide } from '@/types/slides'
-
-interface RotatedElementData {
-  left: number
-  top: number
-  width: number
-  height: number
-  rotate: number
-}
-
-interface IdMap {
-  [id: string]: string
-}
-
-/**
- * 计算元素在画布中的矩形范围旋转后的新位置范围
- * @param element 元素的位置大小和旋转角度信息
- */
-export const getRectRotatedRange = (element: RotatedElementData) => {
-  const { left, top, width, height, rotate = 0 } = element
-
-  const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
-  const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI
-
-  const tlbraRadian = (180 - rotate - auxiliaryAngle) * Math.PI / 180
-  const trblaRadian = (auxiliaryAngle - rotate) * Math.PI / 180
-
-  const middleLeft = left + width / 2
-  const middleTop = top + height / 2
-
-  const xAxis = [
-    middleLeft + radius * Math.cos(tlbraRadian),
-    middleLeft + radius * Math.cos(trblaRadian),
-    middleLeft - radius * Math.cos(tlbraRadian),
-    middleLeft - radius * Math.cos(trblaRadian),
-  ]
-  const yAxis = [
-    middleTop - radius * Math.sin(tlbraRadian),
-    middleTop - radius * Math.sin(trblaRadian),
-    middleTop + radius * Math.sin(tlbraRadian),
-    middleTop + radius * Math.sin(trblaRadian),
-  ]
-
-  return {
-    xRange: [Math.min(...xAxis), Math.max(...xAxis)],
-    yRange: [Math.min(...yAxis), Math.max(...yAxis)],
-  }
-}
-
-/**
- * 计算元素在画布中的矩形范围旋转后的新位置与旋转之前位置的偏离距离
- * @param element 元素的位置大小和旋转角度信息
- */
-export const getRectRotatedOffset = (element: RotatedElementData) => {
-  const { xRange: originXRange, yRange: originYRange } = getRectRotatedRange({
-    left: element.left,
-    top: element.top,
-    width: element.width,
-    height: element.height,
-    rotate: 0,
-  })
-  const { xRange: rotatedXRange, yRange: rotatedYRange } = getRectRotatedRange({
-    left: element.left,
-    top: element.top,
-    width: element.width,
-    height: element.height,
-    rotate: element.rotate,
-  })
-  return {
-    offsetX: rotatedXRange[0] - originXRange[0],
-    offsetY: rotatedYRange[0] - originYRange[0],
-  }
-}
-
-/**
- * 计算元素在画布中的位置范围
- * @param element 元素信息
- */
-export const getElementRange = (element: PPTElement) => {
-  let minX, maxX, minY, maxY
-
-  if (element.type === 'line') {
-    minX = element.left
-    maxX = element.left + Math.max(element.start[0], element.end[0])
-    minY = element.top
-    maxY = element.top + Math.max(element.start[1], element.end[1])
-  }
-  else if ('rotate' in element && element.rotate) {
-    const { left, top, width, height, rotate } = element
-    const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate })
-    minX = xRange[0]
-    maxX = xRange[1]
-    minY = yRange[0]
-    maxY = yRange[1]
-  }
-  else {
-    minX = element.left
-    maxX = element.left + element.width
-    minY = element.top
-    maxY = element.top + element.height
-  }
-  return { minX, maxX, minY, maxY }
-}
-
-/**
- * 计算一组元素在画布中的位置范围
- * @param elementList 一组元素信息
- */
-export const getElementListRange = (elementList: PPTElement[]) => {
-  const leftValues: number[] = []
-  const topValues: number[] = []
-  const rightValues: number[] = []
-  const bottomValues: number[] = []
-
-  elementList.forEach(element => {
-    const { minX, maxX, minY, maxY } = getElementRange(element)
-    leftValues.push(minX)
-    topValues.push(minY)
-    rightValues.push(maxX)
-    bottomValues.push(maxY)
-  })
-
-  const minX = Math.min(...leftValues)
-  const maxX = Math.max(...rightValues)
-  const minY = Math.min(...topValues)
-  const maxY = Math.max(...bottomValues)
-
-  return { minX, maxX, minY, maxY }
-}
-
-/**
- * 计算线条元素的长度
- * @param element 线条元素
- */
-export const getLineElementLength = (element: PPTLineElement) => {
-  const deltaX = element.end[0] - element.start[0]
-  const deltaY = element.end[1] - element.start[1]
-  const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
-  return len
-}
-
-export interface AlignLine {
-  value: number
-  range: [number, number]
-}
-
-/**
- * 将一组对齐吸附线进行去重:同位置的的多条对齐吸附线仅留下一条,取该位置所有对齐吸附线的最大值和最小值为新的范围
- * @param lines 一组对齐吸附线信息
- */
-export const uniqAlignLines = (lines: AlignLine[]) => {
-  const uniqLines: AlignLine[] = []
-  lines.forEach(line => {
-    const index = uniqLines.findIndex(_line => _line.value === line.value)
-    if (index === -1) uniqLines.push(line)
-    else {
-      const uniqLine = uniqLines[index]
-      const rangeMin = Math.min(uniqLine.range[0], line.range[0])
-      const rangeMax = Math.max(uniqLine.range[1], line.range[1])
-      const range: [number, number] = [rangeMin, rangeMax]
-      const _line = { value: line.value, range }
-      uniqLines[index] = _line
-    }
-  })
-  return uniqLines
-}
-
-/**
- * 以页面列表为基础,为每一个页面生成新的ID,并关联到旧ID形成一个字典
- * 主要用于页面元素时,维持数据中各处页面ID原有的关系
- * @param slides 页面列表
- */
-export const createSlideIdMap = (slides: Slide[]) => {
-  const slideIdMap: IdMap = {}
-  for (const slide of slides) {
-    slideIdMap[slide.id] = nanoid(10)
-  }
-  return slideIdMap
-}
-
-/**
-   * 以元素列表为基础,为每一个元素生成新的ID,并关联到旧ID形成一个字典
-   * 主要用于复制元素时,维持数据中各处元素ID原有的关系
-   * 例如:原本两个组合的元素拥有相同的groupId,复制后依然会拥有另一个相同的groupId
-   * @param elements 元素列表数据
-   */
-export const createElementIdMap = (elements: PPTElement[]) => {
-  const groupIdMap: IdMap = {}
-  const elIdMap: IdMap = {}
-  for (const element of elements) {
-    const groupId = element.groupId
-    if (groupId && !groupIdMap[groupId]) {
-      groupIdMap[groupId] = nanoid(10)
-    }
-    elIdMap[element.id] = nanoid(10)
-  }
-  return {
-    groupIdMap,
-    elIdMap,
-  }
-}
-
-/**
- * 根据表格的主题色,获取对应用于配色的子颜色
- * @param themeColor 主题色
- */
-export const getTableSubThemeColor = (themeColor: string) => {
-  const rgba = tinycolor(themeColor)
-  return [
-    rgba.setAlpha(0.3).toRgbString(),
-    rgba.setAlpha(0.1).toRgbString(),
-  ]
-}
-
-/**
- * 获取线条元素路径字符串
- * @param element 线条元素
- */
-export const getLineElementPath = (element: PPTLineElement) => {
-  const start = element.start.join(',')
-  const end = element.end.join(',')
-  if (element.broken) {
-    const mid = element.broken.join(',')
-    return `M${start} L${mid} L${end}`
-  }
-  else if (element.broken2) {
-    const { minX, maxX, minY, maxY } = getElementRange(element)
-    if (maxX - minX >= maxY - minY) return `M${start} L${element.broken2[0]},${element.start[1]} L${element.broken2[0]},${element.end[1]} ${end}`
-    return `M${start} L${element.start[0]},${element.broken2[1]} L${element.end[0]},${element.broken2[1]} ${end}`
-  }
-  else if (element.curve) {
-    const mid = element.curve.join(',')
-    return `M${start} Q${mid} ${end}`
-  }
-  else if (element.cubic) {
-    const [c1, c2] = element.cubic
-    const p1 = c1.join(',')
-    const p2 = c2.join(',')
-    return `M${start} C${p1} ${p2} ${end}`
-  }
-  return `M${start} L${end}`
-}
-
-/**
- * 判断一个元素是否在可视范围内
- * @param element 元素
- * @param parent 父元素
- */
-export const isElementInViewport = (element: HTMLElement, parent: HTMLElement): boolean => {
-  const elementRect = element.getBoundingClientRect()
-  const parentRect = parent.getBoundingClientRect()
-
-  return (
-    elementRect.top >= parentRect.top &&
-    elementRect.bottom <= parentRect.bottom
-  )
+import tinycolor from 'tinycolor2'
+import { nanoid } from 'nanoid'
+import type { PPTElement, PPTLineElement, Slide } from '@/types/slides'
+
+interface RotatedElementData {
+  left: number
+  top: number
+  width: number
+  height: number
+  rotate: number
+}
+
+interface IdMap {
+  [id: string]: string
+}
+
+/**
+ * 计算元素在画布中的矩形范围旋转后的新位置范围
+ * @param element 元素的位置大小和旋转角度信息
+ */
+export const getRectRotatedRange = (element: RotatedElementData) => {
+  const { left, top, width, height, rotate = 0 } = element
+
+  const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
+  const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI
+
+  const tlbraRadian = (180 - rotate - auxiliaryAngle) * Math.PI / 180
+  const trblaRadian = (auxiliaryAngle - rotate) * Math.PI / 180
+
+  const middleLeft = left + width / 2
+  const middleTop = top + height / 2
+
+  const xAxis = [
+    middleLeft + radius * Math.cos(tlbraRadian),
+    middleLeft + radius * Math.cos(trblaRadian),
+    middleLeft - radius * Math.cos(tlbraRadian),
+    middleLeft - radius * Math.cos(trblaRadian),
+  ]
+  const yAxis = [
+    middleTop - radius * Math.sin(tlbraRadian),
+    middleTop - radius * Math.sin(trblaRadian),
+    middleTop + radius * Math.sin(tlbraRadian),
+    middleTop + radius * Math.sin(trblaRadian),
+  ]
+
+  return {
+    xRange: [Math.min(...xAxis), Math.max(...xAxis)],
+    yRange: [Math.min(...yAxis), Math.max(...yAxis)],
+  }
+}
+
+/**
+ * 计算元素在画布中的矩形范围旋转后的新位置与旋转之前位置的偏离距离
+ * @param element 元素的位置大小和旋转角度信息
+ */
+export const getRectRotatedOffset = (element: RotatedElementData) => {
+  const { xRange: originXRange, yRange: originYRange } = getRectRotatedRange({
+    left: element.left,
+    top: element.top,
+    width: element.width,
+    height: element.height,
+    rotate: 0,
+  })
+  const { xRange: rotatedXRange, yRange: rotatedYRange } = getRectRotatedRange({
+    left: element.left,
+    top: element.top,
+    width: element.width,
+    height: element.height,
+    rotate: element.rotate,
+  })
+  return {
+    offsetX: rotatedXRange[0] - originXRange[0],
+    offsetY: rotatedYRange[0] - originYRange[0],
+  }
+}
+
+/**
+ * 计算元素在画布中的位置范围
+ * @param element 元素信息
+ */
+export const getElementRange = (element: PPTElement) => {
+  let minX, maxX, minY, maxY
+
+  if (element.type === 'line') {
+    minX = element.left
+    maxX = element.left + Math.max(element.start[0], element.end[0])
+    minY = element.top
+    maxY = element.top + Math.max(element.start[1], element.end[1])
+  }
+  else if ('rotate' in element && element.rotate) {
+    const { left, top, width, height, rotate } = element
+    const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate })
+    minX = xRange[0]
+    maxX = xRange[1]
+    minY = yRange[0]
+    maxY = yRange[1]
+  }
+  else {
+    minX = element.left
+    maxX = element.left + element.width
+    minY = element.top
+    maxY = element.top + element.height
+  }
+  return { minX, maxX, minY, maxY }
+}
+
+/**
+ * 计算一组元素在画布中的位置范围
+ * @param elementList 一组元素信息
+ */
+export const getElementListRange = (elementList: PPTElement[]) => {
+  const leftValues: number[] = []
+  const topValues: number[] = []
+  const rightValues: number[] = []
+  const bottomValues: number[] = []
+
+  elementList.forEach(element => {
+    const { minX, maxX, minY, maxY } = getElementRange(element)
+    leftValues.push(minX)
+    topValues.push(minY)
+    rightValues.push(maxX)
+    bottomValues.push(maxY)
+  })
+
+  const minX = Math.min(...leftValues)
+  const maxX = Math.max(...rightValues)
+  const minY = Math.min(...topValues)
+  const maxY = Math.max(...bottomValues)
+
+  return { minX, maxX, minY, maxY }
+}
+
+/**
+ * 计算线条元素的长度
+ * @param element 线条元素
+ */
+export const getLineElementLength = (element: PPTLineElement) => {
+  const deltaX = element.end[0] - element.start[0]
+  const deltaY = element.end[1] - element.start[1]
+  const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
+  return len
+}
+
+export interface AlignLine {
+  value: number
+  range: [number, number]
+}
+
+/**
+ * 将一组对齐吸附线进行去重:同位置的的多条对齐吸附线仅留下一条,取该位置所有对齐吸附线的最大值和最小值为新的范围
+ * @param lines 一组对齐吸附线信息
+ */
+export const uniqAlignLines = (lines: AlignLine[]) => {
+  const uniqLines: AlignLine[] = []
+  lines.forEach(line => {
+    const index = uniqLines.findIndex(_line => _line.value === line.value)
+    if (index === -1) uniqLines.push(line)
+    else {
+      const uniqLine = uniqLines[index]
+      const rangeMin = Math.min(uniqLine.range[0], line.range[0])
+      const rangeMax = Math.max(uniqLine.range[1], line.range[1])
+      const range: [number, number] = [rangeMin, rangeMax]
+      const _line = { value: line.value, range }
+      uniqLines[index] = _line
+    }
+  })
+  return uniqLines
+}
+
+/**
+ * 以页面列表为基础,为每一个页面生成新的ID,并关联到旧ID形成一个字典
+ * 主要用于页面元素时,维持数据中各处页面ID原有的关系
+ * @param slides 页面列表
+ */
+export const createSlideIdMap = (slides: Slide[]) => {
+  const slideIdMap: IdMap = {}
+  for (const slide of slides) {
+    slideIdMap[slide.id] = nanoid(10)
+  }
+  return slideIdMap
+}
+
+/**
+   * 以元素列表为基础,为每一个元素生成新的ID,并关联到旧ID形成一个字典
+   * 主要用于复制元素时,维持数据中各处元素ID原有的关系
+   * 例如:原本两个组合的元素拥有相同的groupId,复制后依然会拥有另一个相同的groupId
+   * @param elements 元素列表数据
+   */
+export const createElementIdMap = (elements: PPTElement[]) => {
+  const groupIdMap: IdMap = {}
+  const elIdMap: IdMap = {}
+  for (const element of elements) {
+    const groupId = element.groupId
+    if (groupId && !groupIdMap[groupId]) {
+      groupIdMap[groupId] = nanoid(10)
+    }
+    elIdMap[element.id] = nanoid(10)
+  }
+  return {
+    groupIdMap,
+    elIdMap,
+  }
+}
+
+/**
+ * 根据表格的主题色,获取对应用于配色的子颜色
+ * @param themeColor 主题色
+ */
+export const getTableSubThemeColor = (themeColor: string) => {
+  const rgba = tinycolor(themeColor)
+  return [
+    rgba.setAlpha(0.3).toRgbString(),
+    rgba.setAlpha(0.1).toRgbString(),
+  ]
+}
+
+/**
+ * 获取线条元素路径字符串
+ * @param element 线条元素
+ */
+export const getLineElementPath = (element: PPTLineElement) => {
+  const start = element.start.join(',')
+  const end = element.end.join(',')
+  if (element.broken) {
+    const mid = element.broken.join(',')
+    return `M${start} L${mid} L${end}`
+  }
+  else if (element.broken2) {
+    const { minX, maxX, minY, maxY } = getElementRange(element)
+    if (maxX - minX >= maxY - minY) return `M${start} L${element.broken2[0]},${element.start[1]} L${element.broken2[0]},${element.end[1]} ${end}`
+    return `M${start} L${element.start[0]},${element.broken2[1]} L${element.end[0]},${element.broken2[1]} ${end}`
+  }
+  else if (element.curve) {
+    const mid = element.curve.join(',')
+    return `M${start} Q${mid} ${end}`
+  }
+  else if (element.cubic) {
+    const [c1, c2] = element.cubic
+    const p1 = c1.join(',')
+    const p2 = c2.join(',')
+    return `M${start} C${p1} ${p2} ${end}`
+  }
+  return `M${start} L${end}`
+}
+
+/**
+ * 判断一个元素是否在可视范围内
+ * @param element 元素
+ * @param parent 父元素
+ */
+export const isElementInViewport = (element: HTMLElement, parent: HTMLElement): boolean => {
+  const elementRect = element.getBoundingClientRect()
+  const parentRect = parent.getBoundingClientRect()
+
+  return (
+    elementRect.top >= parentRect.top &&
+    elementRect.bottom <= parentRect.bottom
+  )
 }

+ 12 - 12
src/utils/textParser.ts

@@ -1,13 +1,13 @@
-/**
- * 将普通文本转为带段落信息的HTML字符串
- * @param text 文本
- */
-export const parseText2Paragraphs = (text: string) => {
-  const htmlText = text.replace(/[\n\r]+/g, '<br>')
-  const paragraphs = htmlText.split('<br>')
-  let string = ''
-  for (const paragraph of paragraphs) {
-    if (paragraph) string += `<div>${paragraph}</div>`
-  }
-  return string
+/**
+ * 将普通文本转为带段落信息的HTML字符串
+ * @param text 文本
+ */
+export const parseText2Paragraphs = (text: string) => {
+  const htmlText = text.replace(/[\n\r]+/g, '<br>')
+  const paragraphs = htmlText.split('<br>')
+  let string = ''
+  for (const paragraph of paragraphs) {
+    if (paragraph) string += `<div>${paragraph}</div>`
+  }
+  return string
 }

+ 66 - 66
src/views/Editor/Canvas/Operate/ImageElementOperate.vue

@@ -1,67 +1,67 @@
-<template>
-  <div class="image-element-operate" :class="{ 'cliping': isCliping }">
-    <BorderLine 
-      class="operate-border-line"
-      v-for="line in borderLines" 
-      :key="line.type" 
-      :type="line.type" 
-      :style="line.style"
-    />
-    <template v-if="handlerVisible">
-      <ResizeHandler
-        class="operate-resize-handler" 
-        v-for="point in resizeHandlers"
-        :key="point.direction"
-        :type="point.direction"
-        :rotate="elementInfo.rotate"
-        :style="point.style"
-        @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
-      />
-      <RotateHandler
-        class="operate-rotate-handler" 
-        :style="{ left: scaleWidth / 2 + 'px' }"
-        @mousedown.stop="$event => rotateElement($event, elementInfo)"
-      />
-    </template>
-  </div>
-</template>
-
-<script lang="ts">
-export default {
-  inheritAttrs: false,
-}
-</script>
-
-<script lang="ts" setup>
-import { computed } from 'vue'
-import { storeToRefs } from 'pinia'
-import { useMainStore } from '@/store'
-import type { PPTImageElement } from '@/types/slides'
-import type { OperateResizeHandlers } from '@/types/edit'
-import useCommonOperate from '../hooks/useCommonOperate'
-
-import RotateHandler from './RotateHandler.vue'
-import ResizeHandler from './ResizeHandler.vue'
-import BorderLine from './BorderLine.vue'
-
-const props = defineProps<{
-  elementInfo: PPTImageElement
-  handlerVisible: boolean
-  rotateElement: (e: MouseEvent, element: PPTImageElement) => void
-  scaleElement: (e: MouseEvent, element: PPTImageElement, command: OperateResizeHandlers) => void
-}>()
-
-const { canvasScale, clipingImageElementId } = storeToRefs(useMainStore())
-
-const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
-
-const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
-const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
-const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
-</script>
-
-<style lang="scss" scoped>
-.image-element-operate.cliping {
-  visibility: hidden;
-}
+<template>
+  <div class="image-element-operate" :class="{ 'cliping': isCliping }">
+    <BorderLine 
+      class="operate-border-line"
+      v-for="line in borderLines" 
+      :key="line.type" 
+      :type="line.type" 
+      :style="line.style"
+    />
+    <template v-if="handlerVisible">
+      <ResizeHandler
+        class="operate-resize-handler" 
+        v-for="point in resizeHandlers"
+        :key="point.direction"
+        :type="point.direction"
+        :rotate="elementInfo.rotate"
+        :style="point.style"
+        @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
+      />
+      <RotateHandler
+        class="operate-rotate-handler" 
+        :style="{ left: scaleWidth / 2 + 'px' }"
+        @mousedown.stop="$event => rotateElement($event, elementInfo)"
+      />
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  inheritAttrs: false,
+}
+</script>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore } from '@/store'
+import type { PPTImageElement } from '@/types/slides'
+import type { OperateResizeHandlers } from '@/types/edit'
+import useCommonOperate from '../hooks/useCommonOperate'
+
+import RotateHandler from './RotateHandler.vue'
+import ResizeHandler from './ResizeHandler.vue'
+import BorderLine from './BorderLine.vue'
+
+const props = defineProps<{
+  elementInfo: PPTImageElement
+  handlerVisible: boolean
+  rotateElement: (e: MouseEvent, element: PPTImageElement) => void
+  scaleElement: (e: MouseEvent, element: PPTImageElement, command: OperateResizeHandlers) => void
+}>()
+
+const { canvasScale, clipingImageElementId } = storeToRefs(useMainStore())
+
+const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
+
+const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
+const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
+const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
+</script>
+
+<style lang="scss" scoped>
+.image-element-operate.cliping {
+  visibility: hidden;
+}
 </style>

+ 60 - 60
src/views/Editor/Canvas/Operate/TextElementOperate.vue

@@ -1,61 +1,61 @@
-<template>
-  <div class="text-element-operate">
-    <BorderLine 
-      class="operate-border-line"
-      v-for="line in borderLines" 
-      :key="line.type" 
-      :type="line.type" 
-      :style="line.style"
-    />
-    <template v-if="handlerVisible">
-      <ResizeHandler
-        class="operate-resize-handler" 
-        v-for="point in resizeHandlers"
-        :key="point.direction"
-        :type="point.direction"
-        :rotate="elementInfo.rotate"
-        :style="point.style"
-        @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
-      />
-      <RotateHandler
-        class="operate-rotate-handler" 
-        :style="{ left: scaleWidth / 2 + 'px' }"
-        @mousedown.stop="$event => rotateElement($event, elementInfo)"
-      />
-    </template>
-  </div>
-</template>
-
-<script lang="ts">
-export default {
-  inheritAttrs: false,
-}
-</script>
-
-<script lang="ts" setup>
-import { computed } from 'vue'
-import { storeToRefs } from 'pinia'
-import { useMainStore } from '@/store'
-import type { PPTTextElement } from '@/types/slides'
-import type { OperateResizeHandlers } from '@/types/edit'
-import useCommonOperate from '../hooks/useCommonOperate'
-
-import RotateHandler from './RotateHandler.vue'
-import ResizeHandler from './ResizeHandler.vue'
-import BorderLine from './BorderLine.vue'
-
-const props = defineProps<{
-  elementInfo: PPTTextElement
-  handlerVisible: boolean
-  rotateElement: (e: MouseEvent, element: PPTTextElement) => void
-  scaleElement: (e: MouseEvent, element: PPTTextElement, command: OperateResizeHandlers) => void
-}>()
-
-const { canvasScale } = storeToRefs(useMainStore())
-
-const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
-const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
-
-const { textElementResizeHandlers, verticalTextElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
-const resizeHandlers = computed(() => props.elementInfo.vertical ? verticalTextElementResizeHandlers.value : textElementResizeHandlers.value)
+<template>
+  <div class="text-element-operate">
+    <BorderLine 
+      class="operate-border-line"
+      v-for="line in borderLines" 
+      :key="line.type" 
+      :type="line.type" 
+      :style="line.style"
+    />
+    <template v-if="handlerVisible">
+      <ResizeHandler
+        class="operate-resize-handler" 
+        v-for="point in resizeHandlers"
+        :key="point.direction"
+        :type="point.direction"
+        :rotate="elementInfo.rotate"
+        :style="point.style"
+        @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
+      />
+      <RotateHandler
+        class="operate-rotate-handler" 
+        :style="{ left: scaleWidth / 2 + 'px' }"
+        @mousedown.stop="$event => rotateElement($event, elementInfo)"
+      />
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  inheritAttrs: false,
+}
+</script>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore } from '@/store'
+import type { PPTTextElement } from '@/types/slides'
+import type { OperateResizeHandlers } from '@/types/edit'
+import useCommonOperate from '../hooks/useCommonOperate'
+
+import RotateHandler from './RotateHandler.vue'
+import ResizeHandler from './ResizeHandler.vue'
+import BorderLine from './BorderLine.vue'
+
+const props = defineProps<{
+  elementInfo: PPTTextElement
+  handlerVisible: boolean
+  rotateElement: (e: MouseEvent, element: PPTTextElement) => void
+  scaleElement: (e: MouseEvent, element: PPTTextElement, command: OperateResizeHandlers) => void
+}>()
+
+const { canvasScale } = storeToRefs(useMainStore())
+
+const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
+const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
+
+const { textElementResizeHandlers, verticalTextElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
+const resizeHandlers = computed(() => props.elementInfo.vertical ? verticalTextElementResizeHandlers.value : textElementResizeHandlers.value)
 </script>

+ 137 - 137
src/views/Editor/Canvas/Operate/index.vue

@@ -1,138 +1,138 @@
-<template>
-  <div
-    class="operate"
-    :class="{ 'multi-select': isMultiSelect && !isActive }"
-    :style="{
-      top: elementInfo.top * canvasScale + 'px',
-      left: elementInfo.left * canvasScale + 'px',
-      transform: `rotate(${rotate}deg)`,
-      transformOrigin: `${elementInfo.width * canvasScale / 2}px ${height * canvasScale / 2}px`,
-    }"
-  >
-    <component
-      v-if="isSelected"
-      :is="currentOperateComponent"
-      :elementInfo="elementInfo"
-      :handlerVisible="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)"
-      :rotateElement="rotateElement"
-      :scaleElement="scaleElement"
-      :dragLineElement="dragLineElement"
-      :moveShapeKeypoint="moveShapeKeypoint"
-    ></component>
-
-    <div 
-      class="animation-index"
-      v-if="toolbarState === 'elAnimation' && elementIndexListInAnimation.length"
-    >
-      <div class="index-item" v-for="index in elementIndexListInAnimation" :key="index">{{index + 1}}</div>
-    </div>
-
-    <LinkHandler 
-      :elementInfo="elementInfo" 
-      :link="elementInfo.link"
-      :openLinkDialog="openLinkDialog" 
-      v-if="isActive && elementInfo.link" 
-      @mousedown.stop=""
-    />
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { computed } from 'vue'
-import { storeToRefs } from 'pinia'
-import { useMainStore, useSlidesStore } from '@/store'
-import {
-  ElementTypes,
-  type PPTElement,
-  type PPTLineElement,
-  type PPTVideoElement,
-  type PPTAudioElement,
-  type PPTShapeElement,
-  type PPTChartElement,
-} from '@/types/slides'
-import type { OperateLineHandlers, OperateResizeHandlers } from '@/types/edit'
-
-import ImageElementOperate from './ImageElementOperate.vue'
-import TextElementOperate from './TextElementOperate.vue'
-import ShapeElementOperate from './ShapeElementOperate.vue'
-import LineElementOperate from './LineElementOperate.vue'
-import TableElementOperate from './TableElementOperate.vue'
-import CommonElementOperate from './CommonElementOperate.vue'
-import LinkHandler from './LinkHandler.vue'
-
-const props = defineProps<{
-  elementInfo: PPTElement
-  isSelected: boolean
-  isActive: boolean
-  isActiveGroupElement: boolean
-  isMultiSelect: boolean
-  rotateElement: (e: MouseEvent, element: Exclude<PPTElement, PPTChartElement | PPTLineElement | PPTVideoElement | PPTAudioElement>) => void
-  scaleElement: (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void
-  dragLineElement: (e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void
-  moveShapeKeypoint: (e: MouseEvent, element: PPTShapeElement, index: number) => void
-  openLinkDialog: () => void
-}>()
-
-const { canvasScale, toolbarState } = storeToRefs(useMainStore())
-const { formatedAnimations } = storeToRefs(useSlidesStore())
-
-const currentOperateComponent = computed<unknown>(() => {
-  const elementTypeMap = {
-    [ElementTypes.IMAGE]: ImageElementOperate,
-    [ElementTypes.TEXT]: TextElementOperate,
-    [ElementTypes.SHAPE]: ShapeElementOperate,
-    [ElementTypes.LINE]: LineElementOperate,
-    [ElementTypes.TABLE]: TableElementOperate,
-    [ElementTypes.CHART]: CommonElementOperate,
-    [ElementTypes.LATEX]: CommonElementOperate,
-    [ElementTypes.VIDEO]: CommonElementOperate,
-    [ElementTypes.AUDIO]: CommonElementOperate,
-  }
-  return elementTypeMap[props.elementInfo.type] || null
-})
-
-const elementIndexListInAnimation = computed(() => {
-  const indexList = []
-  for (let i = 0; i < formatedAnimations.value.length; i++) {
-    const elIds = formatedAnimations.value[i].animations.map(item => item.elId)
-    if (elIds.includes(props.elementInfo.id)) indexList.push(i)
-  }
-  return indexList
-})
-
-const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
-const height = computed(() => 'height' in props.elementInfo ? props.elementInfo.height : 0)
-</script>
-
-<style lang="scss" scoped>
-.operate {
-  position: absolute;
-  z-index: 100;
-  user-select: none;
-
-  &.multi-select {
-    opacity: 0.2;
-  }
-}
-.animation-index {
-  position: absolute;
-  top: 0;
-  left: -24px;
-  font-size: 12px;
-
-  .index-item {
-    width: 18px;
-    height: 18px;
-    background-color: #fff;
-    color: $themeColor;
-    border: 1px solid $themeColor;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-
-    & + .index-item {
-      margin-top: 5px;
-    }
-  }
-}
+<template>
+  <div
+    class="operate"
+    :class="{ 'multi-select': isMultiSelect && !isActive }"
+    :style="{
+      top: elementInfo.top * canvasScale + 'px',
+      left: elementInfo.left * canvasScale + 'px',
+      transform: `rotate(${rotate}deg)`,
+      transformOrigin: `${elementInfo.width * canvasScale / 2}px ${height * canvasScale / 2}px`,
+    }"
+  >
+    <component
+      v-if="isSelected"
+      :is="currentOperateComponent"
+      :elementInfo="elementInfo"
+      :handlerVisible="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)"
+      :rotateElement="rotateElement"
+      :scaleElement="scaleElement"
+      :dragLineElement="dragLineElement"
+      :moveShapeKeypoint="moveShapeKeypoint"
+    ></component>
+
+    <div 
+      class="animation-index"
+      v-if="toolbarState === 'elAnimation' && elementIndexListInAnimation.length"
+    >
+      <div class="index-item" v-for="index in elementIndexListInAnimation" :key="index">{{index + 1}}</div>
+    </div>
+
+    <LinkHandler 
+      :elementInfo="elementInfo" 
+      :link="elementInfo.link"
+      :openLinkDialog="openLinkDialog" 
+      v-if="isActive && elementInfo.link" 
+      @mousedown.stop=""
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSlidesStore } from '@/store'
+import {
+  ElementTypes,
+  type PPTElement,
+  type PPTLineElement,
+  type PPTVideoElement,
+  type PPTAudioElement,
+  type PPTShapeElement,
+  type PPTChartElement,
+} from '@/types/slides'
+import type { OperateLineHandlers, OperateResizeHandlers } from '@/types/edit'
+
+import ImageElementOperate from './ImageElementOperate.vue'
+import TextElementOperate from './TextElementOperate.vue'
+import ShapeElementOperate from './ShapeElementOperate.vue'
+import LineElementOperate from './LineElementOperate.vue'
+import TableElementOperate from './TableElementOperate.vue'
+import CommonElementOperate from './CommonElementOperate.vue'
+import LinkHandler from './LinkHandler.vue'
+
+const props = defineProps<{
+  elementInfo: PPTElement
+  isSelected: boolean
+  isActive: boolean
+  isActiveGroupElement: boolean
+  isMultiSelect: boolean
+  rotateElement: (e: MouseEvent, element: Exclude<PPTElement, PPTChartElement | PPTLineElement | PPTVideoElement | PPTAudioElement>) => void
+  scaleElement: (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void
+  dragLineElement: (e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void
+  moveShapeKeypoint: (e: MouseEvent, element: PPTShapeElement, index: number) => void
+  openLinkDialog: () => void
+}>()
+
+const { canvasScale, toolbarState } = storeToRefs(useMainStore())
+const { formatedAnimations } = storeToRefs(useSlidesStore())
+
+const currentOperateComponent = computed<unknown>(() => {
+  const elementTypeMap = {
+    [ElementTypes.IMAGE]: ImageElementOperate,
+    [ElementTypes.TEXT]: TextElementOperate,
+    [ElementTypes.SHAPE]: ShapeElementOperate,
+    [ElementTypes.LINE]: LineElementOperate,
+    [ElementTypes.TABLE]: TableElementOperate,
+    [ElementTypes.CHART]: CommonElementOperate,
+    [ElementTypes.LATEX]: CommonElementOperate,
+    [ElementTypes.VIDEO]: CommonElementOperate,
+    [ElementTypes.AUDIO]: CommonElementOperate,
+  }
+  return elementTypeMap[props.elementInfo.type] || null
+})
+
+const elementIndexListInAnimation = computed(() => {
+  const indexList = []
+  for (let i = 0; i < formatedAnimations.value.length; i++) {
+    const elIds = formatedAnimations.value[i].animations.map(item => item.elId)
+    if (elIds.includes(props.elementInfo.id)) indexList.push(i)
+  }
+  return indexList
+})
+
+const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
+const height = computed(() => 'height' in props.elementInfo ? props.elementInfo.height : 0)
+</script>
+
+<style lang="scss" scoped>
+.operate {
+  position: absolute;
+  z-index: 100;
+  user-select: none;
+
+  &.multi-select {
+    opacity: 0.2;
+  }
+}
+.animation-index {
+  position: absolute;
+  top: 0;
+  left: -24px;
+  font-size: 12px;
+
+  .index-item {
+    width: 18px;
+    height: 18px;
+    background-color: #fff;
+    color: $themeColor;
+    border: 1px solid $themeColor;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    & + .index-item {
+      margin-top: 5px;
+    }
+  }
+}
 </style>

+ 68 - 68
src/views/components/element/ChartElement/BaseChartElement.vue

@@ -1,69 +1,69 @@
-<template>
-  <div class="base-element-chart"
-    :class="{ 'is-thumbnail': target === 'thumbnail' }"
-    :style="{
-      top: elementInfo.top + 'px',
-      left: elementInfo.left + 'px',
-      width: elementInfo.width + 'px',
-      height: elementInfo.height + 'px',
-    }"
-  >
-    <div
-      class="rotate-wrapper"
-      :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
-    >
-      <div 
-        class="element-content"
-        :style="{
-          backgroundColor: elementInfo.fill,
-        }"
-      >
-        <ElementOutline
-          :width="elementInfo.width"
-          :height="elementInfo.height"
-          :outline="elementInfo.outline"
-        />
-        <Chart
-          :width="elementInfo.width"
-          :height="elementInfo.height"
-          :type="elementInfo.chartType"
-          :data="elementInfo.data"
-          :themeColors="elementInfo.themeColors"
-          :textColor="elementInfo.textColor"
-          :lineColor="elementInfo.lineColor"
-          :options="elementInfo.options"
-        />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import type { PPTChartElement } from '@/types/slides'
-
-import ElementOutline from '@/views/components/element/ElementOutline.vue'
-import Chart from './Chart.vue'
-
-defineProps<{
-  elementInfo: PPTChartElement
-  target?: string
-}>()
-</script>
-
-<style lang="scss" scoped>
-.base-element-chart {
-  position: absolute;
-
-  &.is-thumbnail {
-    pointer-events: none;
-  }
-}
-.rotate-wrapper {
-  width: 100%;
-  height: 100%;
-}
-.element-content {
-  width: 100%;
-  height: 100%;
-}
+<template>
+  <div class="base-element-chart"
+    :class="{ 'is-thumbnail': target === 'thumbnail' }"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
+    }"
+  >
+    <div
+      class="rotate-wrapper"
+      :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
+    >
+      <div 
+        class="element-content"
+        :style="{
+          backgroundColor: elementInfo.fill,
+        }"
+      >
+        <ElementOutline
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :outline="elementInfo.outline"
+        />
+        <Chart
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :type="elementInfo.chartType"
+          :data="elementInfo.data"
+          :themeColors="elementInfo.themeColors"
+          :textColor="elementInfo.textColor"
+          :lineColor="elementInfo.lineColor"
+          :options="elementInfo.options"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { PPTChartElement } from '@/types/slides'
+
+import ElementOutline from '@/views/components/element/ElementOutline.vue'
+import Chart from './Chart.vue'
+
+defineProps<{
+  elementInfo: PPTChartElement
+  target?: string
+}>()
+</script>
+
+<style lang="scss" scoped>
+.base-element-chart {
+  position: absolute;
+
+  &.is-thumbnail {
+    pointer-events: none;
+  }
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+}
 </style>

+ 89 - 89
src/views/components/element/ChartElement/index.vue

@@ -1,89 +1,89 @@
-<template>
-  <div class="editable-element-chart"
-    :class="{ 'lock': elementInfo.lock }"
-    :style="{
-      top: elementInfo.top + 'px',
-      left: elementInfo.left + 'px',
-      width: elementInfo.width + 'px',
-      height: elementInfo.height + 'px',
-    }"
-  >
-    <div
-      class="rotate-wrapper"
-      :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
-    >
-      <div 
-        class="element-content" 
-        :style="{
-          backgroundColor: elementInfo.fill,
-        }"
-        v-contextmenu="contextmenus"
-        @mousedown="$event => handleSelectElement($event)"
-        @touchstart="$event => handleSelectElement($event)"
-        @dblclick="openDataEditor()"
-      >
-        <ElementOutline
-          :width="elementInfo.width"
-          :height="elementInfo.height"
-          :outline="elementInfo.outline"
-        />
-        <Chart
-          :width="elementInfo.width"
-          :height="elementInfo.height"
-          :type="elementInfo.chartType"
-          :data="elementInfo.data"
-          :themeColors="elementInfo.themeColors"
-          :textColor="elementInfo.textColor"
-          :lineColor="elementInfo.lineColor"
-          :options="elementInfo.options"
-        />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import type { PPTChartElement } from '@/types/slides'
-import type { ContextmenuItem } from '@/components/Contextmenu/types'
-import emitter, { EmitterEvents } from '@/utils/emitter'
-
-import ElementOutline from '@/views/components/element/ElementOutline.vue'
-import Chart from './Chart.vue'
-
-const props = defineProps<{
-  elementInfo: PPTChartElement
-  selectElement: (e: MouseEvent | TouchEvent, element: PPTChartElement, canMove?: boolean) => void
-  contextmenus: () => ContextmenuItem[] | null
-}>()
-
-const handleSelectElement = (e: MouseEvent | TouchEvent) => {
-  if (props.elementInfo.lock) return
-  e.stopPropagation()
-
-  props.selectElement(e, props.elementInfo)
-}
-
-const openDataEditor = () => {
-  emitter.emit(EmitterEvents.OPEN_CHART_DATA_EDITOR)
-}
-</script>
-
-<style lang="scss" scoped>
-.editable-element-chart {
-  position: absolute;
-
-  &.lock .element-content {
-    cursor: default;
-  }
-}
-.rotate-wrapper {
-  width: 100%;
-  height: 100%;
-}
-.element-content {
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  cursor: move;
-}
-</style>
+<template>
+  <div class="editable-element-chart"
+    :class="{ 'lock': elementInfo.lock }"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
+    }"
+  >
+    <div
+      class="rotate-wrapper"
+      :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
+    >
+      <div 
+        class="element-content" 
+        :style="{
+          backgroundColor: elementInfo.fill,
+        }"
+        v-contextmenu="contextmenus"
+        @mousedown="$event => handleSelectElement($event)"
+        @touchstart="$event => handleSelectElement($event)"
+        @dblclick="openDataEditor()"
+      >
+        <ElementOutline
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :outline="elementInfo.outline"
+        />
+        <Chart
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :type="elementInfo.chartType"
+          :data="elementInfo.data"
+          :themeColors="elementInfo.themeColors"
+          :textColor="elementInfo.textColor"
+          :lineColor="elementInfo.lineColor"
+          :options="elementInfo.options"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { PPTChartElement } from '@/types/slides'
+import type { ContextmenuItem } from '@/components/Contextmenu/types'
+import emitter, { EmitterEvents } from '@/utils/emitter'
+
+import ElementOutline from '@/views/components/element/ElementOutline.vue'
+import Chart from './Chart.vue'
+
+const props = defineProps<{
+  elementInfo: PPTChartElement
+  selectElement: (e: MouseEvent | TouchEvent, element: PPTChartElement, canMove?: boolean) => void
+  contextmenus: () => ContextmenuItem[] | null
+}>()
+
+const handleSelectElement = (e: MouseEvent | TouchEvent) => {
+  if (props.elementInfo.lock) return
+  e.stopPropagation()
+
+  props.selectElement(e, props.elementInfo)
+}
+
+const openDataEditor = () => {
+  emitter.emit(EmitterEvents.OPEN_CHART_DATA_EDITOR)
+}
+</script>
+
+<style lang="scss" scoped>
+.editable-element-chart {
+  position: absolute;
+
+  &.lock .element-content {
+    cursor: default;
+  }
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  cursor: move;
+}
+</style>

+ 29 - 29
src/views/components/element/ShapeElement/GradientDefs.vue

@@ -1,30 +1,30 @@
-<template>
-  <linearGradient 
-    v-if="type === 'linear'"
-    :id="id" 
-    x1="0%" 
-    y1="0%" 
-    x2="100%" 
-    y2="0%" 
-    :gradientTransform="`rotate(${rotate},0.5,0.5)`"
-  >
-    <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
-  </linearGradient>
-
-  <radialGradient :id="id" v-else>
-    <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
-  </radialGradient>
-</template>
-
-<script lang="ts" setup>
-import type { GradientColor, GradientType } from '@/types/slides'
-
-withDefaults(defineProps<{
-  id: string
-  type: GradientType
-  colors: GradientColor[]
-  rotate?: number
-}>(), {
-  rotate: 0,
-})
+<template>
+  <linearGradient 
+    v-if="type === 'linear'"
+    :id="id" 
+    x1="0%" 
+    y1="0%" 
+    x2="100%" 
+    y2="0%" 
+    :gradientTransform="`rotate(${rotate},0.5,0.5)`"
+  >
+    <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
+  </linearGradient>
+
+  <radialGradient :id="id" v-else>
+    <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
+  </radialGradient>
+</template>
+
+<script lang="ts" setup>
+import type { GradientColor, GradientType } from '@/types/slides'
+
+withDefaults(defineProps<{
+  id: string
+  type: GradientType
+  colors: GradientColor[]
+  rotate?: number
+}>(), {
+  rotate: 0,
+})
 </script>