pipipi-pikachu %!s(int64=5) %!d(string=hai) anos
pai
achega
6b67e87699

+ 39 - 1
src/prosemirror/utils.ts

@@ -74,4 +74,42 @@ export const getAttrValueInSelection = (view: EditorView, attr: string) => {
     return keepChecking
   })
   return value
-}
+}
+
+export const getTextAttrs = (view: EditorView) => {
+  const isBold = isActiveMark(view, 'strong')
+  const isEm = isActiveMark(view, 'em')
+  const isUnderline = isActiveMark(view, 'underline')
+  const isStrikethrough = isActiveMark(view, 'strikethrough')
+  const isSuperscript = isActiveMark(view, 'superscript')
+  const isSubscript = isActiveMark(view, 'subscript')
+  const isCode = isActiveMark(view, 'code')
+  const color = getAttrValue(view, 'forecolor', 'color') || '#000'
+  const backcolor = getAttrValue(view, 'backcolor', 'backcolor') || '#000'
+  const fontsize = getAttrValue(view, 'fontsize', 'fontsize') || '12px'
+  const fontname = getAttrValue(view, 'fontname', 'fontname') || '微软雅黑'
+  const align = getAttrValueInSelection(view, 'align')
+  const isBulletList = isActiveOfParentNodeType('bullet_list', view.state)
+  const isOrderedList = isActiveOfParentNodeType('ordered_list', view.state)
+  const isBlockquote = isActiveOfParentNodeType('blockquote', view.state)
+
+  return {
+    bold: isBold,
+    em: isEm,
+    underline: isUnderline,
+    strikethrough: isStrikethrough,
+    superscript: isSuperscript,
+    subscript: isSubscript,
+    code: isCode,
+    color: color,
+    backcolor: backcolor,
+    fontsize: fontsize,
+    fontname: fontname,
+    align: align,
+    bulletList: isBulletList,
+    orderedList: isOrderedList,
+    blockquote: isBlockquote,
+  }
+}
+
+export type TextAttrs = ReturnType<typeof getTextAttrs>

+ 3 - 1
src/utils/emitter.ts

@@ -1,6 +1,8 @@
 import mitt, { Emitter } from 'mitt'
 
-export enum EMITTER_EVENTS {}
+export enum EmitterEvents {
+  UPDATE_TEXT_STATE = 'UPDATE_TEXT_STATE',
+}
 
 const emitter: Emitter = mitt()
 

+ 13 - 21
src/views/Editor/Toolbar/ElementAnimationPanel.vue

@@ -6,16 +6,18 @@
           <div class="animation-pool">
             <div class="pool-type" v-for="type in animations" :key="type.name">
               <div class="type-title">{{type.name}}:</div>
-              <div class="pool-item" v-for="item in type.children" :key="item.name">
+              <div class="pool-item-wrapper">
                 <div 
+                  class="pool-item" 
+                  v-for="item in type.children" :key="item.name"
                   :class="[
-                    'box',
                     'animate__animated',
                     hoverPreviewAnimation === item.value && `animate__${item.value}`,
                   ]"
                   @mouseover="hoverPreviewAnimation = item.value"
-                ></div>
-                <div class="label">{{item.name}}</div>
+                >
+                  {{item.name}}
+                </div>
               </div>
             </div>
           </div>
@@ -130,9 +132,6 @@ export default defineComponent({
   margin-right: -12px;
   padding-right: 12px;
 }
-.pool-type {
-  @include grid-layout-wrapper();
-}
 .type-title {
   width: 100%;
   font-size: 13px;
@@ -141,25 +140,18 @@ export default defineComponent({
   background-color: #eee;
   padding-left: 10px;
 }
+.pool-item-wrapper {
+  @include grid-layout-wrapper();
+}
 .pool-item {
   @include grid-layout-item(4, 24%);
 
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
   margin-bottom: 10px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  background-color: #f5f5f5;
   cursor: pointer;
-
-  .box {
-    width: 30px;
-    height: 30px;
-    background-color: #eee;
-    margin-bottom: 5px;
-  }
-  .label {
-    text-align: center;
-  }
 }
 
 .sequence-item {

+ 190 - 37
src/views/Editor/Toolbar/ElementStylePanel/TextStylePanel.vue

@@ -3,88 +3,171 @@
     <InputGroup compact class="row">
       <Select
         style="flex: 3;"
+        :value="richTextAttrs.fontname"
       >
-        <SelectOption value="jack">Jack</SelectOption>
-        <SelectOption value="lucy">Lucy</SelectOption>
-        <SelectOption value="disabled">Disabled</SelectOption>
-        <SelectOption value="Yiminghe">yiminghe</SelectOption>
+        <SelectOption v-for="font in availableFonts" :key="font.en" :value="font.en">
+          <span :style="{ fontFamily: font.en }">{{font.zh}}</span>
+        </SelectOption>
       </Select>
       <Select
         style="flex: 2;"
+        :value="richTextAttrs.fontsize"
       >
-        <SelectOption value="jack">Jack</SelectOption>
-        <SelectOption value="lucy">Lucy</SelectOption>
-        <SelectOption value="disabled">Disabled</SelectOption>
-        <SelectOption value="Yiminghe">yiminghe</SelectOption>
+        <SelectOption v-for="fontsize in fontSizeOptions" :key="fontsize" :value="fontsize">
+          {{fontsize}}
+        </SelectOption>
       </Select>
     </InputGroup>
 
     <ButtonGroup class="row">
-      <Button style="flex: 1;"><FontColorsOutlined /></Button>
-      <Button style="flex: 1;"><HighlightOutlined /></Button>
-      <Button style="flex: 1;"><BgColorsOutlined /></Button>
+      <Popover trigger="click">
+        <template #content>
+          <ColorPicker v-model="richTextAttrs.color" />
+        </template>
+        <Button class="color-btn" style="flex: 1;">
+          <FontColorsOutlined />
+          <div class="color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
+        </Button>
+      </Popover>
+      <Popover trigger="click">
+        <template #content>
+          <ColorPicker v-model="richTextAttrs.backcolor" />
+        </template>
+        <Button class="color-btn" style="flex: 1;">
+          <HighlightOutlined />
+          <div class="color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
+        </Button>
+      </Popover>
+      <Popover trigger="click">
+        <template #content>
+          <ColorPicker v-model="fill" />
+        </template>
+        <Button class="color-btn" style="flex: 1;">
+          <BgColorsOutlined />
+          <div class="color-block" :style="{ backgroundColor: fill }"></div>
+        </Button>
+      </Popover>
     </ButtonGroup>
 
     <ButtonGroup class="row">
-      <Button style="flex: 1;"><BoldOutlined /></Button>
-      <Button style="flex: 1;"><ItalicOutlined /></Button>
-      <Button style="flex: 1;"><UnderlineOutlined /></Button>
-      <Button style="flex: 1;"><StrikethroughOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.bold ? 'primary' : 'default'"><BoldOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.em ? 'primary' : 'default'"><ItalicOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.underline ? 'primary' : 'default'"><UnderlineOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.strikethrough ? 'primary' : 'default'"><StrikethroughOutlined /></Button>
+    </ButtonGroup>
+
+    <ButtonGroup class="row">
+      <Button style="flex: 1;" :type="richTextAttrs.superscript ? 'primary' : 'default'">上</Button>
+      <Button style="flex: 1;" :type="richTextAttrs.subscript ? 'primary' : 'default'">下</Button>
+      <Button style="flex: 1;" :type="richTextAttrs.code ? 'primary' : 'default'">码</Button>
+      <Button style="flex: 1;" :type="richTextAttrs.blockquote ? 'primary' : 'default'">引</Button>
+      <Button style="flex: 1;">清</Button>
     </ButtonGroup>
 
     <Divider />
 
     <ButtonGroup class="row">
-      <Button style="flex: 1;"><AlignLeftOutlined /></Button>
-      <Button style="flex: 1;"><AlignCenterOutlined /></Button>
-      <Button style="flex: 1;"><AlignRightOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.align === 'left' || '' ? 'primary' : 'default'"><AlignLeftOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.align === 'center' ? 'primary' : 'default'"><AlignCenterOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.align === 'right' ? 'primary' : 'default'"><AlignRightOutlined /></Button>
     </ButtonGroup>
 
     <ButtonGroup class="row">
-      <Button style="flex: 1;"><OrderedListOutlined /></Button>
-      <Button style="flex: 1;"><UnorderedListOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.bulletList ? 'primary' : 'default'"><UnorderedListOutlined /></Button>
+      <Button style="flex: 1;" :type="richTextAttrs.orderedList ? 'primary' : 'default'"><OrderedListOutlined /></Button>
     </ButtonGroup>
 
     <Divider />
 
     <div class="row">
       <div style="flex: 2;">行间距:</div>
-      <Select style="flex: 3;">
+      <Select style="flex: 3;" :value="lineHeight">
         <template #suffixIcon><ColumnHeightOutlined /></template>
-        <SelectOption value="jack">Jack</SelectOption>
-        <SelectOption value="lucy">Lucy</SelectOption>
-        <SelectOption value="disabled">Disabled</SelectOption>
-        <SelectOption value="Yiminghe">yiminghe</SelectOption>
+        <SelectOption v-for="item in lineHeightOptions" :key="item" :value="item">{{item}}</SelectOption>
       </Select>
     </div>
     <div class="row">
       <div style="flex: 2;">字间距:</div>
-      <Select style="flex: 3;">
+      <Select style="flex: 3;" :value="wordSpace">
         <template #suffixIcon><ColumnWidthOutlined /></template>
-        <SelectOption value="jack">Jack</SelectOption>
-        <SelectOption value="lucy">Lucy</SelectOption>
-        <SelectOption value="disabled">Disabled</SelectOption>
-        <SelectOption value="Yiminghe">yiminghe</SelectOption>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
       </Select>
     </div>
 
     <Divider />
 
-    <ButtonGroup class="row">
-      <Button style="flex: 1;">边框</Button>
-      <Button style="flex: 1;">阴影</Button>
-    </ButtonGroup>
+    <div class="row">
+      <div style="flex: 2;">描边样式:</div>
+      <Select style="flex: 3;" :value="wordSpace">
+        <template #suffixIcon><ColumnWidthOutlined /></template>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
+      </Select>
+    </div>
+    <div class="row">
+      <div style="flex: 2;">描边颜色:</div>
+      <Select style="flex: 3;" :value="wordSpace">
+        <template #suffixIcon><ColumnWidthOutlined /></template>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
+      </Select>
+    </div>
+    <div class="row">
+      <div style="flex: 2;">描边粗细:</div>
+      <Select style="flex: 3;" :value="wordSpace">
+        <template #suffixIcon><ColumnWidthOutlined /></template>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
+      </Select>
+    </div>
+
+    <Divider />
+
+    <div class="row">
+      <div style="flex: 2;">水平阴影:</div>
+      <Select style="flex: 3;" :value="wordSpace">
+        <template #suffixIcon><ColumnWidthOutlined /></template>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
+      </Select>
+    </div>
+    <div class="row">
+      <div style="flex: 2;">垂直阴影:</div>
+      <Select style="flex: 3;" :value="wordSpace">
+        <template #suffixIcon><ColumnWidthOutlined /></template>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
+      </Select>
+    </div>
+    <div class="row">
+      <div style="flex: 2;">模糊距离:</div>
+      <Select style="flex: 3;" :value="wordSpace">
+        <template #suffixIcon><ColumnWidthOutlined /></template>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
+      </Select>
+    </div>
+    <div class="row">
+      <div style="flex: 2;">阴影颜色:</div>
+      <Select style="flex: 3;" :value="wordSpace">
+        <template #suffixIcon><ColumnWidthOutlined /></template>
+        <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}</SelectOption>
+      </Select>
+    </div>
+
+    <Divider />
 
     <div class="row">
       <div style="flex: 2;">透明度:</div>
-      <Slider style="flex: 3;" />
+      <Slider :min="0" :max="1" :step="0.1" :value="opacity" style="flex: 3;" />
     </div>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
-import { Select, Input, Button, Divider, Slider } from 'ant-design-vue'
+import { computed, defineComponent, onUnmounted, ref } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
+import { PPTElementOutline, PPTElementShadow } from '@/types/slides'
+import emitter, { EmitterEvents } from '@/utils/emitter'
+import { TextAttrs } from '@/prosemirror/utils'
+
+import ColorPicker from '@/components/ColorPicker/index.vue'
+import { Select, Input, Button, Divider, Slider, Popover } from 'ant-design-vue'
 import {
   FontColorsOutlined,
   HighlightOutlined,
@@ -105,6 +188,7 @@ import {
 export default defineComponent({
   name: 'text-style-panel',
   components: {
+    ColorPicker,
     Select,
     SelectOption: Select.Option,
     InputGroup: Input.Group,
@@ -112,6 +196,7 @@ export default defineComponent({
     ButtonGroup: Button.Group,
     Divider,
     Slider,
+    Popover,
     FontColorsOutlined,
     HighlightOutlined,
     BgColorsOutlined,
@@ -127,6 +212,63 @@ export default defineComponent({
     ColumnHeightOutlined,
     ColumnWidthOutlined,
   },
+  setup() {
+    const store = useStore<State>()
+
+    const fill = ref('#000')
+    const lineHeight = ref(1.5)
+    const wordSpace = ref(0)
+    const opacity = ref(1)
+    const shadow = ref<PPTElementShadow>()
+    const outline = ref<PPTElementOutline>()
+
+    const richTextAttrs = ref<TextAttrs>({
+      bold: false,
+      em: false,
+      underline: false,
+      strikethrough: false,
+      superscript: false,
+      subscript: false,
+      code: false,
+      color: '#000',
+      backcolor: '#000',
+      fontsize: '12px',
+      fontname: '微软雅黑',
+      align: 'left',
+      bulletList: false,
+      orderedList: false,
+      blockquote: false,
+    })
+
+    const availableFonts = computed(() => store.state.availableFonts)
+    const fontSizeOptions = [
+      '12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
+      '36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '80px',
+    ]
+    const lineHeightOptions = [0.5, 1.0, 1.2, 1.5, 1.8, 2.0, 3.0]
+    const wordSpaceOptions = [0, 1, 2, 3, 4, 5, 8]
+
+    const updateRichTextAttrs = (attr: TextAttrs) => richTextAttrs.value = attr
+
+    emitter.on(EmitterEvents.UPDATE_TEXT_STATE, attr => updateRichTextAttrs(attr))
+    onUnmounted(() => {
+      emitter.off(EmitterEvents.UPDATE_TEXT_STATE, attr => updateRichTextAttrs(attr))
+    })
+
+    return {
+      fill,
+      lineHeight,
+      wordSpace,
+      opacity,
+      shadow,
+      outline,
+      richTextAttrs,
+      availableFonts,
+      fontSizeOptions,
+      lineHeightOptions,
+      wordSpaceOptions,
+    }
+  },
 })
 </script>
 
@@ -137,4 +279,15 @@ export default defineComponent({
   align-items: center;
   margin-bottom: 10px;
 }
+.color-btn {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+.color-block {
+  width: 16px;
+  height: 3px;
+  margin-top: 1px;
+}
 </style>

+ 1 - 20
src/views/Editor/Toolbar/SlideStylePanel.vue

@@ -1,32 +1,13 @@
 <template>
   <div class="slide-style-panel">
-    <Popover trigger="click">
-      <template #content>
-        <ColorPicker v-model="color" />
-      </template>
-      <button>Hover me</button>
-    </Popover>
+    slide-style-panel
   </div>
 </template>
 
 <script lang="ts">
 import { defineComponent, ref } from 'vue'
 
-import ColorPicker from '@/components/ColorPicker/index.vue'
-import { Popover } from 'ant-design-vue'
-
 export default defineComponent({
   name: 'slide-style-panel',
-  components: {
-    ColorPicker,
-    Popover,
-  },
-  setup() {
-    const color = ref('#888')
-
-    return {
-      color,
-    }
-  },
 })
 </script>

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

@@ -14,6 +14,8 @@
         backgroundColor: elementInfo.fill,
         opacity: elementInfo.opacity,
         textShadow: shadowStyle,
+        lineHeight: elementInfo.lineHeight,
+        letterSpacing: (elementInfo.wordSpace || 0) + 'px',
       }"
     >
       <ElementOutline

+ 16 - 1
src/views/components/element/TextElement/index.vue

@@ -17,6 +17,8 @@
         backgroundColor: elementInfo.fill,
         opacity: elementInfo.opacity,
         textShadow: shadowStyle,
+        lineHeight: elementInfo.lineHeight,
+        letterSpacing: (elementInfo.wordSpace || 0) + 'px',
       }"
       v-contextmenu="contextmenus"
     >
@@ -43,6 +45,8 @@ import { EditorView } from 'prosemirror-view'
 import { PPTTextElement } from '@/types/slides'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import { initProsemirrorEditor } from '@/prosemirror/'
+import { getTextAttrs } from '@/prosemirror/utils'
+import emitter, { EmitterEvents } from '@/utils/emitter'
 import useElementShadow from '@/views/components/element/hooks/useElementShadow'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 
@@ -113,6 +117,16 @@ export default defineComponent({
       addHistorySnapshot()
     }, 500, { trailing: true })
 
+    const handleClick = debounce(function() {
+      const attr = getTextAttrs(editorView)
+      emitter.emit(EmitterEvents.UPDATE_TEXT_STATE, attr)
+    }, 50, { trailing: true })
+
+    const handleKeydown = () => {
+      handleInput()
+      handleClick()
+    }
+
     const textContent = computed(() => props.elementInfo.content)
     watch(textContent, () => {
       if(!editorView) return
@@ -130,7 +144,8 @@ export default defineComponent({
         handleDOMEvents: {
           focus: handleFocus,
           blur: handleBlur,
-          keydown: handleInput,
+          keydown: handleKeydown,
+          click: handleClick,
         },
         editable: () => editable.value,
       })