|
|
@@ -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>
|