This commit is contained in:
pipipi-pikachu 2020-12-24 16:54:52 +08:00
parent fa1e7f13d3
commit b715809a34
43 changed files with 584 additions and 498 deletions

View File

@ -1,14 +1,5 @@
const DEFAULT_COLOR = '#41464b'
export enum ElementTypes {
TEXT = '文本',
IMAGE = '图片',
SHAPE = '形状',
LINE = '线条',
CHART = '图表',
TABLE = '表格',
}
export const DEFAULT_TEXT = {
left: 0,
top: 0,
@ -23,17 +14,17 @@ export const DEFAULT_TEXT = {
export const DEFAULT_IMAGE = {
left: 0,
top: 0,
lockRatio: true,
fixedRatio: true,
}
export const DEFAULT_SHAPE = {
fill: DEFAULT_COLOR,
lockRatio: false,
fixedRatio: false,
}
export const DEFAULT_LINE = {
style: 'solid',
marker: ['', ''],
points: ['', ''],
width: 4,
color: DEFAULT_COLOR,
}
@ -48,8 +39,9 @@ export const DEFAULT_CHART = {
export const DEFAULT_TABLE = {
left: 0,
top: 0,
isLock: false,
borderStyle: 'solid',
borderWidth: 2,
borderColor: DEFAULT_COLOR,
outline: {
width: 2,
style: 'solid',
color: DEFAULT_COLOR
},
}

View File

@ -1,12 +1,12 @@
export const LINES = [
{ path: 'M0,0 L20,20', style: 'solid', marker: ['', ''] },
{ path: 'M0,0 L20,20', style: 'solid', marker: ['', 'arrow'] },
{ path: 'M0,0 L20,20', style: 'solid', marker: ['arrow', 'arrow'] },
{ path: 'M0,0 L20,20', style: 'solid', marker: ['', 'cusp'] },
{ path: 'M0,0 L20,20', style: 'solid', marker: ['cusp', 'cusp'] },
{ path: 'M0,0 L20,20', style: 'solid', marker: ['', 'dot'] },
{ path: 'M0,0 L20,20', style: 'solid', marker: ['dot', 'dot'] },
{ path: 'M0,0 L20,20', style: 'dashed', marker: ['', ''] },
{ path: 'M0,0 L20,20', style: 'dashed', marker: ['', 'arrow'] },
{ path: 'M0,0 L20,20', style: 'dashed', marker: ['arrow', 'arrow'] },
{ path: 'M0,0 L20,20', style: 'solid', points: ['', ''] },
{ path: 'M0,0 L20,20', style: 'solid', points: ['', 'arrow'] },
{ path: 'M0,0 L20,20', style: 'solid', points: ['arrow', 'arrow'] },
{ path: 'M0,0 L20,20', style: 'solid', points: ['', 'cusp'] },
{ path: 'M0,0 L20,20', style: 'solid', points: ['cusp', 'cusp'] },
{ path: 'M0,0 L20,20', style: 'solid', points: ['', 'dot'] },
{ path: 'M0,0 L20,20', style: 'solid', points: ['dot', 'dot'] },
{ path: 'M0,0 L20,20', style: 'dashed', points: ['', ''] },
{ path: 'M0,0 L20,20', style: 'dashed', points: ['', 'arrow'] },
{ path: 'M0,0 L20,20', style: 'dashed', points: ['arrow', 'arrow'] },
]

View File

@ -20,7 +20,7 @@ export default () => {
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
for(const element of newElementList) {
if(!activeElementIdList.value.includes(element.elId)) continue
if(!activeElementIdList.value.includes(element.id)) continue
if(command === ElementAlignCommands.TOP) {
const offsetY = minY - 0

View File

@ -22,16 +22,16 @@ export default () => {
const combineElementList: PPTElement[] = []
for(const element of newElementList) {
if(activeElementIdList.value.includes(element.elId)) {
if(activeElementIdList.value.includes(element.id)) {
element.groupId = groupId
combineElementList.push(element)
}
}
// 注意,组合元素的层级应该是连续的,所以需要获取该组元素中最顶层的元素,将组内其他成员从原位置移动到最顶层的元素的下面
const combineElementMaxIndex = newElementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
const combineElementIdList = combineElementList.map(_element => _element.elId)
newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.elId))
const combineElementMaxIndex = newElementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id)
const combineElementIdList = combineElementList.map(_element => _element.id)
newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.id))
const insertIndex = combineElementMaxIndex - combineElementList.length + 1
newElementList.splice(insertIndex, 0, ...combineElementList)
@ -48,7 +48,7 @@ export default () => {
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
for(const element of newElementList) {
if(activeElementIdList.value.includes(element.elId) && element.groupId) delete element.groupId
if(activeElementIdList.value.includes(element.id) && element.groupId) delete element.groupId
}
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
addHistorySnapshot()

View File

@ -35,12 +35,12 @@ export default () => {
const createElement = (element: PPTElement) => {
store.commit(MutationTypes.ADD_ELEMENT, element)
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [element.elId])
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [element.id])
addHistorySnapshot()
}
const createImageElement = (imgUrl: string) => {
getImageSize(imgUrl).then(({ width, height }) => {
const createImageElement = (src: string) => {
getImageSize(src).then(({ width, height }) => {
const scale = width / height
if(scale < VIEWPORT_ASPECT_RATIO && width > VIEWPORT_SIZE) {
@ -55,8 +55,8 @@ export default () => {
createElement({
...DEFAULT_IMAGE,
type: 'image',
elId: createRandomCode(),
imgUrl,
id: createRandomCode(),
src,
width,
height,
})
@ -67,7 +67,7 @@ export default () => {
createElement({
...DEFAULT_CHART,
type: 'chart',
elId: createRandomCode(),
id: createRandomCode(),
chartType,
data,
})
@ -87,7 +87,7 @@ export default () => {
createElement({
...DEFAULT_TABLE,
type: 'table',
elId: createRandomCode(),
id: createRandomCode(),
width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH,
height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH,
colSizes,
@ -101,7 +101,7 @@ export default () => {
createElement({
...DEFAULT_TEXT,
type: 'text',
elId: createRandomCode(),
id: createRandomCode(),
left,
top,
width,
@ -114,7 +114,7 @@ export default () => {
createElement({
...DEFAULT_SHAPE,
type: 'shape',
elId: createRandomCode(),
id: createRandomCode(),
left,
top,
width,
@ -123,17 +123,17 @@ export default () => {
})
}
const createLineElement = (position: LineElementPosition, marker: [string, string], lineType: string) => {
const createLineElement = (position: LineElementPosition, points: [string, string], lineType: string) => {
const { left, top, start, end } = position
createElement({
...DEFAULT_LINE,
type: 'line',
elId: createRandomCode(),
id: createRandomCode(),
left,
top,
start,
end,
marker,
points,
lineType,
})
}

View File

@ -13,7 +13,7 @@ export default () => {
const deleteElement = () => {
if(!activeElementIdList.value.length) return
const newElementList = currentSlide.value.elements.filter(el => !activeElementIdList.value.includes(el.elId))
const newElementList = currentSlide.value.elements.filter(el => !activeElementIdList.value.includes(el.id))
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
addHistorySnapshot()

View File

@ -15,7 +15,7 @@ export default () => {
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
for(const element of newElementList) {
if(activeElementIdList.value.includes(element.elId)) element.isLock = true
if(activeElementIdList.value.includes(element.id)) element.lock = true
}
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
addHistorySnapshot()
@ -26,14 +26,14 @@ export default () => {
if(handleElement.groupId) {
for(const element of newElementList) {
if(element.groupId === handleElement.groupId) element.isLock = false
if(element.groupId === handleElement.groupId) element.lock = false
}
return newElementList
}
for(const element of newElementList) {
if(element.elId === handleElement.elId) {
element.isLock = false
if(element.id === handleElement.id) {
element.lock = false
break
}
}

View File

@ -14,7 +14,7 @@ export default () => {
const moveElement = (command: string) => {
const newElementList = currentSlide.value.elements.map(el => {
if(activeElementIdList.value.includes(el.elId)) {
if(activeElementIdList.value.includes(el.id)) {
let { left, top } = el
switch(command) {
case KEYS.LEFT:

View File

@ -13,8 +13,8 @@ export default () => {
// 获取组合元素层级范围(组合成员中的最大层级和最小层级)
const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
const minIndex = elementList.findIndex(_element => _element.elId === combineElementList[0].elId)
const maxIndex = elementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
const minIndex = elementList.findIndex(_element => _element.id === combineElementList[0].id)
const maxIndex = elementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id)
return { minIndex, maxIndex }
}
@ -53,7 +53,7 @@ export default () => {
else {
// 元素在元素列表中的层级
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
const elementIndex = elementList.findIndex(item => item.id === element.id)
// 无法移动(已经处在顶层)
if(elementIndex === elementList.length - 1) return null
@ -96,7 +96,7 @@ export default () => {
}
else {
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
const elementIndex = elementList.findIndex(item => item.id === element.id)
if(elementIndex === 0) return null
const prevElement = copyOfElementList[elementIndex - 1]
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
@ -133,7 +133,7 @@ export default () => {
else {
// 元素在元素列表中的层级
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
const elementIndex = elementList.findIndex(item => item.id === element.id)
// 无法移动(已经处在顶层)
if(elementIndex === elementList.length - 1) return null
@ -159,7 +159,7 @@ export default () => {
}
else {
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
const elementIndex = elementList.findIndex(item => item.id === element.id)
if(elementIndex === 0) return null
copyOfElementList.splice(elementIndex, 1)
copyOfElementList.unshift(element)

View File

@ -25,14 +25,14 @@ export default () => {
if(groupId && !groupIdMap[groupId]) {
groupIdMap[groupId] = createRandomCode()
}
elIdMap[element.elId] = createRandomCode()
elIdMap[element.id] = createRandomCode()
}
const currentSlideElementIdList = currentSlide.value.elements.map(el => el.elId)
const currentSlideElementIdList = currentSlide.value.elements.map(el => el.id)
for(const element of elements) {
const inCurrentSlide = currentSlideElementIdList.includes(element.elId)
const inCurrentSlide = currentSlideElementIdList.includes(element.id)
element.elId = elIdMap[element.elId]
element.id = elIdMap[element.id]
if(inCurrentSlide) {
element.left = element.left + 10

View File

@ -8,8 +8,8 @@ export default () => {
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
const selectAllElement = () => {
const unlockedElements = currentSlide.value.elements.filter(el => !el.isLock)
const newActiveElementIdList = unlockedElements.map(el => el.elId)
const unlockedElements = currentSlide.value.elements.filter(el => !el.lock)
const newActiveElementIdList = unlockedElements.map(el => el.id)
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
}

View File

@ -1,10 +1,11 @@
import { Ref, computed } from 'vue'
import { SlideBackground } from '@/types/slides'
export default (background: Ref<[string, string] | undefined>) => {
export default (background: Ref<SlideBackground | undefined>) => {
const backgroundStyle = computed(() => {
if(!background.value) return { backgroundColor: '#fff' }
const [type, value] = background.value
const { type, value } = background.value
if(type === 'solid') return { backgroundColor: value }
else if(type === 'image') return { backgroundImage: `url(${value}` }

View File

@ -3,66 +3,71 @@ import { Slide } from '@/types/slides'
export const slides: Slide[] = [
{
id: 'xxx1',
background: ['solid', '#fff'],
background: {
type: 'solid',
value: '#fff',
},
elements: [
{
elId: 'xxx1',
id: 'xxx1',
type: 'text',
left: 190,
top: 50,
width: 320,
height: 104,
rotate: 0,
borderStyle: 'solid',
borderWidth: 4,
borderColor: '#5b7d89',
fill: 'rgba(220,220,220,0.8)',
shadow: '1px 1px 3px rgba(10,10,10,.5)',
fill: 'rgba(220, 220, 220, 0.8)',
shadow: {
h: 1,
v: 1,
blur: 3,
color: 'rgba(10, 10, 10, .5)'
},
opacity: 1,
lineHeight: 1.5,
segmentSpacing: 10,
isLock: false,
lock: false,
content: '<div style=\'text-align: center;\'><span style=\'font-size: 28px;\'><span style=\'color: rgb(232, 107, 153); font-weight: bold;\'>一段测试文字</span>,字号固定为<span style=\'font-weight: bold; font-style: italic; text-decoration-line: underline;\'>28px</span></span></div>',
},
{
elId: 'xxx3',
id: 'xxx3',
type: 'image',
left: 80,
top: 250,
width: 180,
height: 180,
rotate: 0,
borderStyle: 'solid',
borderWidth: 4,
borderColor: 'rgba(10,10,10,1)',
filter: '',
outline: {
width: 4,
style: 'solid',
color: '#333'
},
clip: {
range: [[30, 0], [100, 70]],
shape: 'ellipse'
},
lockRatio: false,
isLock: false,
imgUrl: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/1573622467064v2-7aa3ce420052983d91c6d01b47a7441d_hd.jpg',
fixedRatio: false,
lock: false,
src: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/1573622467064v2-7aa3ce420052983d91c6d01b47a7441d_hd.jpg',
},
{
elId: 'xxx2',
id: 'xxx2',
type: 'image',
left: 750,
top: 320,
width: 150,
height: 150,
rotate: 0,
borderStyle: 'solid',
borderWidth: 6,
borderColor: 'rgba(10,10,10,1)',
filter: '',
outline: {
width: 6,
style: 'solid',
color: '#333'
},
clip: {
range: [[0, 0], [100, 100]],
shape: 'roundRect'
},
lockRatio: true,
isLock: false,
imgUrl: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/62d9adb3-e7a6-4dc4-a352-095cffb49f08/b1be1a2f-f893-47d3-a8a3-eac7d04d395f/1596159381259v2-b2c69096d25ae16bf6ca09e30add3e65_hd.jpg',
fixedRatio: true,
lock: false,
src: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/62d9adb3-e7a6-4dc4-a352-095cffb49f08/b1be1a2f-f893-47d3-a8a3-eac7d04d395f/1596159381259v2-b2c69096d25ae16bf6ca09e30add3e65_hd.jpg',
},
],
},
@ -70,7 +75,7 @@ export const slides: Slide[] = [
id: 'sajd172',
elements: [
{
elId: 'yyx1',
id: 'yyx1',
type: 'text',
left: 590,
top: 90,
@ -78,9 +83,7 @@ export const slides: Slide[] = [
height: 188,
rotate: 0,
opacity: 1,
lineHeight: 1.5,
segmentSpacing: 10,
isLock: false,
lock: false,
content: '<div>😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒</div>',
},
],

View File

@ -13,20 +13,20 @@ export const getters: GetterTree<State, State> = {
if(!animations) return null
const els = currentSlide.elements
const elIds = els.map(el => el.elId)
const elIds = els.map(el => el.id)
return animations.filter(animation => elIds.includes(animation.elId))
},
activeElementList(state) {
const currentSlide = state.slides[state.slideIndex]
if(!currentSlide || !currentSlide.elements) return []
return currentSlide.elements.filter(element => state.activeElementIdList.includes(element.elId))
return currentSlide.elements.filter(element => state.activeElementIdList.includes(element.id))
},
handleElement(state) {
const currentSlide = state.slides[state.slideIndex]
if(!currentSlide || !currentSlide.elements) return null
return currentSlide.elements.find(element => state.handleElementId === element.elId) || null
return currentSlide.elements.find(element => state.handleElementId === element.id) || null
},
canUndo(state) {

View File

@ -6,7 +6,7 @@ import { FONT_NAMES } from '@/configs/fontName'
import { isSupportFontFamily } from '@/utils/fontFamily'
interface UpdateElementData {
elId: string | string[];
id: string | string[];
props: Partial<PPTElement>;
}
@ -91,13 +91,13 @@ export const mutations: MutationTree<State> = {
},
[MutationTypes.UPDATE_ELEMENT](state, data: UpdateElementData) {
const { elId, props } = data
const elIdList = typeof elId === 'string' ? [elId] : elId
const { id, props } = data
const elIdList = typeof id === 'string' ? [id] : id
const slideIndex = state.slideIndex
const slide = state.slides[slideIndex]
const elements = slide.elements.map(el => {
return elIdList.includes(el.elId) ? { ...el, ...props } : el
return elIdList.includes(el.id) ? { ...el, ...props } : el
})
state.slides[slideIndex].elements = (elements as PPTElement[])
},

View File

@ -1,4 +1,9 @@
export type ElementType = 'text' | 'image' | 'shape' | 'line' | 'chart' | 'table'
export interface PPTElementShadow {
h: number;
v: number;
blur: number;
color: string;
}
export enum ElementTypes {
TEXT = 'text',
@ -10,58 +15,68 @@ export enum ElementTypes {
}
export interface PPTElementBaseProps {
elId: string;
id: string;
left: number;
top: number;
isLock?: boolean;
lock?: boolean;
groupId?: string;
}
export interface PPTElementSizeProps {
export interface PPTElementOutline {
style?: 'dashed' | 'solid';
width?: number;
color?: string;
}
export interface PPTTextElement extends PPTElementBaseProps {
type: 'text';
width: number;
height: number;
}
export interface PPTElementBorderProps {
borderStyle?: string;
borderWidth?: number;
borderColor?: string;
}
export interface PPTTextElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
type: 'text';
content: string;
rotate?: number;
outline?: PPTElementOutline;
fill?: string;
opacity?: number;
lineHeight?: number;
segmentSpacing?: number;
letterSpacing?: number;
shadow?: string;
shadow?: PPTElementShadow;
}
export interface PPTImageElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
export interface ImageElementFilters {
'blur': string;
'brightness': string;
'contrast': string;
'grayscale': string;
'saturate': string;
'hue-rotate': string;
'opacity': string;
}
export interface PPTImageElement extends PPTElementBaseProps {
type: 'image';
lockRatio: boolean;
imgUrl: string;
width: number;
height: number;
fixedRatio: boolean;
src: string;
rotate?: number;
filter?: string;
outline?: PPTElementOutline;
filters?: ImageElementFilters;
clip?: {
range: [[number, number], [number, number]];
shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
};
flip?: { x?: number; y?: number };
shadow?: string;
shadow?: PPTElementShadow;
}
export interface PPTShapeElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
export interface PPTShapeElement extends PPTElementBaseProps {
type: 'shape';
width: number;
height: number;
svgCode: string;
lockRatio: boolean;
fixedRatio: boolean;
fill: string;
rotate?: number;
outline?: PPTElementOutline;
opacity?: number;
shadow?: string;
shadow?: PPTElementShadow;
}
export interface PPTLineElement extends PPTElementBaseProps {
@ -71,14 +86,17 @@ export interface PPTLineElement extends PPTElementBaseProps {
width: number;
style: string;
color: string;
marker: [string, string];
points: [string, string];
lineType: string;
}
export interface PPTChartElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
export interface PPTChartElement extends PPTElementBaseProps {
type: 'chart';
width: number;
height: number;
chartType: string;
data: string;
outline?: PPTElementOutline;
theme?: string;
}
@ -88,8 +106,10 @@ export interface TableElementCell {
content: string;
bgColor: string;
}
export interface PPTTableElement extends PPTElementBaseProps, PPTElementSizeProps {
export interface PPTTableElement extends PPTElementBaseProps {
type: 'table';
width: number;
height: number;
borderTheme?: string;
theme?: string;
rowSizes: number[];
@ -97,12 +117,7 @@ export interface PPTTableElement extends PPTElementBaseProps, PPTElementSizeProp
data: TableElementCell[][];
}
export type PPTElement = PPTTextElement |
PPTImageElement |
PPTShapeElement |
PPTLineElement |
PPTChartElement |
PPTTableElement
export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement
export interface PPTAnimation {
elId: string;
@ -110,9 +125,14 @@ export interface PPTAnimation {
duration: number;
}
export interface SlideBackground {
type: 'solid' | 'image';
value: string;
}
export interface Slide {
id: string;
elements: PPTElement[];
background?: [string, string];
background?: SlideBackground;
animations?: PPTAnimation[];
}

View File

@ -1,4 +1,4 @@
import { PPTElement } from '@/types/slides'
import { ElementTypes, PPTElement } from '@/types/slides'
// 获取矩形旋转后在画布中的位置范围
interface RotatedElementData {
@ -46,7 +46,7 @@ export const getRectRotatedRange = (element: RotatedElementData) => {
export const getElementRange = (element: PPTElement) => {
let minX, maxX, minY, maxY
if(element.type === 'line') {
if(element.type === ElementTypes.LINE) {
minX = element.left
maxX = element.left + Math.max(element.start[0], element.end[0])
minY = element.top

View File

@ -4,10 +4,10 @@ interface ImageSize {
}
// 获取图片的原始宽高
export const getImageSize = (imgUrl: string): Promise<ImageSize> => {
export const getImageSize = (src: string): Promise<ImageSize> => {
return new Promise(resolve => {
const img = document.createElement('img')
img.src = imgUrl
img.src = src
img.style.opacity = '0'
document.body.appendChild(img)

View File

@ -52,7 +52,7 @@ export default defineComponent({
const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList)
const canvasScale = computed(() => store.state.canvasScale)
const localActiveElementList = computed(() => props.elementList.filter(el => activeElementIdList.value.includes(el.elId)))
const localActiveElementList = computed(() => props.elementList.filter(el => activeElementIdList.value.includes(el.id)))
const range = reactive({
minX: 0,

View File

@ -14,6 +14,7 @@
import { Ref, computed, defineComponent } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store'
import { SlideBackground } from '@/types/slides'
import GridLines from './GridLines.vue'
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
@ -25,7 +26,7 @@ export default defineComponent({
setup() {
const store = useStore<State>()
const showGridLines = computed(() => store.state.showGridLines)
const background: Ref<[string, string] | undefined> = computed(() => store.getters.currentSlide.background)
const background: Ref<SlideBackground | undefined> = computed(() => store.getters.currentSlide.background)
const { backgroundStyle } = useSlideBackgroundStyle(background)

View File

@ -19,7 +19,7 @@ export default (
const { addHistorySnapshot } = useHistorySnapshot()
const dragElement = (e: MouseEvent, element: PPTElement) => {
if(!activeElementIdList.value.includes(element.elId)) return
if(!activeElementIdList.value.includes(element.id)) return
let isMouseDown = true
// 可视范围宽高,用于边缘对齐吸附
@ -27,7 +27,7 @@ export default (
const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.elId))
const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.id))
const sorptionRange = 3
const elOriginLeft = element.left
@ -40,7 +40,7 @@ export default (
let isMisoperation: boolean | null = null
const isActiveGroupElement = element.elId === activeGroupElementId.value
const isActiveGroupElement = element.id === activeGroupElementId.value
// 收集对齐参考线
// 包括页面内出被操作元素以外的所有元素在页面内水平和垂直方向的范围和中心位置、页面边界和水平和垂直的中心位置
@ -50,8 +50,8 @@ export default (
// 元素在页面内水平和垂直方向的范围和中心位置(需要特殊计算线条和被旋转的元素)
for(const el of elementList.value) {
if(el.type === ElementTypes.LINE) continue
if(isActiveGroupElement && el.elId === element.elId) continue
if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) continue
if(isActiveGroupElement && el.id === element.id) continue
if(!isActiveGroupElement && activeElementIdList.value.includes(el.id)) continue
let left, top, width, height
if('rotate' in el && el.rotate) {
@ -145,7 +145,7 @@ export default (
targetMinY = yRange[0]
targetMaxY = yRange[1]
}
else if(element.type === 'line') {
else if(element.type === ElementTypes.LINE) {
targetMinX = targetLeft
targetMaxX = targetLeft + Math.max(element.start[0], element.end[0])
targetMinY = targetTop
@ -179,7 +179,7 @@ export default (
rightValues.push(xRange[1])
bottomValues.push(yRange[1])
}
else if(element.type === 'line') {
else if(element.type === ElementTypes.LINE) {
leftValues.push(left)
topValues.push(top)
rightValues.push(left + Math.max(element.start[0], element.end[0]))
@ -265,19 +265,19 @@ export default (
// 非多选,或者当前操作的元素时激活的组合元素
if(activeElementIdList.value.length === 1 || isActiveGroupElement) {
elementList.value = elementList.value.map(el => {
return el.elId === element.elId ? { ...el, left: targetLeft, top: targetTop } : el
return el.id === element.id ? { ...el, left: targetLeft, top: targetTop } : el
})
}
// 修改元素位置,如果需要修改位置的元素不是被操作的元素(例如多选下的操作)
// 那么其他非操作元素要移动的位置通过操作元素的移动偏移量计算
else {
const handleElement = elementList.value.find(el => el.elId === element.elId)
const handleElement = elementList.value.find(el => el.id === element.id)
if(!handleElement) return
elementList.value = elementList.value.map(el => {
if(activeElementIdList.value.includes(el.elId)) {
if(el.elId === element.elId) {
if(activeElementIdList.value.includes(el.id)) {
if(el.id === element.id) {
return {
...el,
left: targetLeft,

View File

@ -110,19 +110,19 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | n
}
// 被锁定的元素除外
if(isInclude && !element.isLock) inRangeElementList.push(element)
if(isInclude && !element.lock) inRangeElementList.push(element)
}
// 对于组合元素成员,必须所有成员都在选择范围中才算被选中
inRangeElementList = inRangeElementList.filter(inRangeElement => {
if(inRangeElement.groupId) {
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.id)
const groupElementList = elementList.value.filter(element => element.groupId === inRangeElement.groupId)
return groupElementList.every(groupElement => inRangeElementIdList.includes(groupElement.elId))
return groupElementList.every(groupElement => inRangeElementIdList.includes(groupElement.id))
}
return true
})
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.id)
if(inRangeElementIdList.length) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
mouseSelectionState.isShow = false

View File

@ -59,7 +59,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | n
else if( angle < 0 && Math.abs(angle + 180) <= sorptionRange ) angle -= (angle + 180)
// 修改元素角度
elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...el, rotate: angle } : el)
elementList.value = elementList.value.map(el => element.id === el.id ? { ...el, rotate: angle } : el)
}
document.onmouseup = () => {

View File

@ -102,8 +102,8 @@ export default (
const elOriginWidth = element.width
const elOriginHeight = element.height
const isLockRatio = ctrlOrShiftKeyActive.value || ('lockRatio' in element && element.lockRatio)
const lockRatio = elOriginWidth / elOriginHeight
const fixedRatio = ctrlOrShiftKeyActive.value || ('fixedRatio' in element && element.fixedRatio)
const aspectRatio = elOriginWidth / elOriginHeight
const elRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
const rotateRadian = Math.PI * elRotate / 180
@ -133,13 +133,13 @@ export default (
else {
const edgeWidth = VIEWPORT_SIZE
const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const isActiveGroupElement = element.elId === activeGroupElementId.value
const isActiveGroupElement = element.id === activeGroupElementId.value
for(const el of elementList.value) {
if('rotate' in el && el.rotate) continue
if(el.type === ElementTypes.LINE) continue
if(isActiveGroupElement && el.elId === element.elId) continue
if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) continue
if(isActiveGroupElement && el.id === element.id) continue
if(!isActiveGroupElement && activeElementIdList.value.includes(el.id)) continue
const left = el.left
const top = el.top
@ -236,9 +236,9 @@ export default (
let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale.value
// 锁定宽高比例
if(isLockRatio) {
if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) revisedY = revisedX / lockRatio
if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) revisedY = -revisedX / lockRatio
if(fixedRatio) {
if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) revisedY = revisedX / aspectRatio
if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) revisedY = -revisedX / aspectRatio
}
// 根据不同的操作点分别计算元素缩放后的大小和位置
@ -297,18 +297,18 @@ export default (
let moveX = x / canvasScale.value
let moveY = y / canvasScale.value
if(isLockRatio) {
if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) moveY = moveX / lockRatio
if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) moveY = -moveX / lockRatio
if(fixedRatio) {
if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) moveY = moveX / aspectRatio
if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) moveY = -moveX / aspectRatio
}
if(command === OperatePoints.RIGHT_BOTTOM) {
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + elOriginHeight + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = moveY * lockRatio
else moveY = moveX / lockRatio
if(fixedRatio) {
if(offsetY) moveX = moveY * aspectRatio
else moveY = moveX / aspectRatio
}
width = getSizeWithinRange(elOriginWidth + moveX)
height = getSizeWithinRange(elOriginHeight + moveY)
@ -317,9 +317,9 @@ export default (
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + elOriginHeight + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = -moveY * lockRatio
else moveY = -moveX / lockRatio
if(fixedRatio) {
if(offsetY) moveX = -moveY * aspectRatio
else moveY = -moveX / aspectRatio
}
width = getSizeWithinRange(elOriginWidth - moveX)
height = getSizeWithinRange(elOriginHeight + moveY)
@ -329,9 +329,9 @@ export default (
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = moveY * lockRatio
else moveY = moveX / lockRatio
if(fixedRatio) {
if(offsetY) moveX = moveY * aspectRatio
else moveY = moveX / aspectRatio
}
width = getSizeWithinRange(elOriginWidth - moveX)
height = getSizeWithinRange(elOriginHeight - moveY)
@ -342,9 +342,9 @@ export default (
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = -moveY * lockRatio
else moveY = -moveX / lockRatio
if(fixedRatio) {
if(offsetY) moveX = -moveY * aspectRatio
else moveY = -moveX / aspectRatio
}
width = getSizeWithinRange(elOriginWidth + moveX)
height = getSizeWithinRange(elOriginHeight - moveY)
@ -374,7 +374,7 @@ export default (
}
}
elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...el, left, top, width, height } : el)
elementList.value = elementList.value.map(el => element.id === el.id ? { ...el, left, top, width, height } : el)
}
document.onmouseup = e => {
@ -396,7 +396,7 @@ export default (
const { minX, maxX, minY, maxY } = range
const operateWidth = maxX - minX
const operateHeight = maxY - minY
const lockRatio = operateWidth / operateHeight
const aspectRatio = operateWidth / operateHeight
const startPageX = e.pageX
const startPageY = e.pageY
@ -415,8 +415,8 @@ export default (
// 锁定宽高比例
if(ctrlOrShiftKeyActive.value) {
if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) y = x / lockRatio
if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) y = -x / lockRatio
if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) y = x / aspectRatio
if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) y = -x / aspectRatio
}
// 获取鼠标缩放时当前所有激活元素的范围
@ -468,8 +468,8 @@ export default (
// 根据上面计算的比例,修改所有被激活元素的位置大小
// 宽高通过乘以对应的比例得到,位置通过将被操作元素在所有元素整体中的相对位置乘以对应比例获得
elementList.value = elementList.value.map(el => {
if((el.type === ElementTypes.IMAGE || el.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(el.elId)) {
const originElement = originElementList.find(originEl => originEl.elId === el.elId) as PPTImageElement | PPTShapeElement
if((el.type === ElementTypes.IMAGE || el.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(el.id)) {
const originElement = originElementList.find(originEl => originEl.id === el.id) as PPTImageElement | PPTShapeElement
return {
...el,
width: originElement.width * widthScale,

View File

@ -19,25 +19,25 @@ export default (
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
// 如果被点击的元素处于未激活状态,则将他设置为激活元素(单选),或者加入到激活元素中(多选)
if(!activeElementIdList.value.includes(element.elId)) {
if(!activeElementIdList.value.includes(element.id)) {
let newActiveIdList: string[] = []
if(ctrlOrShiftKeyActive.value) {
newActiveIdList = [...activeElementIdList.value, element.elId]
newActiveIdList = [...activeElementIdList.value, element.id]
}
else newActiveIdList = [element.elId]
else newActiveIdList = [element.id]
// 同时如果该元素是分组成员,需要将和他同组的元素一起激活
if(element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
if(el.groupId === element.groupId) groupMembersId.push(el.id)
})
newActiveIdList = [...newActiveIdList, ...groupMembersId]
}
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, uniq(newActiveIdList))
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id)
}
// 如果被点击的元素已激活,且按下了多选按钮,则取消其激活状态(除非该元素或分组是最后的一个激活元素)
@ -48,12 +48,12 @@ export default (
if(element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
if(el.groupId === element.groupId) groupMembersId.push(el.id)
})
newActiveIdList = activeElementIdList.value.filter(elId => !groupMembersId.includes(elId))
newActiveIdList = activeElementIdList.value.filter(id => !groupMembersId.includes(id))
}
else {
newActiveIdList = activeElementIdList.value.filter(elId => elId !== element.elId)
newActiveIdList = activeElementIdList.value.filter(id => id !== element.id)
}
if(newActiveIdList.length > 0) {
@ -62,11 +62,11 @@ export default (
}
// 如果被点击的元素已激活,且没有按下多选按钮,且该元素不是当前操作元素,则将其设置为当前操作元素
else if(handleElementId.value !== element.elId) {
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
else if(handleElementId.value !== element.id) {
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id)
}
else if(activeGroupElementId.value !== element.elId && element.groupId) {
else if(activeGroupElementId.value !== element.id && element.groupId) {
const startPageX = e.pageX
const startPageY = e.pageY
@ -75,7 +75,7 @@ export default (
const currentPageY = e.pageY
if(startPageX === currentPageX && startPageY === currentPageY) {
activeGroupElementId.value = element.elId
activeGroupElementId.value = element.id
;(e.target as HTMLElement).onmouseup = null
}
}
@ -85,8 +85,8 @@ export default (
}
const selectAllElement = () => {
const unlockedElements = elementList.value.filter(el => !el.isLock)
const newActiveElementIdList = unlockedElements.map(el => el.elId)
const unlockedElements = elementList.value.filter(el => !el.lock)
const newActiveElementIdList = unlockedElements.map(el => el.id)
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
}

View File

@ -42,12 +42,12 @@
<EditableElement
v-for="(element, index) in elementList"
:key="element.elId"
:key="element.id"
:elementInfo="element"
:elementIndex="index + 1"
:isActive="activeElementIdList.includes(element.elId)"
:isHandleEl="element.elId === handleElementId"
:isActiveGroupElement="activeGroupElementId === element.elId"
:isActive="activeElementIdList.includes(element.id)"
:isHandleEl="element.id === handleElementId"
:isActiveGroupElement="activeGroupElementId === element.id"
:isMultiSelect="activeElementIdList.length > 1"
:selectElement="selectElement"
:rotateElement="rotateElement"

View File

@ -16,7 +16,7 @@
<div class="background" :style="{ ...backgroundStyle }"></div>
<BaseElement
v-for="(element, index) in slide.elements"
:key="element.elId"
:key="element.id"
:elementInfo="element"
:elementIndex="index + 1"
/>

View File

@ -2,7 +2,7 @@
<div
class="editable-element"
ref="elementRef"
:id="'editable-element-' + elementInfo.elId"
:id="'editable-element-' + elementInfo.id"
:style="{ zIndex: elementIndex }"
>
<component
@ -105,7 +105,7 @@ export default defineComponent({
const { copyElement, cutElement } = useCopyAndPasteElement()
const contextmenus = (): ContextmenuItem[] => {
if(props.elementInfo.isLock) {
if(props.elementInfo.lock) {
return [{
text: '解锁',
icon: 'icon-unlock',

View File

@ -1,59 +0,0 @@
<template>
<SvgWrapper
class="element-border"
overflow="visible"
:width="width"
:height="height"
>
<path
vector-effect="non-scaling-stroke"
stroke-linecap="butt"
stroke-miterlimit="8"
stroke-linejoin
fill="transparent"
:d="`M0,0 L${width},0 L${width},${height} L0,${height} Z`"
:stroke="borderColor"
:stroke-width="borderWidth"
:stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'"
></path>
</SvgWrapper>
</template>
<script lang="ts">
import SvgWrapper from '@/components/SvgWrapper.vue'
export default {
name: 'element-border',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
borderColor: {
type: String,
},
borderWidth: {
type: Number,
},
borderStyle: {
type: String,
},
},
}
</script>
<style lang="scss" scoped>
svg {
overflow: visible;
position: absolute;
top: 0;
left: 0;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<SvgWrapper
class="element-outline"
overflow="visible"
:width="width"
:height="height"
>
<path
vector-effect="non-scaling-stroke"
stroke-linecap="butt"
stroke-miterlimit="8"
stroke-linejoin
fill="transparent"
:d="`M0,0 L${width},0 L${width},${height} L0,${height} Z`"
:stroke="outlineColor"
:stroke-width="outlineWidth"
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
></path>
</SvgWrapper>
</template>
<script lang="ts">
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
export default defineComponent({
name: 'element-outline',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
outline: {
type: Object as PropType<PPTElementOutline>
},
},
setup(props) {
const {
outlineWidth,
outlineStyle,
outlineColor,
} = useElementOutline(toRef(props, 'outline'))
return {
outlineWidth,
outlineStyle,
outlineColor,
}
},
})
</script>
<style lang="scss" scoped>
svg {
overflow: visible;
position: absolute;
top: 0;
left: 0;
}
</style>

View File

@ -12,40 +12,34 @@
<div
class="element-content"
:style="{
filter: elementInfo.shadow ? `drop-shadow(${elementInfo.shadow})` : '',
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
transform: flip,
}"
>
<ImageRectBorder
<ImageRectOutline
v-if="clipShape.type === 'rect'"
:width="elementInfo.width"
:height="elementInfo.height"
:radius="clipShape.radius"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
:outline="elementInfo.outline"
/>
<ImageEllipseBorder
<ImageEllipseOutline
v-else-if="clipShape.type === 'ellipse'"
:width="elementInfo.width"
:height="elementInfo.height"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
:outline="elementInfo.outline"
/>
<ImagePolygonBorder
<ImagePolygonOutline
v-else-if="clipShape.type === 'polygon'"
:width="elementInfo.width"
:height="elementInfo.height"
:createPath="clipShape.createPath"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
:outline="elementInfo.outline"
/>
<div class="img-wrapper" :style="{ clipPath: clipShape.style }">
<img
:src="elementInfo.imgUrl"
:src="elementInfo.src"
:draggable="false"
:style="{
top: imgPosition.top,
@ -67,16 +61,18 @@ import { computed, defineComponent, PropType } from 'vue'
import { PPTImageElement } from '@/types/slides'
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
import ImageRectBorder from './ImageRectBorder.vue'
import ImageEllipseBorder from './ImageEllipseBorder.vue'
import ImagePolygonBorder from './ImagePolygonBorder.vue'
import ImageRectOutline from './ImageRectOutline.vue'
import ImageEllipseOutline from './ImageEllipseOutline.vue'
import ImagePolygonOutline from './ImagePolygonOutline.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
export default defineComponent({
name: 'base-element-image',
components: {
ImageRectBorder,
ImageEllipseBorder,
ImagePolygonBorder,
ImageRectOutline,
ImageEllipseOutline,
ImagePolygonOutline,
},
props: {
elementInfo: {
@ -118,10 +114,10 @@ export default defineComponent({
})
const filter = computed(() => {
if(!props.elementInfo.filter) return ''
if(!props.elementInfo.filters) return ''
let filter = ''
for(const key of Object.keys(props.elementInfo.filter)) {
filter += `${key}(${props.elementInfo.filter[key]}) `
for(const key of Object.keys(props.elementInfo.filters)) {
filter += `${key}(${props.elementInfo.filters[key]}) `
}
return filter
})
@ -135,11 +131,15 @@ export default defineComponent({
return ''
})
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
return {
imgPosition,
clipShape,
filter,
flip,
shadowStyle,
}
},
})

View File

@ -6,7 +6,7 @@
>
<img
class="bottom-img"
:src="imgUrl"
:src="src"
:draggable="false"
alt=""
:style="bottomImgPositionStyle"
@ -21,7 +21,7 @@
>
<img
class="top-img"
:src="imgUrl"
:src="src"
:draggable="false"
alt=""
:style="topImgPositionStyle"
@ -78,7 +78,7 @@ export default defineComponent({
SvgWrapper,
},
props: {
imgUrl: {
src: {
type: String,
required: true,
},

View File

@ -1,66 +0,0 @@
<template>
<SvgWrapper
class="image-ellipse-border"
overflow="visible"
:width="width"
:height="height"
>
<ellipse
vector-effect="non-scaling-stroke"
stroke-linecap="butt"
stroke-miterlimit="8"
stroke-linejoin
fill="transparent"
:cx="width / 2"
:cy="height / 2"
:rx="width / 2"
:ry="height / 2"
:stroke="borderColor"
:stroke-width="borderWidth"
:stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'"
></ellipse>
</SvgWrapper>
</template>
<script lang="ts">
import SvgWrapper from '@/components/SvgWrapper.vue'
export default {
name: 'image-ellipse-border',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
borderColor: {
type: String,
default: '',
},
borderWidth: {
type: Number,
default: 0,
},
borderStyle: {
type: String,
default: '',
},
},
}
</script>
<style lang="scss" scoped>
svg {
overflow: visible;
position: absolute;
z-index: 2;
top: 0;
left: 0;
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<SvgWrapper
class="image-ellipse-outline"
overflow="visible"
:width="width"
:height="height"
>
<ellipse
vector-effect="non-scaling-stroke"
stroke-linecap="butt"
stroke-miterlimit="8"
stroke-linejoin
fill="transparent"
:cx="width / 2"
:cy="height / 2"
:rx="width / 2"
:ry="height / 2"
:stroke="outlineColor"
:stroke-width="outlineWidth"
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
></ellipse>
</SvgWrapper>
</template>
<script lang="ts">
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
export default defineComponent({
name: 'image-ellipse-outline',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
outline: {
type: Object as PropType<PPTElementOutline>
},
},
setup(props) {
const {
outlineWidth,
outlineStyle,
outlineColor,
} = useElementOutline(toRef(props, 'outline'))
return {
outlineWidth,
outlineStyle,
outlineColor,
}
},
})
</script>
<style lang="scss" scoped>
svg {
overflow: visible;
position: absolute;
z-index: 2;
top: 0;
left: 0;
}
</style>

View File

@ -1,67 +0,0 @@
<template>
<SvgWrapper
class="image-polygon-border"
overflow="visible"
:width="width"
:height="height"
>
<path
vector-effect="non-scaling-stroke"
stroke-linecap="butt"
stroke-miterlimit="8"
stroke-linejoin
fill="transparent"
:d="createPath(width, height)"
:stroke="borderColor"
:stroke-width="borderWidth"
:stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'"
></path>
</SvgWrapper>
</template>
<script lang="ts">
import SvgWrapper from '@/components/SvgWrapper.vue'
export default {
name: 'image-polygon-border',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
borderColor: {
type: String,
default: '',
},
borderWidth: {
type: Number,
default: 0,
},
borderStyle: {
type: String,
default: '',
},
createPath: {
type: Function,
required: true,
},
},
}
</script>
<style lang="scss" scoped>
svg {
overflow: visible;
position: absolute;
z-index: 2;
top: 0;
left: 0;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<SvgWrapper
class="image-polygon-outline"
overflow="visible"
:width="width"
:height="height"
>
<path
vector-effect="non-scaling-stroke"
stroke-linecap="butt"
stroke-miterlimit="8"
stroke-linejoin
fill="transparent"
:d="createPath(width, height)"
:stroke="outlineColor"
:stroke-width="outlineWidth"
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
></path>
</SvgWrapper>
</template>
<script lang="ts">
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
export default defineComponent({
name: 'image-polygon-outline',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
outline: {
type: Object as PropType<PPTElementOutline>
},
createPath: {
type: Function,
required: true,
},
},
setup(props) {
const {
outlineWidth,
outlineStyle,
outlineColor,
} = useElementOutline(toRef(props, 'outline'))
return {
outlineWidth,
outlineStyle,
outlineColor,
}
},
})
</script>
<style lang="scss" scoped>
svg {
overflow: visible;
position: absolute;
z-index: 2;
top: 0;
left: 0;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<SvgWrapper
class="image-rect-border"
class="image-rect-outline"
overflow="visible"
:width="width"
:height="height"
@ -15,18 +15,21 @@
:ry="radius"
:width="width"
:height="height"
:stroke="borderColor"
:stroke-width="borderWidth"
:stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'"
:stroke="outlineColor"
:stroke-width="outlineWidth"
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
></rect>
</SvgWrapper>
</template>
<script lang="ts">
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
export default {
name: 'image-rect-border',
export default defineComponent({
name: 'image-rect-outline',
components: {
SvgWrapper,
},
@ -39,24 +42,28 @@ export default {
type: Number,
required: true,
},
borderColor: {
type: String,
default: '',
},
borderWidth: {
type: Number,
default: 0,
},
borderStyle: {
type: String,
default: '',
outline: {
type: Object as PropType<PPTElementOutline>
},
radius: {
type: String,
default: '0',
},
},
}
setup(props) {
const {
outlineWidth,
outlineStyle,
outlineColor,
} = useElementOutline(toRef(props, 'outline'))
return {
outlineWidth,
outlineStyle,
outlineColor,
}
},
})
</script>
<style lang="scss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<div
class="editable-element image"
:class="{ 'lock': elementInfo.isLock }"
:class="{ 'lock': elementInfo.lock }"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
@ -13,7 +13,7 @@
>
<ImageClip
v-if="isCliping"
:imgUrl="elementInfo.imgUrl"
:src="elementInfo.src"
:clipData="elementInfo.clip"
:canvasScale="canvasScale"
:width="elementInfo.width"
@ -29,40 +29,34 @@
v-if="!isCliping"
v-contextmenu="contextmenus"
:style="{
filter: elementInfo.shadow ? `drop-shadow(${elementInfo.shadow})` : '',
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
transform: flip,
}"
>
<ImageRectBorder
<ImageRectOutline
v-if="clipShape.type === 'rect'"
:width="elementInfo.width"
:height="elementInfo.height"
:radius="clipShape.radius"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
:outline="elementInfo.outline"
/>
<ImageEllipseBorder
<ImageEllipseOutline
v-else-if="clipShape.type === 'ellipse'"
:width="elementInfo.width"
:height="elementInfo.height"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
:outline="elementInfo.outline"
/>
<ImagePolygonBorder
<ImagePolygonOutline
v-else-if="clipShape.type === 'polygon'"
:width="elementInfo.width"
:height="elementInfo.height"
:outline="elementInfo.outline"
:createPath="clipShape.createPath"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
/>
<div class="img-wrapper" :style="{clipPath: clipShape.style}">
<img
:src="elementInfo.imgUrl"
:src="elementInfo.src"
:draggable="false"
:style="{
top: imgPosition.top,
@ -93,7 +87,7 @@
:type="line.type"
:style="line.style"
/>
<template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
<ResizablePoint
class="el-resizable-point"
v-for="point in resizablePoints"
@ -119,7 +113,7 @@ import { computed, defineComponent, ref, PropType } from 'vue'
import { PPTImageElement } from '@/types/slides'
import { ElementScaleHandler } from '@/types/edit'
import useCommonOperate from '@/views/_common/_element/useCommonOperate'
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
@ -129,9 +123,11 @@ import BorderLine from '@/views/_common/_operate/BorderLine.vue'
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
import ImageClip, { ClipedEmitData } from './ImageClipHandler.vue'
import ImageRectBorder from './ImageRectBorder.vue'
import ImageEllipseBorder from './ImageEllipseBorder.vue'
import ImagePolygonBorder from './ImagePolygonBorder.vue'
import ImageRectOutline from './ImageRectOutline.vue'
import ImageEllipseOutline from './ImageEllipseOutline.vue'
import ImagePolygonOutline from './ImagePolygonOutline.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
export default defineComponent({
name: 'editable-element-image',
@ -141,9 +137,9 @@ export default defineComponent({
BorderLine,
AnimationIndex,
ImageClip,
ImageRectBorder,
ImageEllipseBorder,
ImagePolygonBorder,
ImageRectOutline,
ImageEllipseOutline,
ImagePolygonOutline,
},
props: {
elementInfo: {
@ -198,7 +194,7 @@ export default defineComponent({
const { resizablePoints, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const isCliping = computed(() => clipingImageElId.value === props.elementInfo.elId)
const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
const imgPosition = computed(() => {
if(!props.elementInfo || !props.elementInfo.clip) {
@ -233,10 +229,10 @@ export default defineComponent({
})
const filter = computed(() => {
if(!props.elementInfo.filter) return ''
if(!props.elementInfo.filters) return ''
let filter = ''
for(const key of Object.keys(props.elementInfo.filter)) {
filter += `${key}(${props.elementInfo.filter[key]}) `
for(const key of Object.keys(props.elementInfo.filters)) {
filter += `${key}(${props.elementInfo.filters[key]}) `
}
return filter
})
@ -250,8 +246,11 @@ export default defineComponent({
return ''
})
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
const handleSelectElement = (e: MouseEvent) => {
if(isCliping.value || props.elementInfo.isLock) return
if(isCliping.value || props.elementInfo.lock) return
e.stopPropagation()
props.selectElement(e, props.elementInfo)
}
@ -283,6 +282,7 @@ export default defineComponent({
borderLines,
filter,
flip,
shadowStyle,
handleSelectElement,
clip,
}

View File

@ -12,17 +12,13 @@
:style="{
backgroundColor: elementInfo.fill,
opacity: elementInfo.opacity,
textShadow: elementInfo.shadow,
lineHeight: elementInfo.lineHeight,
letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
textShadow: shadowStyle,
}"
>
<ElementBorder
<ElementOutline
:width="elementInfo.width"
:height="elementInfo.height"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
:outline="elementInfo.outline"
/>
<div class="text-content"
v-html="elementInfo.content"
@ -32,14 +28,16 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { defineComponent, PropType, computed } from 'vue'
import { PPTTextElement } from '@/types/slides'
import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
export default defineComponent({
name: 'base-element-text',
components: {
ElementBorder,
ElementOutline,
},
props: {
elementInfo: {
@ -47,6 +45,14 @@ export default defineComponent({
required: true,
},
},
setup(props) {
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
return {
shadowStyle,
}
},
})
</script>
@ -58,6 +64,7 @@ export default defineComponent({
.element-content {
position: relative;
padding: 10px;
line-height: 1.5;
.text-content {
position: relative;

View File

@ -1,7 +1,7 @@
<template>
<div
class="editable-element text"
:class="{ 'lock': elementInfo.isLock }"
:class="{ 'lock': elementInfo.lock }"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
@ -14,22 +14,18 @@
:style="{
backgroundColor: elementInfo.fill,
opacity: elementInfo.opacity,
textShadow: elementInfo.shadow,
lineHeight: elementInfo.lineHeight,
letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
textShadow: shadowStyle,
}"
v-contextmenu="contextmenus"
>
<ElementBorder
<ElementOutline
:width="elementInfo.width"
:height="elementInfo.height"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
:outline="elementInfo.outline"
/>
<div class="text-content"
v-html="elementInfo.content"
:contenteditable="isActive && !elementInfo.isLock"
:contenteditable="isActive && !elementInfo.lock"
></div>
</div>
@ -52,7 +48,7 @@
:isWide="true"
@mousedown="handleSelectElement($event)"
/>
<template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
<ResizablePoint class="el-resizable-point"
v-for="point in resizablePoints"
:key="point.type"
@ -77,18 +73,20 @@ import { computed, defineComponent, PropType } from 'vue'
import { PPTTextElement } from '@/types/slides'
import { ElementScaleHandler } from '@/types/edit'
import useCommonOperate from '@/views/_common/_element/useCommonOperate'
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
export default defineComponent({
name: 'editable-element-text',
components: {
ElementBorder,
ElementOutline,
RotateHandler,
ResizablePoint,
BorderLine,
@ -146,17 +144,21 @@ export default defineComponent({
const { resizablePoints, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const handleSelectElement = (e: MouseEvent, canMove = true) => {
if(props.elementInfo.isLock) return
if(props.elementInfo.lock) return
e.stopPropagation()
props.selectElement(e, props.elementInfo, canMove)
}
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
return {
scaleWidth,
resizablePoints,
borderLines,
handleSelectElement,
shadowStyle,
}
},
})
@ -182,6 +184,7 @@ export default defineComponent({
.element-content {
position: relative;
padding: 10px;
line-height: 1.5;
.text-content {
position: relative;

View File

@ -0,0 +1,14 @@
import { computed, Ref } from 'vue'
import { PPTElementOutline } from '@/types/slides'
export default (outline: Ref<PPTElementOutline | undefined>) => {
const outlineWidth = computed(() => (outline.value && outline.value.width !== undefined) ? outline.value.width : 0)
const outlineStyle = computed(() => (outline.value && outline.value.style !== undefined) ? outline.value.style : 'solid')
const outlineColor = computed(() => (outline.value && outline.value.color !== undefined) ? outline.value.color : '#41464b')
return {
outlineWidth,
outlineStyle,
outlineColor,
}
}

View File

@ -0,0 +1,14 @@
import { Ref } from 'vue'
import { PPTElementShadow } from '@/types/slides'
export default (shadow: Ref<PPTElementShadow | undefined>) => {
let shadowStyle = ''
if(shadow.value) {
const { h, v, blur, color } = shadow.value
shadowStyle = `${h} ${v} ${blur} ${color}`
}
return {
shadowStyle,
}
}