useImport.ts 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020
  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 { lang } from '@/main'
  7. import { decrypt } from '@/utils/crypto'
  8. import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
  9. import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
  10. import useSlideHandler from '@/hooks/useSlideHandler'
  11. import useHistorySnapshot from './useHistorySnapshot'
  12. import message from '@/utils/message'
  13. import { getSvgPathRange } from '@/utils/svgPathParser'
  14. import type {
  15. Slide,
  16. TableCellStyle,
  17. TableCell,
  18. ChartType,
  19. SlideBackground,
  20. PPTShapeElement,
  21. PPTLineElement,
  22. PPTImageElement,
  23. ShapeTextAlign,
  24. PPTTextElement,
  25. PPTVideoElement,
  26. PPTAudioElement,
  27. ChartOptions,
  28. Gradient,
  29. } from '@/types/slides'
  30. const convertFontSizePtToPx = (html: string, ratio: number) => {
  31. // return html;
  32. return html.replace(/\s*([\d.]+)pt/g, (match, p1) => {
  33. return `${(parseFloat(p1) * ratio - 1) | 0}px `
  34. })
  35. }
  36. const getStyle = (htmlString: string) => {
  37. // return html;
  38. // 1. 创建 DOMParser 实例
  39. const parser = new DOMParser()
  40. // 2. 解析 HTML 字符串为文档对象
  41. const doc = parser.parseFromString(htmlString, 'text/html')
  42. // 3. 获取 p 元素
  43. const p = doc.querySelector('p')
  44. // 4. 读取 style 属性(内联样式字符串)
  45. const styleAttr = p?.getAttribute('allstyle')
  46. console.log(styleAttr) // 输出完整的 style 字符串
  47. return styleAttr || ''
  48. }
  49. export default () => {
  50. const slidesStore = useSlidesStore()
  51. const { slides, theme, viewportRatio, title, viewportSize } = storeToRefs(useSlidesStore())
  52. const { addHistorySnapshot } = useHistorySnapshot()
  53. const { addSlidesFromData } = useAddSlidesOrElements()
  54. const { isEmptySlide } = useSlideHandler()
  55. const exporting = ref(false)
  56. // 导入JSON文件
  57. const importJSON = (files: FileList, cover = false) => {
  58. const file = files[0]
  59. const reader = new FileReader()
  60. reader.addEventListener('load', () => {
  61. try {
  62. const { slides } = JSON.parse(reader.result as string)
  63. if (cover) {
  64. slidesStore.updateSlideIndex(0)
  65. slidesStore.setSlides(slides)
  66. addHistorySnapshot()
  67. }
  68. else if (isEmptySlide.value) {
  69. slidesStore.setSlides(slides)
  70. addHistorySnapshot()
  71. }
  72. else addSlidesFromData(slides)
  73. }
  74. catch {
  75. message.error(lang.ssFileReadFail)
  76. }
  77. })
  78. reader.readAsText(file)
  79. }
  80. // 直接读取JSON功能,暴露到window.readJSON
  81. const readJSON = (jsonData: string | any, cover = false) => {
  82. try {
  83. console.log('readJSON 开始执行:', { jsonData, cover })
  84. let parsedData
  85. if (typeof jsonData === 'string') {
  86. parsedData = JSON.parse(jsonData)
  87. console.log('解析字符串后的数据:', parsedData)
  88. }
  89. else {
  90. parsedData = jsonData
  91. }
  92. // 提取所有可能的数据
  93. const slides = parsedData.slides || parsedData
  94. const title = parsedData.title
  95. const theme = parsedData.theme
  96. const width = parsedData.width
  97. const height = parsedData.height
  98. const viewportRatio = parsedData.viewportRatio || (height && width ? height / width : undefined)
  99. console.log('提取的数据:', { slides: slides.length, title, theme, width, height, viewportRatio })
  100. // 更新幻灯片数据
  101. if (cover) {
  102. console.log('覆盖模式:更新幻灯片数据')
  103. slidesStore.updateSlideIndex(0)
  104. slidesStore.setSlides(slides)
  105. addHistorySnapshot()
  106. }
  107. else if (isEmptySlide.value) {
  108. console.log('空幻灯片模式:更新幻灯片数据')
  109. slidesStore.setSlides(slides)
  110. addHistorySnapshot()
  111. }
  112. else {
  113. console.log('添加模式:添加幻灯片数据')
  114. addSlidesFromData(slides)
  115. }
  116. // 同步更新其他相关内容
  117. if (title !== undefined) {
  118. console.log('正在更新标题:', title)
  119. slidesStore.setTitle(title)
  120. console.log('标题更新完成')
  121. }
  122. if (theme !== undefined) {
  123. console.log('正在更新主题:', theme)
  124. slidesStore.setTheme(theme)
  125. console.log('主题更新完成')
  126. }
  127. // 更新视口尺寸(如果提供了的话)
  128. if (width !== undefined && height !== undefined) {
  129. console.log('正在触发视口尺寸更新事件:', { width, height, viewportRatio })
  130. // 同时也要更新slidesStore中的相关数据
  131. if (slidesStore.setViewportSize) {
  132. console.log('正在更新store中的视口尺寸')
  133. slidesStore.setViewportSize(width)
  134. if (slidesStore.setViewportRatio && viewportRatio !== undefined) {
  135. slidesStore.setViewportRatio(viewportRatio)
  136. console.log('视口比例已更新:', viewportRatio)
  137. }
  138. }
  139. window.dispatchEvent(new CustomEvent('viewportSizeUpdated', {
  140. detail: { width, height, viewportRatio }
  141. }))
  142. console.log('视口尺寸更新事件已触发')
  143. }
  144. // 导入成功后,触发画布尺寸更新
  145. // 使用 nextTick 确保DOM更新完成后再触发
  146. console.log('开始触发画布尺寸更新事件...')
  147. nextTick(() => {
  148. console.log('DOM更新完成,触发 slidesDataUpdated 事件')
  149. // 触发自定义事件,通知需要更新画布尺寸的组件
  150. window.dispatchEvent(new CustomEvent('slidesDataUpdated', {
  151. detail: {
  152. slides,
  153. cover,
  154. title,
  155. theme,
  156. width,
  157. height,
  158. viewportRatio,
  159. timestamp: Date.now()
  160. }
  161. }))
  162. console.log('slidesDataUpdated 事件已触发')
  163. // 检查并调整幻灯片索引,确保在有效范围内
  164. const newSlideCount = slides.length
  165. const currentIndex = slidesStore.slideIndex
  166. if (currentIndex >= newSlideCount) {
  167. console.log('调整幻灯片索引:', currentIndex, '->', Math.max(0, newSlideCount - 1))
  168. slidesStore.updateSlideIndex(Math.max(0, newSlideCount - 1))
  169. }
  170. console.log('画布尺寸更新事件处理完成')
  171. })
  172. console.log('readJSON 执行成功')
  173. return { success: true, slides, title, theme, width, height, viewportRatio }
  174. }
  175. catch (error) {
  176. console.error('readJSON 执行失败:', error)
  177. const errorMsg = lang.ssJsonReadFail
  178. message.error(errorMsg)
  179. return { success: false, error: errorMsg, details: error }
  180. }
  181. }
  182. // 导出JSON文件
  183. const exportJSON2 = () => {
  184. const json = {
  185. title: title.value,
  186. width: viewportSize.value,
  187. height: viewportSize.value * viewportRatio.value,
  188. theme: theme.value,
  189. slides: slides.value,
  190. }
  191. return json
  192. }
  193. // 优化暴露到 window 对象的方式,避免重复赋值
  194. if (typeof window !== 'undefined') {
  195. const win = window as any
  196. if (!win.exportJSON) win.exportJSON = exportJSON2
  197. if (!win.readJSON) win.readJSON = readJSON
  198. }
  199. // 导入pptist文件
  200. const importSpecificFile = (files: FileList, cover = false) => {
  201. const file = files[0]
  202. const reader = new FileReader()
  203. reader.addEventListener('load', () => {
  204. try {
  205. const { slides } = JSON.parse(decrypt(reader.result as string))
  206. if (cover) {
  207. slidesStore.updateSlideIndex(0)
  208. slidesStore.setSlides(slides)
  209. addHistorySnapshot()
  210. }
  211. else if (isEmptySlide.value) {
  212. slidesStore.setSlides(slides)
  213. addHistorySnapshot()
  214. }
  215. else addSlidesFromData(slides)
  216. }
  217. catch {
  218. message.error(lang.ssFileReadFail)
  219. }
  220. })
  221. reader.readAsText(file)
  222. }
  223. const rotateLine = (line: PPTLineElement, angleDeg: number) => {
  224. const { start, end } = line
  225. const angleRad = angleDeg * Math.PI / 180
  226. const midX = (start[0] + end[0]) / 2
  227. const midY = (start[1] + end[1]) / 2
  228. const startTransX = start[0] - midX
  229. const startTransY = start[1] - midY
  230. const endTransX = end[0] - midX
  231. const endTransY = end[1] - midY
  232. const cosA = Math.cos(angleRad)
  233. const sinA = Math.sin(angleRad)
  234. const startRotX = startTransX * cosA - startTransY * sinA
  235. const startRotY = startTransX * sinA + startTransY * cosA
  236. const endRotX = endTransX * cosA - endTransY * sinA
  237. const endRotY = endTransX * sinA + endTransY * cosA
  238. const startNewX = startRotX + midX
  239. const startNewY = startRotY + midY
  240. const endNewX = endRotX + midX
  241. const endNewY = endRotY + midY
  242. const beforeMinX = Math.min(start[0], end[0])
  243. const beforeMinY = Math.min(start[1], end[1])
  244. const afterMinX = Math.min(startNewX, endNewX)
  245. const afterMinY = Math.min(startNewY, endNewY)
  246. const startAdjustedX = startNewX - afterMinX
  247. const startAdjustedY = startNewY - afterMinY
  248. const endAdjustedX = endNewX - afterMinX
  249. const endAdjustedY = endNewY - afterMinY
  250. const startAdjusted: [number, number] = [startAdjustedX, startAdjustedY]
  251. const endAdjusted: [number, number] = [endAdjustedX, endAdjustedY]
  252. const offset = [afterMinX - beforeMinX, afterMinY - beforeMinY]
  253. return {
  254. start: startAdjusted,
  255. end: endAdjusted,
  256. offset,
  257. }
  258. }
  259. const parseLineElement = (el: Shape, ratio: number) => {
  260. let start: [number, number] = [0, 0]
  261. let end: [number, number] = [0, 0]
  262. if (!el.isFlipV && !el.isFlipH) { // 右下
  263. start = [0, 0]
  264. end = [el.width, el.height]
  265. }
  266. else if (el.isFlipV && el.isFlipH) { // 左上
  267. start = [el.width, el.height]
  268. end = [0, 0]
  269. }
  270. else if (el.isFlipV && !el.isFlipH) { // 右上
  271. start = [0, el.height]
  272. end = [el.width, 0]
  273. }
  274. else { // 左下
  275. start = [el.width, 0]
  276. end = [0, el.height]
  277. }
  278. const data: PPTLineElement = {
  279. type: 'line',
  280. id: nanoid(10),
  281. width: +((el.borderWidth || 1) * ratio).toFixed(2),
  282. left: el.left,
  283. top: el.top,
  284. start,
  285. end,
  286. style: el.borderType,
  287. color: el.borderColor,
  288. points: ['', /straightConnector/.test(el.shapType) ? 'arrow' : '']
  289. }
  290. if (el.rotate) {
  291. const { start, end, offset } = rotateLine(data, el.rotate)
  292. data.start = start
  293. data.end = end
  294. data.left = data.left + offset[0]
  295. data.top = data.top + offset[1]
  296. }
  297. if (/bentConnector/.test(el.shapType)) {
  298. data.broken2 = [
  299. Math.abs(data.start[0] - data.end[0]) / 2,
  300. Math.abs(data.start[1] - data.end[1]) / 2,
  301. ]
  302. }
  303. if (/curvedConnector/.test(el.shapType)) {
  304. const cubic: [number, number] = [
  305. Math.abs(data.start[0] - data.end[0]) / 2,
  306. Math.abs(data.start[1] - data.end[1]) / 2,
  307. ]
  308. data.cubic = [cubic, cubic]
  309. }
  310. return data
  311. }
  312. const flipGroupElements = (elements: BaseElement[], axis: 'x' | 'y') => {
  313. const minX = Math.min(...elements.map(el => el.left))
  314. const maxX = Math.max(...elements.map(el => el.left + el.width))
  315. const minY = Math.min(...elements.map(el => el.top))
  316. const maxY = Math.max(...elements.map(el => el.top + el.height))
  317. const centerX = (minX + maxX) / 2
  318. const centerY = (minY + maxY) / 2
  319. return elements.map(element => {
  320. const newElement = { ...element }
  321. if (axis === 'y') newElement.left = 2 * centerX - element.left - element.width
  322. if (axis === 'x') newElement.top = 2 * centerY - element.top - element.height
  323. return newElement
  324. })
  325. }
  326. const calculateRotatedPosition = (
  327. x: number,
  328. y: number,
  329. w: number,
  330. h: number,
  331. ox: number,
  332. oy: number,
  333. k: number,
  334. ) => {
  335. const radians = k * (Math.PI / 180)
  336. const containerCenterX = x + w / 2
  337. const containerCenterY = y + h / 2
  338. const relativeX = ox - w / 2
  339. const relativeY = oy - h / 2
  340. const rotatedX = relativeX * Math.cos(radians) + relativeY * Math.sin(radians)
  341. const rotatedY = -relativeX * Math.sin(radians) + relativeY * Math.cos(radians)
  342. const graphicX = containerCenterX + rotatedX
  343. const graphicY = containerCenterY + rotatedY
  344. return { x: graphicX, y: graphicY }
  345. }
  346. /**
  347. * 将 base64 字符串或 Blob 转换为 File 对象
  348. */
  349. const dataToFile = async (data: string | Blob, filename: string, videoMimeType: string): File => {
  350. if (typeof data === 'string') {
  351. // 1. 通过 fetch 获取 Blob 数据
  352. const response = await fetch(data);
  353. if (!response.ok) {
  354. throw new Error(`Failed to fetch blob: ${response.statusText}`);
  355. }
  356. const blob = await response.blob();
  357. // 2. 将 Blob 转换为 File 对象
  358. // 如果原 Blob 有 type,会自动保留;否则可手动指定 videoMimeType
  359. const file = new File([blob], filename, { type: videoMimeType || blob.type });
  360. return file;
  361. }
  362. else if (data instanceof Blob) {
  363. return new File([data], filename, { type: data.type })
  364. }
  365. throw new Error('Unsupported data type')
  366. }
  367. /*
  368. const makeWhiteTransparent = async (
  369. data: string | Blob,
  370. filename: string,
  371. options?: { tolerance?: number }
  372. ): Promise<File> => {
  373. const tolerance = options?.tolerance ?? 30 // 容差值,控制哪些颜色被视为白色
  374. const distThreshold = options?.tolerance ?? 50;
  375. // 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
  376. let imageUrl: string
  377. let blob: Blob
  378. if (typeof data === 'string') {
  379. // 如果是 Base64,直接用作 src(data URL)
  380. imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`
  381. }
  382. else if (data instanceof Blob) {
  383. // 如果是 Blob,创建对象 URL
  384. imageUrl = URL.createObjectURL(data)
  385. blob = data // 暂存,后续释放 URL 用
  386. }
  387. else {
  388. throw new Error('Unsupported data type')
  389. }
  390. // 2. 加载图像到 Image 元素
  391. const img = await new Promise<HTMLImageElement>((resolve, reject) => {
  392. const image = new Image()
  393. image.onload = () => resolve(image)
  394. image.onerror = reject
  395. image.src = imageUrl
  396. // 如果图像来自跨域,可能需要设置 crossOrigin
  397. // image.crossOrigin = 'anonymous';
  398. })
  399. // 3. 创建 Canvas 并绘制图像
  400. const canvas = document.createElement('canvas')
  401. canvas.width = img.width
  402. canvas.height = img.height
  403. const ctx = canvas.getContext('2d')!
  404. ctx.drawImage(img, 0, 0)
  405. // 4. 获取像素数据并处理
  406. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  407. const dataArray = imageData.data
  408. for (let i = 0; i < dataArray.length; i += 4) {
  409. const r = dataArray[i]
  410. const g = dataArray[i + 1]
  411. const b = dataArray[i + 2]
  412. // 判断颜色是否接近白色(RGB 都大于 255 - tolerance)
  413. if (r > 255 - tolerance && g > 255 - tolerance && b > 255 - tolerance) {
  414. dataArray[i + 3] = 0 // 设置 Alpha 为 0(完全透明)
  415. }
  416. }
  417. // 5. 将修改后的像素放回 Canvas
  418. ctx.putImageData(imageData, 0, 0)
  419. // 6. 将 Canvas 转换为 PNG Blob
  420. const outputBlob = await new Promise<Blob>((resolve) =>
  421. canvas.toBlob((blob) => resolve(blob!), 'image/png')
  422. )
  423. // 7. 清理对象 URL(如果之前创建过)
  424. if (typeof data !== 'string') {
  425. URL.revokeObjectURL(imageUrl)
  426. }
  427. // 8. 返回 File 对象
  428. return new File([outputBlob], filename, { type: 'image/png' })
  429. }
  430. */
  431. /**
  432. * 将图片中的白色背景变为透明
  433. * @param data 图片数据(Base64字符串 或 Blob)
  434. * @param filename 输出文件名
  435. * @param options 可选配置
  436. * @param options.tolerance 颜色距离容差(默认50,值越大越多的浅色被变透明)
  437. * @param options.removeMatte 是否去除白色边缘(默认true,可改善白边)
  438. * @returns 处理后的 PNG 格式 File 对象
  439. */
  440. const makeWhiteTransparent = async (
  441. data: string | Blob,
  442. filename: string,
  443. options?: { tolerance?: number }
  444. ): Promise<File> => {
  445. const tolerance = options?.tolerance ?? 15; // 只处理非常接近白色的像素
  446. // 1. 将输入统一转为可加载的 URL
  447. let imageUrl: string;
  448. let needRevoke = false;
  449. if (typeof data === 'string') {
  450. imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`;
  451. } else if (data instanceof Blob) {
  452. imageUrl = URL.createObjectURL(data);
  453. needRevoke = true;
  454. } else {
  455. throw new Error('Unsupported data type: expected string or Blob');
  456. }
  457. // 2. 加载图像
  458. const img = await new Promise<HTMLImageElement>((resolve, reject) => {
  459. const image = new Image();
  460. image.onload = () => resolve(image);
  461. image.onerror = reject;
  462. if (typeof data === 'string' && !data.startsWith('data:')) {
  463. image.crossOrigin = 'anonymous';
  464. }
  465. image.src = imageUrl;
  466. });
  467. const canvas = document.createElement('canvas');
  468. try {
  469. // 3. 绘制到 Canvas
  470. canvas.width = img.width;
  471. canvas.height = img.height;
  472. const ctx = canvas.getContext('2d')!;
  473. ctx.drawImage(img, 0, 0);
  474. // 4. 获取像素数据
  475. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  476. const dataArray = imageData.data;
  477. // 5. 遍历像素:只将白色背景区域设为透明,其他颜色原样保留
  478. for (let i = 0; i < dataArray.length; i += 4) {
  479. const r = dataArray[i];
  480. const g = dataArray[i + 1];
  481. const b = dataArray[i + 2];
  482. // 计算与纯白色 (255,255,255) 的欧几里得距离
  483. const dr = r - 255;
  484. const dg = g - 255;
  485. const db = b - 255;
  486. const dist = Math.sqrt(dr * dr + dg * dg + db * db);
  487. if (dist <= tolerance) {
  488. // 非常接近白色 → 设为全透明
  489. dataArray[i + 3] = 0;
  490. }
  491. // 其他所有像素(包括浅蓝、灰色、黑色等)保持原样,不修改颜色和透明度
  492. }
  493. // 6. 将修改后的像素放回 Canvas
  494. ctx.putImageData(imageData, 0, 0);
  495. }
  496. finally {
  497. if (needRevoke) {
  498. URL.revokeObjectURL(imageUrl);
  499. }
  500. }
  501. // 7. 导出为 PNG Blob
  502. const outputBlob = await new Promise<Blob>((resolve, reject) => {
  503. canvas.toBlob((blob) => {
  504. if (blob) resolve(blob);
  505. else reject(new Error('Canvas toBlob failed'));
  506. }, 'image/png');
  507. });
  508. // 8. 返回 File 对象
  509. return new File([outputBlob], filename, { type: 'image/png' });
  510. };
  511. /**
  512. * 上传 File 到 S3,返回公开访问的 URL
  513. */
  514. const uploadFileToS3 = (file: File): Promise<string> => {
  515. return new Promise((resolve, reject) => {
  516. if (typeof window === 'undefined' || !window.AWS) {
  517. reject(new Error('AWS SDK not available'))
  518. return
  519. }
  520. const credentials = {
  521. accessKeyId: 'AKIATLPEDU37QV5CHLMH',
  522. secretAccessKey: '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',
  523. }
  524. window.AWS.config.update(credentials)
  525. window.AWS.config.region = 'cn-northwest-1'
  526. const bucket = new window.AWS.S3({
  527. params: { Bucket: 'ccrb' }, httpOptions: {
  528. timeout: 600000 // 10分钟超时
  529. }
  530. })
  531. const ext = file.name.split('.').pop() || 'bin'
  532. const key = `${file.name.split('.')[0]}_${Date.now()}.${ext}`
  533. const params = {
  534. Key: 'pptto/' + key,
  535. ContentType: file.type,
  536. Body: file,
  537. ACL: 'public-read',
  538. }
  539. const options = {
  540. partSize: 5 * 1024 * 1024, // 2GB 分片,可酌情调小
  541. queueSize: 2,
  542. leavePartsOnError: true,
  543. }
  544. bucket
  545. .upload(params, options)
  546. .promise()
  547. .then(data => resolve(data.Location))
  548. .catch(err => reject(err))
  549. })
  550. }
  551. /*
  552. // 导入PPTX文件
  553. const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal }) => {
  554. console.log('导入', files)
  555. const defaultOptions = {
  556. cover: false,
  557. fixedViewport: false,
  558. }
  559. const { cover, fixedViewport, signal } = { ...defaultOptions, ...options }
  560. const file = files[0]
  561. if (!file) return
  562. exporting.value = true
  563. const shapeList: ShapePoolItem[] = []
  564. for (const item of SHAPE_LIST) {
  565. shapeList.push(...item.children)
  566. }
  567. const reader = new FileReader()
  568. reader.onload = async (e: ProgressEvent<FileReader>) => {
  569. // 检查是否已取消
  570. if (signal?.aborted) {
  571. exporting.value = false
  572. return
  573. }
  574. let json = null
  575. try {
  576. json = await parse(e.target!.result as ArrayBuffer)
  577. }
  578. catch (error) {
  579. exporting.value = false
  580. console.log('导入PPTX文件失败:', error)
  581. message.error('无法正确读取 / 解析该文件')
  582. return
  583. }
  584. let ratio = 96 / 72;
  585. //let ratio = 1
  586. const width = json.size.width
  587. if (fixedViewport) ratio = 1000 / width
  588. else slidesStore.setViewportSize(width * ratio)
  589. slidesStore.setTheme({ themeColors: json.themeColors })
  590. const slides: Slide[] = []
  591. for (const item of json.slides) {
  592. const { type, value } = item.fill
  593. let background: SlideBackground
  594. if (type === 'image') {
  595. background = {
  596. type: 'image',
  597. image: {
  598. src: value.picBase64,
  599. size: 'cover',
  600. },
  601. }
  602. }
  603. else if (type === 'gradient') {
  604. background = {
  605. type: 'gradient',
  606. gradient: {
  607. type: value.path === 'line' ? 'linear' : 'radial',
  608. colors: value.colors.map(item => ({
  609. ...item,
  610. pos: parseInt(item.pos),
  611. })),
  612. rotate: value.rot + 90,
  613. },
  614. }
  615. }
  616. else {
  617. background = {
  618. type: 'solid',
  619. color: value || '#fff',
  620. }
  621. }
  622. const slide: Slide = {
  623. id: nanoid(10),
  624. elements: [],
  625. background,
  626. remark: item.note || '',
  627. }
  628. const parseElements = (elements: Element[]) => {
  629. const sortedElements = elements.sort((a, b) => a.order - b.order)
  630. console.log(sortedElements)
  631. for (const el of sortedElements) {
  632. const originWidth = el.width || 1
  633. const originHeight = el.height || 1
  634. const originLeft = el.left
  635. const originTop = el.top
  636. el.width = el.width * ratio
  637. el.height = el.height * ratio
  638. el.left = el.left * ratio
  639. el.top = el.top * ratio
  640. if (el.type === 'text') {
  641. const textEl: PPTTextElement = {
  642. type: 'text',
  643. id: nanoid(10),
  644. width: el.width,
  645. height: el.height,
  646. left: el.left,
  647. top: el.top,
  648. rotate: el.rotate,
  649. defaultFontName: theme.value.fontName,
  650. defaultColor: theme.value.fontColor,
  651. content: convertFontSizePtToPx(el.content, ratio),
  652. lineHeight: 1,
  653. outline: {
  654. color: el.borderColor,
  655. width: +(el.borderWidth * ratio).toFixed(2),
  656. style: el.borderType,
  657. },
  658. fill: el.fill.type === 'color' ? el.fill.value : '',
  659. vertical: el.isVertical,
  660. }
  661. if (el.shadow) {
  662. textEl.shadow = {
  663. h: el.shadow.h * ratio,
  664. v: el.shadow.v * ratio,
  665. blur: el.shadow.blur * ratio,
  666. color: el.shadow.color,
  667. }
  668. }
  669. slide.elements.push(textEl)
  670. }
  671. else if (el.type === 'image') {
  672. const element: PPTImageElement = {
  673. type: 'image',
  674. id: nanoid(10),
  675. src: el.src,
  676. width: el.width,
  677. height: el.height,
  678. left: el.left,
  679. top: el.top,
  680. fixedRatio: true,
  681. rotate: el.rotate,
  682. flipH: el.isFlipH,
  683. flipV: el.isFlipV,
  684. }
  685. if (el.borderWidth) {
  686. element.outline = {
  687. color: el.borderColor,
  688. width: +(el.borderWidth * ratio).toFixed(2),
  689. style: el.borderType,
  690. }
  691. }
  692. const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid']
  693. if (el.rect) {
  694. element.clip = {
  695. shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
  696. range: [
  697. [
  698. el.rect.l || 0,
  699. el.rect.t || 0,
  700. ],
  701. [
  702. 100 - (el.rect.r || 0),
  703. 100 - (el.rect.b || 0),
  704. ],
  705. ]
  706. }
  707. }
  708. else if (el.geom && clipShapeTypes.includes(el.geom)) {
  709. element.clip = {
  710. shape: el.geom,
  711. range: [[0, 0], [100, 100]]
  712. }
  713. }
  714. slide.elements.push(element)
  715. }
  716. else if (el.type === 'math') {
  717. slide.elements.push({
  718. type: 'image',
  719. id: nanoid(10),
  720. src: el.picBase64,
  721. width: el.width,
  722. height: el.height,
  723. left: el.left,
  724. top: el.top,
  725. fixedRatio: true,
  726. rotate: 0,
  727. })
  728. }
  729. else if (el.type === 'audio') {
  730. slide.elements.push({
  731. type: 'audio',
  732. id: nanoid(10),
  733. src: el.blob,
  734. width: el.width,
  735. height: el.height,
  736. left: el.left,
  737. top: el.top,
  738. rotate: 0,
  739. fixedRatio: false,
  740. color: theme.value.themeColors[0],
  741. loop: false,
  742. autoplay: false,
  743. })
  744. }
  745. else if (el.type === 'video') {
  746. slide.elements.push({
  747. type: 'video',
  748. id: nanoid(10),
  749. src: (el.blob || el.src)!,
  750. width: el.width,
  751. height: el.height,
  752. left: el.left,
  753. top: el.top,
  754. rotate: 0,
  755. autoplay: false,
  756. })
  757. }
  758. else if (el.type === 'shape') {
  759. if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
  760. const lineElement = parseLineElement(el, ratio)
  761. slide.elements.push(lineElement)
  762. }
  763. else {
  764. const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
  765. const vAlignMap: { [key: string]: ShapeTextAlign } = {
  766. 'mid': 'middle',
  767. 'down': 'bottom',
  768. 'up': 'top',
  769. }
  770. const gradient: Gradient | undefined = el.fill?.type === 'gradient' ? {
  771. type: el.fill.value.path === 'line' ? 'linear' : 'radial',
  772. colors: el.fill.value.colors.map(item => ({
  773. ...item,
  774. pos: parseInt(item.pos),
  775. })),
  776. rotate: el.fill.value.rot,
  777. } : undefined
  778. const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
  779. const fill = el.fill?.type === 'color' ? el.fill.value : ''
  780. const element: PPTShapeElement = {
  781. type: 'shape',
  782. id: nanoid(10),
  783. width: el.width,
  784. height: el.height,
  785. left: el.left,
  786. top: el.top,
  787. viewBox: [200, 200],
  788. path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
  789. fill,
  790. gradient,
  791. pattern,
  792. fixedRatio: false,
  793. rotate: el.rotate,
  794. outline: {
  795. color: el.borderColor,
  796. width: +(el.borderWidth * ratio).toFixed(2),
  797. style: el.borderType,
  798. },
  799. text: {
  800. content: convertFontSizePtToPx(el.content, ratio),
  801. defaultFontName: theme.value.fontName,
  802. defaultColor: theme.value.fontColor,
  803. align: vAlignMap[el.vAlign] || 'middle',
  804. },
  805. flipH: el.isFlipH,
  806. flipV: el.isFlipV,
  807. }
  808. if (el.shadow) {
  809. element.shadow = {
  810. h: el.shadow.h * ratio,
  811. v: el.shadow.v * ratio,
  812. blur: el.shadow.blur * ratio,
  813. color: el.shadow.color,
  814. }
  815. }
  816. if (shape) {
  817. element.path = shape.path
  818. element.viewBox = shape.viewBox
  819. if (shape.pathFormula) {
  820. element.pathFormula = shape.pathFormula
  821. element.viewBox = [el.width, el.height]
  822. const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
  823. if ('editable' in pathFormula && pathFormula.editable) {
  824. element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
  825. element.keypoints = pathFormula.defaultValue
  826. }
  827. else element.path = pathFormula.formula(el.width, el.height)
  828. }
  829. }
  830. else if (el.path && el.path.indexOf('NaN') === -1) {
  831. const { maxX, maxY } = getSvgPathRange(el.path)
  832. element.path = el.path
  833. element.viewBox = [maxX || originWidth, maxY || originHeight]
  834. }
  835. if (el.shapType === 'custom') {
  836. if (el.path!.indexOf('NaN') !== -1) {
  837. if (element.width === 0) element.width = 0.1
  838. if (element.height === 0) element.height = 0.1
  839. element.path = el.path!.replace(/NaN/g, '0')
  840. }
  841. else {
  842. element.special = true
  843. element.path = el.path!
  844. }
  845. const { maxX, maxY } = getSvgPathRange(element.path)
  846. element.viewBox = [maxX || originWidth, maxY || originHeight]
  847. }
  848. if (element.path) slide.elements.push(element)
  849. }
  850. }
  851. else if (el.type === 'table') {
  852. const row = el.data.length
  853. const col = el.data[0].length
  854. const style: TableCellStyle = {
  855. fontname: theme.value.fontName,
  856. color: theme.value.fontColor,
  857. }
  858. const data: TableCell[][] = []
  859. for (let i = 0; i < row; i++) {
  860. const rowCells: TableCell[] = []
  861. for (let j = 0; j < col; j++) {
  862. const cellData = el.data[i][j]
  863. let textDiv: HTMLDivElement | null = document.createElement('div')
  864. textDiv.innerHTML = cellData.text
  865. const p = textDiv.querySelector('p')
  866. const align = p?.style.textAlign || 'left'
  867. const span = textDiv.querySelector('span')
  868. const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : ''
  869. const fontname = span?.style.fontFamily || ''
  870. const color = span?.style.color || cellData.fontColor
  871. rowCells.push({
  872. id: nanoid(10),
  873. colspan: cellData.colSpan || 1,
  874. rowspan: cellData.rowSpan || 1,
  875. text: textDiv.innerText,
  876. style: {
  877. ...style,
  878. align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
  879. fontsize,
  880. fontname,
  881. color,
  882. bold: cellData.fontBold,
  883. backcolor: cellData.fillColor,
  884. },
  885. })
  886. textDiv = null
  887. }
  888. data.push(rowCells)
  889. }
  890. const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
  891. const colWidths: number[] = el.colWidths.map(item => item / allWidth)
  892. const firstCell = el.data[0][0]
  893. const border = firstCell.borders.top ||
  894. firstCell.borders.bottom ||
  895. el.borders.top ||
  896. el.borders.bottom ||
  897. firstCell.borders.left ||
  898. firstCell.borders.right ||
  899. el.borders.left ||
  900. el.borders.right
  901. const borderWidth = border?.borderWidth || 0
  902. const borderStyle = border?.borderType || 'solid'
  903. const borderColor = border?.borderColor || '#eeece1'
  904. slide.elements.push({
  905. type: 'table',
  906. id: nanoid(10),
  907. width: el.width,
  908. height: el.height,
  909. left: el.left,
  910. top: el.top,
  911. colWidths,
  912. rotate: 0,
  913. data,
  914. outline: {
  915. width: +(borderWidth * ratio || 2).toFixed(2),
  916. style: borderStyle,
  917. color: borderColor,
  918. },
  919. cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
  920. })
  921. }
  922. else if (el.type === 'chart') {
  923. let labels: string[]
  924. let legends: string[]
  925. let series: number[][]
  926. if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
  927. labels = el.data[0].map((item, index) => `坐标${index + 1}`)
  928. legends = ['X', 'Y']
  929. series = el.data
  930. }
  931. else {
  932. const data = el.data as ChartItem[]
  933. labels = Object.values(data[0].xlabels)
  934. legends = data.map(item => item.key)
  935. series = data.map(item => item.values.map(v => v.y))
  936. }
  937. const options: ChartOptions = {}
  938. let chartType: ChartType = 'bar'
  939. switch (el.chartType) {
  940. case 'barChart':
  941. case 'bar3DChart':
  942. chartType = 'bar'
  943. if (el.barDir === 'bar') chartType = 'column'
  944. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  945. break
  946. case 'lineChart':
  947. case 'line3DChart':
  948. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  949. chartType = 'line'
  950. break
  951. case 'areaChart':
  952. case 'area3DChart':
  953. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  954. chartType = 'area'
  955. break
  956. case 'scatterChart':
  957. case 'bubbleChart':
  958. chartType = 'scatter'
  959. break
  960. case 'pieChart':
  961. case 'pie3DChart':
  962. chartType = 'pie'
  963. break
  964. case 'radarChart':
  965. chartType = 'radar'
  966. break
  967. case 'doughnutChart':
  968. chartType = 'ring'
  969. break
  970. default:
  971. }
  972. slide.elements.push({
  973. type: 'chart',
  974. id: nanoid(10),
  975. chartType: chartType,
  976. width: el.width,
  977. height: el.height,
  978. left: el.left,
  979. top: el.top,
  980. rotate: 0,
  981. themeColors: el.colors.length ? el.colors : theme.value.themeColors,
  982. textColor: theme.value.fontColor,
  983. data: {
  984. labels,
  985. legends,
  986. series,
  987. },
  988. options,
  989. })
  990. }
  991. else if (el.type === 'group') {
  992. let elements: BaseElement[] = el.elements.map(_el => {
  993. let left = _el.left + originLeft
  994. let top = _el.top + originTop
  995. if (el.rotate) {
  996. const { x, y } = calculateRotatedPosition(originLeft, originTop, originWidth, originHeight, _el.left, _el.top, el.rotate)
  997. left = x
  998. top = y
  999. }
  1000. const element = {
  1001. ..._el,
  1002. left,
  1003. top,
  1004. }
  1005. if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
  1006. if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
  1007. return element
  1008. })
  1009. if (el.isFlipH) elements = flipGroupElements(elements, 'y')
  1010. if (el.isFlipV) elements = flipGroupElements(elements, 'x')
  1011. parseElements(elements)
  1012. }
  1013. else if (el.type === 'diagram') {
  1014. const elements = el.elements.map(_el => ({
  1015. ..._el,
  1016. left: _el.left + originLeft,
  1017. top: _el.top + originTop,
  1018. }))
  1019. parseElements(elements)
  1020. }
  1021. }
  1022. }
  1023. parseElements([...item.elements, ...item.layoutElements])
  1024. slides.push(slide)
  1025. }
  1026. if (cover) {
  1027. slidesStore.updateSlideIndex(0)
  1028. slidesStore.setSlides(slides)
  1029. addHistorySnapshot()
  1030. }
  1031. else if (isEmptySlide.value) {
  1032. slidesStore.setSlides(slides)
  1033. addHistorySnapshot()
  1034. }
  1035. else addSlidesFromData(slides)
  1036. exporting.value = false
  1037. }
  1038. reader.readAsArrayBuffer(file)
  1039. // 监听取消信号
  1040. signal?.addEventListener('abort', () => {
  1041. reader.abort()
  1042. exporting.value = false
  1043. })
  1044. // 监听取消信号
  1045. signal?.addEventListener('abort', () => {
  1046. reader.abort()
  1047. exporting.value = false
  1048. })
  1049. }
  1050. */
  1051. const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void }) => {
  1052. console.log('导入', files)
  1053. const defaultOptions = {
  1054. cover: false,
  1055. fixedViewport: false,
  1056. }
  1057. const { cover, fixedViewport, signal, onclose } = { ...defaultOptions, ...options }
  1058. const file = files[0]
  1059. if (!file) return
  1060. exporting.value = true // 假设 exporting 是一个全局 ref
  1061. // 预加载形状库(用于后续形状匹配)
  1062. const shapeList: ShapePoolItem[] = []
  1063. for (const item of SHAPE_LIST) {
  1064. shapeList.push(...item.children)
  1065. }
  1066. const reader = new FileReader()
  1067. reader.onload = async (e: ProgressEvent<FileReader>) => {
  1068. // 检查是否已取消
  1069. if (signal?.aborted) {
  1070. exporting.value = false
  1071. return
  1072. }
  1073. let json = null
  1074. try {
  1075. json = await parse(e.target!.result as ArrayBuffer)
  1076. }
  1077. catch (error) {
  1078. exporting.value = false
  1079. console.log('导入PPTX文件失败:', error)
  1080. message.error(lang.ssFileReadFail)
  1081. return
  1082. }
  1083. if (signal?.aborted) {
  1084. exporting.value = false
  1085. return
  1086. }
  1087. // 计算缩放比例
  1088. let ratio = 96 / 72 // PPTX 默认 72 DPI,屏幕 96 DPI
  1089. const width = json.size.width
  1090. const height = json.size.height
  1091. const viewportRatio = json.size.viewportRatio || (height && width ? height / width : undefined)
  1092. if (fixedViewport) {
  1093. ratio = 1000 / width // 固定视口宽度为 1000px
  1094. }
  1095. else {
  1096. slidesStore.setViewportSize(width * ratio) // 调整画布大小
  1097. }
  1098. // 设置主题色
  1099. slidesStore.setTheme({ themeColors: json.themeColors })
  1100. const slides: Slide[] = []
  1101. // 收集当前幻灯片内所有上传任务
  1102. const uploadTasks: Promise<void>[] = []
  1103. // 遍历每一张幻灯片
  1104. for (const item of json.slides) {
  1105. // ----- 解析背景 -----
  1106. const { type, value } = item.fill
  1107. let background: SlideBackground
  1108. if (type === 'image') {
  1109. // 背景图片也可能需要上传(但 PPTX 背景图通常是内嵌 base64)
  1110. // 这里为了简化,暂不处理背景图片上传,如有需要可类似元素上传
  1111. background = {
  1112. type: 'image',
  1113. image: {
  1114. src: value.picBase64,
  1115. size: 'cover',
  1116. },
  1117. }
  1118. }
  1119. else if (type === 'gradient') {
  1120. background = {
  1121. type: 'gradient',
  1122. gradient: {
  1123. type: value.path === 'line' ? 'linear' : 'radial',
  1124. colors: value.colors.map(item => ({
  1125. ...item,
  1126. pos: parseInt(item.pos),
  1127. })),
  1128. rotate: value.rot + 90,
  1129. },
  1130. }
  1131. }
  1132. else {
  1133. background = {
  1134. type: 'solid',
  1135. color: value || '#fff',
  1136. }
  1137. }
  1138. const slide: Slide = {
  1139. id: nanoid(10),
  1140. elements: [],
  1141. background,
  1142. remark: item.note || '',
  1143. }
  1144. // ----- 解析元素(递归函数)-----
  1145. const parseElements = async (elements: any[], pelements: any = null) => {
  1146. // 按绘制顺序排序
  1147. const sortedElements = elements.sort((a, b) => a.order - b.order)
  1148. console.log(sortedElements)
  1149. for (const el of sortedElements) {
  1150. // 保存原始尺寸用于后续可能的路径计算
  1151. const originWidth = el.width || 1
  1152. const originHeight = el.height || 1
  1153. const originLeft = el.left
  1154. const originTop = el.top
  1155. // 保存原始尺寸用于后续可能的路径计算
  1156. const poriginWidth = pelements?.width
  1157. const poriginHeight = pelements?.height
  1158. const poriginLeft = pelements?.left
  1159. const poriginTop = pelements?.top
  1160. // 应用缩放
  1161. el.width = el.width * ratio
  1162. el.height = el.height * ratio
  1163. el.left = el.left * ratio
  1164. el.top = el.top * ratio
  1165. if (el.type === 'text') {
  1166. const textEl: PPTTextElement = {
  1167. type: 'text',
  1168. id: nanoid(10),
  1169. width: el.width,
  1170. height: el.height,
  1171. left: el.left,
  1172. top: el.top,
  1173. rotate: el.rotate,
  1174. defaultFontName: theme.value.fontName,
  1175. defaultColor: theme.value.fontColor,
  1176. content: convertFontSizePtToPx(el.content, ratio),
  1177. style: getStyle(convertFontSizePtToPx(el.content, ratio)),
  1178. lineHeight: 1.5,
  1179. outline: {
  1180. color: el.borderColor,
  1181. width: +(el.borderWidth * ratio).toFixed(2),
  1182. style: el.borderType,
  1183. },
  1184. fill: el.fill.type === 'color' ? el.fill.value : '',
  1185. vertical: el.isVertical,
  1186. }
  1187. if (el.shadow) {
  1188. textEl.shadow = {
  1189. h: el.shadow.h * ratio,
  1190. v: el.shadow.v * ratio,
  1191. blur: el.shadow.blur * ratio,
  1192. color: el.shadow.color,
  1193. }
  1194. }
  1195. slide.elements.push(textEl)
  1196. }
  1197. // ---------- 图片 ----------
  1198. if (el.type === 'image') {
  1199. const element: PPTImageElement = {
  1200. type: 'image',
  1201. id: nanoid(10),
  1202. src: el.src, // 可能是 base64 或已有 URL
  1203. width: el.width,
  1204. height: el.height,
  1205. left: el.left,
  1206. top: el.top,
  1207. fixedRatio: true,
  1208. rotate: el.rotate,
  1209. flipH: el.isFlipH,
  1210. flipV: el.isFlipV,
  1211. }
  1212. // 边框
  1213. if (el.borderWidth) {
  1214. element.outline = {
  1215. color: el.borderColor,
  1216. width: +(el.borderWidth * ratio).toFixed(2),
  1217. style: el.borderType,
  1218. }
  1219. }
  1220. // 裁剪(形状剪裁)
  1221. const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid']
  1222. if (el.rect) {
  1223. element.clip = {
  1224. shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
  1225. range: [
  1226. [el.rect.l || 0, el.rect.t || 0],
  1227. [100 - (el.rect.r || 0), 100 - (el.rect.b || 0)],
  1228. ],
  1229. }
  1230. }
  1231. else if (el.geom && clipShapeTypes.includes(el.geom)) {
  1232. element.clip = {
  1233. shape: el.geom,
  1234. range: [[0, 0], [100, 100]],
  1235. }
  1236. }
  1237. // 如果 src 是 base64,触发上传
  1238. if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
  1239. const uploadTask = (async () => {
  1240. try {
  1241. const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`)
  1242. if (file) {
  1243. const url = await uploadFileToS3(file)
  1244. element.src = url // 替换为远程 URL
  1245. const slidesStore = useSlidesStore()
  1246. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1247. }
  1248. }
  1249. catch (error) {
  1250. console.error('Image upload failed:', error)
  1251. // 失败时保留原 base64(或可置空)
  1252. }
  1253. })()
  1254. uploadTasks.push(uploadTask)
  1255. }
  1256. slide.elements.push(element)
  1257. }
  1258. else if (el.type === 'math') {
  1259. const element: PPTImageElement = {
  1260. type: 'image',
  1261. id: nanoid(10),
  1262. src: el.picBase64,
  1263. width: el.width,
  1264. height: el.height,
  1265. left: el.left,
  1266. top: el.top,
  1267. fixedRatio: true,
  1268. rotate: 0,
  1269. }
  1270. // 如果 src 是 base64,触发上传
  1271. if (el.picBase64 && typeof el.picBase64 === 'string' && el.picBase64.startsWith('data:')) {
  1272. const uploadTask = (async () => {
  1273. try {
  1274. const file = makeWhiteTransparent(el.picBase64, `image_${Date.now()}.png`)
  1275. if (file) {
  1276. const url = await uploadFileToS3(file)
  1277. element.src = url // 替换为远程 URL
  1278. const slidesStore = useSlidesStore()
  1279. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1280. }
  1281. }
  1282. catch (error) {
  1283. console.error('Image upload failed:', error)
  1284. // 失败时保留原 base64(或可置空)
  1285. }
  1286. })()
  1287. uploadTasks.push(uploadTask)
  1288. }
  1289. slide.elements.push(element)
  1290. }
  1291. // ---------- 音频 ----------
  1292. else if (el.type === 'audio') {
  1293. const element: PPTAudioElement = {
  1294. type: 'audio',
  1295. id: nanoid(10),
  1296. src: el.blob,
  1297. width: el.width,
  1298. height: el.height,
  1299. left: el.left,
  1300. top: el.top,
  1301. rotate: 0,
  1302. fixedRatio: false,
  1303. color: theme.value.themeColors[0],
  1304. loop: false,
  1305. autoplay: false,
  1306. }
  1307. const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
  1308. if (localData) {
  1309. const uploadTask = (async () => {
  1310. try {
  1311. const file = await dataToFile(localData, `audio_${Date.now()}.mp3`, 'audio/mpeg')
  1312. if (file) {
  1313. const url = await uploadFileToS3(file)
  1314. element.src = url
  1315. const slidesStore = useSlidesStore()
  1316. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1317. }
  1318. }
  1319. catch (error) {
  1320. console.error('Audio upload failed:', error)
  1321. }
  1322. })()
  1323. uploadTasks.push(uploadTask)
  1324. }
  1325. slide.elements.push(element)
  1326. }
  1327. // ---------- 视频 ----------
  1328. else if (el.type === 'video') {
  1329. const element: PPTVideoElement = {
  1330. type: 'video',
  1331. id: nanoid(10),
  1332. src: (el.blob || el.src)!,
  1333. width: el.width,
  1334. height: el.height,
  1335. left: el.left,
  1336. top: el.top,
  1337. rotate: 0,
  1338. autoplay: false,
  1339. }
  1340. const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
  1341. if (localData) {
  1342. const uploadTask = (async () => {
  1343. try {
  1344. let file: File
  1345. file = await dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4')
  1346. if (file) {
  1347. const url = await uploadFileToS3(file)
  1348. element.src = url
  1349. const slidesStore = useSlidesStore()
  1350. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1351. }
  1352. }
  1353. catch (error) {
  1354. console.error('Video upload failed:', error)
  1355. }
  1356. })()
  1357. uploadTasks.push(uploadTask)
  1358. }
  1359. slide.elements.push(element)
  1360. }
  1361. // ---------- 形状 ----------
  1362. else if (el.type === 'shape') {
  1363. if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
  1364. // 线条元素(单独处理)
  1365. const lineElement = parseLineElement(el, ratio)
  1366. slide.elements.push(lineElement)
  1367. }
  1368. else {
  1369. const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
  1370. const vAlignMap: { [key: string]: ShapeTextAlign } = {
  1371. mid: 'middle',
  1372. down: 'bottom',
  1373. up: 'top',
  1374. }
  1375. const gradient: Gradient | undefined = el.fill?.type === 'gradient'
  1376. ? {
  1377. type: el.fill.value.path === 'line' ? 'linear' : 'radial',
  1378. colors: el.fill.value.colors.map(item => ({
  1379. ...item,
  1380. pos: parseInt(item.pos),
  1381. })),
  1382. rotate: el.fill.value.rot,
  1383. }
  1384. : undefined
  1385. const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
  1386. const fill = el.fill?.type === 'color' ? el.fill.value : ''
  1387. const style = getStyle(convertFontSizePtToPx(el.content, ratio)) + (el.pathBBox.pWidth ? ';width:' + (el.pathBBox.pWidth * ratio) + 'px;height:' + (el.pathBBox.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
  1388. const element: PPTShapeElement = {
  1389. type: 'shape',
  1390. id: nanoid(10),
  1391. width: el.width,
  1392. height: el.height,
  1393. left: el.left,
  1394. top: el.top,
  1395. viewBox: [200, 200],
  1396. path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
  1397. fill,
  1398. gradient,
  1399. pattern,
  1400. fixedRatio: false,
  1401. rotate: el.rotate,
  1402. pathBBox: el.pathBBox,
  1403. outline: {
  1404. color: el.borderColor,
  1405. width: +(el.borderWidth * ratio).toFixed(2),
  1406. style: el.borderType,
  1407. },
  1408. text: {
  1409. content: convertFontSizePtToPx(el.content, ratio),
  1410. style: style,
  1411. defaultFontName: theme.value.fontName,
  1412. defaultColor: theme.value.fontColor,
  1413. align: vAlignMap[el.vAlign] || 'middle',
  1414. },
  1415. flipH: el.isFlipH,
  1416. flipV: el.isFlipV,
  1417. }
  1418. if (el.shadow) {
  1419. element.shadow = {
  1420. h: el.shadow.h * ratio,
  1421. v: el.shadow.v * ratio,
  1422. blur: el.shadow.blur * ratio,
  1423. color: el.shadow.color,
  1424. }
  1425. }
  1426. if (shape) {
  1427. element.path = shape.path
  1428. // const { maxX, maxY } = getSvgPathRange(el.path);
  1429. element.viewBox = shape.viewBox
  1430. // element.viewBox = [originWidth || maxX, originHeight || maxY];
  1431. if (shape.pathFormula) {
  1432. element.pathFormula = shape.pathFormula
  1433. element.viewBox = [el.width, el.height]
  1434. // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
  1435. const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
  1436. if ('editable' in pathFormula && pathFormula.editable) {
  1437. element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
  1438. element.keypoints = pathFormula.defaultValue
  1439. }
  1440. else {
  1441. element.path = pathFormula.formula(el.width, el.height)
  1442. }
  1443. }
  1444. }
  1445. else if (el.path && el.path.indexOf('NaN') === -1) {
  1446. const { maxX, maxY } = getSvgPathRange(el.path)
  1447. element.path = el.path
  1448. element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
  1449. // element.viewBox = [originWidth || maxX, originHeight || maxY];
  1450. // element.viewBox = originWidth? [(originWidth/(poriginWidth||1)), (originHeight/(poriginHeight||1))] : [maxX, maxY];
  1451. // element.viewBox = [poriginWidth || maxX, poriginHeight || maxY];
  1452. }
  1453. if (el.shapType === 'custom') {
  1454. if (el.path!.indexOf('NaN') !== -1) {
  1455. if (element.width === 0) element.width = 0.1
  1456. if (element.height === 0) element.height = 0.1
  1457. element.path = el.path!.replace(/NaN/g, '0')
  1458. }
  1459. const { maxX, maxY } = getSvgPathRange(element.path)
  1460. element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
  1461. // element.viewBox = [originWidth || maxX, originHeight || maxY];
  1462. // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
  1463. // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
  1464. // element.viewBox = [Math.max(maxX, originWidth), Math.max(maxY, originHeight)];
  1465. // element.viewBox = [originWidth, originHeight];
  1466. }
  1467. if (element.path) slide.elements.push(element)
  1468. }
  1469. }
  1470. // ---------- 表格 ----------
  1471. else if (el.type === 'table') {
  1472. const row = el.data.length
  1473. const col = el.data[0].length
  1474. const style: TableCellStyle = {
  1475. fontname: theme.value.fontName,
  1476. color: theme.value.fontColor,
  1477. }
  1478. const data: TableCell[][] = []
  1479. for (let i = 0; i < row; i++) {
  1480. const rowCells: TableCell[] = []
  1481. for (let j = 0; j < col; j++) {
  1482. const cellData = el.data[i][j]
  1483. let textDiv: HTMLDivElement | null = document.createElement('div')
  1484. textDiv.innerHTML = cellData.text
  1485. const p = textDiv.querySelector('p')
  1486. const align = p?.style.textAlign || 'left'
  1487. const span = textDiv.querySelector('span')
  1488. const fontsize = span?.style.fontSize
  1489. ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px'
  1490. : ''
  1491. const fontname = span?.style.fontFamily || ''
  1492. const color = span?.style.color || cellData.fontColor
  1493. rowCells.push({
  1494. id: nanoid(10),
  1495. colspan: cellData.colSpan || 1,
  1496. rowspan: cellData.rowSpan || 1,
  1497. text: textDiv.innerText,
  1498. style: {
  1499. ...style,
  1500. align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
  1501. fontsize,
  1502. fontname,
  1503. color,
  1504. bold: cellData.fontBold,
  1505. backcolor: cellData.fillColor,
  1506. },
  1507. })
  1508. textDiv = null
  1509. }
  1510. data.push(rowCells)
  1511. }
  1512. const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
  1513. const colWidths: number[] = el.colWidths.map(item => item / allWidth)
  1514. const firstCell = el.data[0][0]
  1515. const border = firstCell.borders.top ||
  1516. firstCell.borders.bottom ||
  1517. el.borders.top ||
  1518. el.borders.bottom ||
  1519. firstCell.borders.left ||
  1520. firstCell.borders.right ||
  1521. el.borders.left ||
  1522. el.borders.right
  1523. const borderWidth = border?.borderWidth || 0
  1524. const borderStyle = border?.borderType || 'solid'
  1525. const borderColor = border?.borderColor || '#eeece1'
  1526. slide.elements.push({
  1527. type: 'table',
  1528. id: nanoid(10),
  1529. width: el.width,
  1530. height: el.height,
  1531. left: el.left,
  1532. top: el.top,
  1533. colWidths,
  1534. rotate: 0,
  1535. data,
  1536. outline: {
  1537. width: +(borderWidth * ratio || 2).toFixed(2),
  1538. style: borderStyle,
  1539. color: borderColor,
  1540. },
  1541. cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
  1542. })
  1543. }
  1544. // ---------- 图表 ----------
  1545. else if (el.type === 'chart') {
  1546. let labels: string[]
  1547. let legends: string[]
  1548. let series: number[][]
  1549. if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
  1550. labels = el.data[0].map((_, index) => lang.ssCoord.replace(/\*/g, (index + 1)))
  1551. legends = ['X', 'Y']
  1552. series = el.data
  1553. }
  1554. else {
  1555. const data = el.data as ChartItem[]
  1556. labels = Object.values(data[0].xlabels)
  1557. legends = data.map(item => item.key)
  1558. series = data.map(item => item.values.map(v => v.y))
  1559. }
  1560. const options: ChartOptions = {}
  1561. let chartType: ChartType = 'bar'
  1562. switch (el.chartType) {
  1563. case 'barChart':
  1564. case 'bar3DChart':
  1565. chartType = 'bar'
  1566. if (el.barDir === 'bar') chartType = 'column'
  1567. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  1568. break
  1569. case 'lineChart':
  1570. case 'line3DChart':
  1571. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  1572. chartType = 'line'
  1573. break
  1574. case 'areaChart':
  1575. case 'area3DChart':
  1576. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  1577. chartType = 'area'
  1578. break
  1579. case 'scatterChart':
  1580. case 'bubbleChart':
  1581. chartType = 'scatter'
  1582. break
  1583. case 'pieChart':
  1584. case 'pie3DChart':
  1585. chartType = 'pie'
  1586. break
  1587. case 'radarChart':
  1588. chartType = 'radar'
  1589. break
  1590. case 'doughnutChart':
  1591. chartType = 'ring'
  1592. break
  1593. default:
  1594. }
  1595. slide.elements.push({
  1596. type: 'chart',
  1597. id: nanoid(10),
  1598. chartType,
  1599. width: el.width,
  1600. height: el.height,
  1601. left: el.left,
  1602. top: el.top,
  1603. rotate: 0,
  1604. themeColors: el.colors.length ? el.colors : theme.value.themeColors,
  1605. textColor: theme.value.fontColor,
  1606. data: {
  1607. labels,
  1608. legends,
  1609. series,
  1610. },
  1611. options,
  1612. })
  1613. }
  1614. // ---------- 组合 ----------
  1615. else if (el.type === 'group') {
  1616. // 先将子元素坐标转换到画布绝对坐标
  1617. let elements: BaseElement[] = el.elements.map((_el: any) => {
  1618. let left = _el.left + originLeft
  1619. let top = _el.top + originTop
  1620. if (el.rotate) {
  1621. const { x, y } = calculateRotatedPosition(
  1622. originLeft, originTop, originWidth, originHeight,
  1623. _el.left, _el.top, el.rotate
  1624. )
  1625. left = x
  1626. top = y
  1627. }
  1628. const element = {
  1629. ..._el,
  1630. left,
  1631. top,
  1632. }
  1633. if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
  1634. if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
  1635. return element
  1636. })
  1637. if (el.isFlipH) elements = flipGroupElements(elements, 'y')
  1638. if (el.isFlipV) elements = flipGroupElements(elements, 'x')
  1639. // 递归解析子元素(注意:子元素的上传任务会加入同一个 uploadTasks 数组)
  1640. await parseElements(elements, el)
  1641. }
  1642. // ---------- 图表组合(SmartArt)----------
  1643. else if (el.type === 'diagram') {
  1644. const elements = el.elements.map((_el: any) => ({
  1645. ..._el,
  1646. left: _el.left + originLeft,
  1647. top: _el.top + originTop,
  1648. }))
  1649. await parseElements(elements, el)
  1650. }
  1651. }
  1652. }
  1653. // 开始解析当前幻灯片的所有元素(包括布局元素)
  1654. await parseElements([...item.elements, ...item.layoutElements])
  1655. // 幻灯片构建完成,加入数组
  1656. slides.push(slide)
  1657. }
  1658. // 根据选项将幻灯片插入 store
  1659. if (cover) {
  1660. slidesStore.updateSlideIndex(0)
  1661. slidesStore.setSlides(slides)
  1662. addHistorySnapshot()
  1663. }
  1664. else if (isEmptySlide.value) {
  1665. slidesStore.setSlides(slides)
  1666. addHistorySnapshot()
  1667. }
  1668. else {
  1669. addSlidesFromData(slides)
  1670. }
  1671. // 等待当前幻灯片内所有上传任务完成
  1672. await Promise.all(uploadTasks)
  1673. exporting.value = false
  1674. onclose?.()
  1675. /*
  1676. // 更新视口尺寸(如果提供了的话)
  1677. if (width !== undefined && height !== undefined) {
  1678. console.log('正在触发视口尺寸更新事件:', { width, height, viewportRatio })
  1679. // 同时也要更新slidesStore中的相关数据
  1680. if (slidesStore.setViewportSize) {
  1681. console.log('正在更新store中的视口尺寸')
  1682. slidesStore.setViewportSize(width)
  1683. if (slidesStore.setViewportRatio && viewportRatio !== undefined) {
  1684. slidesStore.setViewportRatio(viewportRatio)
  1685. console.log('视口比例已更新:', viewportRatio)
  1686. }
  1687. }
  1688. window.dispatchEvent(new CustomEvent('viewportSizeUpdated', {
  1689. detail: { width, height, viewportRatio }
  1690. }))
  1691. console.log('视口尺寸更新事件已触发')
  1692. }
  1693. // 导入成功后,触发画布尺寸更新
  1694. // 使用 nextTick 确保DOM更新完成后再触发
  1695. console.log('开始触发画布尺寸更新事件...')
  1696. nextTick(() => {
  1697. console.log('DOM更新完成,触发 slidesDataUpdated 事件')
  1698. // 触发自定义事件,通知需要更新画布尺寸的组件
  1699. window.dispatchEvent(new CustomEvent('slidesDataUpdated', {
  1700. detail: {
  1701. slides,
  1702. cover,
  1703. title,
  1704. theme,
  1705. width,
  1706. height,
  1707. viewportRatio,
  1708. timestamp: Date.now()
  1709. }
  1710. }))
  1711. console.log('slidesDataUpdated 事件已触发')
  1712. // 检查并调整幻灯片索引,确保在有效范围内
  1713. const newSlideCount = slides.length
  1714. const currentIndex = slidesStore.slideIndex
  1715. if (currentIndex >= newSlideCount) {
  1716. console.log('调整幻灯片索引:', currentIndex, '->', Math.max(0, newSlideCount - 1))
  1717. slidesStore.updateSlideIndex(Math.max(0, newSlideCount - 1))
  1718. }
  1719. console.log('画布尺寸更新事件处理完成')
  1720. })
  1721. */
  1722. }
  1723. reader.readAsArrayBuffer(file)
  1724. }
  1725. const getFile = (url: string): Promise<{ data: any }> => {
  1726. return new Promise((resolve, reject) => {
  1727. // 检查 AWS SDK 是否可用
  1728. if (typeof window !== 'undefined' && !window.AWS) {
  1729. reject(new Error('AWS SDK not available'))
  1730. return
  1731. }
  1732. const credentials = {
  1733. accessKeyId: 'AKIATLPEDU37QV5CHLMH',
  1734. secretAccessKey: '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',
  1735. } // 秘钥形式的登录上传
  1736. window.AWS.config.update(credentials)
  1737. window.AWS.config.region = 'cn-northwest-1' // 设置区域
  1738. const s3 = new window.AWS.S3({ params: { Bucket: 'ccrb' } })
  1739. // 解析文件名
  1740. const bucketUrl = 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/'
  1741. if (!url.startsWith(bucketUrl)) {
  1742. reject(new Error('Invalid S3 URL format'))
  1743. return
  1744. }
  1745. const name = decodeURIComponent(url.split(bucketUrl)[1])
  1746. // const name = url.split(bucketUrl)[1]
  1747. console.log('aws-name:', name)
  1748. if (!name) {
  1749. reject(new Error('Could not extract file name from URL'))
  1750. return
  1751. }
  1752. const params = {
  1753. Bucket: 'ccrb',
  1754. Key: name,
  1755. }
  1756. s3.getObject(params, (err: any, data: any) => {
  1757. if (err) {
  1758. console.error('S3 getObject error:', err, err.stack)
  1759. reject(err)
  1760. }
  1761. else {
  1762. console.log('S3 getObject success:', data)
  1763. resolve({ data: data.Body })
  1764. }
  1765. })
  1766. })
  1767. }
  1768. const getFile2 = (url: string): Promise<{ data: any }> => {
  1769. return new Promise((resolve, reject) => {
  1770. console.log('直接使用原始 URL 获取文件:', url)
  1771. // 直接使用 fetch 获取文件,浏览器会自动处理 URL 解码
  1772. fetch(url)
  1773. .then(response => {
  1774. if (!response.ok) {
  1775. console.error('HTTP 错误:', response.status, response.statusText)
  1776. throw new Error(`HTTP error! status: ${response.status}`)
  1777. }
  1778. console.log('文件获取成功,大小:', response.headers.get('content-length'))
  1779. return response.arrayBuffer()
  1780. })
  1781. .then(buffer => {
  1782. console.log('文件内容读取成功,大小:', buffer.byteLength)
  1783. resolve({ data: buffer })
  1784. })
  1785. .catch(error => {
  1786. console.error('Fetch error:', error)
  1787. reject(error)
  1788. })
  1789. })
  1790. }
  1791. return {
  1792. importSpecificFile,
  1793. importJSON,
  1794. importPPTXFile,
  1795. readJSON,
  1796. exportJSON2,
  1797. exporting,
  1798. getFile,
  1799. getFile2,
  1800. dataToFile,
  1801. uploadFileToS3
  1802. }
  1803. }