mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 添加文字格式刷
This commit is contained in:
parent
5b8695e9c0
commit
10229ec8bc
28
src/hooks/useTextFormatPainter.ts
Normal file
28
src/hooks/useTextFormatPainter.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useMainStore } from '@/store'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const mainStore = useMainStore()
|
||||||
|
const { richTextAttrs, textFormatPainter } = storeToRefs(mainStore)
|
||||||
|
|
||||||
|
const toggleFormatPainter = () => {
|
||||||
|
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
|
||||||
|
else {
|
||||||
|
mainStore.setTextFormatPainter({
|
||||||
|
bold: richTextAttrs.value.bold,
|
||||||
|
em: richTextAttrs.value.em,
|
||||||
|
underline: richTextAttrs.value.underline,
|
||||||
|
strikethrough: richTextAttrs.value.strikethrough,
|
||||||
|
color: richTextAttrs.value.color,
|
||||||
|
backcolor: richTextAttrs.value.backcolor,
|
||||||
|
fontname: richTextAttrs.value.fontsize,
|
||||||
|
fontsize: richTextAttrs.value.fontsize,
|
||||||
|
align: richTextAttrs.value.align,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toggleFormatPainter,
|
||||||
|
}
|
||||||
|
}
|
@ -109,6 +109,7 @@ import {
|
|||||||
Needle,
|
Needle,
|
||||||
TextRotationNone,
|
TextRotationNone,
|
||||||
TextRotationDown,
|
TextRotationDown,
|
||||||
|
FormatBrush,
|
||||||
} from '@icon-park/vue-next'
|
} from '@icon-park/vue-next'
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
@ -219,6 +220,7 @@ export const icons = {
|
|||||||
IconNeedle: Needle,
|
IconNeedle: Needle,
|
||||||
IconTextRotationNone: TextRotationNone,
|
IconTextRotationNone: TextRotationNone,
|
||||||
IconTextRotationDown: TextRotationDown,
|
IconTextRotationDown: TextRotationDown,
|
||||||
|
IconFormatBrush: FormatBrush,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { customAlphabet } from 'nanoid'
|
import { customAlphabet } from 'nanoid'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { CreatingElement } from '@/types/edit'
|
import { CreatingElement, TextFormatPainter } from '@/types/edit'
|
||||||
import { ToolbarStates } from '@/types/toolbar'
|
import { ToolbarStates } from '@/types/toolbar'
|
||||||
import { DialogForExportTypes } from '@/types/export'
|
import { DialogForExportTypes } from '@/types/export'
|
||||||
import { SYS_FONTS } from '@/configs/font'
|
import { SYS_FONTS } from '@/configs/font'
|
||||||
@ -31,6 +31,7 @@ export interface MainState {
|
|||||||
selectedSlidesIndex: number[]
|
selectedSlidesIndex: number[]
|
||||||
dialogForExport: DialogForExportTypes
|
dialogForExport: DialogForExportTypes
|
||||||
databaseId: string
|
databaseId: string
|
||||||
|
textFormatPainter: TextFormatPainter | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
||||||
@ -59,6 +60,7 @@ export const useMainStore = defineStore('main', {
|
|||||||
selectedSlidesIndex: [], // 当前被选中的页面索引集合
|
selectedSlidesIndex: [], // 当前被选中的页面索引集合
|
||||||
dialogForExport: '', // 导出面板
|
dialogForExport: '', // 导出面板
|
||||||
databaseId, // 标识当前应用的indexedDB数据库ID
|
databaseId, // 标识当前应用的indexedDB数据库ID
|
||||||
|
textFormatPainter: null, // 文字格式刷
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -160,5 +162,9 @@ export const useMainStore = defineStore('main', {
|
|||||||
setDialogForExport(type: DialogForExportTypes) {
|
setDialogForExport(type: DialogForExportTypes) {
|
||||||
this.dialogForExport = type
|
this.dialogForExport = type
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setTextFormatPainter(textFormatPainter: TextFormatPainter | null) {
|
||||||
|
this.textFormatPainter = textFormatPainter
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
@ -91,3 +91,15 @@ export interface CreatingLineElement {
|
|||||||
data: LinePoolItem
|
data: LinePoolItem
|
||||||
}
|
}
|
||||||
export type CreatingElement = CreatingTextElement | CreatingShapeElement | CreatingLineElement
|
export type CreatingElement = CreatingTextElement | CreatingShapeElement | CreatingLineElement
|
||||||
|
|
||||||
|
export interface TextFormatPainter {
|
||||||
|
bold?: boolean
|
||||||
|
em?: boolean
|
||||||
|
underline?: boolean
|
||||||
|
strikethrough?: boolean
|
||||||
|
color?: string
|
||||||
|
backcolor?: string
|
||||||
|
fontsize?: string
|
||||||
|
fontname?: string
|
||||||
|
align?: 'left' | 'right' | 'center'
|
||||||
|
}
|
@ -159,16 +159,18 @@ export const getAttrValueInSelection = (view: EditorView, attr: string) => {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Align = 'left' | 'right' | 'center'
|
||||||
|
|
||||||
interface DefaultAttrs {
|
interface DefaultAttrs {
|
||||||
color?: string
|
color?: string
|
||||||
backcolor?: string
|
backcolor?: string
|
||||||
fontsize?: string
|
fontsize?: string
|
||||||
fontname?: string
|
fontname?: string
|
||||||
align?: string
|
align?: Align
|
||||||
}
|
}
|
||||||
const _defaultAttrs: DefaultAttrs = {
|
const _defaultAttrs: DefaultAttrs = {
|
||||||
color: '#000',
|
color: '#000',
|
||||||
backcolor: '#000',
|
backcolor: '',
|
||||||
fontsize: '20px',
|
fontsize: '20px',
|
||||||
fontname: '微软雅黑',
|
fontname: '微软雅黑',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -190,7 +192,7 @@ export const getTextAttrs = (view: EditorView, defaultAttrs: DefaultAttrs = {})
|
|||||||
const fontsize = getAttrValue(marks, 'fontsize', 'fontsize') || defaultAttrs.fontsize
|
const fontsize = getAttrValue(marks, 'fontsize', 'fontsize') || defaultAttrs.fontsize
|
||||||
const fontname = getAttrValue(marks, 'fontname', 'fontname') || defaultAttrs.fontname
|
const fontname = getAttrValue(marks, 'fontname', 'fontname') || defaultAttrs.fontname
|
||||||
const link = getAttrValue(marks, 'link', 'href') || ''
|
const link = getAttrValue(marks, 'link', 'href') || ''
|
||||||
const align = getAttrValueInSelection(view, 'align') || defaultAttrs.align
|
const align = (getAttrValueInSelection(view, 'align') || defaultAttrs.align) as Align
|
||||||
const isBulletList = isActiveOfParentNodeType('bullet_list', view.state)
|
const isBulletList = isActiveOfParentNodeType('bullet_list', view.state)
|
||||||
const isOrderedList = isActiveOfParentNodeType('ordered_list', view.state)
|
const isOrderedList = isActiveOfParentNodeType('ordered_list', view.state)
|
||||||
const isBlockquote = isActiveOfParentNodeType('blockquote', view.state)
|
const isBlockquote = isActiveOfParentNodeType('blockquote', view.state)
|
||||||
@ -232,7 +234,7 @@ export const defaultRichTextAttrs: TextAttrs = {
|
|||||||
subscript: false,
|
subscript: false,
|
||||||
code: false,
|
code: false,
|
||||||
color: '#000',
|
color: '#000',
|
||||||
backcolor: '#000',
|
backcolor: '',
|
||||||
fontsize: '20px',
|
fontsize: '20px',
|
||||||
fontname: '微软雅黑',
|
fontname: '微软雅黑',
|
||||||
link: '',
|
link: '',
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, provide, ref, watch, watchEffect } from 'vue'
|
import { nextTick, onMounted, onUnmounted, provide, ref, watch, watchEffect } from 'vue'
|
||||||
import { throttle } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
|
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
|
||||||
@ -142,6 +142,7 @@ const {
|
|||||||
showRuler,
|
showRuler,
|
||||||
creatingElement,
|
creatingElement,
|
||||||
canvasScale,
|
canvasScale,
|
||||||
|
textFormatPainter,
|
||||||
} = storeToRefs(mainStore)
|
} = storeToRefs(mainStore)
|
||||||
const { currentSlide } = storeToRefs(useSlidesStore())
|
const { currentSlide } = storeToRefs(useSlidesStore())
|
||||||
const { ctrlKeyState, spaceKeyState } = storeToRefs(useKeyboardStore())
|
const { ctrlKeyState, spaceKeyState } = storeToRefs(useKeyboardStore())
|
||||||
@ -190,17 +191,23 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 点击画布的空白区域:清空焦点元素、设置画布焦点、清除文字选区
|
// 点击画布的空白区域:清空焦点元素、设置画布焦点、清除文字选区、清空格式刷状态
|
||||||
const handleClickBlankArea = (e: MouseEvent) => {
|
const handleClickBlankArea = (e: MouseEvent) => {
|
||||||
mainStore.setActiveElementIdList([])
|
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
|
||||||
|
|
||||||
if (!spaceKeyState.value) updateMouseSelection(e)
|
if (!spaceKeyState.value) updateMouseSelection(e)
|
||||||
else dragViewport(e)
|
else dragViewport(e)
|
||||||
|
|
||||||
if (!editorAreaFocus.value) mainStore.setEditorareaFocus(true)
|
if (!editorAreaFocus.value) mainStore.setEditorareaFocus(true)
|
||||||
|
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
|
||||||
removeAllRanges()
|
removeAllRanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 画布注销时清空格式刷状态
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
|
||||||
|
})
|
||||||
|
|
||||||
// 移除画布编辑区域焦点
|
// 移除画布编辑区域焦点
|
||||||
const removeEditorAreaFocus = () => {
|
const removeEditorAreaFocus = () => {
|
||||||
if (editorAreaFocus.value) mainStore.setEditorareaFocus(false)
|
if (editorAreaFocus.value) mainStore.setEditorareaFocus(false)
|
||||||
|
@ -87,10 +87,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||||
<Button class="text-color-btn" style="flex: 3;">
|
<TextColorButton :color="richTextAttrs.color" style="flex: 3;">
|
||||||
<IconText />
|
<IconText />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover trigger="click">
|
<Popover trigger="click">
|
||||||
@ -101,10 +100,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
||||||
<Button class="text-color-btn" style="flex: 3;">
|
<TextColorButton :color="richTextAttrs.backcolor" style="flex: 3;">
|
||||||
<IconHighLight />
|
<IconHighLight />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
||||||
@ -151,6 +149,7 @@ import { WEB_FONTS } from '@/configs/font'
|
|||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
|
||||||
import ColorButton from '../common/ColorButton.vue'
|
import ColorButton from '../common/ColorButton.vue'
|
||||||
|
import TextColorButton from '../common/TextColorButton.vue'
|
||||||
|
|
||||||
const slidesStore = useSlidesStore()
|
const slidesStore = useSlidesStore()
|
||||||
const { richTextAttrs, availableFonts, activeElementList } = storeToRefs(useMainStore())
|
const { richTextAttrs, availableFonts, activeElementList } = storeToRefs(useMainStore())
|
||||||
@ -251,18 +250,6 @@ const updateFontStyle = (command: string, value: string) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.text-color-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.text-color-block {
|
|
||||||
width: 16px;
|
|
||||||
height: 3px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
.font-size-btn {
|
.font-size-btn {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -111,10 +111,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||||
<Button class="text-color-btn" style="flex: 3;">
|
<TextColorButton :color="richTextAttrs.color" style="flex: 3;">
|
||||||
<IconText />
|
<IconText />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover trigger="click">
|
<Popover trigger="click">
|
||||||
@ -125,10 +124,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
||||||
<Button class="text-color-btn" style="flex: 3;">
|
<TextColorButton :color="richTextAttrs.backcolor" style="flex: 3;">
|
||||||
<IconHighLight />
|
<IconHighLight />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
||||||
@ -169,14 +167,33 @@
|
|||||||
@click="emitRichTextCommand('underline')"
|
@click="emitRichTextCommand('underline')"
|
||||||
><IconTextUnderline /></CheckboxButton>
|
><IconTextUnderline /></CheckboxButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除线">
|
||||||
|
<CheckboxButton
|
||||||
|
style="flex: 1;"
|
||||||
|
:checked="richTextAttrs.strikethrough"
|
||||||
|
@click="emitRichTextCommand('strikethrough')"
|
||||||
|
><IconStrikethrough /></CheckboxButton>
|
||||||
|
</Tooltip>
|
||||||
|
</CheckboxButtonGroup>
|
||||||
|
|
||||||
|
<CheckboxButtonGroup class="row">
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
||||||
<CheckboxButton
|
<CheckboxButton
|
||||||
style="flex: 1;"
|
style="flex: 1;"
|
||||||
@click="emitRichTextCommand('clear')"
|
@click="emitRichTextCommand('clear')"
|
||||||
><IconFormat /></CheckboxButton>
|
><IconFormat /></CheckboxButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="格式刷">
|
||||||
|
<CheckboxButton
|
||||||
|
style="flex: 1;"
|
||||||
|
:checked="!!textFormatPainter"
|
||||||
|
@click="toggleFormatPainter()"
|
||||||
|
><IconFormatBrush /></CheckboxButton>
|
||||||
|
</Tooltip>
|
||||||
</CheckboxButtonGroup>
|
</CheckboxButtonGroup>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
class="row"
|
class="row"
|
||||||
button-style="solid"
|
button-style="solid"
|
||||||
@ -230,16 +247,18 @@ import { PPTShapeElement, ShapeGradient, ShapeText } from '@/types/slides'
|
|||||||
import { WEB_FONTS } from '@/configs/font'
|
import { WEB_FONTS } from '@/configs/font'
|
||||||
import emitter, { EmitterEvents } from '@/utils/emitter'
|
import emitter, { EmitterEvents } from '@/utils/emitter'
|
||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
import useTextFormatPainter from '@/hooks/useTextFormatPainter'
|
||||||
|
|
||||||
import ElementOpacity from '../common/ElementOpacity.vue'
|
import ElementOpacity from '../common/ElementOpacity.vue'
|
||||||
import ElementOutline from '../common/ElementOutline.vue'
|
import ElementOutline from '../common/ElementOutline.vue'
|
||||||
import ElementShadow from '../common/ElementShadow.vue'
|
import ElementShadow from '../common/ElementShadow.vue'
|
||||||
import ElementFlip from '../common/ElementFlip.vue'
|
import ElementFlip from '../common/ElementFlip.vue'
|
||||||
import ColorButton from '../common/ColorButton.vue'
|
import ColorButton from '../common/ColorButton.vue'
|
||||||
|
import TextColorButton from '../common/TextColorButton.vue'
|
||||||
|
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const slidesStore = useSlidesStore()
|
const slidesStore = useSlidesStore()
|
||||||
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(mainStore)
|
const { handleElement, handleElementId, richTextAttrs, availableFonts, textFormatPainter } = storeToRefs(mainStore)
|
||||||
|
|
||||||
const handleShapeElement = handleElement as Ref<PPTShapeElement>
|
const handleShapeElement = handleElement as Ref<PPTShapeElement>
|
||||||
|
|
||||||
@ -262,6 +281,7 @@ watch(handleElement, () => {
|
|||||||
}, { deep: true, immediate: true })
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
const { addHistorySnapshot } = useHistorySnapshot()
|
||||||
|
const { toggleFormatPainter } = useTextFormatPainter()
|
||||||
|
|
||||||
const updateElement = (props: Partial<PPTShapeElement>) => {
|
const updateElement = (props: Partial<PPTShapeElement>) => {
|
||||||
slidesStore.updateElement({ id: handleElementId.value, props })
|
slidesStore.updateElement({ id: handleElementId.value, props })
|
||||||
@ -323,18 +343,6 @@ const emitRichTextCommand = (command: string, value?: string) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.text-color-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.text-color-block {
|
|
||||||
width: 16px;
|
|
||||||
height: 3px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
.font-size-btn {
|
.font-size-btn {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||||
<Button class="text-color-btn" style="flex: 1;">
|
<TextColorButton :color="textAttrs.color" style="flex: 1;">
|
||||||
<IconText />
|
<IconText />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: textAttrs.color }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover trigger="click">
|
<Popover trigger="click">
|
||||||
@ -53,10 +52,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="单元格填充">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="单元格填充">
|
||||||
<Button class="text-color-btn" style="flex: 1;">
|
<TextColorButton :color="textAttrs.backcolor" style="flex: 1;">
|
||||||
<IconFill />
|
<IconFill />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: textAttrs.backcolor }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
@ -196,6 +194,7 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
|||||||
|
|
||||||
import ElementOutline from '../common/ElementOutline.vue'
|
import ElementOutline from '../common/ElementOutline.vue'
|
||||||
import ColorButton from '../common/ColorButton.vue'
|
import ColorButton from '../common/ColorButton.vue'
|
||||||
|
import TextColorButton from '../common/TextColorButton.vue'
|
||||||
|
|
||||||
const slidesStore = useSlidesStore()
|
const slidesStore = useSlidesStore()
|
||||||
const { handleElement, handleElementId, selectedTableCells: selectedCells, availableFonts } = storeToRefs(useMainStore())
|
const { handleElement, handleElementId, selectedTableCells: selectedCells, availableFonts } = storeToRefs(useMainStore())
|
||||||
@ -211,7 +210,7 @@ const textAttrs = ref({
|
|||||||
underline: false,
|
underline: false,
|
||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
color: '#000',
|
color: '#000',
|
||||||
backcolor: '#000',
|
backcolor: '',
|
||||||
fontsize: '12px',
|
fontsize: '12px',
|
||||||
fontname: '微软雅黑',
|
fontname: '微软雅黑',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -259,7 +258,7 @@ const updateTextAttrState = () => {
|
|||||||
underline: false,
|
underline: false,
|
||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
color: '#000',
|
color: '#000',
|
||||||
backcolor: '#000',
|
backcolor: '',
|
||||||
fontsize: '12px',
|
fontsize: '12px',
|
||||||
fontname: '微软雅黑',
|
fontname: '微软雅黑',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -272,7 +271,7 @@ const updateTextAttrState = () => {
|
|||||||
underline: !!style.underline,
|
underline: !!style.underline,
|
||||||
strikethrough: !!style.strikethrough,
|
strikethrough: !!style.strikethrough,
|
||||||
color: style.color || '#000',
|
color: style.color || '#000',
|
||||||
backcolor: style.backcolor || '#000',
|
backcolor: style.backcolor || '',
|
||||||
fontsize: style.fontsize || '12px',
|
fontsize: style.fontsize || '12px',
|
||||||
fontname: style.fontname || '微软雅黑',
|
fontname: style.fontname || '微软雅黑',
|
||||||
align: style.align || 'left',
|
align: style.align || 'left',
|
||||||
@ -404,17 +403,6 @@ const setTableCol = (value: number) => {
|
|||||||
.switch-wrapper {
|
.switch-wrapper {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.text-color-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.text-color-block {
|
|
||||||
width: 16px;
|
|
||||||
height: 3px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
.set-count {
|
.set-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -51,10 +51,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||||
<Button class="text-color-btn" style="flex: 3;">
|
<TextColorButton :color="richTextAttrs.color" style="flex: 3;">
|
||||||
<IconText />
|
<IconText />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover trigger="click">
|
<Popover trigger="click">
|
||||||
@ -65,10 +64,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
||||||
<Button class="text-color-btn" style="flex: 3;">
|
<TextColorButton :color="richTextAttrs.backcolor" style="flex: 3;">
|
||||||
<IconHighLight />
|
<IconHighLight />
|
||||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
|
</TextColorButton>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
||||||
@ -116,12 +114,6 @@
|
|||||||
@click="emitRichTextCommand('strikethrough')"
|
@click="emitRichTextCommand('strikethrough')"
|
||||||
><IconStrikethrough /></CheckboxButton>
|
><IconStrikethrough /></CheckboxButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
|
||||||
<CheckboxButton
|
|
||||||
style="flex: 1;"
|
|
||||||
@click="emitRichTextCommand('clear')"
|
|
||||||
><IconFormat /></CheckboxButton>
|
|
||||||
</Tooltip>
|
|
||||||
</CheckboxButtonGroup>
|
</CheckboxButtonGroup>
|
||||||
|
|
||||||
<CheckboxButtonGroup class="row">
|
<CheckboxButtonGroup class="row">
|
||||||
@ -153,6 +145,22 @@
|
|||||||
@click="emitRichTextCommand('blockquote')"
|
@click="emitRichTextCommand('blockquote')"
|
||||||
><IconQuote /></CheckboxButton>
|
><IconQuote /></CheckboxButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</CheckboxButtonGroup>
|
||||||
|
|
||||||
|
<CheckboxButtonGroup class="row">
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
||||||
|
<CheckboxButton
|
||||||
|
style="flex: 1;"
|
||||||
|
@click="emitRichTextCommand('clear')"
|
||||||
|
><IconFormat /></CheckboxButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="格式刷">
|
||||||
|
<CheckboxButton
|
||||||
|
style="flex: 1;"
|
||||||
|
:checked="!!textFormatPainter"
|
||||||
|
@click="toggleFormatPainter()"
|
||||||
|
><IconFormatBrush /></CheckboxButton>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="超链接">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="超链接">
|
||||||
<Popover placement="bottomRight" trigger="click" v-model:visible="linkPopoverVisible">
|
<Popover placement="bottomRight" trigger="click" v-model:visible="linkPopoverVisible">
|
||||||
<template #content>
|
<template #content>
|
||||||
@ -278,12 +286,15 @@ import { PPTTextElement } from '@/types/slides'
|
|||||||
import emitter, { EmitterEvents, RichTextAction } from '@/utils/emitter'
|
import emitter, { EmitterEvents, RichTextAction } from '@/utils/emitter'
|
||||||
import { WEB_FONTS } from '@/configs/font'
|
import { WEB_FONTS } from '@/configs/font'
|
||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
import useTextFormatPainter from '@/hooks/useTextFormatPainter'
|
||||||
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
import ElementOpacity from '../common/ElementOpacity.vue'
|
import ElementOpacity from '../common/ElementOpacity.vue'
|
||||||
import ElementOutline from '../common/ElementOutline.vue'
|
import ElementOutline from '../common/ElementOutline.vue'
|
||||||
import ElementShadow from '../common/ElementShadow.vue'
|
import ElementShadow from '../common/ElementShadow.vue'
|
||||||
import ColorButton from '../common/ColorButton.vue'
|
import ColorButton from '../common/ColorButton.vue'
|
||||||
|
import TextColorButton from '../common/TextColorButton.vue'
|
||||||
|
|
||||||
// 注意,存在一个未知原因的BUG,如果文本加粗后文本框高度增加,画布的可视区域定位会出现错误
|
// 注意,存在一个未知原因的BUG,如果文本加粗后文本框高度增加,画布的可视区域定位会出现错误
|
||||||
// 因此在执行预置样式命令时,将加粗命令放在尽可能靠前的位置,避免字号增大后再加粗
|
// 因此在执行预置样式命令时,将加粗命令放在尽可能靠前的位置,避免字号增大后再加粗
|
||||||
@ -360,10 +371,12 @@ const presetStyles = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const mainStore = useMainStore()
|
||||||
const slidesStore = useSlidesStore()
|
const slidesStore = useSlidesStore()
|
||||||
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(useMainStore())
|
const { handleElement, handleElementId, richTextAttrs, availableFonts, textFormatPainter } = storeToRefs(mainStore)
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
const { addHistorySnapshot } = useHistorySnapshot()
|
||||||
|
const { toggleFormatPainter } = useTextFormatPainter()
|
||||||
|
|
||||||
const updateElement = (props: Partial<PPTTextElement>) => {
|
const updateElement = (props: Partial<PPTTextElement>) => {
|
||||||
slidesStore.updateElement({ id: handleElementId.value, props })
|
slidesStore.updateElement({ id: handleElementId.value, props })
|
||||||
@ -491,18 +504,6 @@ const updateLink = (link?: string) => {
|
|||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.text-color-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.text-color-block {
|
|
||||||
width: 16px;
|
|
||||||
height: 3px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
.font-size-btn {
|
.font-size-btn {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
38
src/views/Editor/Toolbar/common/TextColorButton.vue
Normal file
38
src/views/Editor/Toolbar/common/TextColorButton.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<Button class="text-color-btn">
|
||||||
|
<slot></slot>
|
||||||
|
<div class="text-color-block">
|
||||||
|
<div class="text-color-block-content" :style="{ backgroundColor: color }"></div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.text-color-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.text-color-block {
|
||||||
|
width: 17px;
|
||||||
|
height: 4px;
|
||||||
|
margin-top: 1px;
|
||||||
|
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAACdJREFUGFdjfPbs2X8GBgYGSUlJEMXAiCHw//9/sIrnz59DVKALAADNxxVfaiODNQAAAABJRU5ErkJggg==);
|
||||||
|
|
||||||
|
.text-color-block-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="prosemirror-editor"
|
class="prosemirror-editor"
|
||||||
|
:class="{ 'format-painter': textFormatPainter }"
|
||||||
ref="editorViewRef"
|
ref="editorViewRef"
|
||||||
></div>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
@ -14,7 +15,7 @@ import { EditorView } from 'prosemirror-view'
|
|||||||
import { toggleMark, wrapIn } from 'prosemirror-commands'
|
import { toggleMark, wrapIn } from 'prosemirror-commands'
|
||||||
import { initProsemirrorEditor, createDocument } from '@/utils/prosemirror'
|
import { initProsemirrorEditor, createDocument } from '@/utils/prosemirror'
|
||||||
import { findNodesWithSameMark, getTextAttrs, autoSelectAll, addMark, markActive, getFontsize } from '@/utils/prosemirror/utils'
|
import { findNodesWithSameMark, getTextAttrs, autoSelectAll, addMark, markActive, getFontsize } from '@/utils/prosemirror/utils'
|
||||||
import emitter, { EmitterEvents, RichTextCommand } from '@/utils/emitter'
|
import emitter, { EmitterEvents, RichTextAction, RichTextCommand } from '@/utils/emitter'
|
||||||
import { alignmentCommand } from '@/utils/prosemirror/commands/setTextAlign'
|
import { alignmentCommand } from '@/utils/prosemirror/commands/setTextAlign'
|
||||||
import { indentCommand } from '@/utils/prosemirror/commands/setTextIndent'
|
import { indentCommand } from '@/utils/prosemirror/commands/setTextIndent'
|
||||||
import { toggleList } from '@/utils/prosemirror/commands/toggleList'
|
import { toggleList } from '@/utils/prosemirror/commands/toggleList'
|
||||||
@ -53,7 +54,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const { handleElementId } = storeToRefs(mainStore)
|
const { handleElementId, textFormatPainter } = storeToRefs(mainStore)
|
||||||
|
|
||||||
const editorViewRef = ref<HTMLElement>()
|
const editorViewRef = ref<HTMLElement>()
|
||||||
let editorView: EditorView
|
let editorView: EditorView
|
||||||
@ -104,23 +105,6 @@ watch(() => props.editable, () => {
|
|||||||
editorView.setProps({ editable: () => props.editable })
|
editorView.setProps({ editable: () => props.editable })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Prosemirror编辑器的初始化和卸载
|
|
||||||
onMounted(() => {
|
|
||||||
editorView = initProsemirrorEditor((editorViewRef.value as Element), textContent.value, {
|
|
||||||
handleDOMEvents: {
|
|
||||||
focus: handleFocus,
|
|
||||||
blur: handleBlur,
|
|
||||||
keydown: handleKeydown,
|
|
||||||
click: handleClick,
|
|
||||||
},
|
|
||||||
editable: () => props.editable,
|
|
||||||
})
|
|
||||||
if (props.autoFocus) editorView.focus()
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
editorView && editorView.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 暴露 focus 方法
|
// 暴露 focus 方法
|
||||||
const focus = () => editorView.focus()
|
const focus = () => editorView.focus()
|
||||||
defineExpose({ focus })
|
defineExpose({ focus })
|
||||||
@ -249,6 +233,38 @@ const execCommand = ({ target, action }: RichTextCommand) => {
|
|||||||
handleClick()
|
handleClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鼠标抬起时,执行格式刷命令
|
||||||
|
const handleMouseup = () => {
|
||||||
|
if (!textFormatPainter.value) return
|
||||||
|
|
||||||
|
const actions: RichTextAction[] = [{ command: 'clear' }]
|
||||||
|
for (const key of Object.keys(textFormatPainter.value)) {
|
||||||
|
const command = key
|
||||||
|
const value = textFormatPainter.value[key]
|
||||||
|
if (value) actions.push({ command, value })
|
||||||
|
}
|
||||||
|
execCommand({ action: actions })
|
||||||
|
mainStore.setTextFormatPainter(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prosemirror编辑器的初始化和卸载
|
||||||
|
onMounted(() => {
|
||||||
|
editorView = initProsemirrorEditor((editorViewRef.value as Element), textContent.value, {
|
||||||
|
handleDOMEvents: {
|
||||||
|
focus: handleFocus,
|
||||||
|
blur: handleBlur,
|
||||||
|
keydown: handleKeydown,
|
||||||
|
click: handleClick,
|
||||||
|
mouseup: handleMouseup,
|
||||||
|
},
|
||||||
|
editable: () => props.editable,
|
||||||
|
})
|
||||||
|
if (props.autoFocus) editorView.focus()
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
editorView && editorView.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
emitter.on(EmitterEvents.RICH_TEXT_COMMAND, execCommand)
|
emitter.on(EmitterEvents.RICH_TEXT_COMMAND, execCommand)
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
emitter.off(EmitterEvents.RICH_TEXT_COMMAND, execCommand)
|
emitter.off(EmitterEvents.RICH_TEXT_COMMAND, execCommand)
|
||||||
@ -258,5 +274,9 @@ onUnmounted(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.prosemirror-editor {
|
.prosemirror-editor {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
|
|
||||||
|
&.format-painter {
|
||||||
|
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAVCAYAAACzK0UYAAAABHNCSVQICAgIfAhkiAAAAVRJREFUSInt1DFuwjAYBeCXUrFavgBN9yB6AKR6Bi7AVLrlBpFYgAUpp2i37AysVDIXcCIuwJRMEEYk9LrQDlVQ7EiVOvSt/v1/tmUbeZ7TGMPL5WLgEJLzNE2ptabWmsfjkTeLjTGUUvJ8Pjsjo9GIUkpKKam1voncuTRumn/EKfd1BSQnAF4qhvyK2k1VD88YQ6UUiqJI2+12r2LiPI7j2Xa7rV9yRZbLpRWiAKhGwjW1x3XN828jD9PpVK3X60bAarWy20lZltjv940QwO4KPzbu7oCgLMu/g3Q6ncZI73Q6WSFhGDZGnrIss0LG4zGEEG4ISZUkiW8DDAYDCCEQBIEbAmAWx7GNgSiKAOB1OBzaIyQnSZIom/cRRRG63e7C87z3MAw/fu7Gy/OcRVEgCIK01Wp9/10k37Ism9TdLCHEFzC/zvMPh8Nmt9v5ANDv9/EJD8ykxYswZDkAAAAASUVORK5CYII=) 1 10, default !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user