useImport.ts 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038
  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. // ----- 辅助函数:将输入统一转换为 { blob, mime } -----
  447. async function getBlobAndMime(input: string | Blob): Promise<{ blob: Blob; mime: string }> {
  448. // 1. 已经是 Blob
  449. if (input instanceof Blob) {
  450. return { blob: input, mime: input.type }
  451. }
  452. // 2. 处理字符串
  453. if (input.startsWith('data:')) {
  454. // data URL → 通过 fetch 获取 Blob(自动获得正确的 MIME 类型)
  455. const response = await fetch(input)
  456. const blob = await response.blob()
  457. return { blob, mime: blob.type }
  458. }
  459. // 纯 base64 字符串 → 按原逻辑默认当作 PNG
  460. const binary = atob(input)
  461. const bytes = new Uint8Array(binary.length)
  462. for (let i = 0; i < binary.length; i++) {
  463. bytes[i] = binary.charCodeAt(i)
  464. }
  465. // 默认 MIME 为 image/png(与原函数行为一致)
  466. const blob = new Blob([bytes], { type: 'image/png' })
  467. return { blob, mime: 'image/png' }
  468. }
  469. // 获取统一的 blob 和实际 MIME 类型
  470. const { blob, mime } = await getBlobAndMime(data)
  471. // ----- 非 PNG 格式:直接返回原始文件(不处理透明)-----
  472. if (mime !== 'image/png') {
  473. return new File([blob], filename, { type: mime })
  474. }
  475. // ----- PNG 格式:执行白色变透明处理 -----
  476. // 1. 创建对象 URL 用于加载图片
  477. const imageUrl = URL.createObjectURL(blob)
  478. const needRevoke = true
  479. // 2. 加载图像
  480. const img = await new Promise<HTMLImageElement>((resolve, reject) => {
  481. const image = new Image()
  482. image.onload = () => resolve(image)
  483. image.onerror = reject
  484. // Blob URL 不需要设置 crossOrigin
  485. image.src = imageUrl
  486. })
  487. const canvas = document.createElement('canvas')
  488. try {
  489. canvas.width = img.width
  490. canvas.height = img.height
  491. const ctx = canvas.getContext('2d')!
  492. ctx.drawImage(img, 0, 0)
  493. // 3. 获取像素数据,将接近白色的像素设为透明
  494. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  495. const dataArray = imageData.data
  496. for (let i = 0; i < dataArray.length; i += 4) {
  497. const r = dataArray[i]
  498. const g = dataArray[i + 1]
  499. const b = dataArray[i + 2]
  500. const dr = r - 255
  501. const dg = g - 255
  502. const db = b - 255
  503. const dist = Math.sqrt(dr * dr + dg * dg + db * db)
  504. if (dist <= tolerance) {
  505. dataArray[i + 3] = 0 // 完全透明
  506. }
  507. }
  508. ctx.putImageData(imageData, 0, 0)
  509. }
  510. finally {
  511. if (needRevoke) {
  512. URL.revokeObjectURL(imageUrl)
  513. }
  514. }
  515. // 4. 导出为 PNG Blob
  516. const outputBlob = await new Promise<Blob>((resolve, reject) => {
  517. canvas.toBlob((blob) => {
  518. if (blob) resolve(blob)
  519. else reject(new Error('Canvas toBlob failed'))
  520. }, 'image/png')
  521. })
  522. return new File([outputBlob], filename, { type: 'image/png' })
  523. }
  524. /**
  525. * 上传 File 到 S3,返回公开访问的 URL
  526. */
  527. const uploadFileToS3 = (file: File): Promise<string> => {
  528. return new Promise((resolve, reject) => {
  529. if (typeof window === 'undefined' || !window.AWS) {
  530. reject(new Error('AWS SDK not available'))
  531. return
  532. }
  533. const credentials = {
  534. accessKeyId: 'AKIATLPEDU37QV5CHLMH',
  535. secretAccessKey: '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',
  536. }
  537. window.AWS.config.update(credentials)
  538. window.AWS.config.region = 'cn-northwest-1'
  539. const bucket = new window.AWS.S3({
  540. params: { Bucket: 'ccrb' }, httpOptions: {
  541. timeout: 600000 // 10分钟超时
  542. }
  543. })
  544. const ext = file.name.split('.').pop() || 'bin'
  545. const key = `${file.name.split('.')[0]}_${Date.now()}.${ext}`
  546. const params = {
  547. Key: 'pptto/' + key,
  548. ContentType: file.type,
  549. Body: file,
  550. ACL: 'public-read',
  551. }
  552. const options = {
  553. partSize: 5 * 1024 * 1024, // 2GB 分片,可酌情调小
  554. queueSize: 2,
  555. leavePartsOnError: true,
  556. }
  557. bucket
  558. .upload(params, options)
  559. .promise()
  560. .then(data => resolve(data.Location))
  561. .catch(err => reject(err))
  562. })
  563. }
  564. /*
  565. // 导入PPTX文件
  566. const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal }) => {
  567. console.log('导入', files)
  568. const defaultOptions = {
  569. cover: false,
  570. fixedViewport: false,
  571. }
  572. const { cover, fixedViewport, signal } = { ...defaultOptions, ...options }
  573. const file = files[0]
  574. if (!file) return
  575. exporting.value = true
  576. const shapeList: ShapePoolItem[] = []
  577. for (const item of SHAPE_LIST) {
  578. shapeList.push(...item.children)
  579. }
  580. const reader = new FileReader()
  581. reader.onload = async (e: ProgressEvent<FileReader>) => {
  582. // 检查是否已取消
  583. if (signal?.aborted) {
  584. exporting.value = false
  585. return
  586. }
  587. let json = null
  588. try {
  589. json = await parse(e.target!.result as ArrayBuffer)
  590. }
  591. catch (error) {
  592. exporting.value = false
  593. console.log('导入PPTX文件失败:', error)
  594. message.error('无法正确读取 / 解析该文件')
  595. return
  596. }
  597. let ratio = 96 / 72;
  598. //let ratio = 1
  599. const width = json.size.width
  600. if (fixedViewport) ratio = 1000 / width
  601. else slidesStore.setViewportSize(width * ratio)
  602. slidesStore.setTheme({ themeColors: json.themeColors })
  603. const slides: Slide[] = []
  604. for (const item of json.slides) {
  605. const { type, value } = item.fill
  606. let background: SlideBackground
  607. if (type === 'image') {
  608. background = {
  609. type: 'image',
  610. image: {
  611. src: value.picBase64,
  612. size: 'cover',
  613. },
  614. }
  615. }
  616. else if (type === 'gradient') {
  617. background = {
  618. type: 'gradient',
  619. gradient: {
  620. type: value.path === 'line' ? 'linear' : 'radial',
  621. colors: value.colors.map(item => ({
  622. ...item,
  623. pos: parseInt(item.pos),
  624. })),
  625. rotate: value.rot + 90,
  626. },
  627. }
  628. }
  629. else {
  630. background = {
  631. type: 'solid',
  632. color: value || '#fff',
  633. }
  634. }
  635. const slide: Slide = {
  636. id: nanoid(10),
  637. elements: [],
  638. background,
  639. remark: item.note || '',
  640. }
  641. const parseElements = (elements: Element[]) => {
  642. const sortedElements = elements.sort((a, b) => a.order - b.order)
  643. console.log(sortedElements)
  644. for (const el of sortedElements) {
  645. const originWidth = el.width || 1
  646. const originHeight = el.height || 1
  647. const originLeft = el.left
  648. const originTop = el.top
  649. el.width = el.width * ratio
  650. el.height = el.height * ratio
  651. el.left = el.left * ratio
  652. el.top = el.top * ratio
  653. if (el.type === 'text') {
  654. const textEl: PPTTextElement = {
  655. type: 'text',
  656. id: nanoid(10),
  657. width: el.width,
  658. height: el.height,
  659. left: el.left,
  660. top: el.top,
  661. rotate: el.rotate,
  662. defaultFontName: theme.value.fontName,
  663. defaultColor: theme.value.fontColor,
  664. content: convertFontSizePtToPx(el.content, ratio),
  665. lineHeight: 1,
  666. outline: {
  667. color: el.borderColor,
  668. width: +(el.borderWidth * ratio).toFixed(2),
  669. style: el.borderType,
  670. },
  671. fill: el.fill.type === 'color' ? el.fill.value : '',
  672. vertical: el.isVertical,
  673. }
  674. if (el.shadow) {
  675. textEl.shadow = {
  676. h: el.shadow.h * ratio,
  677. v: el.shadow.v * ratio,
  678. blur: el.shadow.blur * ratio,
  679. color: el.shadow.color,
  680. }
  681. }
  682. slide.elements.push(textEl)
  683. }
  684. else if (el.type === 'image') {
  685. const element: PPTImageElement = {
  686. type: 'image',
  687. id: nanoid(10),
  688. src: el.src,
  689. width: el.width,
  690. height: el.height,
  691. left: el.left,
  692. top: el.top,
  693. fixedRatio: true,
  694. rotate: el.rotate,
  695. flipH: el.isFlipH,
  696. flipV: el.isFlipV,
  697. }
  698. if (el.borderWidth) {
  699. element.outline = {
  700. color: el.borderColor,
  701. width: +(el.borderWidth * ratio).toFixed(2),
  702. style: el.borderType,
  703. }
  704. }
  705. const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid']
  706. if (el.rect) {
  707. element.clip = {
  708. shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
  709. range: [
  710. [
  711. el.rect.l || 0,
  712. el.rect.t || 0,
  713. ],
  714. [
  715. 100 - (el.rect.r || 0),
  716. 100 - (el.rect.b || 0),
  717. ],
  718. ]
  719. }
  720. }
  721. else if (el.geom && clipShapeTypes.includes(el.geom)) {
  722. element.clip = {
  723. shape: el.geom,
  724. range: [[0, 0], [100, 100]]
  725. }
  726. }
  727. slide.elements.push(element)
  728. }
  729. else if (el.type === 'math') {
  730. slide.elements.push({
  731. type: 'image',
  732. id: nanoid(10),
  733. src: el.picBase64,
  734. width: el.width,
  735. height: el.height,
  736. left: el.left,
  737. top: el.top,
  738. fixedRatio: true,
  739. rotate: 0,
  740. })
  741. }
  742. else if (el.type === 'audio') {
  743. slide.elements.push({
  744. type: 'audio',
  745. id: nanoid(10),
  746. src: el.blob,
  747. width: el.width,
  748. height: el.height,
  749. left: el.left,
  750. top: el.top,
  751. rotate: 0,
  752. fixedRatio: false,
  753. color: theme.value.themeColors[0],
  754. loop: false,
  755. autoplay: false,
  756. })
  757. }
  758. else if (el.type === 'video') {
  759. slide.elements.push({
  760. type: 'video',
  761. id: nanoid(10),
  762. src: (el.blob || el.src)!,
  763. width: el.width,
  764. height: el.height,
  765. left: el.left,
  766. top: el.top,
  767. rotate: 0,
  768. autoplay: false,
  769. })
  770. }
  771. else if (el.type === 'shape') {
  772. if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
  773. const lineElement = parseLineElement(el, ratio)
  774. slide.elements.push(lineElement)
  775. }
  776. else {
  777. const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
  778. const vAlignMap: { [key: string]: ShapeTextAlign } = {
  779. 'mid': 'middle',
  780. 'down': 'bottom',
  781. 'up': 'top',
  782. }
  783. const gradient: Gradient | undefined = el.fill?.type === 'gradient' ? {
  784. type: el.fill.value.path === 'line' ? 'linear' : 'radial',
  785. colors: el.fill.value.colors.map(item => ({
  786. ...item,
  787. pos: parseInt(item.pos),
  788. })),
  789. rotate: el.fill.value.rot,
  790. } : undefined
  791. const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
  792. const fill = el.fill?.type === 'color' ? el.fill.value : ''
  793. const element: PPTShapeElement = {
  794. type: 'shape',
  795. id: nanoid(10),
  796. width: el.width,
  797. height: el.height,
  798. left: el.left,
  799. top: el.top,
  800. viewBox: [200, 200],
  801. path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
  802. fill,
  803. gradient,
  804. pattern,
  805. fixedRatio: false,
  806. rotate: el.rotate,
  807. outline: {
  808. color: el.borderColor,
  809. width: +(el.borderWidth * ratio).toFixed(2),
  810. style: el.borderType,
  811. },
  812. text: {
  813. content: convertFontSizePtToPx(el.content, ratio),
  814. defaultFontName: theme.value.fontName,
  815. defaultColor: theme.value.fontColor,
  816. align: vAlignMap[el.vAlign] || 'middle',
  817. },
  818. flipH: el.isFlipH,
  819. flipV: el.isFlipV,
  820. }
  821. if (el.shadow) {
  822. element.shadow = {
  823. h: el.shadow.h * ratio,
  824. v: el.shadow.v * ratio,
  825. blur: el.shadow.blur * ratio,
  826. color: el.shadow.color,
  827. }
  828. }
  829. if (shape) {
  830. element.path = shape.path
  831. element.viewBox = shape.viewBox
  832. if (shape.pathFormula) {
  833. element.pathFormula = shape.pathFormula
  834. element.viewBox = [el.width, el.height]
  835. const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
  836. if ('editable' in pathFormula && pathFormula.editable) {
  837. element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
  838. element.keypoints = pathFormula.defaultValue
  839. }
  840. else element.path = pathFormula.formula(el.width, el.height)
  841. }
  842. }
  843. else if (el.path && el.path.indexOf('NaN') === -1) {
  844. const { maxX, maxY } = getSvgPathRange(el.path)
  845. element.path = el.path
  846. element.viewBox = [maxX || originWidth, maxY || originHeight]
  847. }
  848. if (el.shapType === 'custom') {
  849. if (el.path!.indexOf('NaN') !== -1) {
  850. if (element.width === 0) element.width = 0.1
  851. if (element.height === 0) element.height = 0.1
  852. element.path = el.path!.replace(/NaN/g, '0')
  853. }
  854. else {
  855. element.special = true
  856. element.path = el.path!
  857. }
  858. const { maxX, maxY } = getSvgPathRange(element.path)
  859. element.viewBox = [maxX || originWidth, maxY || originHeight]
  860. }
  861. if (element.path) slide.elements.push(element)
  862. }
  863. }
  864. else if (el.type === 'table') {
  865. const row = el.data.length
  866. const col = el.data[0].length
  867. const style: TableCellStyle = {
  868. fontname: theme.value.fontName,
  869. color: theme.value.fontColor,
  870. }
  871. const data: TableCell[][] = []
  872. for (let i = 0; i < row; i++) {
  873. const rowCells: TableCell[] = []
  874. for (let j = 0; j < col; j++) {
  875. const cellData = el.data[i][j]
  876. let textDiv: HTMLDivElement | null = document.createElement('div')
  877. textDiv.innerHTML = cellData.text
  878. const p = textDiv.querySelector('p')
  879. const align = p?.style.textAlign || 'left'
  880. const span = textDiv.querySelector('span')
  881. const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : ''
  882. const fontname = span?.style.fontFamily || ''
  883. const color = span?.style.color || cellData.fontColor
  884. rowCells.push({
  885. id: nanoid(10),
  886. colspan: cellData.colSpan || 1,
  887. rowspan: cellData.rowSpan || 1,
  888. text: textDiv.innerText,
  889. style: {
  890. ...style,
  891. align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
  892. fontsize,
  893. fontname,
  894. color,
  895. bold: cellData.fontBold,
  896. backcolor: cellData.fillColor,
  897. },
  898. })
  899. textDiv = null
  900. }
  901. data.push(rowCells)
  902. }
  903. const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
  904. const colWidths: number[] = el.colWidths.map(item => item / allWidth)
  905. const firstCell = el.data[0][0]
  906. const border = firstCell.borders.top ||
  907. firstCell.borders.bottom ||
  908. el.borders.top ||
  909. el.borders.bottom ||
  910. firstCell.borders.left ||
  911. firstCell.borders.right ||
  912. el.borders.left ||
  913. el.borders.right
  914. const borderWidth = border?.borderWidth || 0
  915. const borderStyle = border?.borderType || 'solid'
  916. const borderColor = border?.borderColor || '#eeece1'
  917. slide.elements.push({
  918. type: 'table',
  919. id: nanoid(10),
  920. width: el.width,
  921. height: el.height,
  922. left: el.left,
  923. top: el.top,
  924. colWidths,
  925. rotate: 0,
  926. data,
  927. outline: {
  928. width: +(borderWidth * ratio || 2).toFixed(2),
  929. style: borderStyle,
  930. color: borderColor,
  931. },
  932. cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
  933. })
  934. }
  935. else if (el.type === 'chart') {
  936. let labels: string[]
  937. let legends: string[]
  938. let series: number[][]
  939. if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
  940. labels = el.data[0].map((item, index) => `坐标${index + 1}`)
  941. legends = ['X', 'Y']
  942. series = el.data
  943. }
  944. else {
  945. const data = el.data as ChartItem[]
  946. labels = Object.values(data[0].xlabels)
  947. legends = data.map(item => item.key)
  948. series = data.map(item => item.values.map(v => v.y))
  949. }
  950. const options: ChartOptions = {}
  951. let chartType: ChartType = 'bar'
  952. switch (el.chartType) {
  953. case 'barChart':
  954. case 'bar3DChart':
  955. chartType = 'bar'
  956. if (el.barDir === 'bar') chartType = 'column'
  957. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  958. break
  959. case 'lineChart':
  960. case 'line3DChart':
  961. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  962. chartType = 'line'
  963. break
  964. case 'areaChart':
  965. case 'area3DChart':
  966. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  967. chartType = 'area'
  968. break
  969. case 'scatterChart':
  970. case 'bubbleChart':
  971. chartType = 'scatter'
  972. break
  973. case 'pieChart':
  974. case 'pie3DChart':
  975. chartType = 'pie'
  976. break
  977. case 'radarChart':
  978. chartType = 'radar'
  979. break
  980. case 'doughnutChart':
  981. chartType = 'ring'
  982. break
  983. default:
  984. }
  985. slide.elements.push({
  986. type: 'chart',
  987. id: nanoid(10),
  988. chartType: chartType,
  989. width: el.width,
  990. height: el.height,
  991. left: el.left,
  992. top: el.top,
  993. rotate: 0,
  994. themeColors: el.colors.length ? el.colors : theme.value.themeColors,
  995. textColor: theme.value.fontColor,
  996. data: {
  997. labels,
  998. legends,
  999. series,
  1000. },
  1001. options,
  1002. })
  1003. }
  1004. else if (el.type === 'group') {
  1005. let elements: BaseElement[] = el.elements.map(_el => {
  1006. let left = _el.left + originLeft
  1007. let top = _el.top + originTop
  1008. if (el.rotate) {
  1009. const { x, y } = calculateRotatedPosition(originLeft, originTop, originWidth, originHeight, _el.left, _el.top, el.rotate)
  1010. left = x
  1011. top = y
  1012. }
  1013. const element = {
  1014. ..._el,
  1015. left,
  1016. top,
  1017. }
  1018. if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
  1019. if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
  1020. return element
  1021. })
  1022. if (el.isFlipH) elements = flipGroupElements(elements, 'y')
  1023. if (el.isFlipV) elements = flipGroupElements(elements, 'x')
  1024. parseElements(elements)
  1025. }
  1026. else if (el.type === 'diagram') {
  1027. const elements = el.elements.map(_el => ({
  1028. ..._el,
  1029. left: _el.left + originLeft,
  1030. top: _el.top + originTop,
  1031. }))
  1032. parseElements(elements)
  1033. }
  1034. }
  1035. }
  1036. parseElements([...item.elements, ...item.layoutElements])
  1037. slides.push(slide)
  1038. }
  1039. if (cover) {
  1040. slidesStore.updateSlideIndex(0)
  1041. slidesStore.setSlides(slides)
  1042. addHistorySnapshot()
  1043. }
  1044. else if (isEmptySlide.value) {
  1045. slidesStore.setSlides(slides)
  1046. addHistorySnapshot()
  1047. }
  1048. else addSlidesFromData(slides)
  1049. exporting.value = false
  1050. }
  1051. reader.readAsArrayBuffer(file)
  1052. // 监听取消信号
  1053. signal?.addEventListener('abort', () => {
  1054. reader.abort()
  1055. exporting.value = false
  1056. })
  1057. // 监听取消信号
  1058. signal?.addEventListener('abort', () => {
  1059. reader.abort()
  1060. exporting.value = false
  1061. })
  1062. }
  1063. */
  1064. const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void }) => {
  1065. console.log('导入', files)
  1066. const defaultOptions = {
  1067. cover: false,
  1068. fixedViewport: false,
  1069. }
  1070. const { cover, fixedViewport, signal, onclose } = { ...defaultOptions, ...options }
  1071. const file = files[0]
  1072. if (!file) return
  1073. exporting.value = true // 假设 exporting 是一个全局 ref
  1074. // 预加载形状库(用于后续形状匹配)
  1075. const shapeList: ShapePoolItem[] = []
  1076. for (const item of SHAPE_LIST) {
  1077. shapeList.push(...item.children)
  1078. }
  1079. const reader = new FileReader()
  1080. reader.onload = async (e: ProgressEvent<FileReader>) => {
  1081. // 检查是否已取消
  1082. if (signal?.aborted) {
  1083. exporting.value = false
  1084. return
  1085. }
  1086. let json = null
  1087. try {
  1088. json = await parse(e.target!.result as ArrayBuffer)
  1089. }
  1090. catch (error) {
  1091. exporting.value = false
  1092. console.log('导入PPTX文件失败:', error)
  1093. message.error(lang.ssFileReadFail)
  1094. return
  1095. }
  1096. if (signal?.aborted) {
  1097. exporting.value = false
  1098. return
  1099. }
  1100. // 计算缩放比例
  1101. let ratio = 96 / 72 // PPTX 默认 72 DPI,屏幕 96 DPI
  1102. const width = json.size.width
  1103. const height = json.size.height
  1104. const viewportRatio = json.size.viewportRatio || (height && width ? height / width : undefined)
  1105. if (fixedViewport) {
  1106. ratio = 1000 / width // 固定视口宽度为 1000px
  1107. }
  1108. else {
  1109. slidesStore.setViewportSize(width * ratio) // 调整画布大小
  1110. }
  1111. // 设置主题色
  1112. slidesStore.setTheme({ themeColors: json.themeColors })
  1113. const slides: Slide[] = []
  1114. // 收集当前幻灯片内所有上传任务
  1115. const uploadTasks: Promise<void>[] = []
  1116. // 遍历每一张幻灯片
  1117. for (const item of json.slides) {
  1118. // ----- 解析背景 -----
  1119. const { type, value } = item.fill
  1120. let background: SlideBackground
  1121. if (type === 'image') {
  1122. // 背景图片也可能需要上传(但 PPTX 背景图通常是内嵌 base64)
  1123. // 这里为了简化,暂不处理背景图片上传,如有需要可类似元素上传
  1124. background = {
  1125. type: 'image',
  1126. image: {
  1127. src: value.picBase64,
  1128. size: 'cover',
  1129. },
  1130. }
  1131. }
  1132. else if (type === 'gradient') {
  1133. background = {
  1134. type: 'gradient',
  1135. gradient: {
  1136. type: value.path === 'line' ? 'linear' : 'radial',
  1137. colors: value.colors.map(item => ({
  1138. ...item,
  1139. pos: parseInt(item.pos),
  1140. })),
  1141. rotate: value.rot + 90,
  1142. },
  1143. }
  1144. }
  1145. else {
  1146. background = {
  1147. type: 'solid',
  1148. color: value || '#fff',
  1149. }
  1150. }
  1151. const slide: Slide = {
  1152. id: nanoid(10),
  1153. elements: [],
  1154. background,
  1155. remark: item.note || '',
  1156. }
  1157. // ----- 解析元素(递归函数)-----
  1158. const parseElements = async (elements: any[], pelements: any = null) => {
  1159. // 按绘制顺序排序
  1160. const sortedElements = elements.sort((a, b) => a.order - b.order)
  1161. console.log(sortedElements)
  1162. for (const el of sortedElements) {
  1163. // 保存原始尺寸用于后续可能的路径计算
  1164. const originWidth = el.width || 1
  1165. const originHeight = el.height || 1
  1166. const originLeft = el.left
  1167. const originTop = el.top
  1168. // 保存原始尺寸用于后续可能的路径计算
  1169. const poriginWidth = pelements?.width
  1170. const poriginHeight = pelements?.height
  1171. const poriginLeft = pelements?.left
  1172. const poriginTop = pelements?.top
  1173. // 应用缩放
  1174. el.width = el.width * ratio
  1175. el.height = el.height * ratio
  1176. el.left = el.left * ratio
  1177. el.top = el.top * ratio
  1178. if (el.type === 'text') {
  1179. const textEl: PPTTextElement = {
  1180. type: 'text',
  1181. id: nanoid(10),
  1182. width: el.width,
  1183. height: el.height,
  1184. left: el.left,
  1185. top: el.top,
  1186. rotate: el.rotate,
  1187. defaultFontName: theme.value.fontName,
  1188. defaultColor: theme.value.fontColor,
  1189. content: convertFontSizePtToPx(el.content, ratio),
  1190. style: getStyle(convertFontSizePtToPx(el.content, ratio)),
  1191. lineHeight: 1.15,
  1192. outline: {
  1193. color: el.borderColor,
  1194. width: +(el.borderWidth * ratio).toFixed(2),
  1195. style: el.borderType,
  1196. },
  1197. fill: el.fill.type === 'color' ? el.fill.value : '',
  1198. vertical: el.isVertical,
  1199. }
  1200. if (el.shadow) {
  1201. textEl.shadow = {
  1202. h: el.shadow.h * ratio,
  1203. v: el.shadow.v * ratio,
  1204. blur: el.shadow.blur * ratio,
  1205. color: el.shadow.color,
  1206. }
  1207. }
  1208. slide.elements.push(textEl)
  1209. }
  1210. // ---------- 图片 ----------
  1211. if (el.type === 'image') {
  1212. const element: PPTImageElement = {
  1213. type: 'image',
  1214. id: nanoid(10),
  1215. src: el.src, // 可能是 base64 或已有 URL
  1216. width: el.width,
  1217. height: el.height,
  1218. left: el.left,
  1219. top: el.top,
  1220. fixedRatio: true,
  1221. rotate: el.rotate,
  1222. flipH: el.isFlipH,
  1223. flipV: el.isFlipV,
  1224. }
  1225. // 边框
  1226. if (el.borderWidth) {
  1227. element.outline = {
  1228. color: el.borderColor,
  1229. width: +(el.borderWidth * ratio).toFixed(2),
  1230. style: el.borderType,
  1231. }
  1232. }
  1233. // 裁剪(形状剪裁)
  1234. const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid']
  1235. if (el.rect) {
  1236. element.clip = {
  1237. shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
  1238. range: [
  1239. [el.rect.l || 0, el.rect.t || 0],
  1240. [100 - (el.rect.r || 0), 100 - (el.rect.b || 0)],
  1241. ],
  1242. }
  1243. }
  1244. else if (el.geom && clipShapeTypes.includes(el.geom)) {
  1245. element.clip = {
  1246. shape: el.geom,
  1247. range: [[0, 0], [100, 100]],
  1248. }
  1249. }
  1250. // 如果 src 是 base64,触发上传
  1251. if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
  1252. const uploadTask = (async () => {
  1253. try {
  1254. const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`)
  1255. if (file) {
  1256. const url = await uploadFileToS3(file)
  1257. element.src = url // 替换为远程 URL
  1258. const slidesStore = useSlidesStore()
  1259. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1260. }
  1261. }
  1262. catch (error) {
  1263. console.error('Image upload failed:', error)
  1264. // 失败时保留原 base64(或可置空)
  1265. }
  1266. })()
  1267. uploadTasks.push(uploadTask)
  1268. }
  1269. slide.elements.push(element)
  1270. }
  1271. else if (el.type === 'math') {
  1272. const element: PPTImageElement = {
  1273. type: 'image',
  1274. id: nanoid(10),
  1275. src: el.picBase64,
  1276. width: el.width,
  1277. height: el.height,
  1278. left: el.left,
  1279. top: el.top,
  1280. fixedRatio: true,
  1281. rotate: 0,
  1282. }
  1283. // 如果 src 是 base64,触发上传
  1284. if (el.picBase64 && typeof el.picBase64 === 'string' && el.picBase64.startsWith('data:')) {
  1285. const uploadTask = (async () => {
  1286. try {
  1287. const file = makeWhiteTransparent(el.picBase64, `image_${Date.now()}.png`)
  1288. if (file) {
  1289. const url = await uploadFileToS3(file)
  1290. element.src = url // 替换为远程 URL
  1291. const slidesStore = useSlidesStore()
  1292. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1293. }
  1294. }
  1295. catch (error) {
  1296. console.error('Image upload failed:', error)
  1297. // 失败时保留原 base64(或可置空)
  1298. }
  1299. })()
  1300. uploadTasks.push(uploadTask)
  1301. }
  1302. slide.elements.push(element)
  1303. }
  1304. // ---------- 音频 ----------
  1305. else if (el.type === 'audio') {
  1306. const element: PPTAudioElement = {
  1307. type: 'audio',
  1308. id: nanoid(10),
  1309. src: el.blob,
  1310. width: el.width,
  1311. height: el.height,
  1312. left: el.left,
  1313. top: el.top,
  1314. rotate: 0,
  1315. fixedRatio: false,
  1316. color: theme.value.themeColors[0],
  1317. loop: false,
  1318. autoplay: false,
  1319. }
  1320. const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
  1321. if (localData) {
  1322. const uploadTask = (async () => {
  1323. try {
  1324. const file = await dataToFile(localData, `audio_${Date.now()}.mp3`, 'audio/mpeg')
  1325. if (file) {
  1326. const url = await uploadFileToS3(file)
  1327. element.src = url
  1328. const slidesStore = useSlidesStore()
  1329. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1330. }
  1331. }
  1332. catch (error) {
  1333. console.error('Audio upload failed:', error)
  1334. }
  1335. })()
  1336. uploadTasks.push(uploadTask)
  1337. }
  1338. slide.elements.push(element)
  1339. }
  1340. // ---------- 视频 ----------
  1341. else if (el.type === 'video') {
  1342. const element: PPTVideoElement = {
  1343. type: 'video',
  1344. id: nanoid(10),
  1345. src: (el.blob || el.src)!,
  1346. width: el.width,
  1347. height: el.height,
  1348. left: el.left,
  1349. top: el.top,
  1350. rotate: 0,
  1351. autoplay: false,
  1352. }
  1353. const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
  1354. if (localData) {
  1355. const uploadTask = (async () => {
  1356. try {
  1357. const file = await dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4')
  1358. if (file) {
  1359. const url = await uploadFileToS3(file)
  1360. element.src = url
  1361. const slidesStore = useSlidesStore()
  1362. slidesStore.updateElement({ id: element.id, props: { src: url } })
  1363. }
  1364. }
  1365. catch (error) {
  1366. console.error('Video upload failed:', error)
  1367. }
  1368. })()
  1369. uploadTasks.push(uploadTask)
  1370. }
  1371. slide.elements.push(element)
  1372. }
  1373. // ---------- 形状 ----------
  1374. else if (el.type === 'shape') {
  1375. if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
  1376. // 线条元素(单独处理)
  1377. const lineElement = parseLineElement(el, ratio)
  1378. slide.elements.push(lineElement)
  1379. }
  1380. else {
  1381. const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
  1382. const vAlignMap: { [key: string]: ShapeTextAlign } = {
  1383. mid: 'middle',
  1384. down: 'bottom',
  1385. up: 'top',
  1386. }
  1387. const gradient: Gradient | undefined = el.fill?.type === 'gradient'
  1388. ? {
  1389. type: el.fill.value.path === 'line' ? 'linear' : 'radial',
  1390. colors: el.fill.value.colors.map(item => ({
  1391. ...item,
  1392. pos: parseInt(item.pos),
  1393. })),
  1394. rotate: el.fill.value.rot,
  1395. }
  1396. : undefined
  1397. const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
  1398. const fill = el.fill?.type === 'color' ? el.fill.value : ''
  1399. const style = getStyle(convertFontSizePtToPx(el.content, ratio)) + (el.pathBBox.pWidth ? ';width:' + (el.pathBBox.pWidth * ratio) + 'px;height:' + (el.pathBBox.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
  1400. const element: PPTShapeElement = {
  1401. type: 'shape',
  1402. id: nanoid(10),
  1403. width: el.width,
  1404. height: el.height,
  1405. left: el.left,
  1406. top: el.top,
  1407. viewBox: [200, 200],
  1408. path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
  1409. fill,
  1410. gradient,
  1411. pattern,
  1412. fixedRatio: false,
  1413. rotate: el.rotate,
  1414. pathBBox: el.pathBBox,
  1415. outline: {
  1416. color: el.borderColor,
  1417. width: +(el.borderWidth * ratio).toFixed(2),
  1418. style: el.borderType,
  1419. },
  1420. text: {
  1421. content: convertFontSizePtToPx(el.content, ratio),
  1422. style: style,
  1423. defaultFontName: theme.value.fontName,
  1424. defaultColor: theme.value.fontColor,
  1425. align: vAlignMap[el.vAlign] || 'middle',
  1426. },
  1427. flipH: el.isFlipH,
  1428. flipV: el.isFlipV,
  1429. }
  1430. if (el.shadow) {
  1431. element.shadow = {
  1432. h: el.shadow.h * ratio,
  1433. v: el.shadow.v * ratio,
  1434. blur: el.shadow.blur * ratio,
  1435. color: el.shadow.color,
  1436. }
  1437. }
  1438. if (shape) {
  1439. element.path = shape.path
  1440. // const { maxX, maxY } = getSvgPathRange(el.path);
  1441. element.viewBox = shape.viewBox
  1442. // element.viewBox = [originWidth || maxX, originHeight || maxY];
  1443. if (shape.pathFormula) {
  1444. element.pathFormula = shape.pathFormula
  1445. element.viewBox = [el.width, el.height]
  1446. // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
  1447. const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
  1448. if ('editable' in pathFormula && pathFormula.editable) {
  1449. element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
  1450. element.keypoints = pathFormula.defaultValue
  1451. }
  1452. else {
  1453. element.path = pathFormula.formula(el.width, el.height)
  1454. }
  1455. }
  1456. }
  1457. else if (el.path && el.path.indexOf('NaN') === -1) {
  1458. const { maxX, maxY } = getSvgPathRange(el.path)
  1459. element.path = el.path
  1460. element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
  1461. // element.viewBox = [originWidth || maxX, originHeight || maxY];
  1462. // element.viewBox = originWidth? [(originWidth/(poriginWidth||1)), (originHeight/(poriginHeight||1))] : [maxX, maxY];
  1463. // element.viewBox = [poriginWidth || maxX, poriginHeight || maxY];
  1464. }
  1465. if (el.shapType === 'custom') {
  1466. if (el.path!.indexOf('NaN') !== -1) {
  1467. if (element.width === 0) element.width = 0.1
  1468. if (element.height === 0) element.height = 0.1
  1469. element.path = el.path!.replace(/NaN/g, '0')
  1470. }
  1471. const { maxX, maxY } = getSvgPathRange(element.path)
  1472. element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
  1473. // element.viewBox = [originWidth || maxX, originHeight || maxY];
  1474. // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
  1475. // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
  1476. // element.viewBox = [Math.max(maxX, originWidth), Math.max(maxY, originHeight)];
  1477. // element.viewBox = [originWidth, originHeight];
  1478. }
  1479. if (element.path) slide.elements.push(element)
  1480. }
  1481. }
  1482. // ---------- 表格 ----------
  1483. else if (el.type === 'table') {
  1484. const row = el.data.length
  1485. const col = el.data[0].length
  1486. const style: TableCellStyle = {
  1487. fontname: theme.value.fontName,
  1488. color: theme.value.fontColor,
  1489. }
  1490. const data: TableCell[][] = []
  1491. for (let i = 0; i < row; i++) {
  1492. const rowCells: TableCell[] = []
  1493. for (let j = 0; j < col; j++) {
  1494. const cellData = el.data[i][j]
  1495. let textDiv: HTMLDivElement | null = document.createElement('div')
  1496. textDiv.innerHTML = cellData.text
  1497. const p = textDiv.querySelector('p')
  1498. const align = p?.style.textAlign || 'left'
  1499. const span = textDiv.querySelector('span')
  1500. const fontsize = span?.style.fontSize
  1501. ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px'
  1502. : ''
  1503. const fontname = span?.style.fontFamily || ''
  1504. const color = span?.style.color || cellData.fontColor
  1505. rowCells.push({
  1506. id: nanoid(10),
  1507. colspan: cellData.colSpan || 1,
  1508. rowspan: cellData.rowSpan || 1,
  1509. text: textDiv.innerText,
  1510. style: {
  1511. ...style,
  1512. align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
  1513. fontsize,
  1514. fontname,
  1515. color,
  1516. bold: cellData.fontBold,
  1517. backcolor: cellData.fillColor,
  1518. },
  1519. })
  1520. textDiv = null
  1521. }
  1522. data.push(rowCells)
  1523. }
  1524. const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
  1525. const colWidths: number[] = el.colWidths.map(item => item / allWidth)
  1526. const firstCell = el.data[0][0]
  1527. const border = firstCell.borders.top ||
  1528. firstCell.borders.bottom ||
  1529. el.borders.top ||
  1530. el.borders.bottom ||
  1531. firstCell.borders.left ||
  1532. firstCell.borders.right ||
  1533. el.borders.left ||
  1534. el.borders.right
  1535. const borderWidth = border?.borderWidth || 0
  1536. const borderStyle = border?.borderType || 'solid'
  1537. const borderColor = border?.borderColor || '#eeece1'
  1538. slide.elements.push({
  1539. type: 'table',
  1540. id: nanoid(10),
  1541. width: el.width,
  1542. height: el.height,
  1543. left: el.left,
  1544. top: el.top,
  1545. colWidths,
  1546. rotate: 0,
  1547. data,
  1548. outline: {
  1549. width: +(borderWidth * ratio || 2).toFixed(2),
  1550. style: borderStyle,
  1551. color: borderColor,
  1552. },
  1553. cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
  1554. })
  1555. }
  1556. // ---------- 图表 ----------
  1557. else if (el.type === 'chart') {
  1558. let labels: string[]
  1559. let legends: string[]
  1560. let series: number[][]
  1561. if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
  1562. labels = el.data[0].map((_, index) => lang.ssCoord.replace(/\*/g, (index + 1)))
  1563. legends = ['X', 'Y']
  1564. series = el.data
  1565. }
  1566. else {
  1567. const data = el.data as ChartItem[]
  1568. labels = Object.values(data[0].xlabels)
  1569. legends = data.map(item => item.key)
  1570. series = data.map(item => item.values.map(v => v.y))
  1571. }
  1572. const options: ChartOptions = {}
  1573. let chartType: ChartType = 'bar'
  1574. switch (el.chartType) {
  1575. case 'barChart':
  1576. case 'bar3DChart':
  1577. chartType = 'bar'
  1578. if (el.barDir === 'bar') chartType = 'column'
  1579. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  1580. break
  1581. case 'lineChart':
  1582. case 'line3DChart':
  1583. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  1584. chartType = 'line'
  1585. break
  1586. case 'areaChart':
  1587. case 'area3DChart':
  1588. if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
  1589. chartType = 'area'
  1590. break
  1591. case 'scatterChart':
  1592. case 'bubbleChart':
  1593. chartType = 'scatter'
  1594. break
  1595. case 'pieChart':
  1596. case 'pie3DChart':
  1597. chartType = 'pie'
  1598. break
  1599. case 'radarChart':
  1600. chartType = 'radar'
  1601. break
  1602. case 'doughnutChart':
  1603. chartType = 'ring'
  1604. break
  1605. default:
  1606. }
  1607. slide.elements.push({
  1608. type: 'chart',
  1609. id: nanoid(10),
  1610. chartType,
  1611. width: el.width,
  1612. height: el.height,
  1613. left: el.left,
  1614. top: el.top,
  1615. rotate: 0,
  1616. themeColors: el.colors.length ? el.colors : theme.value.themeColors,
  1617. textColor: theme.value.fontColor,
  1618. data: {
  1619. labels,
  1620. legends,
  1621. series,
  1622. },
  1623. options,
  1624. })
  1625. }
  1626. // ---------- 组合 ----------
  1627. else if (el.type === 'group') {
  1628. // 先将子元素坐标转换到画布绝对坐标
  1629. let elements: BaseElement[] = el.elements.map((_el: any) => {
  1630. let left = _el.left + originLeft
  1631. let top = _el.top + originTop
  1632. if (el.rotate) {
  1633. const { x, y } = calculateRotatedPosition(
  1634. originLeft, originTop, originWidth, originHeight,
  1635. _el.left, _el.top, el.rotate
  1636. )
  1637. left = x
  1638. top = y
  1639. }
  1640. const element = {
  1641. ..._el,
  1642. left,
  1643. top,
  1644. }
  1645. if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
  1646. if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
  1647. return element
  1648. })
  1649. if (el.isFlipH) elements = flipGroupElements(elements, 'y')
  1650. if (el.isFlipV) elements = flipGroupElements(elements, 'x')
  1651. // 递归解析子元素(注意:子元素的上传任务会加入同一个 uploadTasks 数组)
  1652. await parseElements(elements, el)
  1653. }
  1654. // ---------- 图表组合(SmartArt)----------
  1655. else if (el.type === 'diagram') {
  1656. const elements = el.elements.map((_el: any) => ({
  1657. ..._el,
  1658. left: _el.left + originLeft,
  1659. top: _el.top + originTop,
  1660. }))
  1661. await parseElements(elements, el)
  1662. }
  1663. }
  1664. }
  1665. // 开始解析当前幻灯片的所有元素(包括布局元素)
  1666. await parseElements([...item.elements, ...item.layoutElements])
  1667. // 幻灯片构建完成,加入数组
  1668. slides.push(slide)
  1669. }
  1670. // 根据选项将幻灯片插入 store
  1671. if (cover) {
  1672. slidesStore.updateSlideIndex(0)
  1673. slidesStore.setSlides(slides)
  1674. addHistorySnapshot()
  1675. }
  1676. else if (isEmptySlide.value) {
  1677. slidesStore.setSlides(slides)
  1678. addHistorySnapshot()
  1679. }
  1680. else {
  1681. addSlidesFromData(slides)
  1682. }
  1683. // 等待当前幻灯片内所有上传任务完成
  1684. // await Promise.all(uploadTasks)
  1685. Promise.all(uploadTasks)
  1686. exporting.value = false
  1687. onclose?.()
  1688. /*
  1689. // 更新视口尺寸(如果提供了的话)
  1690. if (width !== undefined && height !== undefined) {
  1691. console.log('正在触发视口尺寸更新事件:', { width, height, viewportRatio })
  1692. // 同时也要更新slidesStore中的相关数据
  1693. if (slidesStore.setViewportSize) {
  1694. console.log('正在更新store中的视口尺寸')
  1695. slidesStore.setViewportSize(width)
  1696. if (slidesStore.setViewportRatio && viewportRatio !== undefined) {
  1697. slidesStore.setViewportRatio(viewportRatio)
  1698. console.log('视口比例已更新:', viewportRatio)
  1699. }
  1700. }
  1701. window.dispatchEvent(new CustomEvent('viewportSizeUpdated', {
  1702. detail: { width, height, viewportRatio }
  1703. }))
  1704. console.log('视口尺寸更新事件已触发')
  1705. }
  1706. // 导入成功后,触发画布尺寸更新
  1707. // 使用 nextTick 确保DOM更新完成后再触发
  1708. console.log('开始触发画布尺寸更新事件...')
  1709. nextTick(() => {
  1710. console.log('DOM更新完成,触发 slidesDataUpdated 事件')
  1711. // 触发自定义事件,通知需要更新画布尺寸的组件
  1712. window.dispatchEvent(new CustomEvent('slidesDataUpdated', {
  1713. detail: {
  1714. slides,
  1715. cover,
  1716. title,
  1717. theme,
  1718. width,
  1719. height,
  1720. viewportRatio,
  1721. timestamp: Date.now()
  1722. }
  1723. }))
  1724. console.log('slidesDataUpdated 事件已触发')
  1725. // 检查并调整幻灯片索引,确保在有效范围内
  1726. const newSlideCount = slides.length
  1727. const currentIndex = slidesStore.slideIndex
  1728. if (currentIndex >= newSlideCount) {
  1729. console.log('调整幻灯片索引:', currentIndex, '->', Math.max(0, newSlideCount - 1))
  1730. slidesStore.updateSlideIndex(Math.max(0, newSlideCount - 1))
  1731. }
  1732. console.log('画布尺寸更新事件处理完成')
  1733. })
  1734. */
  1735. }
  1736. reader.readAsArrayBuffer(file)
  1737. }
  1738. const getFile = (url: string): Promise<{ data: any }> => {
  1739. return new Promise((resolve, reject) => {
  1740. // 检查 AWS SDK 是否可用
  1741. if (typeof window !== 'undefined' && !window.AWS) {
  1742. reject(new Error('AWS SDK not available'))
  1743. return
  1744. }
  1745. const credentials = {
  1746. accessKeyId: 'AKIATLPEDU37QV5CHLMH',
  1747. secretAccessKey: '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',
  1748. } // 秘钥形式的登录上传
  1749. window.AWS.config.update(credentials)
  1750. window.AWS.config.region = 'cn-northwest-1' // 设置区域
  1751. const s3 = new window.AWS.S3({ params: { Bucket: 'ccrb' } })
  1752. // 解析文件名
  1753. const bucketUrl = 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/'
  1754. if (!url.startsWith(bucketUrl)) {
  1755. reject(new Error('Invalid S3 URL format'))
  1756. return
  1757. }
  1758. const name = decodeURIComponent(url.split(bucketUrl)[1])
  1759. // const name = url.split(bucketUrl)[1]
  1760. console.log('aws-name:', name)
  1761. if (!name) {
  1762. reject(new Error('Could not extract file name from URL'))
  1763. return
  1764. }
  1765. const params = {
  1766. Bucket: 'ccrb',
  1767. Key: name,
  1768. }
  1769. s3.getObject(params, (err: any, data: any) => {
  1770. if (err) {
  1771. console.error('S3 getObject error:', err, err.stack)
  1772. reject(err)
  1773. }
  1774. else {
  1775. console.log('S3 getObject success:', data)
  1776. resolve({ data: data.Body })
  1777. }
  1778. })
  1779. })
  1780. }
  1781. const getFile2 = (url: string): Promise<{ data: any }> => {
  1782. return new Promise((resolve, reject) => {
  1783. console.log('直接使用原始 URL 获取文件:', url)
  1784. // 直接使用 fetch 获取文件,浏览器会自动处理 URL 解码
  1785. fetch(url)
  1786. .then(response => {
  1787. if (!response.ok) {
  1788. console.error('HTTP 错误:', response.status, response.statusText)
  1789. throw new Error(`HTTP error! status: ${response.status}`)
  1790. }
  1791. console.log('文件获取成功,大小:', response.headers.get('content-length'))
  1792. return response.arrayBuffer()
  1793. })
  1794. .then(buffer => {
  1795. console.log('文件内容读取成功,大小:', buffer.byteLength)
  1796. resolve({ data: buffer })
  1797. })
  1798. .catch(error => {
  1799. console.error('Fetch error:', error)
  1800. reject(error)
  1801. })
  1802. })
  1803. }
  1804. return {
  1805. importSpecificFile,
  1806. importJSON,
  1807. importPPTXFile,
  1808. readJSON,
  1809. exportJSON2,
  1810. exporting,
  1811. getFile,
  1812. getFile2,
  1813. dataToFile,
  1814. uploadFileToS3
  1815. }
  1816. }