useCreateElement.ts 9.0 KB

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