useImport.ts 73 KB

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