useImport.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. import { ref } from 'vue'
  2. import { storeToRefs } from 'pinia'
  3. import { parse, type Shape, type Element, type ChartItem } from 'pptxtojson'
  4. import { nanoid } from 'nanoid'
  5. import { useSlidesStore } from '@/store'
  6. import { decrypt } from '@/utils/crypto'
  7. import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
  8. import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
  9. import useSlideHandler from '@/hooks/useSlideHandler'
  10. import message from '@/utils/message'
  11. import { getSvgPathRange } from '@/utils/svgPathParser'
  12. import type {
  13. Slide,
  14. TableCellStyle,
  15. TableCell,
  16. ChartType,
  17. SlideBackground,
  18. PPTShapeElement,
  19. PPTLineElement,
  20. ShapeTextAlign,
  21. PPTTextElement,
  22. ChartOptions,
  23. } from '@/types/slides'
  24. const convertFontSizePtToPx = (html: string, ratio: number) => {
  25. return html.replace(/font-size:\s*([\d.]+)pt/g, (match, p1) => {
  26. return `font-size: ${(parseFloat(p1) * ratio).toFixed(1)}px`
  27. })
  28. }
  29. export default () => {
  30. const slidesStore = useSlidesStore()
  31. const { theme } = storeToRefs(useSlidesStore())
  32. const { addSlidesFromData } = useAddSlidesOrElements()
  33. const { isEmptySlide } = useSlideHandler()
  34. const exporting = ref(false)
  35. // 导入pptist文件
  36. const importSpecificFile = (files: FileList, cover = false) => {
  37. const file = files[0]
  38. const reader = new FileReader()
  39. reader.addEventListener('load', () => {
  40. try {
  41. const slides = JSON.parse(decrypt(reader.result as string))
  42. if (cover) {
  43. slidesStore.updateSlideIndex(0)
  44. slidesStore.setSlides(slides)
  45. }
  46. else if (isEmptySlide.value) slidesStore.setSlides(slides)
  47. else addSlidesFromData(slides)
  48. }
  49. catch {
  50. message.error('无法正确读取 / 解析该文件')
  51. }
  52. })
  53. reader.readAsText(file)
  54. }
  55. const parseLineElement = (el: Shape) => {
  56. let start: [number, number] = [0, 0]
  57. let end: [number, number] = [0, 0]
  58. if (!el.isFlipV && !el.isFlipH) { // 右下
  59. start = [0, 0]
  60. end = [el.width, el.height]
  61. }
  62. else if (el.isFlipV && el.isFlipH) { // 左上
  63. start = [el.width, el.height]
  64. end = [0, 0]
  65. }
  66. else if (el.isFlipV && !el.isFlipH) { // 右上
  67. start = [0, el.height]
  68. end = [el.width, 0]
  69. }
  70. else { // 左下
  71. start = [el.width, 0]
  72. end = [0, el.height]
  73. }
  74. const data: PPTLineElement = {
  75. type: 'line',
  76. id: nanoid(10),
  77. width: el.borderWidth || 1,
  78. left: el.left,
  79. top: el.top,
  80. start,
  81. end,
  82. style: el.borderType,
  83. color: el.borderColor,
  84. points: ['', /straightConnector/.test(el.shapType) ? 'arrow' : '']
  85. }
  86. if (/bentConnector/.test(el.shapType)) {
  87. data.broken2 = [
  88. Math.abs(start[0] - end[0]) / 2,
  89. Math.abs(start[1] - end[1]) / 2,
  90. ]
  91. }
  92. return data
  93. }
  94. // 导入PPTX文件
  95. const importPPTXFile = (files: FileList) => {
  96. const file = files[0]
  97. if (!file) return
  98. exporting.value = true
  99. const shapeList: ShapePoolItem[] = []
  100. for (const item of SHAPE_LIST) {
  101. shapeList.push(...item.children)
  102. }
  103. const reader = new FileReader()
  104. reader.onload = async e => {
  105. const json = await parse(e.target!.result as ArrayBuffer)
  106. const ratio = 96 / 72
  107. const width = json.size.width
  108. slidesStore.setViewportSize(width * ratio)
  109. const slides: Slide[] = []
  110. for (const item of json.slides) {
  111. const { type, value } = item.fill
  112. let background: SlideBackground
  113. if (type === 'image') {
  114. background = {
  115. type: 'image',
  116. image: {
  117. src: value.picBase64,
  118. size: 'cover',
  119. },
  120. }
  121. }
  122. else if (type === 'gradient') {
  123. background = {
  124. type: 'gradient',
  125. gradient: {
  126. type: 'linear',
  127. colors: value.colors.map(item => ({
  128. ...item,
  129. pos: parseInt(item.pos),
  130. })),
  131. rotate: value.rot,
  132. },
  133. }
  134. }
  135. else {
  136. background = {
  137. type: 'solid',
  138. color: value,
  139. }
  140. }
  141. const slide: Slide = {
  142. id: nanoid(10),
  143. elements: [],
  144. background,
  145. }
  146. const parseElements = (elements: Element[]) => {
  147. for (const el of elements) {
  148. const originWidth = el.width || 1
  149. const originHeight = el.height || 1
  150. const originLeft = el.left
  151. const originTop = el.top
  152. el.width = el.width * ratio
  153. el.height = el.height * ratio
  154. el.left = el.left * ratio
  155. el.top = el.top * ratio
  156. if (el.type === 'text') {
  157. const textEl: PPTTextElement = {
  158. type: 'text',
  159. id: nanoid(10),
  160. width: el.width,
  161. height: el.height,
  162. left: el.left,
  163. top: el.top,
  164. rotate: el.rotate,
  165. defaultFontName: theme.value.fontName,
  166. defaultColor: theme.value.fontColor,
  167. content: convertFontSizePtToPx(el.content, ratio),
  168. lineHeight: 1,
  169. outline: {
  170. color: el.borderColor,
  171. width: el.borderWidth,
  172. style: el.borderType,
  173. },
  174. fill: el.fillColor,
  175. vertical: el.isVertical,
  176. }
  177. if (el.shadow) {
  178. textEl.shadow = {
  179. h: el.shadow.h * ratio,
  180. v: el.shadow.v * ratio,
  181. blur: el.shadow.blur * ratio,
  182. color: el.shadow.color,
  183. }
  184. }
  185. slide.elements.push(textEl)
  186. }
  187. else if (el.type === 'image') {
  188. slide.elements.push({
  189. type: 'image',
  190. id: nanoid(10),
  191. src: el.src,
  192. width: el.width,
  193. height: el.height,
  194. left: el.left,
  195. top: el.top,
  196. fixedRatio: true,
  197. rotate: el.rotate,
  198. flipH: el.isFlipH,
  199. flipV: el.isFlipV,
  200. })
  201. }
  202. else if (el.type === 'audio') {
  203. slide.elements.push({
  204. type: 'audio',
  205. id: nanoid(10),
  206. src: el.blob,
  207. width: el.width,
  208. height: el.height,
  209. left: el.left,
  210. top: el.top,
  211. rotate: 0,
  212. fixedRatio: false,
  213. color: theme.value.themeColor,
  214. loop: false,
  215. autoplay: false,
  216. })
  217. }
  218. else if (el.type === 'video') {
  219. slide.elements.push({
  220. type: 'video',
  221. id: nanoid(10),
  222. src: (el.blob || el.src)!,
  223. width: el.width,
  224. height: el.height,
  225. left: el.left,
  226. top: el.top,
  227. rotate: 0,
  228. autoplay: false,
  229. })
  230. }
  231. else if (el.type === 'shape') {
  232. if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
  233. const lineElement = parseLineElement(el)
  234. slide.elements.push(lineElement)
  235. }
  236. else {
  237. const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
  238. const vAlignMap: { [key: string]: ShapeTextAlign } = {
  239. 'mid': 'middle',
  240. 'down': 'bottom',
  241. 'up': 'top',
  242. }
  243. const element: PPTShapeElement = {
  244. type: 'shape',
  245. id: nanoid(10),
  246. width: el.width,
  247. height: el.height,
  248. left: el.left,
  249. top: el.top,
  250. viewBox: [200, 200],
  251. path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
  252. fill: el.fillColor || 'none',
  253. fixedRatio: false,
  254. rotate: el.rotate,
  255. outline: {
  256. color: el.borderColor,
  257. width: el.borderWidth,
  258. style: el.borderType,
  259. },
  260. text: {
  261. content: convertFontSizePtToPx(el.content, ratio),
  262. defaultFontName: theme.value.fontName,
  263. defaultColor: theme.value.fontColor,
  264. align: vAlignMap[el.vAlign] || 'middle',
  265. },
  266. flipH: el.isFlipH,
  267. flipV: el.isFlipV,
  268. }
  269. if (el.shadow) {
  270. element.shadow = {
  271. h: el.shadow.h * ratio,
  272. v: el.shadow.v * ratio,
  273. blur: el.shadow.blur * ratio,
  274. color: el.shadow.color,
  275. }
  276. }
  277. if (shape) {
  278. element.path = shape.path
  279. element.viewBox = shape.viewBox
  280. if (shape.pathFormula) {
  281. element.pathFormula = shape.pathFormula
  282. element.viewBox = [el.width, el.height]
  283. const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
  284. if ('editable' in pathFormula && pathFormula.editable) {
  285. element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
  286. element.keypoints = pathFormula.defaultValue
  287. }
  288. else element.path = pathFormula.formula(el.width, el.height)
  289. }
  290. }
  291. if (el.shapType === 'custom') {
  292. if (el.path!.indexOf('NaN') !== -1) element.path = ''
  293. else {
  294. element.special = true
  295. element.path = el.path!
  296. const { maxX, maxY } = getSvgPathRange(element.path)
  297. element.viewBox = [maxX || originWidth, maxY || originHeight]
  298. }
  299. }
  300. if (element.path) slide.elements.push(element)
  301. }
  302. }
  303. else if (el.type === 'table') {
  304. const row = el.data.length
  305. const col = el.data[0].length
  306. const style: TableCellStyle = {
  307. fontname: theme.value.fontName,
  308. color: theme.value.fontColor,
  309. }
  310. const data: TableCell[][] = []
  311. for (let i = 0; i < row; i++) {
  312. const rowCells: TableCell[] = []
  313. for (let j = 0; j < col; j++) {
  314. const cellData = el.data[i][j]
  315. let textDiv: HTMLDivElement | null = document.createElement('div')
  316. textDiv.innerHTML = cellData.text
  317. const p = textDiv.querySelector('p')
  318. const align = p?.style.textAlign || 'left'
  319. const span = textDiv.querySelector('span')
  320. const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : ''
  321. const fontname = span?.style.fontFamily || ''
  322. const color = span?.style.color || cellData.fontColor
  323. rowCells.push({
  324. id: nanoid(10),
  325. colspan: cellData.colSpan || 1,
  326. rowspan: cellData.rowSpan || 1,
  327. text: textDiv.innerText,
  328. style: {
  329. ...style,
  330. align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
  331. fontsize,
  332. fontname,
  333. color,
  334. bold: cellData.fontBold,
  335. backcolor: cellData.fillColor,
  336. },
  337. })
  338. textDiv = null
  339. }
  340. data.push(rowCells)
  341. }
  342. const colWidths: number[] = new Array(col).fill(1 / col)
  343. slide.elements.push({
  344. type: 'table',
  345. id: nanoid(10),
  346. width: el.width,
  347. height: el.height,
  348. left: el.left,
  349. top: el.top,
  350. colWidths,
  351. rotate: 0,
  352. data,
  353. outline: {
  354. width: el.borderWidth || 2,
  355. style: el.borderType,
  356. color: el.borderColor || '#eeece1',
  357. },
  358. cellMinHeight: 36,
  359. })
  360. }
  361. else if (el.type === 'chart') {
  362. let labels: string[]
  363. let legends: string[]
  364. let series: number[][]
  365. if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
  366. labels = el.data[0].map((item, index) => `坐标${index + 1}`)
  367. legends = ['X', 'Y']
  368. series = el.data
  369. }
  370. else {
  371. const data = el.data as ChartItem[]
  372. labels = Object.values(data[0].xlabels)
  373. legends = data.map(item => item.key)
  374. series = data.map(item => item.values.map(v => v.y))
  375. }
  376. const options: ChartOptions = {}
  377. let chartType: ChartType = 'bar'
  378. switch (el.chartType) {
  379. case 'barChart':
  380. case 'bar3DChart':
  381. chartType = 'bar'
  382. if (el.barDir === 'bar') chartType = 'column'
  383. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  384. break
  385. case 'lineChart':
  386. case 'line3DChart':
  387. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  388. chartType = 'line'
  389. break
  390. case 'areaChart':
  391. case 'area3DChart':
  392. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  393. chartType = 'area'
  394. break
  395. case 'scatterChart':
  396. case 'bubbleChart':
  397. chartType = 'scatter'
  398. break
  399. case 'pieChart':
  400. case 'pie3DChart':
  401. chartType = 'pie'
  402. break
  403. case 'radarChart':
  404. chartType = 'radar'
  405. break
  406. case 'doughnutChart':
  407. chartType = 'ring'
  408. break
  409. default:
  410. }
  411. slide.elements.push({
  412. type: 'chart',
  413. id: nanoid(10),
  414. chartType: chartType,
  415. width: el.width,
  416. height: el.height,
  417. left: el.left,
  418. top: el.top,
  419. rotate: 0,
  420. themeColors: [theme.value.themeColor],
  421. textColor: theme.value.fontColor,
  422. data: {
  423. labels,
  424. legends,
  425. series,
  426. },
  427. options,
  428. })
  429. }
  430. else if (el.type === 'group' || el.type === 'diagram') {
  431. const elements = el.elements.map(_el => ({
  432. ..._el,
  433. left: _el.left + originLeft,
  434. top: _el.top + originTop,
  435. }))
  436. parseElements(elements)
  437. }
  438. }
  439. }
  440. parseElements(item.elements)
  441. slides.push(slide)
  442. }
  443. slidesStore.updateSlideIndex(0)
  444. slidesStore.setSlides(slides)
  445. exporting.value = false
  446. }
  447. reader.readAsArrayBuffer(file)
  448. }
  449. return {
  450. importSpecificFile,
  451. importPPTXFile,
  452. exporting,
  453. }
  454. }