CatPtain's picture
Upload 339 files
89ce340 verified
<template>
<div class="element-toolbar">
<Tabs
:tabs="tabs"
v-model:value="activeTab"
:tabsStyle="{ marginBottom: '8px' }"
:tabStyle="{
width: '30%',
margin: '0 10%',
}"
/>
<div class="content">
<div class="style" v-if="activeTab === 'style'">
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.bold"
@click="emitRichTextCommand('bold')"
><IconTextBold /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.em"
@click="emitRichTextCommand('em')"
><IconTextItalic /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.underline"
@click="emitRichTextCommand('underline')"
><IconTextUnderline /></CheckboxButton>
<CheckboxButton
style="flex: 1;"
:checked="richTextAttrs.strikethrough"
@click="emitRichTextCommand('strikethrough')"
><IconStrikethrough /></CheckboxButton>
</ButtonGroup>
<ButtonGroup class="row">
<Button
style="flex: 1;"
@click="emitRichTextCommand('fontsize-add')"
><IconFontSize />+</Button>
<Button
style="flex: 1;"
@click="emitRichTextCommand('fontsize-reduce')"
><IconFontSize />-</Button>
</ButtonGroup>
<Divider :margin="20" />
<RadioGroup
class="row"
button-style="solid"
:value="richTextAttrs.align"
@update:value="value => emitRichTextCommand('align', value)"
>
<RadioButton value="left" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
<RadioButton value="center" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
<RadioButton value="right" style="flex: 1;"><IconAlignTextRight /></RadioButton>
</RadioGroup>
<Divider :margin="20" />
<div class="row-block">
<div class="label">文字颜色:</div>
<div class="colors">
<div class="color"
v-for="color in colors"
:key="color"
@click="updateFontColor(color)"
>
<div class="color-block" :style="{ backgroundColor: color }"></div>
</div>
</div>
</div>
<div class="row-block">
<div class="label">填充色:</div>
<div class="colors">
<div class="color"
v-for="color in colors"
:key="color"
@click="updateFill(color)"
>
<div class="color-block" :style="{ backgroundColor: color }"></div>
</div>
</div>
</div>
</div>
<div class="common" v-if="activeTab === 'common'">
<ButtonGroup class="row">
<Button style="flex: 1;" @click="copyElement()"><IconCopy class="icon" /> 复制</Button>
<Button style="flex: 1;" @click="deleteElement()"><IconDelete class="icon" /> 删除</Button>
</ButtonGroup>
<Divider :margin="20" />
<ButtonGroup class="row">
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.TOP)"><IconSendToBack class="icon" /> 置顶</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.BOTTOM)"><IconBringToFrontOne class="icon" /> 置底</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.UP)"><IconBringToFront class="icon" /> 上移</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.DOWN)"><IconSentToBack class="icon" /> 下移</Button>
</ButtonGroup>
<Divider :margin="20" />
<ButtonGroup class="row">
<Button style="flex: 1;" @click="alignElementToCanvas(ElementAlignCommands.LEFT)"><IconAlignLeft class="icon" /> 左对齐</Button>
<Button style="flex: 1;" @click="alignElementToCanvas(ElementAlignCommands.HORIZONTAL)"><IconAlignVertically class="icon" /> 水平居中</Button>
<Button style="flex: 1;" @click="alignElementToCanvas(ElementAlignCommands.RIGHT)"><IconAlignRight class="icon" /> 右对齐</Button>
</ButtonGroup>
<ButtonGroup class="row">
<Button style="flex: 1;" @click="alignElementToCanvas(ElementAlignCommands.TOP)"><IconAlignTop class="icon" /> 上对齐</Button>
<Button style="flex: 1;" @click="alignElementToCanvas(ElementAlignCommands.VERTICAL)"><IconAlignHorizontally class="icon" /> 垂直居中</Button>
<Button style="flex: 1;" @click="alignElementToCanvas(ElementAlignCommands.BOTTOM)"><IconAlignBottom class="icon" /> 下对齐</Button>
</ButtonGroup>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import type { PPTElement, TableCell } from '@/types/slides'
import { ElementAlignCommands, ElementOrderCommands } from '@/types/edit'
import emitter, { EmitterEvents } from '@/utils/emitter'
import useOrderElement from '@/hooks/useOrderElement'
import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
import useDeleteElement from '@/hooks/useDeleteElement'
import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import CheckboxButton from '@/components/CheckboxButton.vue'
import Tabs from '@/components/Tabs.vue'
import Divider from '@/components/Divider.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'
interface TabItem {
key: 'style' | 'common'
label: string
}
const colors = ['#000000', '#ffffff', '#eeece1', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c', '#c21401', '#ff1e02', '#ffc12a', '#ffff3a', '#90cf5b', '#00af57']
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs } = storeToRefs(mainStore)
const { addHistorySnapshot } = useHistorySnapshot()
const updateElement = (id: string, props: Partial<PPTElement>) => {
slidesStore.updateElement({ id, props })
addHistorySnapshot()
}
const tabs: TabItem[] = [
{ key: 'style', label: '样式' },
{ key: 'common', label: '布局' },
]
const activeTab = ref('common')
const { orderElement } = useOrderElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { addElementsFromData } = useAddSlidesOrElements()
const { deleteElement } = useDeleteElement()
const copyElement = () => {
const element: PPTElement = JSON.parse(JSON.stringify(handleElement.value))
addElementsFromData([element])
}
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
const updateFontColor = (color: string) => {
if (!handleElement.value) return
if (handleElement.value.type === 'text' || (handleElement.value.type === 'shape' && handleElement.value.text?.content)) {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command: 'color', value: color } })
}
if (handleElement.value.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(handleElement.value.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, color }
}
}
updateElement(handleElementId.value, { data })
}
if (handleElement.value.type === 'latex') {
updateElement(handleElementId.value, { color })
}
}
const updateFill = (color: string) => {
if (!handleElement.value) return
if (
handleElement.value.type === 'text' ||
handleElement.value.type === 'shape' ||
handleElement.value.type === 'chart'
) updateElement(handleElementId.value, { fill: color })
if (handleElement.value.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(handleElement.value.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, backcolor: color }
}
}
updateElement(handleElementId.value, { data })
}
if (handleElement.value.type === 'audio') updateElement(handleElementId.value, { color })
}
</script>
<style lang="scss" scoped>
.element-toolbar {
width: 100%;
height: 240px;
position: absolute;
z-index: 99;
bottom: 0;
left: 0;
background-color: #fff;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
animation: slideInUp .15s;
}
@keyframes slideInUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.content {
padding: 10px;
flex: 1;
overflow: auto;
}
.row {
width: 100%;
display: flex;
align-items: center;
margin-bottom: 10px;
.icon {
margin-right: 3px;
}
}
.row-block {
margin-bottom: 10px;
background-color: $lightGray;
border-radius: $borderRadius;
padding: 10px;
}
.label {
font-size: 13px;
margin-bottom: 20px;
margin-left: 6px;
}
.colors {
@include flex-grid-layout();
}
.color {
@include flex-grid-layout-children(8, 12%);
padding-bottom: 5px;
display: flex;
justify-content: center;
align-items: center;
.color-block {
width: 30px;
height: 30px;
border-radius: 50%;
}
}
</style>