| <template> |
| <div class="rich-text-base"> |
| <SelectGroup class="row"> |
| <Select |
| style="width: 60%;" |
| :value="richTextAttrs.fontname" |
| search |
| searchLabel="搜索字体" |
| @update:value="value => emitRichTextCommand('fontname', value as string)" |
| :options="FONTS" |
| > |
| <template #icon> |
| <IconFontSize /> |
| </template> |
| </Select> |
| <Select |
| style="width: 40%;" |
| :value="richTextAttrs.fontsize" |
| search |
| searchLabel="搜索字号" |
| @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')" |
| >A²</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 { FONTS } from '@/configs/font' |
| import useTextFormatPainter from '@/hooks/useTextFormatPainter' |
| import message from '@/utils/message' |
| |
| import TextColorButton from '@/components/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, 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: -3px; |
| |
| span { |
| width: 100%; |
| height: 2px; |
| display: inline-block; |
| position: absolute; |
| top: 8px; |
| background-color: #666; |
| } |
| } |
| .popover-btn { |
| padding: 0 3px; |
| } |
| </style> |