perf: 富文本编辑优化

This commit is contained in:
pipipi-pikachu 2024-03-23 13:38:59 +08:00
parent ae4656bdce
commit 5a9f232330
6 changed files with 399 additions and 500 deletions

View File

@ -452,7 +452,7 @@ export interface TableCellStyle {
backcolor?: string
fontsize?: string
fontname?: string
align?: 'left' | 'center' | 'right'
align?: 'left' | 'center' | 'right' | 'justify'
}

View File

@ -123,6 +123,7 @@
<RadioButton value="left" style="flex: 1;" v-tooltip="'左对齐'"><IconAlignTextLeft /></RadioButton>
<RadioButton value="center" style="flex: 1;" v-tooltip="'居中'"><IconAlignTextCenter /></RadioButton>
<RadioButton value="right" style="flex: 1;" v-tooltip="'右对齐'"><IconAlignTextRight /></RadioButton>
<RadioButton value="justify" style="flex: 1;" v-tooltip="'两端对齐'"><IconAlignTextBoth /></RadioButton>
</RadioGroup>
</div>
</template>

View File

@ -95,127 +95,8 @@
<Divider />
<template v-if="handleShapeElement.text?.content">
<SelectGroup class="row">
<Select
class="font-select"
style="width: 60%;"
:value="richTextAttrs.fontname"
@update:value="value => emitRichTextCommand('fontname', value as string)"
:options="[
...availableFonts,
...WEB_FONTS
]"
>
<template #icon>
<IconFontSize />
</template>
</Select>
<Select
style="width: 40%;"
:value="richTextAttrs.fontsize"
@update:value="value => emitRichTextCommand('fontsize', value as string)"
:options="fontSizeOptions.map(item => ({
label: item, value: item
}))"
>
<template #icon>
<IconAddText />
</template>
</Select>
</SelectGroup>
<ButtonGroup class="row" passive>
<Popover trigger="click" style="width: 30%;">
<template #content>
<ColorPicker
:modelValue="richTextAttrs.color"
@update:modelValue="value => emitRichTextCommand('color', value)"
/>
</template>
<TextColorButton first v-tooltip="'文字颜色'" :color="richTextAttrs.color">
<IconText />
</TextColorButton>
</Popover>
<Popover trigger="click" style="width: 30%;">
<template #content>
<ColorPicker
:modelValue="richTextAttrs.backcolor"
@update:modelValue="value => emitRichTextCommand('backcolor', value)"
/>
</template>
<TextColorButton v-tooltip="'文字高亮'" :color="richTextAttrs.backcolor">
<IconHighLight />
</TextColorButton>
</Popover>
<Button
class="font-size-btn"
style="width: 20%;"
v-tooltip="'增大字号'"
@click="emitRichTextCommand('fontsize-add')"
><IconFontSize />+</Button>
<Button
last
class="font-size-btn"
style="width: 20%;"
v-tooltip="'减小字号'"
@click="emitRichTextCommand('fontsize-reduce')"
><IconFontSize />-</Button>
</ButtonGroup>
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.bold"
v-tooltip="'加粗'"
@click="emitRichTextCommand('bold')"
><IconTextBold /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.em"
v-tooltip="'斜体'"
@click="emitRichTextCommand('em')"
><IconTextItalic /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.underline"
v-tooltip="'下划线'"
@click="emitRichTextCommand('underline')"
><IconTextUnderline /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.strikethrough"
v-tooltip="'删除线'"
@click="emitRichTextCommand('strikethrough')"
><IconStrikethrough /></CheckboxButton>
</ButtonGroup>
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
v-tooltip="'清除格式'"
@click="emitRichTextCommand('clear')"
><IconFormat /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="!!textFormatPainter"
v-tooltip="'格式刷(双击连续使用)'"
@click="toggleTextFormatPainter()"
@dblclick="toggleTextFormatPainter(true)"
><IconFormatBrush /></CheckboxButton>
</ButtonGroup>
<Divider />
<RadioGroup
class="row"
button-style="solid"
:value="richTextAttrs.align"
@update:value="value => emitRichTextCommand('align', value)"
>
<RadioButton value="left" v-tooltip="'左对齐'" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
<RadioButton value="center" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
<RadioButton value="right" v-tooltip="'右对齐'" style="flex: 1;"><IconAlignTextRight /></RadioButton>
</RadioGroup>
<RichTextBase />
<Divider />
<RadioGroup
class="row"
@ -228,7 +109,7 @@
<RadioButton value="bottom" v-tooltip="'底对齐'" style="flex: 1;"><IconAlignTextBottomOne /></RadioButton>
</RadioGroup>
<Divider />
<Divider />
</template>
<ElementOutline />
@ -255,11 +136,8 @@ import { type Ref, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import type { PPTShapeElement, ShapeGradient, ShapeText } from '@/types/slides'
import { WEB_FONTS } from '@/configs/font'
import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
import emitter, { EmitterEvents } from '@/utils/emitter'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import useTextFormatPainter from '@/hooks/useTextFormatPainter'
import useShapeFormatPainter from '@/hooks/useShapeFormatPainter'
import ElementOpacity from '../common/ElementOpacity.vue'
@ -267,23 +145,20 @@ import ElementOutline from '../common/ElementOutline.vue'
import ElementShadow from '../common/ElementShadow.vue'
import ElementFlip from '../common/ElementFlip.vue'
import ColorButton from '../common/ColorButton.vue'
import TextColorButton from '../common/TextColorButton.vue'
import RichTextBase from '../common/RichTextBase.vue'
import ShapeItemThumbnail from '@/views/Editor/CanvasTool/ShapeItemThumbnail.vue'
import CheckboxButton from '@/components/CheckboxButton.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import Divider from '@/components/Divider.vue'
import Slider from '@/components/Slider.vue'
import Button from '@/components/Button.vue'
import ButtonGroup from '@/components/ButtonGroup.vue'
import RadioButton from '@/components/RadioButton.vue'
import RadioGroup from '@/components/RadioGroup.vue'
import Select from '@/components/Select.vue'
import SelectGroup from '@/components/SelectGroup.vue'
import Popover from '@/components/Popover.vue'
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs, availableFonts, textFormatPainter, shapeFormatPainter } = storeToRefs(mainStore)
const { handleElement, handleElementId, shapeFormatPainter } = storeToRefs(mainStore)
const handleShapeElement = handleElement as Ref<PPTShapeElement>
@ -306,7 +181,6 @@ watch(handleElement, () => {
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const { toggleTextFormatPainter } = useTextFormatPainter()
const { toggleShapeFormatPainter } = useShapeFormatPainter()
const updateElement = (props: Partial<PPTShapeElement>) => {
@ -373,16 +247,6 @@ const updateTextAlign = (align: 'top' | 'middle' | 'bottom') => {
const _text = _handleElement.text || defaultText
updateElement({ text: { ..._text, align } })
}
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
</script>
<style lang="scss" scoped>

View File

@ -84,11 +84,12 @@
class="row"
button-style="solid"
:value="textAttrs.align"
@update:value="value => updateTextAttrs({ align: value as 'left' | 'center' | 'right' })"
@update:value="value => updateTextAttrs({ align: value as 'left' | 'center' | 'right' | 'justify' })"
>
<RadioButton value="left" v-tooltip="'左对齐'" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
<RadioButton value="center" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
<RadioButton value="right" v-tooltip="'右对齐'" style="flex: 1;"><IconAlignTextRight /></RadioButton>
<RadioButton value="justify" v-tooltip="'两端对齐'" style="flex: 1;"><IconAlignTextBoth /></RadioButton>
</RadioGroup>
<Divider />

View File

@ -11,250 +11,7 @@
</div>
<Divider />
<SelectGroup class="row">
<Select
class="font-select"
style="width: 60%;"
:value="richTextAttrs.fontname"
@update:value="value => emitRichTextCommand('fontname', value as string)"
:options="[
...availableFonts,
...WEB_FONTS
]"
>
<template #icon>
<IconFontSize />
</template>
</Select>
<Select
style="width: 40%;"
:value="richTextAttrs.fontsize"
@update:value="value => emitRichTextCommand('fontsize', value as string)"
:options="fontSizeOptions.map(item => ({
label: item, value: item
}))"
>
<template #icon>
<IconAddText />
</template>
</Select>
</SelectGroup>
<ButtonGroup class="row" passive>
<Popover trigger="click" style="width: 30%;">
<template #content>
<ColorPicker
:modelValue="richTextAttrs.color"
@update:modelValue="value => emitRichTextCommand('color', value)"
/>
</template>
<TextColorButton first v-tooltip="'文字颜色'" :color="richTextAttrs.color">
<IconText />
</TextColorButton>
</Popover>
<Popover trigger="click" style="width: 30%;">
<template #content>
<ColorPicker
:modelValue="richTextAttrs.backcolor"
@update:modelValue="value => emitRichTextCommand('backcolor', value)"
/>
</template>
<TextColorButton v-tooltip="'文字高亮'" :color="richTextAttrs.backcolor">
<IconHighLight />
</TextColorButton>
</Popover>
<Button
class="font-size-btn"
style="width: 20%;"
v-tooltip="'增大字号'"
@click="emitRichTextCommand('fontsize-add')"
><IconFontSize />+</Button>
<Button
last
class="font-size-btn"
style="width: 20%;"
v-tooltip="'减小字号'"
@click="emitRichTextCommand('fontsize-reduce')"
><IconFontSize />-</Button>
</ButtonGroup>
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.bold"
v-tooltip="'加粗'"
@click="emitRichTextCommand('bold')"
><IconTextBold /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.em"
v-tooltip="'斜体'"
@click="emitRichTextCommand('em')"
><IconTextItalic /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.underline"
v-tooltip="'下划线'"
@click="emitRichTextCommand('underline')"
><IconTextUnderline /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.strikethrough"
v-tooltip="'删除线'"
@click="emitRichTextCommand('strikethrough')"
><IconStrikethrough /></CheckboxButton>
</ButtonGroup>
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.superscript"
v-tooltip="'上标'"
@click="emitRichTextCommand('superscript')"
></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.subscript"
v-tooltip="'下标'"
@click="emitRichTextCommand('subscript')"
>A₂</CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.code"
v-tooltip="'行内代码'"
@click="emitRichTextCommand('code')"
><IconCode /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.blockquote"
v-tooltip="'引用'"
@click="emitRichTextCommand('blockquote')"
><IconQuote /></CheckboxButton>
</ButtonGroup>
<ButtonGroup class="row" passive>
<CheckboxButton
first
style="flex: 1;"
v-tooltip="'清除格式'"
@click="emitRichTextCommand('clear')"
><IconFormat /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="!!textFormatPainter"
v-tooltip="'格式刷(双击连续使用)'"
@click="toggleTextFormatPainter()"
@dblclick="toggleTextFormatPainter(true)"
><IconFormatBrush /></CheckboxButton>
<Popover placement="bottom-end" trigger="click" v-model:value="linkPopoverVisible" style="width: 33.33%;">
<template #content>
<div class="link-popover">
<Input v-model:value="link" placeholder="请输入超链接" />
<div class="btns">
<Button size="small" :disabled="!richTextAttrs.link" @click="updateLink()" style="margin-right: 5px;">移除</Button>
<Button size="small" type="primary" @click="updateLink(link)">确认</Button>
</div>
</div>
</template>
<CheckboxButton
last
style="width: 100%;"
:checked="!!richTextAttrs.link"
v-tooltip="'超链接'"
@click="openLinkPopover()"
><IconLinkOne /></CheckboxButton>
</Popover>
</ButtonGroup>
<Divider />
<RadioGroup
class="row"
button-style="solid"
:value="richTextAttrs.align"
@update:value="value => emitRichTextCommand('align', value)"
>
<RadioButton value="left" v-tooltip="'左对齐'" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
<RadioButton value="center" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
<RadioButton value="right" v-tooltip="'右对齐'" style="flex: 1;"><IconAlignTextRight /></RadioButton>
<RadioButton value="justify" v-tooltip="'两端对齐'" style="flex: 1;"><IconAlignTextBoth /></RadioButton>
</RadioGroup>
<div class="row" passive>
<ButtonGroup style="flex: 1;">
<Button
first
:type="richTextAttrs.bulletList ? 'primary' : 'default'"
style="flex: 1;"
v-tooltip="'项目符号'"
@click="emitRichTextCommand('bulletList')"
><IconList /></Button>
<Popover trigger="click" v-model:value="bulletListPanelVisible">
<template #content>
<div class="list-wrap">
<ul class="list"
v-for="item in bulletListStyleTypeOption"
:key="item"
:style="{ listStyleType: item }"
@click="emitRichTextCommand('bulletList', item)"
>
<li class="list-item" v-for="key in 3" :key="key"><span></span></li>
</ul>
</div>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
<div style="width: 10px;"></div>
<ButtonGroup style="flex: 1;" passive>
<Button
first
:type="richTextAttrs.orderedList ? 'primary' : 'default'"
style="flex: 1;"
v-tooltip="'编号'"
@click="emitRichTextCommand('orderedList')"
><IconOrderedList /></Button>
<Popover trigger="click" v-model:value="orderedListPanelVisible">
<template #content>
<div class="list-wrap">
<ul class="list"
v-for="item in orderedListStyleTypeOption"
:key="item"
:style="{ listStyleType: item }"
@click="emitRichTextCommand('orderedList', item)"
>
<li class="list-item" v-for="key in 3" :key="key"><span></span></li>
</ul>
</div>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
</div>
<div class="row">
<ButtonGroup style="flex: 1;" passive>
<Button first style="flex: 1;" v-tooltip="'减小段落缩进'" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
<Popover trigger="click" v-model:value="indentLeftPanelVisible">
<template #content>
<PopoverMenuItem @click="emitRichTextCommand('textIndent', '-1')">减小首行缩进</PopoverMenuItem>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
<div style="width: 10px;"></div>
<ButtonGroup style="flex: 1;" passive>
<Button first style="flex: 1;" v-tooltip="'增大段落缩进'" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button>
<Popover trigger="click" v-model:value="indentRightPanelVisible">
<template #content>
<PopoverMenuItem @click="emitRichTextCommand('textIndent', '+1')">增大首行缩进</PopoverMenuItem>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
</div>
<RichTextBase />
<Divider />
<div class="row">
@ -327,28 +84,17 @@ import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import type { PPTTextElement } from '@/types/slides'
import emitter, { EmitterEvents, type RichTextAction } from '@/utils/emitter'
import { WEB_FONTS } from '@/configs/font'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import useTextFormatPainter from '@/hooks/useTextFormatPainter'
import message from '@/utils/message'
import ElementOpacity from '../common/ElementOpacity.vue'
import ElementOutline from '../common/ElementOutline.vue'
import ElementShadow from '../common/ElementShadow.vue'
import ColorButton from '../common/ColorButton.vue'
import TextColorButton from '../common/TextColorButton.vue'
import CheckboxButton from '@/components/CheckboxButton.vue'
import RichTextBase from '../common/RichTextBase.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import Divider from '@/components/Divider.vue'
import Input from '@/components/Input.vue'
import Button from '@/components/Button.vue'
import ButtonGroup from '@/components/ButtonGroup.vue'
import RadioButton from '@/components/RadioButton.vue'
import RadioGroup from '@/components/RadioGroup.vue'
import Select from '@/components/Select.vue'
import SelectGroup from '@/components/SelectGroup.vue'
import Popover from '@/components/Popover.vue'
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
// BUG
//
@ -427,24 +173,15 @@ const presetStyles = [
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs, availableFonts, textFormatPainter } = storeToRefs(mainStore)
const { handleElement, handleElementId } = storeToRefs(mainStore)
const { addHistorySnapshot } = useHistorySnapshot()
const { toggleTextFormatPainter } = useTextFormatPainter()
const updateElement = (props: Partial<PPTTextElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const bulletListPanelVisible = ref(false)
const orderedListPanelVisible = ref(false)
const indentLeftPanelVisible = ref(false)
const indentRightPanelVisible = ref(false)
const bulletListStyleTypeOption = ref(['disc', 'circle', 'square'])
const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'lower-greek'])
const fill = ref<string>('#000')
const lineHeight = ref<number>()
const wordSpace = ref<number>()
@ -460,11 +197,6 @@ watch(handleElement, () => {
emitter.emit(EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE)
}, { deep: true, immediate: true })
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const lineHeightOptions = [0.9, 1.0, 1.15, 1.2, 1.4, 1.5, 1.8, 2.0, 2.5, 3.0]
const wordSpaceOptions = [0, 1, 2, 3, 4, 5, 6, 8, 10]
const paragraphSpaceOptions = [0, 5, 10, 15, 20, 25, 30, 40, 50, 80]
@ -489,32 +221,10 @@ const updateFill = (value: string) => {
updateElement({ fill: value })
}
//
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
//
//
const emitBatchRichTextCommand = (action: RichTextAction[]) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action })
}
//
const link = ref('')
const linkPopoverVisible = ref(false)
watch(richTextAttrs, () => linkPopoverVisible.value = false)
const openLinkPopover = () => {
link.value = richTextAttrs.value.link
}
const updateLink = (link?: string) => {
const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
if (!link || !linkRegExp.test(link)) return message.error('不是正确的网页链接地址')
emitRichTextCommand('link', link)
linkPopoverVisible.value = false
}
</script>
<style lang="scss" scoped>
@ -557,67 +267,4 @@ const updateLink = (link?: string) => {
margin-top: -1px;
}
}
.font-size-btn {
padding: 0;
}
.link-popover {
width: 240px;
.btns {
margin-top: 10px;
text-align: right;
}
}
.list-wrap {
width: 176px;
color: #666;
padding: 8px;
margin: -12px;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
}
.list {
background-color: $lightGray;
padding: 4px 4px 4px 20px;
cursor: pointer;
&:not(:nth-child(3n)) {
margin-right: 8px;
}
&:nth-child(4),
&:nth-child(5),
&:nth-child(6) {
margin-top: 8px;
}
&:hover {
color: $themeColor;
span {
background-color: $themeColor;
}
}
}
.list-item {
width: 24px;
height: 12px;
position: relative;
font-size: 12px;
top: -5px;
span {
width: 100%;
height: 2px;
display: inline-block;
position: absolute;
top: 10px;
background-color: #666;
}
}
.popover-btn {
padding: 0 3px;
}
</style>

View File

@ -0,0 +1,386 @@
<template>
<div class="rich-text-base">
<SelectGroup class="row">
<Select
class="font-select"
style="width: 60%;"
:value="richTextAttrs.fontname"
@update:value="value => emitRichTextCommand('fontname', value as string)"
:options="[
...availableFonts,
...WEB_FONTS
]"
>
<template #icon>
<IconFontSize />
</template>
</Select>
<Select
style="width: 40%;"
:value="richTextAttrs.fontsize"
@update:value="value => emitRichTextCommand('fontsize', value as string)"
:options="fontSizeOptions.map(item => ({
label: item, value: item
}))"
>
<template #icon>
<IconAddText />
</template>
</Select>
</SelectGroup>
<ButtonGroup class="row" passive>
<Popover trigger="click" style="width: 30%;">
<template #content>
<ColorPicker
:modelValue="richTextAttrs.color"
@update:modelValue="value => emitRichTextCommand('color', value)"
/>
</template>
<TextColorButton first v-tooltip="'文字颜色'" :color="richTextAttrs.color">
<IconText />
</TextColorButton>
</Popover>
<Popover trigger="click" style="width: 30%;">
<template #content>
<ColorPicker
:modelValue="richTextAttrs.backcolor"
@update:modelValue="value => emitRichTextCommand('backcolor', value)"
/>
</template>
<TextColorButton v-tooltip="'文字高亮'" :color="richTextAttrs.backcolor">
<IconHighLight />
</TextColorButton>
</Popover>
<Button
class="font-size-btn"
style="width: 20%;"
v-tooltip="'增大字号'"
@click="emitRichTextCommand('fontsize-add')"
><IconFontSize />+</Button>
<Button
last
class="font-size-btn"
style="width: 20%;"
v-tooltip="'减小字号'"
@click="emitRichTextCommand('fontsize-reduce')"
><IconFontSize />-</Button>
</ButtonGroup>
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.bold"
v-tooltip="'加粗'"
@click="emitRichTextCommand('bold')"
><IconTextBold /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.em"
v-tooltip="'斜体'"
@click="emitRichTextCommand('em')"
><IconTextItalic /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.underline"
v-tooltip="'下划线'"
@click="emitRichTextCommand('underline')"
><IconTextUnderline /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.strikethrough"
v-tooltip="'删除线'"
@click="emitRichTextCommand('strikethrough')"
><IconStrikethrough /></CheckboxButton>
</ButtonGroup>
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.superscript"
v-tooltip="'上标'"
@click="emitRichTextCommand('superscript')"
></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.subscript"
v-tooltip="'下标'"
@click="emitRichTextCommand('subscript')"
>A₂</CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.code"
v-tooltip="'行内代码'"
@click="emitRichTextCommand('code')"
><IconCode /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.blockquote"
v-tooltip="'引用'"
@click="emitRichTextCommand('blockquote')"
><IconQuote /></CheckboxButton>
</ButtonGroup>
<ButtonGroup class="row" passive>
<CheckboxButton
first
style="flex: 1;"
v-tooltip="'清除格式'"
@click="emitRichTextCommand('clear')"
><IconFormat /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="!!textFormatPainter"
v-tooltip="'格式刷(双击连续使用)'"
@click="toggleTextFormatPainter()"
@dblclick="toggleTextFormatPainter(true)"
><IconFormatBrush /></CheckboxButton>
<Popover placement="bottom-end" trigger="click" v-model:value="linkPopoverVisible" style="width: 33.33%;">
<template #content>
<div class="link-popover">
<Input v-model:value="link" placeholder="请输入超链接" />
<div class="btns">
<Button size="small" :disabled="!richTextAttrs.link" @click="removeLink()" style="margin-right: 5px;">移除</Button>
<Button size="small" type="primary" @click="updateLink(link)">确认</Button>
</div>
</div>
</template>
<CheckboxButton
last
style="width: 100%;"
:checked="!!richTextAttrs.link"
v-tooltip="'超链接'"
@click="openLinkPopover()"
><IconLinkOne /></CheckboxButton>
</Popover>
</ButtonGroup>
<Divider />
<RadioGroup
class="row"
button-style="solid"
:value="richTextAttrs.align"
@update:value="value => emitRichTextCommand('align', value)"
>
<RadioButton value="left" v-tooltip="'左对齐'" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
<RadioButton value="center" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
<RadioButton value="right" v-tooltip="'右对齐'" style="flex: 1;"><IconAlignTextRight /></RadioButton>
<RadioButton value="justify" v-tooltip="'两端对齐'" style="flex: 1;"><IconAlignTextBoth /></RadioButton>
</RadioGroup>
<div class="row" passive>
<ButtonGroup style="flex: 1;">
<Button
first
:type="richTextAttrs.bulletList ? 'primary' : 'default'"
style="flex: 1;"
v-tooltip="'项目符号'"
@click="emitRichTextCommand('bulletList')"
><IconList /></Button>
<Popover trigger="click" v-model:value="bulletListPanelVisible">
<template #content>
<div class="list-wrap">
<ul class="list"
v-for="item in bulletListStyleTypeOption"
:key="item"
:style="{ listStyleType: item }"
@click="emitRichTextCommand('bulletList', item)"
>
<li class="list-item" v-for="key in 3" :key="key"><span></span></li>
</ul>
</div>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
<div style="width: 10px;"></div>
<ButtonGroup style="flex: 1;" passive>
<Button
first
:type="richTextAttrs.orderedList ? 'primary' : 'default'"
style="flex: 1;"
v-tooltip="'编号'"
@click="emitRichTextCommand('orderedList')"
><IconOrderedList /></Button>
<Popover trigger="click" v-model:value="orderedListPanelVisible">
<template #content>
<div class="list-wrap">
<ul class="list"
v-for="item in orderedListStyleTypeOption"
:key="item"
:style="{ listStyleType: item }"
@click="emitRichTextCommand('orderedList', item)"
>
<li class="list-item" v-for="key in 3" :key="key"><span></span></li>
</ul>
</div>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
</div>
<div class="row">
<ButtonGroup style="flex: 1;" passive>
<Button first style="flex: 1;" v-tooltip="'减小段落缩进'" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
<Popover trigger="click" v-model:value="indentLeftPanelVisible">
<template #content>
<PopoverMenuItem @click="emitRichTextCommand('textIndent', '-1')">减小首行缩进</PopoverMenuItem>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
<div style="width: 10px;"></div>
<ButtonGroup style="flex: 1;" passive>
<Button first style="flex: 1;" v-tooltip="'增大段落缩进'" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button>
<Popover trigger="click" v-model:value="indentRightPanelVisible">
<template #content>
<PopoverMenuItem @click="emitRichTextCommand('textIndent', '+1')">增大首行缩进</PopoverMenuItem>
</template>
<Button last class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import emitter, { EmitterEvents } from '@/utils/emitter'
import { WEB_FONTS } from '@/configs/font'
import useTextFormatPainter from '@/hooks/useTextFormatPainter'
import message from '@/utils/message'
import TextColorButton from '../common/TextColorButton.vue'
import CheckboxButton from '@/components/CheckboxButton.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import Input from '@/components/Input.vue'
import Button from '@/components/Button.vue'
import ButtonGroup from '@/components/ButtonGroup.vue'
import Select from '@/components/Select.vue'
import SelectGroup from '@/components/SelectGroup.vue'
import Divider from '@/components/Divider.vue'
import Popover from '@/components/Popover.vue'
import RadioButton from '@/components/RadioButton.vue'
import RadioGroup from '@/components/RadioGroup.vue'
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
const { richTextAttrs, availableFonts, textFormatPainter } = storeToRefs(useMainStore())
const { toggleTextFormatPainter } = useTextFormatPainter()
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
const bulletListPanelVisible = ref(false)
const orderedListPanelVisible = ref(false)
const indentLeftPanelVisible = ref(false)
const indentRightPanelVisible = ref(false)
const bulletListStyleTypeOption = ref(['disc', 'circle', 'square'])
const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'lower-greek'])
const link = ref('')
const linkPopoverVisible = ref(false)
watch(richTextAttrs, () => linkPopoverVisible.value = false)
const openLinkPopover = () => {
link.value = richTextAttrs.value.link
}
const updateLink = (link?: string) => {
const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
if (!link || !linkRegExp.test(link)) return message.error('不是正确的网页链接地址')
emitRichTextCommand('link', link)
linkPopoverVisible.value = false
}
const removeLink = () => {
emitRichTextCommand('link')
linkPopoverVisible.value = false
}
</script>
<style lang="scss" scoped>
.rich-text-base {
user-select: none;
}
.row {
width: 100%;
display: flex;
align-items: center;
margin-bottom: 10px;
}
.font-size-btn {
padding: 0;
}
.link-popover {
width: 240px;
.btns {
margin-top: 10px;
text-align: right;
}
}
.list-wrap {
width: 176px;
color: #666;
padding: 8px;
margin: -12px;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
}
.list {
background-color: $lightGray;
padding: 4px 4px 4px 20px;
cursor: pointer;
&:not(:nth-child(3n)) {
margin-right: 8px;
}
&:nth-child(4),
&:nth-child(5),
&:nth-child(6) {
margin-top: 8px;
}
&:hover {
color: $themeColor;
span {
background-color: $themeColor;
}
}
}
.list-item {
width: 24px;
height: 12px;
position: relative;
font-size: 12px;
top: -5px;
span {
width: 100%;
height: 2px;
display: inline-block;
position: absolute;
top: 10px;
background-color: #666;
}
}
.popover-btn {
padding: 0 3px;
}
</style>