| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- <template>
- <div class="rich-text-base">
- <SelectGroup class="row">
- <Select
- style="width: 60%;"
- :value="richTextAttrs.fontname"
- search
- :searchLabel="lang.searchFont"
- @update:value="value => emitRichTextCommand('fontname', value as string)"
- :options="FONTS"
- >
- <template #icon>
- <IconFontSize />
- </template>
- </Select>
- <Select
- style="width: 40%;"
- :value="richTextAttrs.fontsize"
- search
- :searchLabel="lang.searchFontSize"
- @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="lang.textColor" :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="lang.highlight" :color="richTextAttrs.backcolor">
- <IconHighLight />
- </TextColorButton>
- </Popover>
- <Button
- style="display: flex;align-items: center;"
- class="font-size-btn"
- v-tooltip="lang.fontSizeAdd"
- @click="emitRichTextCommand('fontsize-add')"
- ><IconFontSize />+</Button>
- <Button
- style="display: flex;align-items: center;"
- last
- class="font-size-btn"
- v-tooltip="lang.fontSizeReduce"
- @click="emitRichTextCommand('fontsize-reduce')"
- ><IconFontSize />-</Button>
- </ButtonGroup>
- <ButtonGroup class="row">
- <CheckboxButton
- style="flex: 1;"
- :checked="richTextAttrs.bold"
- v-tooltip="lang.bold"
- @click="emitRichTextCommand('bold')"
- ><IconTextBold /></CheckboxButton>
- <CheckboxButton
- style="flex: 1;"
- :checked="richTextAttrs.em"
- v-tooltip="lang.italic"
- @click="emitRichTextCommand('em')"
- ><IconTextItalic /></CheckboxButton>
- <CheckboxButton
- style="flex: 1;"
- :checked="richTextAttrs.underline"
- v-tooltip="lang.underline"
- @click="emitRichTextCommand('underline')"
- ><IconTextUnderline /></CheckboxButton>
- <CheckboxButton
- style="flex: 1;"
- :checked="richTextAttrs.strikethrough"
- v-tooltip="lang.strikethrough"
- @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>
- <Popover trigger="click" v-model:value="AIPopoverVisible" style="width: 25%;">
- <template #content>
- <PopoverMenuItem center @click="execAI('美化改写')">美化</PopoverMenuItem>
- <PopoverMenuItem center @click="execAI('扩写丰富')">扩写</PopoverMenuItem>
- <PopoverMenuItem center @click="execAI('精简提炼')">精简</PopoverMenuItem>
- </template>
- <CheckboxButton
- first
- style="width: 100%;"
- v-tooltip="'AI辅助'"
- ><span :class="{ 'ai-loading': isAIWriting }">{{ isAIWriting ? '' : 'AI' }}</span></CheckboxButton>
- </Popover>
- <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>
- <Popover placement="bottom-end" trigger="click" v-model:value="linkPopoverVisible" style="width: 25%;">
- <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 center @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 center @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 api from '@/services'
- 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 { htmlToText } from '@/utils/common'
- import TextColorButton from '@/components/TextColorButton2.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'
- import { lang } from '@/main'
- const { handleElement, handleElementId, 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)
- const AIPopoverVisible = ref(false)
- const isAIWriting = ref(false)
- watch(richTextAttrs, () => linkPopoverVisible.value = false)
- watch(handleElementId, () => {
- if (isAIWriting.value) isAIWriting.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
- }
- const execAI = async (command: string) => {
- AIPopoverVisible.value = false
- if (!handleElement.value) return
- let content = ''
- if (handleElement.value.type === 'text' && handleElement.value.content) {
- content = handleElement.value.content
- }
- if (handleElement.value.type === 'shape' && handleElement.value.text && handleElement.value.text.content) {
- content = handleElement.value.text.content
- }
- if (!content) return message.error('没有可以执行的文本内容')
- let resultText = ''
- const stream = await api.AI_Writing({
- content: htmlToText(content),
- command,
- })
- isAIWriting.value = true
- const reader: ReadableStreamDefaultReader = stream.body.getReader()
- const decoder = new TextDecoder('utf-8')
-
- const readStream = () => {
- reader.read().then(({ done, value }) => {
- if (!isAIWriting.value) return
- if (done) {
- isAIWriting.value = false
- return
- }
- const chunk = decoder.decode(value, { stream: true })
- resultText += chunk
- emitRichTextCommand('replace', resultText)
- readStream()
- })
- }
- readStream()
- }
- </script>
- <style lang="scss" scoped>
- .rich-text-base {
- user-select: none;
- display: flex;
- align-items: center;
- gap: 10px;
- ::v-deep(.ai-loading) {
- width: 16px;
- height: 16px;
- display: inline-block;
- margin-top: 8px;
- border: 1px solid $themeColor;
- border-top-color: transparent;
- border-radius: 50%;
- animation: spinner .8s linear infinite;
- }
- }
- .row {
- width: 100%;
- display: flex;
- align-items: center;
- // margin-bottom: 10px;
- }
- .font-size-btn {
- padding: 0 15px;
- }
- .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;
- }
- @keyframes spinner {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- </style>
|