Просмотр исходного кода

add ChartShareCanvas component

liaoxg 3 месяцев назад
Родитель
Сommit
b36294e51a
1 измененных файлов с 225 добавлено и 0 удалено
  1. 225 0
      client/components/ChartShareCanvas.vue

+ 225 - 0
client/components/ChartShareCanvas.vue

@@ -0,0 +1,225 @@
+<template>
+  <view class="chart-share-canvas">
+    <canvas canvas-id="chartCanvas" id="chartCanvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, watch } from 'vue'
+
+const props = defineProps({
+  chart: { type: Object, required: true },
+  name: { type: String, default: '' },
+  birthday: { type: String, default: '' }
+})
+
+const canvasWidth = 1080
+const canvasHeight = 800
+
+const emit = defineEmits(['ready'])
+
+function getNumColor(num) {
+  const colors = { 1: '#D94A4A', 2: '#4A90D9', 3: '#E8A238', 4: '#2D8B67', 5: '#D4A017', 6: '#7B6FA0', 7: '#4A6FA5', 8: '#C9A84C', 9: '#B85C3A' }
+  return colors[num] || '#FFFFFF'
+}
+
+function isMasterNum(num) {
+  return num === 11 || num === 22 || num === 33
+}
+
+function drawCell(ctx, x, y, num, label, isOuter) {
+  const radius = isOuter ? 28 : 32
+  ctx.beginPath()
+  ctx.arc(x, y, radius, 0, Math.PI * 2)
+  ctx.setFillStyle('#1F2937')
+  ctx.fill()
+  ctx.setStrokeStyle(getNumColor(num))
+  ctx.setLineWidth(4)
+  ctx.stroke()
+  ctx.setFillStyle(getNumColor(num))
+  ctx.setFontSize(isOuter ? 26 : 30)
+  ctx.setTextAlign('center')
+  ctx.setTextBaseline('middle')
+  ctx.fillText(num, x, y)
+  if (isMasterNum(num)) {
+    ctx.setFontSize(18)
+    ctx.fillText('★', x, y + 24)
+  }
+  if (label) {
+    ctx.setFontSize(12)
+    ctx.setFillStyle('#9CA3AF')
+    ctx.fillText(label, x, y + radius + 16)
+  }
+}
+
+function drawChart() {
+  return new Promise((resolve, reject) => {
+    try {
+      const ctx = uni.createCanvasContext('chartCanvas')
+      const w = canvasWidth
+      const h = canvasHeight
+
+      // Background gradient
+      const grad = ctx.createLinearGradient(0, 0, 0, h)
+      grad.addColorStop(0, '#0c0a1a')
+      grad.addColorStop(0.6, '#1a1040')
+      grad.addColorStop(1, '#0f0c2e')
+      ctx.setFillStyle(grad)
+      ctx.fillRect(0, 0, w, h)
+
+      // Coordinates
+      const cx = w / 2
+      const topY = 120
+      const midY = 340
+      const bottomY = 600
+
+      const O = { x: cx, y: topY }
+      const M = { x: cx - 140, y: midY }
+      const N = { x: cx + 140, y: midY }
+      const I = { x: cx - 260, y: bottomY - 20 }
+      const J = { x: cx - 85, y: bottomY - 20 }
+      const K = { x: cx + 85, y: bottomY - 20 }
+      const L = { x: cx + 260, y: bottomY - 20 }
+
+      // Outer positions
+      const P = { x: I.x - 80, y: I.y + 100 }
+      const Q = { x: J.x - 40, y: J.y + 120 }
+      const R = { x: (P.x + Q.x) / 2, y: P.y + 80 }
+      const S = { x: K.x + 40, y: K.y + 120 }
+      const T = { x: L.x + 80, y: L.y + 100 }
+      const U = { x: (S.x + T.x) / 2, y: S.y + 80 }
+      const V = { x: M.x, y: M.y - 100 }
+      const W = { x: N.x, y: N.y - 100 }
+      const X = { x: cx, y: O.y - 100 }
+
+      // Connect outer groups
+      ctx.setStrokeStyle('rgba(255,255,255,0.15)')
+      ctx.setLineWidth(2)
+      ctx.beginPath()
+      ctx.moveTo(I.x, I.y + 25).lineTo(P.x, P.y - 25)
+      ctx.moveTo(J.x, J.y + 25).lineTo(Q.x, Q.y - 25)
+      ctx.moveTo(P.x, P.y + 25).lineTo(Q.x, Q.y - 20).lineTo(R.x, R.y - 25)
+      ctx.stroke()
+      ctx.beginPath()
+      ctx.moveTo(K.x, K.y + 25).lineTo(S.x, S.y - 25)
+      ctx.moveTo(L.x, L.y + 25).lineTo(T.x, T.y - 25)
+      ctx.moveTo(S.x, S.y + 25).lineTo(T.x, T.y - 20).lineTo(U.x, U.y - 25)
+      ctx.stroke()
+      ctx.beginPath()
+      ctx.moveTo(M.x, M.y - 25).lineTo(V.x, V.y + 25)
+      ctx.moveTo(N.x, N.y - 25).lineTo(W.x, W.y + 25)
+      ctx.moveTo(V.x, V.y - 25).lineTo(W.x, W.y - 20).lineTo(X.x, X.y - 25)
+      ctx.stroke()
+
+      // Inner triangle lines
+      ctx.setStrokeStyle('rgba(255,255,255,0.25)')
+      ctx.setLineWidth(3)
+      ctx.beginPath()
+      ctx.moveTo(M.x, M.y).lineTo(N.x, N.y).lineTo(O.x, O.y).closePath()
+      ctx.stroke()
+      ctx.beginPath()
+      ctx.moveTo(I.x, I.y).lineTo(J.x, J.y).lineTo(K.x, K.y).lineTo(L.x, L.y).lineTo(I.x, I.y)
+      ctx.stroke()
+      ctx.beginPath()
+      ctx.moveTo(O.x, O.y).lineTo((I.x + L.x) / 2, I.y + 20)
+      ctx.stroke()
+
+      // Draw cells inner then outer
+      drawCell(ctx, I.x, I.y, props.chart.I, 'I', false)
+      drawCell(ctx, J.x, J.y, props.chart.J, 'J', false)
+      drawCell(ctx, K.x, K.y, props.chart.K, 'K', false)
+      drawCell(ctx, L.x, L.y, props.chart.L, 'L', false)
+      drawCell(ctx, M.x, M.y, props.chart.M, 'M', false)
+      drawCell(ctx, N.x, N.y, props.chart.N, 'N', false)
+      drawCell(ctx, O.x, O.y, props.chart.O, 'O', false)
+      drawCell(ctx, P.x, P.y, props.chart.P, 'P', true)
+      drawCell(ctx, Q.x, Q.y, props.chart.Q, 'Q', true)
+      drawCell(ctx, R.x, R.y, props.chart.R, 'R', true)
+      drawCell(ctx, S.x, S.y, props.chart.S, 'S', true)
+      drawCell(ctx, T.x, T.y, props.chart.T, 'T', true)
+      drawCell(ctx, U.x, U.y, props.chart.U, 'U', true)
+      drawCell(ctx, V.x, V.y, props.chart.V, 'V', true)
+      drawCell(ctx, W.x, W.y, props.chart.W, 'W', true)
+      drawCell(ctx, X.x, X.y, props.chart.X, 'X', true)
+
+      // Footer
+      ctx.setFillStyle('#FEF3C7')
+      ctx.setFontSize(32)
+      ctx.setTextAlign('center')
+      const footerY = h - 80
+      const displayName = props.name || '数字能量命盘'
+      ctx.fillText(displayName, cx, footerY)
+      ctx.setFontSize(24)
+      ctx.setFillStyle('#D1D5DB')
+      const dateStr = props.birthday ? `出生: ${props.birthday}` : ''
+      ctx.fillText(dateStr, cx, footerY + 40)
+
+      // Watermark
+      ctx.setFontSize(18)
+      ctx.setFillStyle('rgba(255,255,255,0.3)')
+      ctx.fillText('数字能量学 · AI智能命理分析', cx, h - 30)
+
+      ctx.draw(false, () => {
+        emit('ready')
+        resolve()
+      })
+    } catch (e) {
+      reject(e)
+    }
+  })
+}
+
+function getTempFilePath() {
+  return new Promise((resolve, reject) => {
+    uni.canvasToTempFilePath({
+      canvasId: 'chartCanvas',
+      width: canvasWidth,
+      height: canvasHeight,
+      destWidth: canvasWidth,
+      destHeight: canvasHeight,
+      quality: 1,
+      success: (res) => resolve(res.tempFilePath),
+      fail: (err) => reject(err)
+    })
+  })
+}
+
+function saveToAlbum() {
+  return getTempFilePath().then(path => {
+    return new Promise((resolve, reject) => {
+      uni.saveImageToPhotosAlbum({
+        filePath: path,
+        success: () => resolve(path),
+        fail: (err) => reject(err)
+      })
+    })
+  })
+}
+
+function generate() {
+  return drawChart().then(() => getTempFilePath())
+}
+
+defineExpose({
+  generate,
+  saveToAlbum,
+  getTempFilePath
+})
+
+onMounted(() => {
+  drawChart().catch(e => console.error('Canvas draw failed', e))
+})
+
+watch(() => [props.chart, props.name, props.birthday], () => {
+  drawChart().catch(e => console.error('Canvas redraw failed', e))
+})
+</script>
+
+<style scoped>
+.chart-share-canvas {
+  position: fixed;
+  left: -9999px;
+  width: 1080px;
+  height: 800px;
+}
+</style>