李志伟 hai 2 meses
pai
achega
781036f10d
Modificáronse 1 ficheiros con 368 adicións e 16 borrados
  1. 368 16
      src/components/view_file/vendors/pptx/PPT.vue

+ 368 - 16
src/components/view_file/vendors/pptx/PPT.vue

@@ -2,7 +2,7 @@
  * @Author: LiZhiWei
  * @Date: 2025-04-10 14:38:27
  * @LastEditors: LiZhiWei
- * @LastEditTime: 2025-04-25 09:32:40
+ * @LastEditTime: 2025-04-27 08:36:57
  * @Description:
 -->
 <template>
@@ -20,6 +20,7 @@ const pptxContainer = ref(null)
 
 // 渲染幻灯片
 const renderSlides = () => {
+  console.log("props.pptxJson", props.pptxJson)
   if (!pptxContainer.value || !props.pptxJson) return
 
   pptxContainer.value.innerHTML = ""
@@ -1931,18 +1932,9 @@ const createShapeElement = (element) => {
         const fillColor = element.fill.value || "transparent"
         topRect.setAttribute("fill", fillColor)
         topTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 1.2))
-        bottomTrapezoid.setAttribute(
-          "fill",
-          adjustBrightness(fillColor, 0.8)
-        )
-        leftTrapezoid.setAttribute(
-          "fill",
-          adjustBrightness(fillColor, 0.9)
-        )
-        rightTrapezoid.setAttribute(
-          "fill",
-          adjustBrightness(fillColor, 0.7)
-        )
+        bottomTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 0.8))
+        leftTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 0.9))
+        rightTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 0.7))
       } else {
         topRect.setAttribute("fill", "transparent")
         topTrapezoid.setAttribute("fill", "transparent")
@@ -3817,7 +3809,7 @@ const createShapeElement = (element) => {
 
       // 构建路径
       const path2 = `
-            M 0,${ry} 
+            M 0,${ry}
             A ${rx} ${ry} 0 1 1 ${element.width} ${ry}
             L ${element.width - (rx - innerRx)},${ry}
             A ${innerRx} ${innerRy} 0 1 0 ${rx - innerRx},${ry}
@@ -4207,8 +4199,59 @@ const createTableElement = (element) => {
 }
 
 const createChartElement = (element) => {
-  // 实现图表元素创建逻辑
-  return document.createElement("div") // 临时返回空div
+  // 1. 创建基础容器
+  const el = document.createElement("div")
+  el.style.position = "absolute"
+  el.style.top = element.top + "px"
+  el.style.left = element.left + "px"
+  el.style.width = element.width + "px"
+  el.style.height = element.height + "px"
+  el.style.zIndex = element.order
+  // 2. 创建SVG画布
+  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
+  svg.setAttribute("width", element.width)
+  svg.setAttribute("height", element.height)
+  svg.setAttribute("viewBox", `0 0 ${element.width} ${element.height}`)
+
+  // 3. 设置图表内边距
+  const padding = {
+    top: 60, // 为图例留出空间
+    right: 40, // 右侧边距
+    bottom: 60, // X轴标签空间
+    left: 60, // Y轴标签空间
+  }
+
+  // 4. 计算实际绘图区域
+  const chartWidth = element.width - padding.left - padding.right
+  const chartHeight = element.height - padding.top - padding.bottom
+  // 处理不同图表类型
+  switch (element.chartType) {
+    case "barChart":
+      // 绘制柱状图
+      drawBarChart(svg, element, {
+        padding,
+        chartWidth,
+        chartHeight,
+        barDir: element.barDir || "col",
+        grouping: element.grouping || "clustered",
+      })
+      break
+    case "pieChart":
+    case "doughnutChart":
+      drawDonutChart(svg, element, chartWidth, chartHeight)
+      break
+    default:
+      // 占位符
+      svg.innerHTML = `<text x="${element.width / 2}" y="${
+        element.height / 2
+      }" text-anchor="middle" fill="#999" font-size="12px">
+            暂不支持该类型图表
+          </text>`
+      console.warn("Unsupported chart type:", element.chartType)
+  }
+
+  el.appendChild(svg)
+  return el
 }
 
 // 创建图表元素
@@ -4489,6 +4532,315 @@ const drawCategoryLabels = (svg, categories, padding, groupWidth, barDir) => {
   return labelGroup
 }
 
+/**
+ * 绘制甜甜圈图表
+ * @param {SVGElement} svg - SVG容器元素
+ * @param {Object} element - 图表配置对象
+ * @param {number} chartWidth - 图表宽度
+ * @param {number} chartHeight - 图表高度
+ */
+const drawDonutChart = (svg, element, chartWidth, chartHeight) => {
+  // 数据验证
+  if (!element?.data?.[0]?.values) return
+
+  const series = element.data[0]
+  const values = series.values.map((v) => Number(v.y) || 0)
+  const total = values.reduce((sum, v) => sum + v, 0)
+  if (total === 0) return
+
+  // 基础配置
+  const cx = chartWidth / 2
+  const cy = chartHeight / 2
+  // 保持较大的外半径
+  const outerR = (Math.min(chartWidth, chartHeight) / 2) * 0.98
+
+  // 处理空心大小
+  const holeSize = element.holeSize ? parseInt(element.holeSize) : 50
+  const innerR = outerR * (holeSize / 100)
+
+  // 默认颜色和自定义颜色处理
+  const defaultColors = [
+    "#4e79a7",
+    "#f28e2b",
+    "#e15759",
+    "#76b7b2",
+    "#59a14f",
+    "#edc949",
+    "#af7aa1",
+    "#ff9da7",
+  ]
+  const colors =
+    element.colors?.map(
+      (color, i) => color || defaultColors[i % defaultColors.length]
+    ) || defaultColors
+
+  // 标签数据
+  const labels = series.xlabels || {}
+
+  // 创建图表组
+  const chartGroup = document.createElementNS("http://www.w3.org/2000/svg", "g")
+  chartGroup.setAttribute("transform", `translate(${cx},${cy})`)
+
+  // 绘制扇形
+  let startAngle = -Math.PI / 2 // 从顶部开始绘制
+  values.forEach((value, index) => {
+    if (value <= 0) return
+
+    const percentage = value / total
+    const angle = percentage * Math.PI * 2
+    const endAngle = startAngle + angle
+
+    // 计算路径点 (相对于组中心 0,0)
+    const x1 = outerR * Math.cos(startAngle)
+    const y1 = outerR * Math.sin(startAngle)
+    const x2 = outerR * Math.cos(endAngle)
+    const y2 = outerR * Math.sin(endAngle)
+    const x3 = innerR * Math.cos(endAngle)
+    const y3 = innerR * Math.sin(endAngle)
+    const x4 = innerR * Math.cos(startAngle)
+    const y4 = innerR * Math.sin(startAngle)
+
+    // 构建路径
+    const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
+    const largeArc = angle > Math.PI ? 1 : 0
+    const d = [
+      `M ${x1} ${y1}`,
+      `A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2} ${y2}`,
+      `L ${x3} ${y3}`,
+      `A ${innerR} ${innerR} 0 ${largeArc} 0 ${x4} ${y4}`,
+      "Z",
+    ].join(" ")
+
+    path.setAttribute("d", d)
+    path.setAttribute("fill", colors[index % colors.length]) // 确保颜色循环使用
+
+    // 添加交互效果
+    path.style.transition = "transform 0.2s"
+    path.addEventListener("mouseover", () => {
+      path.style.transform = "scale(1.03)"
+    })
+    path.addEventListener("mouseout", () => {
+      path.style.transform = "scale(1)"
+    })
+
+    chartGroup.appendChild(path)
+
+    // 添加标签
+    const midAngle = startAngle + angle / 2
+    const labelR = outerR * 1.1
+    const labelX = labelR * Math.cos(midAngle)
+    const labelY = labelR * Math.sin(midAngle)
+
+    const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
+    text.setAttribute("x", labelX)
+    text.setAttribute("y", labelY)
+    text.setAttribute("text-anchor", labelX < 0 ? "end" : "start")
+    text.setAttribute("dominant-baseline", "middle")
+    text.setAttribute("fill", "#555")
+    text.setAttribute("font-size", "9")
+    text.setAttribute("font-weight", "normal")
+    const tspanLabel = document.createElementNS(
+      "http://www.w3.org/2000/svg",
+      "tspan"
+    )
+    tspanLabel.setAttribute("x", labelX)
+    tspanLabel.textContent = labels[index] || `类别 ${index + 1}`
+    text.appendChild(tspanLabel)
+    const tspanPercent = document.createElementNS(
+      "http://www.w3.org/2000/svg",
+      "tspan"
+    )
+    tspanPercent.setAttribute("x", labelX)
+    tspanPercent.setAttribute("dy", "1.2em")
+    tspanPercent.textContent = `${(percentage * 100).toFixed(1)}%`
+    text.appendChild(tspanPercent)
+
+    chartGroup.appendChild(text)
+
+    startAngle = endAngle
+  })
+
+  svg.appendChild(chartGroup)
+  const padding = 50
+  svg.setAttribute(
+    "viewBox",
+    `${-cx / 2} ${-cy / 2} ${chartWidth + padding * 2} ${
+      chartHeight + padding * 2
+    }`
+  )
+  svg.style.overflow = "visible"
+}
+
+const drawBarChart = (svg, element, options) => {
+  const { padding, chartWidth, chartHeight, barDir, grouping } = options
+  const series = element.data
+  const categories = series[0].xlabels
+  const categoryCount = Object.keys(categories).length
+
+  // 1. 计算最大值
+  let maxValue = 0
+  series.forEach((serie) => {
+    const seriesMax = Math.max(...serie.values.map((v) => v.y))
+    maxValue = Math.max(maxValue, seriesMax)
+  })
+  maxValue = maxValue * 1.2 // 增加20%空间
+
+  // 2. 计算柱子布局
+  const groupWidth = chartWidth / categoryCount
+  const barWidth =
+    grouping === "clustered"
+      ? (groupWidth * 0.6) / series.length // 分组模式
+      : groupWidth * 0.6 // 堆叠模式
+  const barSpacing =
+    (groupWidth * 0.4) / (grouping === "clustered" ? series.length + 1 : 2)
+
+  // 3. 绘制坐标轴
+  // X轴
+  const xAxisPath = document.createElementNS(
+    "http://www.w3.org/2000/svg",
+    "path"
+  )
+  xAxisPath.setAttribute(
+    "d",
+    `M${padding.left},${element.height - padding.bottom} L${
+      element.width - padding.right
+    },${element.height - padding.bottom}`
+  )
+  xAxisPath.setAttribute("stroke", "#000")
+  xAxisPath.setAttribute("stroke-width", "1")
+  svg.appendChild(xAxisPath)
+
+  // Y轴
+  const yAxisPath = document.createElementNS(
+    "http://www.w3.org/2000/svg",
+    "path"
+  )
+  yAxisPath.setAttribute(
+    "d",
+    `M${padding.left},${padding.top} L${padding.left},${
+      element.height - padding.bottom
+    }`
+  )
+  yAxisPath.setAttribute("stroke", "#000")
+  yAxisPath.setAttribute("stroke-width", "1")
+  svg.appendChild(yAxisPath)
+
+  // 4. 绘制Y轴刻度和网格线
+  const yTickCount = 5
+  for (let i = 0; i <= yTickCount; i++) {
+    const y = padding.top + (chartHeight * i) / yTickCount
+    const value = maxValue - (maxValue * i) / yTickCount
+
+    // 水平网格线
+    const gridLine = document.createElementNS(
+      "http://www.w3.org/2000/svg",
+      "line"
+    )
+    gridLine.setAttribute("x1", padding.left)
+    gridLine.setAttribute("y1", y)
+    gridLine.setAttribute("x2", padding.left + chartWidth)
+    gridLine.setAttribute("y2", y)
+    gridLine.setAttribute("stroke", "#eee")
+    gridLine.setAttribute("stroke-width", "1")
+    svg.appendChild(gridLine)
+
+    // 刻度线
+    const tick = document.createElementNS("http://www.w3.org/2000/svg", "line")
+    tick.setAttribute("x1", padding.left - 6)
+    tick.setAttribute("y1", y)
+    tick.setAttribute("x2", padding.left)
+    tick.setAttribute("y2", y)
+    tick.setAttribute("stroke", "#000")
+    tick.setAttribute("stroke-width", "1")
+    svg.appendChild(tick)
+
+    // 刻度值
+    const label = document.createElementNS("http://www.w3.org/2000/svg", "text")
+    label.setAttribute("x", padding.left - 10)
+    label.setAttribute("y", y + 4)
+    label.setAttribute("text-anchor", "end")
+    label.setAttribute("font-size", "12px")
+    label.textContent = value.toFixed(1)
+    svg.appendChild(label)
+  }
+
+  // 5. 绘制数据条
+  series.forEach((serie, serieIndex) => {
+    serie.values.forEach((value, index) => {
+      const barHeight = (value.y / maxValue) * chartHeight
+      const x =
+        padding.left +
+        groupWidth * index +
+        (grouping === "clustered"
+          ? barSpacing * (serieIndex + 1) + barWidth * serieIndex
+          : barSpacing)
+      const y = element.height - padding.bottom - barHeight
+
+      // 绘制柱子
+      const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect")
+      bar.setAttribute("x", x)
+      bar.setAttribute("y", y)
+      bar.setAttribute("width", barWidth)
+      bar.setAttribute("height", barHeight)
+      bar.setAttribute(
+        "fill",
+        element.colors[serieIndex] || `hsl(${serieIndex * 60}, 70%, 50%)`
+      )
+      svg.appendChild(bar)
+
+      // 数值标签
+      if (element.marker) {
+        const label = document.createElementNS(
+          "http://www.w3.org/2000/svg",
+          "text"
+        )
+        label.setAttribute("x", x + barWidth / 2)
+        label.setAttribute("y", y - 5)
+        label.setAttribute("text-anchor", "middle")
+        label.setAttribute("font-size", "12px")
+        label.textContent = value.y.toFixed(1)
+        svg.appendChild(label)
+      }
+    })
+  })
+
+  // 6. 绘制X轴类别标签
+  Object.values(categories).forEach((label, index) => {
+    const x = padding.left + groupWidth * (index + 0.5)
+    const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
+    text.setAttribute("x", x)
+    text.setAttribute("y", element.height - padding.bottom + 20)
+    text.setAttribute("text-anchor", "middle")
+    text.setAttribute("font-size", "12px")
+    text.textContent = label
+    svg.appendChild(text)
+  })
+
+  // 7. 绘制图例
+  series.forEach((serie, index) => {
+    const legendX = padding.left + index * 120
+    const legendY = 20
+
+    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect")
+    rect.setAttribute("x", legendX)
+    rect.setAttribute("y", legendY)
+    rect.setAttribute("width", 15)
+    rect.setAttribute("height", 15)
+    rect.setAttribute(
+      "fill",
+      element.colors[index] || `hsl(${index * 60}, 70%, 50%)`
+    )
+    svg.appendChild(rect)
+
+    const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
+    text.setAttribute("x", legendX + 25)
+    text.setAttribute("y", legendY + 12)
+    text.setAttribute("font-size", "12px")
+    text.textContent = serie.key
+    svg.appendChild(text)
+  })
+}
+
 // 绘制网格线
 const drawGrid = (svg, padding, width, height, maxValue) => {
   // 创建网格组