useImport.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. import { ref, nextTick } from 'vue'
  2. import { storeToRefs } from 'pinia'
  3. import { parse, type Shape, type Element, type ChartItem, type BaseElement } 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 useHistorySnapshot from './useHistorySnapshot'
  11. import message from '@/utils/message'
  12. import { getSvgPathRange } from '@/utils/svgPathParser'
  13. import type {
  14. Slide,
  15. TableCellStyle,
  16. TableCell,
  17. ChartType,
  18. SlideBackground,
  19. PPTShapeElement,
  20. PPTLineElement,
  21. PPTImageElement,
  22. ShapeTextAlign,
  23. PPTTextElement,
  24. ChartOptions,
  25. Gradient,
  26. } from '@/types/slides'
  27. const convertFontSizePtToPx = (html: string, ratio: number) => {
  28. //return html;
  29. return html.replace(/font-size:\s*([\d.]+)pt/g, (match, p1) => {
  30. return `font-size: ${(parseFloat(p1) * ratio) | 0}px`
  31. })
  32. }
  33. export default () => {
  34. const slidesStore = useSlidesStore()
  35. const { slides, theme, viewportRatio, title, viewportSize } = storeToRefs(useSlidesStore())
  36. const { addHistorySnapshot } = useHistorySnapshot()
  37. const { addSlidesFromData } = useAddSlidesOrElements()
  38. const { isEmptySlide } = useSlideHandler()
  39. const exporting = ref(false)
  40. // 导入JSON文件
  41. const importJSON = (files: FileList, cover = false) => {
  42. const file = files[0]
  43. const reader = new FileReader()
  44. reader.addEventListener('load', () => {
  45. try {
  46. const { slides } = JSON.parse(reader.result as string)
  47. if (cover) {
  48. slidesStore.updateSlideIndex(0)
  49. slidesStore.setSlides(slides)
  50. addHistorySnapshot()
  51. }
  52. else if (isEmptySlide.value) {
  53. slidesStore.setSlides(slides)
  54. addHistorySnapshot()
  55. }
  56. else addSlidesFromData(slides)
  57. }
  58. catch {
  59. message.error('无法正确读取 / 解析该文件')
  60. }
  61. })
  62. reader.readAsText(file)
  63. }
  64. // 直接读取JSON功能,暴露到window.readJSON
  65. const readJSON = (jsonData: string | any, cover = false) => {
  66. try {
  67. console.log('readJSON 开始执行:', { jsonData, cover })
  68. let parsedData
  69. if (typeof jsonData === 'string') {
  70. parsedData = JSON.parse(jsonData)
  71. console.log('解析字符串后的数据:', parsedData)
  72. }
  73. else {
  74. parsedData = jsonData
  75. }
  76. // 提取所有可能的数据
  77. const slides = parsedData.slides || parsedData
  78. const title = parsedData.title
  79. const theme = parsedData.theme
  80. const width = parsedData.width
  81. const height = parsedData.height
  82. const viewportRatio = parsedData.viewportRatio || (height && width ? height / width : undefined)
  83. console.log('提取的数据:', { slides: slides.length, title, theme, width, height, viewportRatio })
  84. // 更新幻灯片数据
  85. if (cover) {
  86. console.log('覆盖模式:更新幻灯片数据')
  87. slidesStore.updateSlideIndex(0)
  88. slidesStore.setSlides(slides)
  89. addHistorySnapshot()
  90. }
  91. else if (isEmptySlide.value) {
  92. console.log('空幻灯片模式:更新幻灯片数据')
  93. slidesStore.setSlides(slides)
  94. addHistorySnapshot()
  95. }
  96. else {
  97. console.log('添加模式:添加幻灯片数据')
  98. addSlidesFromData(slides)
  99. }
  100. // 同步更新其他相关内容
  101. if (title !== undefined) {
  102. console.log('正在更新标题:', title)
  103. slidesStore.setTitle(title)
  104. console.log('标题更新完成')
  105. }
  106. if (theme !== undefined) {
  107. console.log('正在更新主题:', theme)
  108. slidesStore.setTheme(theme)
  109. console.log('主题更新完成')
  110. }
  111. // 更新视口尺寸(如果提供了的话)
  112. if (width !== undefined && height !== undefined) {
  113. console.log('正在触发视口尺寸更新事件:', { width, height, viewportRatio })
  114. // 同时也要更新slidesStore中的相关数据
  115. if (slidesStore.setViewportSize) {
  116. console.log('正在更新store中的视口尺寸')
  117. slidesStore.setViewportSize(width)
  118. if (slidesStore.setViewportRatio && viewportRatio !== undefined) {
  119. slidesStore.setViewportRatio(viewportRatio)
  120. console.log('视口比例已更新:', viewportRatio)
  121. }
  122. }
  123. window.dispatchEvent(new CustomEvent('viewportSizeUpdated', {
  124. detail: { width, height, viewportRatio }
  125. }))
  126. console.log('视口尺寸更新事件已触发')
  127. }
  128. // 导入成功后,触发画布尺寸更新
  129. // 使用 nextTick 确保DOM更新完成后再触发
  130. console.log('开始触发画布尺寸更新事件...')
  131. nextTick(() => {
  132. console.log('DOM更新完成,触发 slidesDataUpdated 事件')
  133. // 触发自定义事件,通知需要更新画布尺寸的组件
  134. window.dispatchEvent(new CustomEvent('slidesDataUpdated', {
  135. detail: {
  136. slides,
  137. cover,
  138. title,
  139. theme,
  140. width,
  141. height,
  142. viewportRatio,
  143. timestamp: Date.now()
  144. }
  145. }))
  146. console.log('slidesDataUpdated 事件已触发')
  147. // 检查并调整幻灯片索引,确保在有效范围内
  148. const newSlideCount = slides.length
  149. const currentIndex = slidesStore.slideIndex
  150. if (currentIndex >= newSlideCount) {
  151. console.log('调整幻灯片索引:', currentIndex, '->', Math.max(0, newSlideCount - 1))
  152. slidesStore.updateSlideIndex(Math.max(0, newSlideCount - 1))
  153. }
  154. console.log('画布尺寸更新事件处理完成')
  155. })
  156. console.log('readJSON 执行成功')
  157. return { success: true, slides, title, theme, width, height, viewportRatio }
  158. }
  159. catch (error) {
  160. console.error('readJSON 执行失败:', error)
  161. const errorMsg = '无法正确读取 / 解析该JSON数据'
  162. message.error(errorMsg)
  163. return { success: false, error: errorMsg, details: error }
  164. }
  165. }
  166. // 导出JSON文件
  167. const exportJSON2 = () => {
  168. const json = {
  169. title: title.value,
  170. width: viewportSize.value,
  171. height: viewportSize.value * viewportRatio.value,
  172. theme: theme.value,
  173. slides: slides.value,
  174. }
  175. return json
  176. }
  177. // 优化暴露到 window 对象的方式,避免重复赋值
  178. if (typeof window !== 'undefined') {
  179. const win = window as any
  180. if (!win.exportJSON) win.exportJSON = exportJSON2
  181. if (!win.readJSON) win.readJSON = readJSON
  182. }
  183. // 导入pptist文件
  184. const importSpecificFile = (files: FileList, cover = false) => {
  185. const file = files[0]
  186. const reader = new FileReader()
  187. reader.addEventListener('load', () => {
  188. try {
  189. const { slides } = JSON.parse(decrypt(reader.result as string))
  190. if (cover) {
  191. slidesStore.updateSlideIndex(0)
  192. slidesStore.setSlides(slides)
  193. addHistorySnapshot()
  194. }
  195. else if (isEmptySlide.value) {
  196. slidesStore.setSlides(slides)
  197. addHistorySnapshot()
  198. }
  199. else addSlidesFromData(slides)
  200. }
  201. catch {
  202. message.error('无法正确读取 / 解析该文件')
  203. }
  204. })
  205. reader.readAsText(file)
  206. }
  207. const rotateLine = (line: PPTLineElement, angleDeg: number) => {
  208. const { start, end } = line
  209. const angleRad = angleDeg * Math.PI / 180
  210. const midX = (start[0] + end[0]) / 2
  211. const midY = (start[1] + end[1]) / 2
  212. const startTransX = start[0] - midX
  213. const startTransY = start[1] - midY
  214. const endTransX = end[0] - midX
  215. const endTransY = end[1] - midY
  216. const cosA = Math.cos(angleRad)
  217. const sinA = Math.sin(angleRad)
  218. const startRotX = startTransX * cosA - startTransY * sinA
  219. const startRotY = startTransX * sinA + startTransY * cosA
  220. const endRotX = endTransX * cosA - endTransY * sinA
  221. const endRotY = endTransX * sinA + endTransY * cosA
  222. const startNewX = startRotX + midX
  223. const startNewY = startRotY + midY
  224. const endNewX = endRotX + midX
  225. const endNewY = endRotY + midY
  226. const beforeMinX = Math.min(start[0], end[0])
  227. const beforeMinY = Math.min(start[1], end[1])
  228. const afterMinX = Math.min(startNewX, endNewX)
  229. const afterMinY = Math.min(startNewY, endNewY)
  230. const startAdjustedX = startNewX - afterMinX
  231. const startAdjustedY = startNewY - afterMinY
  232. const endAdjustedX = endNewX - afterMinX
  233. const endAdjustedY = endNewY - afterMinY
  234. const startAdjusted: [number, number] = [startAdjustedX, startAdjustedY]
  235. const endAdjusted: [number, number] = [endAdjustedX, endAdjustedY]
  236. const offset = [afterMinX - beforeMinX, afterMinY - beforeMinY]
  237. return {
  238. start: startAdjusted,
  239. end: endAdjusted,
  240. offset,
  241. }
  242. }
  243. const parseLineElement = (el: Shape, ratio: number) => {
  244. let start: [number, number] = [0, 0]
  245. let end: [number, number] = [0, 0]
  246. if (!el.isFlipV && !el.isFlipH) { // 右下
  247. start = [0, 0]
  248. end = [el.width, el.height]
  249. }
  250. else if (el.isFlipV && el.isFlipH) { // 左上
  251. start = [el.width, el.height]
  252. end = [0, 0]
  253. }
  254. else if (el.isFlipV && !el.isFlipH) { // 右上
  255. start = [0, el.height]
  256. end = [el.width, 0]
  257. }
  258. else { // 左下
  259. start = [el.width, 0]
  260. end = [0, el.height]
  261. }
  262. const data: PPTLineElement = {
  263. type: 'line',
  264. id: nanoid(10),
  265. width: +((el.borderWidth || 1) * ratio).toFixed(2),
  266. left: el.left,
  267. top: el.top,
  268. start,
  269. end,
  270. style: el.borderType,
  271. color: el.borderColor,
  272. points: ['', /straightConnector/.test(el.shapType) ? 'arrow' : '']
  273. }
  274. if (el.rotate) {
  275. const { start, end, offset } = rotateLine(data, el.rotate)
  276. data.start = start
  277. data.end = end
  278. data.left = data.left + offset[0]
  279. data.top = data.top + offset[1]
  280. }
  281. if (/bentConnector/.test(el.shapType)) {
  282. data.broken2 = [
  283. Math.abs(data.start[0] - data.end[0]) / 2,
  284. Math.abs(data.start[1] - data.end[1]) / 2,
  285. ]
  286. }
  287. if (/curvedConnector/.test(el.shapType)) {
  288. const cubic: [number, number] = [
  289. Math.abs(data.start[0] - data.end[0]) / 2,
  290. Math.abs(data.start[1] - data.end[1]) / 2,
  291. ]
  292. data.cubic = [cubic, cubic]
  293. }
  294. return data
  295. }
  296. const flipGroupElements = (elements: BaseElement[], axis: 'x' | 'y') => {
  297. const minX = Math.min(...elements.map(el => el.left))
  298. const maxX = Math.max(...elements.map(el => el.left + el.width))
  299. const minY = Math.min(...elements.map(el => el.top))
  300. const maxY = Math.max(...elements.map(el => el.top + el.height))
  301. const centerX = (minX + maxX) / 2
  302. const centerY = (minY + maxY) / 2
  303. return elements.map(element => {
  304. const newElement = { ...element }
  305. if (axis === 'y') newElement.left = 2 * centerX - element.left - element.width
  306. if (axis === 'x') newElement.top = 2 * centerY - element.top - element.height
  307. return newElement
  308. })
  309. }
  310. const calculateRotatedPosition = (
  311. x: number,
  312. y: number,
  313. w: number,
  314. h: number,
  315. ox: number,
  316. oy: number,
  317. k: number,
  318. ) => {
  319. const radians = k * (Math.PI / 180)
  320. const containerCenterX = x + w / 2
  321. const containerCenterY = y + h / 2
  322. const relativeX = ox - w / 2
  323. const relativeY = oy - h / 2
  324. const rotatedX = relativeX * Math.cos(radians) + relativeY * Math.sin(radians)
  325. const rotatedY = -relativeX * Math.sin(radians) + relativeY * Math.cos(radians)
  326. const graphicX = containerCenterX + rotatedX
  327. const graphicY = containerCenterY + rotatedY
  328. return { x: graphicX, y: graphicY }
  329. }
  330. // 导入PPTX文件
  331. const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean }) => {
  332. console.log('导入', files)
  333. const defaultOptions = {
  334. cover: false,
  335. fixedViewport: false,
  336. }
  337. const { cover, fixedViewport } = { ...defaultOptions, ...options }
  338. const file = files[0]
  339. if (!file) return
  340. exporting.value = true
  341. const shapeList: ShapePoolItem[] = []
  342. for (const item of SHAPE_LIST) {
  343. shapeList.push(...item.children)
  344. }
  345. const reader = new FileReader()
  346. reader.onload = async e => {
  347. let json = null
  348. try {
  349. json = await parse(e.target!.result as ArrayBuffer)
  350. }
  351. catch (error) {
  352. exporting.value = false
  353. console.log('导入PPTX文件失败:', error)
  354. message.error('无法正确读取 / 解析该文件')
  355. return
  356. }
  357. let ratio = 96 / 72;
  358. //let ratio = 1
  359. const width = json.size.width
  360. if (fixedViewport) ratio = 1000 / width
  361. else slidesStore.setViewportSize(width * ratio)
  362. slidesStore.setTheme({ themeColors: json.themeColors })
  363. const slides: Slide[] = []
  364. for (const item of json.slides) {
  365. const { type, value } = item.fill
  366. let background: SlideBackground
  367. if (type === 'image') {
  368. background = {
  369. type: 'image',
  370. image: {
  371. src: value.picBase64,
  372. size: 'cover',
  373. },
  374. }
  375. }
  376. else if (type === 'gradient') {
  377. background = {
  378. type: 'gradient',
  379. gradient: {
  380. type: value.path === 'line' ? 'linear' : 'radial',
  381. colors: value.colors.map(item => ({
  382. ...item,
  383. pos: parseInt(item.pos),
  384. })),
  385. rotate: value.rot + 90,
  386. },
  387. }
  388. }
  389. else {
  390. background = {
  391. type: 'solid',
  392. color: value || '#fff',
  393. }
  394. }
  395. const slide: Slide = {
  396. id: nanoid(10),
  397. elements: [],
  398. background,
  399. remark: item.note || '',
  400. }
  401. const parseElements = (elements: Element[]) => {
  402. const sortedElements = elements.sort((a, b) => a.order - b.order)
  403. console.log(sortedElements)
  404. for (const el of sortedElements) {
  405. const originWidth = el.width || 1
  406. const originHeight = el.height || 1
  407. const originLeft = el.left
  408. const originTop = el.top
  409. el.width = el.width * ratio
  410. el.height = el.height * ratio
  411. el.left = el.left * ratio
  412. el.top = el.top * ratio
  413. if (el.type === 'text') {
  414. const textEl: PPTTextElement = {
  415. type: 'text',
  416. id: nanoid(10),
  417. width: el.width,
  418. height: el.height,
  419. left: el.left,
  420. top: el.top,
  421. rotate: el.rotate,
  422. defaultFontName: theme.value.fontName,
  423. defaultColor: theme.value.fontColor,
  424. content: convertFontSizePtToPx(el.content, ratio),
  425. lineHeight: 1,
  426. outline: {
  427. color: el.borderColor,
  428. width: +(el.borderWidth * ratio).toFixed(2),
  429. style: el.borderType,
  430. },
  431. fill: el.fill.type === 'color' ? el.fill.value : '',
  432. vertical: el.isVertical,
  433. }
  434. if (el.shadow) {
  435. textEl.shadow = {
  436. h: el.shadow.h * ratio,
  437. v: el.shadow.v * ratio,
  438. blur: el.shadow.blur * ratio,
  439. color: el.shadow.color,
  440. }
  441. }
  442. slide.elements.push(textEl)
  443. }
  444. else if (el.type === 'image') {
  445. const element: PPTImageElement = {
  446. type: 'image',
  447. id: nanoid(10),
  448. src: el.src,
  449. width: el.width,
  450. height: el.height,
  451. left: el.left,
  452. top: el.top,
  453. fixedRatio: true,
  454. rotate: el.rotate,
  455. flipH: el.isFlipH,
  456. flipV: el.isFlipV,
  457. }
  458. if (el.borderWidth) {
  459. element.outline = {
  460. color: el.borderColor,
  461. width: +(el.borderWidth * ratio).toFixed(2),
  462. style: el.borderType,
  463. }
  464. }
  465. const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid']
  466. if (el.rect) {
  467. element.clip = {
  468. shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
  469. range: [
  470. [
  471. el.rect.l || 0,
  472. el.rect.t || 0,
  473. ],
  474. [
  475. 100 - (el.rect.r || 0),
  476. 100 - (el.rect.b || 0),
  477. ],
  478. ]
  479. }
  480. }
  481. else if (el.geom && clipShapeTypes.includes(el.geom)) {
  482. element.clip = {
  483. shape: el.geom,
  484. range: [[0, 0], [100, 100]]
  485. }
  486. }
  487. slide.elements.push(element)
  488. }
  489. else if (el.type === 'math') {
  490. slide.elements.push({
  491. type: 'image',
  492. id: nanoid(10),
  493. src: el.picBase64,
  494. width: el.width,
  495. height: el.height,
  496. left: el.left,
  497. top: el.top,
  498. fixedRatio: true,
  499. rotate: 0,
  500. })
  501. }
  502. else if (el.type === 'audio') {
  503. slide.elements.push({
  504. type: 'audio',
  505. id: nanoid(10),
  506. src: el.blob,
  507. width: el.width,
  508. height: el.height,
  509. left: el.left,
  510. top: el.top,
  511. rotate: 0,
  512. fixedRatio: false,
  513. color: theme.value.themeColors[0],
  514. loop: false,
  515. autoplay: false,
  516. })
  517. }
  518. else if (el.type === 'video') {
  519. slide.elements.push({
  520. type: 'video',
  521. id: nanoid(10),
  522. src: (el.blob || el.src)!,
  523. width: el.width,
  524. height: el.height,
  525. left: el.left,
  526. top: el.top,
  527. rotate: 0,
  528. autoplay: false,
  529. })
  530. }
  531. else if (el.type === 'shape') {
  532. if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
  533. const lineElement = parseLineElement(el, ratio)
  534. slide.elements.push(lineElement)
  535. }
  536. else {
  537. const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
  538. const vAlignMap: { [key: string]: ShapeTextAlign } = {
  539. 'mid': 'middle',
  540. 'down': 'bottom',
  541. 'up': 'top',
  542. }
  543. const gradient: Gradient | undefined = el.fill?.type === 'gradient' ? {
  544. type: el.fill.value.path === 'line' ? 'linear' : 'radial',
  545. colors: el.fill.value.colors.map(item => ({
  546. ...item,
  547. pos: parseInt(item.pos),
  548. })),
  549. rotate: el.fill.value.rot,
  550. } : undefined
  551. const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
  552. const fill = el.fill?.type === 'color' ? el.fill.value : ''
  553. const element: PPTShapeElement = {
  554. type: 'shape',
  555. id: nanoid(10),
  556. width: el.width,
  557. height: el.height,
  558. left: el.left,
  559. top: el.top,
  560. viewBox: [200, 200],
  561. path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
  562. fill,
  563. gradient,
  564. pattern,
  565. fixedRatio: false,
  566. rotate: el.rotate,
  567. outline: {
  568. color: el.borderColor,
  569. width: +(el.borderWidth * ratio).toFixed(2),
  570. style: el.borderType,
  571. },
  572. text: {
  573. content: convertFontSizePtToPx(el.content, ratio),
  574. defaultFontName: theme.value.fontName,
  575. defaultColor: theme.value.fontColor,
  576. align: vAlignMap[el.vAlign] || 'middle',
  577. },
  578. flipH: el.isFlipH,
  579. flipV: el.isFlipV,
  580. }
  581. if (el.shadow) {
  582. element.shadow = {
  583. h: el.shadow.h * ratio,
  584. v: el.shadow.v * ratio,
  585. blur: el.shadow.blur * ratio,
  586. color: el.shadow.color,
  587. }
  588. }
  589. if (shape) {
  590. element.path = shape.path
  591. element.viewBox = shape.viewBox
  592. if (shape.pathFormula) {
  593. element.pathFormula = shape.pathFormula
  594. element.viewBox = [el.width, el.height]
  595. const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
  596. if ('editable' in pathFormula && pathFormula.editable) {
  597. element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
  598. element.keypoints = pathFormula.defaultValue
  599. }
  600. else element.path = pathFormula.formula(el.width, el.height)
  601. }
  602. }
  603. else if (el.path && el.path.indexOf('NaN') === -1) {
  604. const { maxX, maxY } = getSvgPathRange(el.path)
  605. element.path = el.path
  606. element.viewBox = [maxX || originWidth, maxY || originHeight]
  607. }
  608. if (el.shapType === 'custom') {
  609. if (el.path!.indexOf('NaN') !== -1) {
  610. if (element.width === 0) element.width = 0.1
  611. if (element.height === 0) element.height = 0.1
  612. element.path = el.path!.replace(/NaN/g, '0')
  613. }
  614. else {
  615. element.special = true
  616. element.path = el.path!
  617. }
  618. const { maxX, maxY } = getSvgPathRange(element.path)
  619. element.viewBox = [maxX || originWidth, maxY || originHeight]
  620. }
  621. if (element.path) slide.elements.push(element)
  622. }
  623. }
  624. else if (el.type === 'table') {
  625. const row = el.data.length
  626. const col = el.data[0].length
  627. const style: TableCellStyle = {
  628. fontname: theme.value.fontName,
  629. color: theme.value.fontColor,
  630. }
  631. const data: TableCell[][] = []
  632. for (let i = 0; i < row; i++) {
  633. const rowCells: TableCell[] = []
  634. for (let j = 0; j < col; j++) {
  635. const cellData = el.data[i][j]
  636. let textDiv: HTMLDivElement | null = document.createElement('div')
  637. textDiv.innerHTML = cellData.text
  638. const p = textDiv.querySelector('p')
  639. const align = p?.style.textAlign || 'left'
  640. const span = textDiv.querySelector('span')
  641. const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : ''
  642. const fontname = span?.style.fontFamily || ''
  643. const color = span?.style.color || cellData.fontColor
  644. rowCells.push({
  645. id: nanoid(10),
  646. colspan: cellData.colSpan || 1,
  647. rowspan: cellData.rowSpan || 1,
  648. text: textDiv.innerText,
  649. style: {
  650. ...style,
  651. align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
  652. fontsize,
  653. fontname,
  654. color,
  655. bold: cellData.fontBold,
  656. backcolor: cellData.fillColor,
  657. },
  658. })
  659. textDiv = null
  660. }
  661. data.push(rowCells)
  662. }
  663. const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
  664. const colWidths: number[] = el.colWidths.map(item => item / allWidth)
  665. const firstCell = el.data[0][0]
  666. const border = firstCell.borders.top ||
  667. firstCell.borders.bottom ||
  668. el.borders.top ||
  669. el.borders.bottom ||
  670. firstCell.borders.left ||
  671. firstCell.borders.right ||
  672. el.borders.left ||
  673. el.borders.right
  674. const borderWidth = border?.borderWidth || 0
  675. const borderStyle = border?.borderType || 'solid'
  676. const borderColor = border?.borderColor || '#eeece1'
  677. slide.elements.push({
  678. type: 'table',
  679. id: nanoid(10),
  680. width: el.width,
  681. height: el.height,
  682. left: el.left,
  683. top: el.top,
  684. colWidths,
  685. rotate: 0,
  686. data,
  687. outline: {
  688. width: +(borderWidth * ratio || 2).toFixed(2),
  689. style: borderStyle,
  690. color: borderColor,
  691. },
  692. cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
  693. })
  694. }
  695. else if (el.type === 'chart') {
  696. let labels: string[]
  697. let legends: string[]
  698. let series: number[][]
  699. if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
  700. labels = el.data[0].map((item, index) => `坐标${index + 1}`)
  701. legends = ['X', 'Y']
  702. series = el.data
  703. }
  704. else {
  705. const data = el.data as ChartItem[]
  706. labels = Object.values(data[0].xlabels)
  707. legends = data.map(item => item.key)
  708. series = data.map(item => item.values.map(v => v.y))
  709. }
  710. const options: ChartOptions = {}
  711. let chartType: ChartType = 'bar'
  712. switch (el.chartType) {
  713. case 'barChart':
  714. case 'bar3DChart':
  715. chartType = 'bar'
  716. if (el.barDir === 'bar') chartType = 'column'
  717. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  718. break
  719. case 'lineChart':
  720. case 'line3DChart':
  721. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  722. chartType = 'line'
  723. break
  724. case 'areaChart':
  725. case 'area3DChart':
  726. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  727. chartType = 'area'
  728. break
  729. case 'scatterChart':
  730. case 'bubbleChart':
  731. chartType = 'scatter'
  732. break
  733. case 'pieChart':
  734. case 'pie3DChart':
  735. chartType = 'pie'
  736. break
  737. case 'radarChart':
  738. chartType = 'radar'
  739. break
  740. case 'doughnutChart':
  741. chartType = 'ring'
  742. break
  743. default:
  744. }
  745. slide.elements.push({
  746. type: 'chart',
  747. id: nanoid(10),
  748. chartType: chartType,
  749. width: el.width,
  750. height: el.height,
  751. left: el.left,
  752. top: el.top,
  753. rotate: 0,
  754. themeColors: el.colors.length ? el.colors : theme.value.themeColors,
  755. textColor: theme.value.fontColor,
  756. data: {
  757. labels,
  758. legends,
  759. series,
  760. },
  761. options,
  762. })
  763. }
  764. else if (el.type === 'group') {
  765. let elements: BaseElement[] = el.elements.map(_el => {
  766. let left = _el.left + originLeft
  767. let top = _el.top + originTop
  768. if (el.rotate) {
  769. const { x, y } = calculateRotatedPosition(originLeft, originTop, originWidth, originHeight, _el.left, _el.top, el.rotate)
  770. left = x
  771. top = y
  772. }
  773. const element = {
  774. ..._el,
  775. left,
  776. top,
  777. }
  778. if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
  779. if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
  780. return element
  781. })
  782. if (el.isFlipH) elements = flipGroupElements(elements, 'y')
  783. if (el.isFlipV) elements = flipGroupElements(elements, 'x')
  784. parseElements(elements)
  785. }
  786. else if (el.type === 'diagram') {
  787. const elements = el.elements.map(_el => ({
  788. ..._el,
  789. left: _el.left + originLeft,
  790. top: _el.top + originTop,
  791. }))
  792. parseElements(elements)
  793. }
  794. }
  795. }
  796. parseElements([...item.elements, ...item.layoutElements])
  797. slides.push(slide)
  798. }
  799. if (cover) {
  800. slidesStore.updateSlideIndex(0)
  801. slidesStore.setSlides(slides)
  802. addHistorySnapshot()
  803. }
  804. else if (isEmptySlide.value) {
  805. slidesStore.setSlides(slides)
  806. addHistorySnapshot()
  807. }
  808. else addSlidesFromData(slides)
  809. exporting.value = false
  810. }
  811. reader.readAsArrayBuffer(file)
  812. }
  813. const getFile = (url: string): Promise<{ data: any }> => {
  814. return new Promise((resolve, reject) => {
  815. // 检查 AWS SDK 是否可用
  816. if (typeof window !== 'undefined' && !window.AWS) {
  817. reject(new Error('AWS SDK not available'))
  818. return
  819. }
  820. const credentials = {
  821. accessKeyId: 'AKIATLPEDU37QV5CHLMH',
  822. secretAccessKey: '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',
  823. } // 秘钥形式的登录上传
  824. window.AWS.config.update(credentials)
  825. window.AWS.config.region = 'cn-northwest-1' // 设置区域
  826. const s3 = new window.AWS.S3({ params: { Bucket: 'ccrb' } })
  827. // 解析文件名
  828. const bucketUrl = 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/'
  829. if (!url.startsWith(bucketUrl)) {
  830. reject(new Error('Invalid S3 URL format'))
  831. return
  832. }
  833. const name = decodeURIComponent(url.split(bucketUrl)[1])
  834. // const name = url.split(bucketUrl)[1]
  835. console.log('aws-name:', name)
  836. if (!name) {
  837. reject(new Error('Could not extract file name from URL'))
  838. return
  839. }
  840. const params = {
  841. Bucket: 'ccrb',
  842. Key: name,
  843. }
  844. s3.getObject(params, (err: any, data: any) => {
  845. if (err) {
  846. console.error('S3 getObject error:', err, err.stack)
  847. reject(err)
  848. }
  849. else {
  850. console.log('S3 getObject success:', data)
  851. resolve({ data: data.Body })
  852. }
  853. })
  854. })
  855. }
  856. const getFile2 = (url: string): Promise<{ data: any }> => {
  857. return new Promise((resolve, reject) => {
  858. console.log('直接使用原始 URL 获取文件:', url)
  859. // 直接使用 fetch 获取文件,浏览器会自动处理 URL 解码
  860. fetch(url)
  861. .then(response => {
  862. if (!response.ok) {
  863. console.error('HTTP 错误:', response.status, response.statusText)
  864. throw new Error(`HTTP error! status: ${response.status}`)
  865. }
  866. console.log('文件获取成功,大小:', response.headers.get('content-length'))
  867. return response.arrayBuffer()
  868. })
  869. .then(buffer => {
  870. console.log('文件内容读取成功,大小:', buffer.byteLength)
  871. resolve({ data: buffer })
  872. })
  873. .catch(error => {
  874. console.error('Fetch error:', error)
  875. reject(error)
  876. })
  877. })
  878. }
  879. return {
  880. importSpecificFile,
  881. importJSON,
  882. importPPTXFile,
  883. readJSON,
  884. exportJSON2,
  885. exporting,
  886. getFile,
  887. getFile2
  888. }
  889. }