useCreateElement.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import { storeToRefs } from 'pinia'
  2. import { nanoid } from 'nanoid'
  3. import { useMainStore, useSlidesStore } from '@/store'
  4. import { getImageSize } from '@/utils/image'
  5. import message from '@/utils/message'
  6. import { lang } from '@/main'
  7. import type { PPTLineElement, PPTElement, TableCell, TableCellStyle, PPTShapeElement, ChartType } from '@/types/slides'
  8. import { type ShapePoolItem, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
  9. import type { LinePoolItem } from '@/configs/lines'
  10. import { getChartDefaultData } from '@/configs/chart'
  11. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  12. interface CommonElementPosition {
  13. top: number
  14. left: number
  15. width: number
  16. height: number
  17. }
  18. interface LineElementPosition {
  19. top: number
  20. left: number
  21. start: [number, number]
  22. end: [number, number]
  23. }
  24. interface CreateTextData {
  25. content?: string
  26. vertical?: boolean
  27. }
  28. export default () => {
  29. const mainStore = useMainStore()
  30. const slidesStore = useSlidesStore()
  31. const { creatingElement } = storeToRefs(mainStore)
  32. const { theme, viewportRatio, viewportSize } = storeToRefs(slidesStore)
  33. const { addHistorySnapshot } = useHistorySnapshot()
  34. // 创建(插入)一个元素并将其设置为被选中元素
  35. const createElement = (element: PPTElement, callback?: () => void) => {
  36. slidesStore.addElement(element)
  37. mainStore.setActiveElementIdList([element.id])
  38. if (creatingElement.value) mainStore.setCreatingElement(null)
  39. setTimeout(() => {
  40. mainStore.setEditorareaFocus(true)
  41. }, 0)
  42. if (callback) callback()
  43. addHistorySnapshot()
  44. }
  45. /**
  46. * 创建图片元素
  47. * @param src 图片地址
  48. */
  49. const createImageElement = (src: string) => {
  50. getImageSize(src).then(({ width, height }) => {
  51. const scale = height / width
  52. if (scale < viewportRatio.value && width > viewportSize.value) {
  53. width = viewportSize.value
  54. height = width * scale
  55. }
  56. else if (height > viewportSize.value * viewportRatio.value) {
  57. height = viewportSize.value * viewportRatio.value
  58. width = height / scale
  59. }
  60. createElement({
  61. type: 'image',
  62. id: nanoid(10),
  63. src,
  64. width,
  65. height,
  66. left: (viewportSize.value - width) / 2,
  67. top: (viewportSize.value * viewportRatio.value - height) / 2,
  68. fixedRatio: true,
  69. rotate: 0,
  70. })
  71. })
  72. }
  73. /**
  74. * 创建图表元素
  75. * @param chartType 图表类型
  76. */
  77. const createChartElement = (type: ChartType) => {
  78. const CHART_DEFAULT_DATA = getChartDefaultData()
  79. createElement({
  80. type: 'chart',
  81. id: nanoid(10),
  82. chartType: type,
  83. left: 300,
  84. top: 81.25,
  85. width: 400,
  86. height: 400,
  87. rotate: 0,
  88. themeColors: theme.value.themeColors,
  89. textColor: theme.value.fontColor,
  90. data: CHART_DEFAULT_DATA[type],
  91. })
  92. }
  93. /**
  94. * 创建表格元素
  95. * @param row 行数
  96. * @param col 列数
  97. */
  98. const createTableElement = (row: number, col: number) => {
  99. const style: TableCellStyle = {
  100. fontname: theme.value.fontName,
  101. color: theme.value.fontColor,
  102. }
  103. const data: TableCell[][] = []
  104. for (let i = 0; i < row; i++) {
  105. const rowCells: TableCell[] = []
  106. for (let j = 0; j < col; j++) {
  107. rowCells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '', style })
  108. }
  109. data.push(rowCells)
  110. }
  111. const DEFAULT_CELL_WIDTH = 100
  112. const DEFAULT_CELL_HEIGHT = 36
  113. const colWidths: number[] = new Array(col).fill(1 / col)
  114. const width = col * DEFAULT_CELL_WIDTH
  115. const height = row * DEFAULT_CELL_HEIGHT
  116. createElement({
  117. type: 'table',
  118. id: nanoid(10),
  119. width,
  120. height,
  121. colWidths,
  122. rotate: 0,
  123. data,
  124. left: (viewportSize.value - width) / 2,
  125. top: (viewportSize.value * viewportRatio.value - height) / 2,
  126. outline: {
  127. width: 2,
  128. style: 'solid',
  129. color: '#eeece1',
  130. },
  131. theme: {
  132. color: theme.value.themeColors[0],
  133. rowHeader: true,
  134. rowFooter: false,
  135. colHeader: false,
  136. colFooter: false,
  137. },
  138. cellMinHeight: 36,
  139. })
  140. }
  141. /**
  142. * 创建文本元素
  143. * @param position 位置大小信息
  144. * @param content 文本内容
  145. */
  146. const createTextElement = (position: CommonElementPosition, data?: CreateTextData) => {
  147. const { left, top, width, height } = position
  148. const content = data?.content || ''
  149. const vertical = data?.vertical || false
  150. const id = nanoid(10)
  151. createElement({
  152. type: 'text',
  153. id,
  154. left,
  155. top,
  156. width,
  157. height,
  158. content,
  159. rotate: 0,
  160. defaultFontName: theme.value.fontName,
  161. defaultColor: theme.value.fontColor,
  162. vertical,
  163. }, () => {
  164. setTimeout(() => {
  165. const editorRef: HTMLElement | null = document.querySelector(`#editable-element-${id} .ProseMirror`)
  166. if (editorRef) editorRef.focus()
  167. }, 0)
  168. })
  169. }
  170. /**
  171. * 创建形状元素
  172. * @param position 位置大小信息
  173. * @param data 形状路径信息
  174. */
  175. const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem, supplement: Partial<PPTShapeElement> = {}) => {
  176. const { left, top, width, height } = position
  177. const newElement: PPTShapeElement = {
  178. type: 'shape',
  179. id: nanoid(10),
  180. left,
  181. top,
  182. width,
  183. height,
  184. viewBox: data.viewBox,
  185. path: data.path,
  186. fill: theme.value.themeColors[0],
  187. fixedRatio: false,
  188. rotate: 0,
  189. ...supplement,
  190. }
  191. if (data.withborder) newElement.outline = theme.value.outline
  192. if (data.special) newElement.special = true
  193. if (data.pathFormula) {
  194. newElement.pathFormula = data.pathFormula
  195. newElement.viewBox = [width, height]
  196. const pathFormula = SHAPE_PATH_FORMULAS[data.pathFormula]
  197. if ('editable' in pathFormula && pathFormula.editable) {
  198. newElement.path = pathFormula.formula(width, height, pathFormula.defaultValue!)
  199. newElement.keypoints = pathFormula.defaultValue
  200. }
  201. else newElement.path = pathFormula.formula(width, height)
  202. }
  203. createElement(newElement)
  204. }
  205. /**
  206. * 创建线条元素
  207. * @param position 位置大小信息
  208. * @param data 线条的路径和样式
  209. */
  210. const createLineElement = (position: LineElementPosition, data: LinePoolItem) => {
  211. const { left, top, start, end } = position
  212. const newElement: PPTLineElement = {
  213. type: 'line',
  214. id: nanoid(10),
  215. left,
  216. top,
  217. start,
  218. end,
  219. points: data.points,
  220. color: theme.value.themeColors[0],
  221. style: data.style,
  222. width: 2,
  223. }
  224. if (data.isBroken) newElement.broken = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
  225. if (data.isBroken2) newElement.broken2 = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
  226. if (data.isCurve) newElement.curve = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
  227. if (data.isCubic) newElement.cubic = [[(start[0] + end[0]) / 2, (start[1] + end[1]) / 2], [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]]
  228. createElement(newElement)
  229. }
  230. /**
  231. * 创建LaTeX元素
  232. * @param svg SVG代码
  233. */
  234. const createLatexElement = (data: { path: string; latex: string; w: number; h: number; }) => {
  235. createElement({
  236. type: 'latex',
  237. id: nanoid(10),
  238. width: data.w,
  239. height: data.h,
  240. rotate: 0,
  241. left: (viewportSize.value - data.w) / 2,
  242. top: (viewportSize.value * viewportRatio.value - data.h) / 2,
  243. path: data.path,
  244. latex: data.latex,
  245. color: theme.value.fontColor,
  246. strokeWidth: 2,
  247. viewBox: [data.w, data.h],
  248. fixedRatio: true,
  249. })
  250. }
  251. /**
  252. * 创建视频元素
  253. * @param src 视频地址
  254. */
  255. const createVideoElement = (src: string) => {
  256. createElement({
  257. type: 'video',
  258. id: nanoid(10),
  259. width: 500,
  260. height: 300,
  261. rotate: 0,
  262. left: (viewportSize.value - 500) / 2,
  263. top: (viewportSize.value * viewportRatio.value - 300) / 2,
  264. src,
  265. autoplay: false,
  266. })
  267. }
  268. /**
  269. * 创建音频元素
  270. * @param src 音频地址
  271. */
  272. const createAudioElement = (src: string) => {
  273. createElement({
  274. type: 'audio',
  275. id: nanoid(10),
  276. width: 50,
  277. height: 50,
  278. rotate: 0,
  279. left: (viewportSize.value - 50) / 2,
  280. top: (viewportSize.value * viewportRatio.value - 50) / 2,
  281. loop: false,
  282. autoplay: false,
  283. fixedRatio: true,
  284. color: theme.value.themeColors[0],
  285. src,
  286. })
  287. }
  288. /**
  289. * 创建网页元素
  290. * @param url 网页链接地址
  291. */
  292. const createFrameElement = (url: string, type: number, id?: string) => {
  293. // 检查当前幻灯片是否已经包含网页元素
  294. const { currentSlide } = storeToRefs(useSlidesStore())
  295. const hasWebpage = currentSlide.value?.elements?.some(element => element.type === 'frame')
  296. if (hasWebpage) {
  297. message.error(lang.ssSlideLearn)
  298. return
  299. }
  300. console.log(viewportSize)
  301. const width = viewportSize.value
  302. const height = width * viewportRatio.value
  303. createElement({
  304. type: 'frame',
  305. id: nanoid(10),
  306. webId: id || '',
  307. url,
  308. width: width,
  309. height: height,
  310. // left: (viewportSize.value - 800) / 2,
  311. // top: (viewportSize.value * viewportRatio.value - 480) / 2,
  312. left: 0,
  313. top: 0,
  314. rotate: 0,
  315. toolType: type,
  316. })
  317. }
  318. return {
  319. createImageElement,
  320. createChartElement,
  321. createTableElement,
  322. createTextElement,
  323. createShapeElement,
  324. createLineElement,
  325. createLatexElement,
  326. createVideoElement,
  327. createAudioElement,
  328. createFrameElement,
  329. }
  330. }