useImport.ts 70 KB

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