mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 多选状态批量修改样式
This commit is contained in:
parent
d6ba32c190
commit
82d56c382d
@ -20,7 +20,7 @@
|
||||
<MenuItem @click="createSlide()">添加页面</MenuItem>
|
||||
<MenuItem @click="deleteSlide()">删除页面</MenuItem>
|
||||
<MenuItem @click="toggleGridLines()">{{ showGridLines ? '关闭网格线' : '打开网格线' }}</MenuItem>
|
||||
<MenuItem @click="toggleRuler()">{{ toggleRuler ? '关闭标尺' : '打开标尺' }}</MenuItem>
|
||||
<MenuItem @click="toggleRuler()">{{ showRuler ? '关闭标尺' : '打开标尺' }}</MenuItem>
|
||||
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
@ -126,6 +126,7 @@ export default defineComponent({
|
||||
redo,
|
||||
undo,
|
||||
showGridLines,
|
||||
showRuler,
|
||||
hotkeyDrawerVisible,
|
||||
exportImgDialogVisible,
|
||||
exporting,
|
||||
|
248
src/views/Editor/Toolbar/ElementStylePanel/MultiStylePanel.vue
Normal file
248
src/views/Editor/Toolbar/ElementStylePanel/MultiStylePanel.vue
Normal file
@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="multi-style-panel">
|
||||
<div class="row">
|
||||
<div style="flex: 2;">填充颜色:</div>
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<ColorPicker
|
||||
:modelValue="fill"
|
||||
@update:modelValue="value => updateFill(value)"
|
||||
/>
|
||||
</template>
|
||||
<ColorButton :color="fill" style="flex: 3;" />
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div class="row">
|
||||
<div style="flex: 2;">边框样式:</div>
|
||||
<Select
|
||||
style="flex: 3;"
|
||||
:value="outline.style"
|
||||
@change="value => updateOutline({ style: value })"
|
||||
>
|
||||
<SelectOption value="solid">实线边框</SelectOption>
|
||||
<SelectOption value="dashed">虚线边框</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="flex: 2;">边框颜色:</div>
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<ColorPicker
|
||||
:modelValue="outline.color"
|
||||
@update:modelValue="value => updateOutline({ color: value })"
|
||||
/>
|
||||
</template>
|
||||
<ColorButton :color="outline.color" style="flex: 3;" />
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="flex: 2;">边框粗细:</div>
|
||||
<InputNumber
|
||||
:value="outline.width"
|
||||
@change="value => updateOutline({ width: value })"
|
||||
style="flex: 3;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<InputGroup compact class="row">
|
||||
<Select
|
||||
style="flex: 3;"
|
||||
:value="richTextAttrs.fontname"
|
||||
@change="value => updateFontStyle('fontname', value)"
|
||||
>
|
||||
<template #suffixIcon><IconFontSize /></template>
|
||||
<SelectOptGroup label="系统字体">
|
||||
<SelectOption v-for="font in availableFonts" :key="font.value" :value="font.value">
|
||||
<span :style="{ fontFamily: font.value }">{{font.label}}</span>
|
||||
</SelectOption>
|
||||
</SelectOptGroup>
|
||||
<SelectOptGroup label="在线字体">
|
||||
<SelectOption v-for="font in webFonts" :key="font.value" :value="font.value">
|
||||
<span>{{font.label}}</span>
|
||||
</SelectOption>
|
||||
</SelectOptGroup>
|
||||
</Select>
|
||||
<Select
|
||||
style="flex: 2;"
|
||||
:value="richTextAttrs.fontsize"
|
||||
@change="value => updateFontStyle('fontsize', value)"
|
||||
>
|
||||
<template #suffixIcon><IconAddText /></template>
|
||||
<SelectOption v-for="fontsize in fontSizeOptions" :key="fontsize" :value="fontsize">
|
||||
{{fontsize}}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
<ButtonGroup class="row">
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<ColorPicker
|
||||
:modelValue="richTextAttrs.color"
|
||||
@update:modelValue="value => updateFontStyle('color', value)"
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||
<Button class="text-color-btn" style="flex: 1;">
|
||||
<IconText />
|
||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
<RadioGroup
|
||||
class="row"
|
||||
button-style="solid"
|
||||
:value="richTextAttrs.align"
|
||||
@change="e => updateFontStyle('align', e.target.value)"
|
||||
>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="左对齐">
|
||||
<RadioButton value="left" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="居中">
|
||||
<RadioButton value="center" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="右对齐">
|
||||
<RadioButton value="right" style="flex: 1;"><IconAlignTextRight /></RadioButton>
|
||||
</Tooltip>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMainStore, useSlidesStore } from '@/store'
|
||||
import { PPTElement, PPTElementOutline, TableCell } from '@/types/slides'
|
||||
import emitter, { EmitterEvents } from '@/utils/emitter'
|
||||
import { WEB_FONTS } from '@/configs/font'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import ColorButton from '../common/ColorButton.vue'
|
||||
|
||||
const webFonts = WEB_FONTS
|
||||
|
||||
export default defineComponent({
|
||||
name: 'multi-style-panel',
|
||||
components: {
|
||||
ColorButton,
|
||||
},
|
||||
setup() {
|
||||
const slidesStore = useSlidesStore()
|
||||
const { richTextAttrs, availableFonts, activeElementList } = storeToRefs(useMainStore())
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
const updateElement = (id: string, props: Partial<PPTElement>) => {
|
||||
slidesStore.updateElement({ id, props })
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
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 fill = ref('#000')
|
||||
const outline = ref<PPTElementOutline>({
|
||||
width: 0,
|
||||
color: '#000',
|
||||
style: 'solid',
|
||||
})
|
||||
|
||||
// 批量修改填充色(表格元素为单元格填充、音频元素为图标颜色)
|
||||
const updateFill = (value: string) => {
|
||||
for (const el of activeElementList.value) {
|
||||
if (
|
||||
el.type === 'text' ||
|
||||
el.type === 'shape' ||
|
||||
el.type === 'chart'
|
||||
) updateElement(el.id, { fill: value })
|
||||
|
||||
if (el.type === 'table') {
|
||||
const data: TableCell[][] = JSON.parse(JSON.stringify(el.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: value }
|
||||
}
|
||||
}
|
||||
updateElement(el.id, { data })
|
||||
}
|
||||
|
||||
if (el.type === 'audio') updateElement(el.id, { color: value })
|
||||
}
|
||||
fill.value = value
|
||||
}
|
||||
|
||||
// 修改边框/线条样式
|
||||
const updateOutline = (outlineProps: Partial<PPTElementOutline>) => {
|
||||
|
||||
for (const el of activeElementList.value) {
|
||||
if (
|
||||
el.type === 'text' ||
|
||||
el.type === 'image' ||
|
||||
el.type === 'shape' ||
|
||||
el.type === 'table' ||
|
||||
el.type === 'chart'
|
||||
) {
|
||||
const outline = el.outline || { width: 2, color: '#000', style: 'solid' }
|
||||
const props = { outline: { ...outline, ...outlineProps } }
|
||||
updateElement(el.id, props)
|
||||
}
|
||||
|
||||
if (el.type === 'line') updateElement(el.id, outlineProps)
|
||||
}
|
||||
outline.value = { ...outline.value, ...outlineProps }
|
||||
}
|
||||
|
||||
// 修改文字样式
|
||||
const updateFontStyle = (command: string, value: string) => {
|
||||
for (const el of activeElementList.value) {
|
||||
if (el.type === 'text' || (el.type === 'shape' && el.text?.content)) {
|
||||
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { target: el.id, action: { command, value } })
|
||||
}
|
||||
if (el.type === 'table') {
|
||||
const data: TableCell[][] = JSON.parse(JSON.stringify(el.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, [command]: value }
|
||||
}
|
||||
}
|
||||
updateElement(el.id, { data })
|
||||
}
|
||||
if (el.type === 'latex' && command === 'color') {
|
||||
updateElement(el.id, { color: value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
webFonts,
|
||||
richTextAttrs,
|
||||
availableFonts,
|
||||
fontSizeOptions,
|
||||
fill,
|
||||
outline,
|
||||
updateFill,
|
||||
updateOutline,
|
||||
updateFontStyle,
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="element-style-panel">
|
||||
<div v-if="!currentPanelComponent">
|
||||
请先选中要编辑的元素
|
||||
</div>
|
||||
<component v-if="handleElement" :is="currentPanelComponent"></component>
|
||||
<component :is="currentPanelComponent"></component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -22,27 +19,34 @@ import TableStylePanel from './TableStylePanel.vue'
|
||||
import LatexStylePanel from './LatexStylePanel.vue'
|
||||
import VideoStylePanel from './VideoStylePanel.vue'
|
||||
import AudioStylePanel from './AudioStylePanel.vue'
|
||||
import MultiStylePanel from './MultiStylePanel.vue'
|
||||
|
||||
const panelMap = {
|
||||
[ElementTypes.TEXT]: TextStylePanel,
|
||||
[ElementTypes.IMAGE]: ImageStylePanel,
|
||||
[ElementTypes.SHAPE]: ShapeStylePanel,
|
||||
[ElementTypes.LINE]: LineStylePanel,
|
||||
[ElementTypes.CHART]: ChartStylePanel,
|
||||
[ElementTypes.TABLE]: TableStylePanel,
|
||||
[ElementTypes.LATEX]: LatexStylePanel,
|
||||
[ElementTypes.VIDEO]: VideoStylePanel,
|
||||
[ElementTypes.AUDIO]: AudioStylePanel,
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'element-style-panel',
|
||||
setup() {
|
||||
const { handleElement } = storeToRefs(useMainStore())
|
||||
const { activeElementIdList, activeElementList, handleElement, activeGroupElementId } = storeToRefs(useMainStore())
|
||||
|
||||
const currentPanelComponent = computed(() => {
|
||||
if (!handleElement.value) return null
|
||||
|
||||
const panelMap = {
|
||||
[ElementTypes.TEXT]: TextStylePanel,
|
||||
[ElementTypes.IMAGE]: ImageStylePanel,
|
||||
[ElementTypes.SHAPE]: ShapeStylePanel,
|
||||
[ElementTypes.LINE]: LineStylePanel,
|
||||
[ElementTypes.CHART]: ChartStylePanel,
|
||||
[ElementTypes.TABLE]: TableStylePanel,
|
||||
[ElementTypes.LATEX]: LatexStylePanel,
|
||||
[ElementTypes.VIDEO]: VideoStylePanel,
|
||||
[ElementTypes.AUDIO]: AudioStylePanel,
|
||||
if (activeElementIdList.value.length > 1) {
|
||||
if (!activeGroupElementId.value) return MultiStylePanel
|
||||
|
||||
const activeGroupElement = activeElementList.value.find(item => item.id === activeGroupElementId.value)
|
||||
return activeGroupElement ? (panelMap[activeGroupElement.type] || null) : null
|
||||
}
|
||||
return panelMap[handleElement.value.type] || null
|
||||
|
||||
return handleElement.value ? (panelMap[handleElement.value.type] || null) : null
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -61,8 +61,8 @@ export default defineComponent({
|
||||
{ label: '动画', value: ToolbarStates.EL_ANIMATION },
|
||||
]
|
||||
const multiSelectTabs = [
|
||||
{ label: '位置', value: ToolbarStates.MULTI_POSITION },
|
||||
{ label: '样式', value: ToolbarStates.EL_STYLE },
|
||||
{ label: '位置', value: ToolbarStates.MULTI_POSITION },
|
||||
]
|
||||
|
||||
const setToolbarState = (value: ToolbarStates) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user