useImport.ts 33 KB

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