123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- <template>
- <div class="element-positopn-panel">
- <div class="title">层级:</div>
- <ButtonGroup class="row">
- <Button style="flex: 1" @click="orderElement(handleElement!, ElementOrderCommands.TOP)"><IconSendToBack class="btn-icon" /> 置顶</Button>
- <Button style="flex: 1" @click="orderElement(handleElement!, ElementOrderCommands.BOTTOM)"
- ><IconBringToFrontOne class="btn-icon" /> 置底</Button
- >
- </ButtonGroup>
- <ButtonGroup class="row">
- <Button style="flex: 1" @click="orderElement(handleElement!, ElementOrderCommands.UP)"><IconBringToFront class="btn-icon" /> 上移</Button>
- <Button style="flex: 1" @click="orderElement(handleElement!, ElementOrderCommands.DOWN)"><IconSentToBack class="btn-icon" /> 下移</Button>
- </ButtonGroup>
- <Divider />
- <div class="title">对齐:</div>
- <ButtonGroup class="row">
- <Button style="flex: 1" v-tooltip="'左对齐'" @click="alignElementToCanvas(ElementAlignCommands.LEFT)"><IconAlignLeft /></Button>
- <Button style="flex: 1" v-tooltip="'水平居中'" @click="alignElementToCanvas(ElementAlignCommands.HORIZONTAL)"><IconAlignVertically /></Button>
- <Button style="flex: 1" v-tooltip="'右对齐'" @click="alignElementToCanvas(ElementAlignCommands.RIGHT)"><IconAlignRight /></Button>
- </ButtonGroup>
- <ButtonGroup class="row">
- <Button style="flex: 1" v-tooltip="'上对齐'" @click="alignElementToCanvas(ElementAlignCommands.TOP)"><IconAlignTop /></Button>
- <Button style="flex: 1" v-tooltip="'垂直居中'" @click="alignElementToCanvas(ElementAlignCommands.VERTICAL)"><IconAlignHorizontally /></Button>
- <Button style="flex: 1" v-tooltip="'下对齐'" @click="alignElementToCanvas(ElementAlignCommands.BOTTOM)"><IconAlignBottom /></Button>
- </ButtonGroup>
- <Divider />
- <div class="row">
- <NumberInput :step="5" :value="left" @update:value="value => updateLeft(value)" style="width: 45%">
- <template #prefix> 水平: </template>
- </NumberInput>
- <div style="width: 10%"></div>
- <NumberInput :step="5" :value="top" @update:value="value => updateTop(value)" style="width: 45%">
- <template #prefix> 垂直: </template>
- </NumberInput>
- </div>
- <template v-if="handleElement!.type !== 'line' && !(handleElement!.type === 'elf' && handleElement!.subtype === 'elf-enjoy')">
- <div class="row">
- <NumberInput
- :min="minSize"
- :max="1920"
- :step="5"
- :disabled="isVerticalText"
- :value="width"
- @update:value="value => updateWidth(value)"
- style="width: 45%"
- >
- <template #prefix> 宽度: </template>
- </NumberInput>
- <template v-if="['image', 'shape'].includes(handleElement!.type) || ['elf-audio'].includes(handleElement!.subtype)">
- <IconLock style="width: 10%" class="icon-btn active" v-tooltip="'解除宽高比锁定'" @click="updateFixedRatio(false)" v-if="fixedRatio" />
- <IconUnlock style="width: 10%" class="icon-btn" v-tooltip="'宽高比锁定'" @click="updateFixedRatio(true)" v-else />
- </template>
- <div style="width: 10%" v-else></div>
- <NumberInput
- :min="minSize"
- :max="1080"
- :step="5"
- :disabled="isHorizontalText || handleElement!.type === 'table'"
- :value="height"
- @update:value="value => updateHeight(value)"
- style="width: 45%"
- >
- <template #prefix> 高度: </template>
- </NumberInput>
- </div>
- </template>
- <template v-if="!['line', 'elf'].includes(handleElement!.type)">
- <Divider />
- <div class="row">
- <NumberInput :min="-180" :max="180" :step="5" :value="rotate" @update:value="value => updateRotate(value)" style="width: 45%">
- <template #prefix> 旋转: </template>
- </NumberInput>
- <div style="width: 7%"></div>
- <div class="text-btn" @click="updateRotate45('-')" style="width: 24%"><IconRotate /> -45°</div>
- <div class="text-btn" @click="updateRotate45('+')" style="width: 24%"><IconRotate :style="{ transform: 'rotateY(180deg)' }" /> +45°</div>
- </div>
- </template>
- </div>
- </template>
- <script lang="ts" setup>
- import { computed, ref, watch } from "vue"
- import { round } from "lodash"
- import { storeToRefs } from "pinia"
- import { useMainStore, useSlidesStore } from "@/store"
- import type { PPTElement } from "@/types/slides"
- import { ElementAlignCommands, ElementOrderCommands } from "@/types/edit"
- import { MIN_SIZE } from "@/configs/element"
- import { SHAPE_PATH_FORMULAS } from "@/configs/shapes"
- import useOrderElement from "@/hooks/useOrderElement"
- import useAlignElementToCanvas from "@/hooks/useAlignElementToCanvas"
- import useHistorySnapshot from "@/hooks/useHistorySnapshot"
- import Divider from "@/components/Divider.vue"
- import Button from "@/components/Button.vue"
- import ButtonGroup from "@/components/ButtonGroup.vue"
- import NumberInput from "@/components/NumberInput.vue"
- const slidesStore = useSlidesStore()
- const { handleElement, handleElementId } = storeToRefs(useMainStore())
- const left = ref(0)
- const top = ref(0)
- const width = ref(0)
- const height = ref(0)
- const rotate = ref(0)
- const fixedRatio = ref(false)
- const minSize = computed(() => {
- if (!handleElement.value) return 20
- return MIN_SIZE[handleElement.value.type] || 20
- })
- const isHorizontalText = computed(() => {
- return handleElement.value?.type === "text" && !handleElement.value.vertical
- })
- const isVerticalText = computed(() => {
- return handleElement.value?.type === "text" && handleElement.value.vertical
- })
- watch(
- handleElement,
- () => {
- if (!handleElement.value) return
- left.value = round(handleElement.value.left, 1)
- top.value = round(handleElement.value.top, 1)
- fixedRatio.value = "fixedRatio" in handleElement.value && !!handleElement.value.fixedRatio
- if (handleElement.value.type !== "line") {
- width.value = round(handleElement.value.width, 1)
- height.value = round(handleElement.value.height, 1)
- rotate.value = "rotate" in handleElement.value && handleElement.value.rotate !== undefined ? round(handleElement.value.rotate, 1) : 0
- }
- },
- { deep: true, immediate: true }
- )
- const { orderElement } = useOrderElement()
- const { alignElementToCanvas } = useAlignElementToCanvas()
- const { addHistorySnapshot } = useHistorySnapshot()
- // 设置元素位置
- const updateLeft = (value: number) => {
- const props = { left: value }
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- const updateTop = (value: number) => {
- const props = { top: value }
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- // 设置元素宽度、高度、旋转角度
- // 对形状设置宽高时,需要检查是否需要更新形状路径
- const updateShapePathData = (width: number, height: number) => {
- if (handleElement.value && handleElement.value.type === "shape" && "pathFormula" in handleElement.value && handleElement.value.pathFormula) {
- const pathFormula = SHAPE_PATH_FORMULAS[handleElement.value.pathFormula]
- let path = ""
- if ("editable" in pathFormula && pathFormula.editable) path = pathFormula.formula(width, height, handleElement.value.keypoints!)
- else path = pathFormula.formula(width, height)
- return {
- viewBox: [width, height],
- path
- }
- }
- return null
- }
- const updateWidth = (value: number) => {
- if (!handleElement.value) return
- if (handleElement.value.type === "line" || isVerticalText.value) return
- let h = height.value
- if (fixedRatio.value) {
- const ratio = width.value / height.value
- h = value / ratio < minSize.value ? minSize.value : value / ratio
- }
- let props: Partial<PPTElement> = { width: value, height: h }
- const shapePathData = updateShapePathData(value, h)
- if (shapePathData) {
- props = {
- width: value,
- height: h,
- ...shapePathData
- }
- }
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- const updateHeight = (value: number) => {
- if (!handleElement.value) return
- if (handleElement.value.type === "line" || handleElement.value.type === "table" || isHorizontalText.value) return
- let w = width.value
- if (fixedRatio.value) {
- const ratio = width.value / height.value
- w = value * ratio < minSize.value ? minSize.value : value * ratio
- }
- let props: Partial<PPTElement> = { width: w, height: value }
- const shapePathData = updateShapePathData(w, value)
- if (shapePathData) {
- props = {
- width: w,
- height: value,
- ...shapePathData
- }
- }
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- const updateRotate = (value: number) => {
- const props = { rotate: value }
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- // 固定元素的宽高比
- const updateFixedRatio = (value: boolean) => {
- const props = { fixedRatio: value }
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- // 将元素旋转45度(顺时针或逆时针)
- const updateRotate45 = (command: "+" | "-") => {
- let _rotate = Math.floor(rotate.value / 45) * 45
- if (command === "+") _rotate = _rotate + 45
- else if (command === "-") _rotate = _rotate - 45
- if (_rotate < -180) _rotate = -180
- if (_rotate > 180) _rotate = 180
- const props = { rotate: _rotate }
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- </script>
- <style lang="scss" scoped>
- .row {
- width: 100%;
- display: flex;
- align-items: center;
- margin-bottom: 10px;
- }
- .title {
- margin-bottom: 10px;
- }
- .label {
- text-align: center;
- }
- .btn-icon {
- margin-right: 3px;
- }
- .icon-btn {
- cursor: pointer;
- &.active {
- color: $themeColor;
- }
- }
- .text-btn {
- height: 30px;
- line-height: 30px;
- text-align: center;
- cursor: pointer;
- &:hover {
- background-color: #efefef;
- border-radius: $borderRadius;
- }
- }
- </style>
|