guanwei 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +202 -0
- package/README.md +247 -0
- package/dist/assets/index-CsOD5rSo.js +102 -0
- package/dist/assets/index-CsOD5rSo.js.map +1 -0
- package/dist/assets/index-DQx62TUr.css +1 -0
- package/dist/favicon.svg +12 -0
- package/dist/index.html +15 -0
- package/eslint.config.js +28 -0
- package/index.html +25 -0
- package/package.json +96 -0
- package/postcss.config.js +10 -0
- package/public/favicon.svg +12 -0
- package/scripts/guanwei +245 -0
- package/scripts/install.sh +79 -0
- package/scripts/release.sh +42 -0
- package/scripts/setup.bat +36 -0
- package/scripts/setup.sh +123 -0
- package/server/src/data/db.json +12059 -0
- package/server/src/data/guanwei.db +0 -0
- package/server/src/data/lifeContext.ts +423 -0
- package/server/src/data/spreads.ts +255 -0
- package/server/src/data/tarotCards.ts +2353 -0
- package/server/src/index.ts +71 -0
- package/server/src/routes/ai.ts +376 -0
- package/server/src/routes/divine.ts +140 -0
- package/server/src/routes/hour.ts +40 -0
- package/server/src/routes/tarot.ts +129 -0
- package/server/src/routes/users.ts +140 -0
- package/server/src/services/chartBrief.ts +131 -0
- package/server/src/services/divineStore.ts +188 -0
- package/server/src/services/hourInference.ts +150 -0
- package/server/src/services/llmProvider.ts +181 -0
- package/server/src/services/promptBuilder.ts +327 -0
- package/server/src/services/relativesCheck.ts +139 -0
- package/server/src/services/sixRelatives.ts +70 -0
- package/server/src/services/skills.ts +116 -0
- package/server/src/types/index.ts +104 -0
- package/server/src/utils/semanticAnalyzer.ts +555 -0
- package/server/src/utils/tarotEngine.ts +849 -0
- package/server/tsconfig.json +18 -0
- package/shared/core/data/classics.ts +276 -0
- package/shared/core/data/courses.ts +211 -0
- package/shared/core/data/duanyu.ts +148 -0
- package/shared/core/data/ganzhi.ts +53 -0
- package/shared/core/data/gua64.ts +93 -0
- package/shared/core/data/liuren.ts +18 -0
- package/shared/core/data/liuyao.ts +73 -0
- package/shared/core/data/qimen.ts +27 -0
- package/shared/core/data/region-full.ts +3 -0
- package/shared/core/data/region.ts +310 -0
- package/shared/core/data/tarotSpreads.ts +78 -0
- package/shared/core/data/xiaoliuren.ts +18 -0
- package/shared/core/data/ziwei.ts +97 -0
- package/shared/core/data/zodiac.ts +20 -0
- package/shared/core/engine/astrology.ts +169 -0
- package/shared/core/engine/bazi.ts +306 -0
- package/shared/core/engine/calendar.ts +127 -0
- package/shared/core/engine/daily.ts +133 -0
- package/shared/core/engine/liuren.ts +86 -0
- package/shared/core/engine/liuyao.ts +93 -0
- package/shared/core/engine/meihua.ts +87 -0
- package/shared/core/engine/qimen.ts +78 -0
- package/shared/core/engine/questionFit.ts +94 -0
- package/shared/core/engine/tarot.ts +69 -0
- package/shared/core/engine/trueSolarTime.ts +86 -0
- package/shared/core/engine/xiaoliuren.ts +19 -0
- package/shared/core/engine/ziwei.ts +217 -0
- package/shared/core/engine/ziweiInterpret.ts +161 -0
- package/shared/core/types.ts +206 -0
- package/src/App.tsx +69 -0
- package/src/arts/registry.ts +31 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Backdrop.tsx +27 -0
- package/src/components/BambooArt.tsx +41 -0
- package/src/components/CalendarInput.tsx +131 -0
- package/src/components/DateInput.tsx +103 -0
- package/src/components/Disclaimer.tsx +12 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/ErrorBoundary.tsx +38 -0
- package/src/components/HomeSideAnchor.tsx +62 -0
- package/src/components/InkVein.tsx +10 -0
- package/src/components/LifeEventsInput.tsx +47 -0
- package/src/components/LocationPicker.tsx +117 -0
- package/src/components/Motes.tsx +65 -0
- package/src/components/Navigation.tsx +80 -0
- package/src/components/ProfileForm.tsx +40 -0
- package/src/components/ProfilePicker.tsx +40 -0
- package/src/components/QuestionFields.tsx +47 -0
- package/src/components/ReportView.tsx +211 -0
- package/src/components/ResultCard.tsx +40 -0
- package/src/components/SealButton.tsx +18 -0
- package/src/components/SiteFooter.tsx +17 -0
- package/src/components/SiteNav.tsx +103 -0
- package/src/components/SongDialog.tsx +64 -0
- package/src/components/SongSearchSelect.tsx +81 -0
- package/src/components/SongSelect.tsx +61 -0
- package/src/components/StarField.tsx +119 -0
- package/src/components/TarotCard.tsx +129 -0
- package/src/components/TimeShichenInput.tsx +29 -0
- package/src/components/arts/AstrologyArt.tsx +141 -0
- package/src/components/arts/BaziArt.tsx +269 -0
- package/src/components/arts/LiurenArt.tsx +87 -0
- package/src/components/arts/LiuyaoArt.tsx +111 -0
- package/src/components/arts/MeihuaArt.tsx +105 -0
- package/src/components/arts/QimenArt.tsx +86 -0
- package/src/components/arts/TarotArt.tsx +110 -0
- package/src/components/arts/XiaoliurenArt.tsx +93 -0
- package/src/components/arts/ZiweiArt.tsx +296 -0
- package/src/data/arts.ts +84 -0
- package/src/data/lifeContext.ts +423 -0
- package/src/data/sample-report.json +146 -0
- package/src/data/shichen.ts +21 -0
- package/src/data/spreads.ts +255 -0
- package/src/data/tarotCards.ts +2353 -0
- package/src/hooks/useAIInterpret.ts +76 -0
- package/src/hooks/useDivine.ts +39 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/index.css +190 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +12 -0
- package/src/pages/AboutPage.tsx +25 -0
- package/src/pages/AcademyPage.tsx +128 -0
- package/src/pages/AstrologyPage.tsx +33 -0
- package/src/pages/AuthPage.tsx +263 -0
- package/src/pages/BaZiPage.tsx +33 -0
- package/src/pages/ClassicsPage.tsx +120 -0
- package/src/pages/DemoPage.tsx +161 -0
- package/src/pages/HistoryPage.tsx +133 -0
- package/src/pages/Home.tsx +179 -0
- package/src/pages/HomePage.tsx +174 -0
- package/src/pages/ModulePage.tsx +320 -0
- package/src/pages/SpreadEditor.tsx +230 -0
- package/src/pages/SpreadEditorPage.tsx +105 -0
- package/src/pages/SpreadLibrary.tsx +199 -0
- package/src/pages/SpreadLibraryPage.tsx +17 -0
- package/src/pages/TarotPage.tsx +738 -0
- package/src/pages/ZiWeiPage.tsx +33 -0
- package/src/services/api.ts +342 -0
- package/src/stores/useDivinationStore.ts +22 -0
- package/src/styles/song.css +1066 -0
- package/src/styles/tokens.css +60 -0
- package/src/types/index.ts +104 -0
- package/src/utils/astrologyCalc.ts +61 -0
- package/src/utils/comboEngine.ts +134 -0
- package/src/utils/dst.ts +24 -0
- package/src/utils/panTone.ts +206 -0
- package/src/utils/recordStore.ts +60 -0
- package/src/utils/semanticAnalyzer.ts +555 -0
- package/src/utils/spreadStorage.ts +32 -0
- package/src/utils/storage.ts +27 -0
- package/src/utils/tarotEngine.ts +849 -0
- package/src/utils/userStore.ts +234 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +52 -0
- package/tsconfig.json +40 -0
- package/vite.config.ts +34 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Star {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
size: number;
|
|
7
|
+
speedX: number;
|
|
8
|
+
speedY: number;
|
|
9
|
+
opacity: number;
|
|
10
|
+
twinkleSpeed: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function StarField() {
|
|
14
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
15
|
+
const starsRef = useRef<Star[]>([]);
|
|
16
|
+
const mouseRef = useRef({ x: 0, y: 0 });
|
|
17
|
+
const animationRef = useRef<number>(0);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const canvas = canvasRef.current;
|
|
21
|
+
if (!canvas) return;
|
|
22
|
+
|
|
23
|
+
const ctx = canvas.getContext('2d');
|
|
24
|
+
if (!ctx) return;
|
|
25
|
+
|
|
26
|
+
const resize = () => {
|
|
27
|
+
canvas.width = window.innerWidth;
|
|
28
|
+
canvas.height = window.innerHeight;
|
|
29
|
+
};
|
|
30
|
+
resize();
|
|
31
|
+
window.addEventListener('resize', resize);
|
|
32
|
+
|
|
33
|
+
// Initialize stars
|
|
34
|
+
const starCount = Math.floor((canvas.width * canvas.height) / 4000);
|
|
35
|
+
starsRef.current = Array.from({ length: starCount }, () => ({
|
|
36
|
+
x: Math.random() * canvas.width,
|
|
37
|
+
y: Math.random() * canvas.height,
|
|
38
|
+
size: Math.random() * 2 + 0.5,
|
|
39
|
+
speedX: (Math.random() - 0.5) * 0.3,
|
|
40
|
+
speedY: (Math.random() - 0.5) * 0.3,
|
|
41
|
+
opacity: Math.random(),
|
|
42
|
+
twinkleSpeed: Math.random() * 0.02 + 0.005,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
46
|
+
mouseRef.current = { x: e.clientX, y: e.clientY };
|
|
47
|
+
};
|
|
48
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
49
|
+
|
|
50
|
+
const animate = () => {
|
|
51
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
52
|
+
|
|
53
|
+
starsRef.current.forEach((star) => {
|
|
54
|
+
// Update position
|
|
55
|
+
star.x += star.speedX;
|
|
56
|
+
star.y += star.speedY;
|
|
57
|
+
|
|
58
|
+
// Wrap around
|
|
59
|
+
if (star.x < 0) star.x = canvas.width;
|
|
60
|
+
if (star.x > canvas.width) star.x = 0;
|
|
61
|
+
if (star.y < 0) star.y = canvas.height;
|
|
62
|
+
if (star.y > canvas.height) star.y = 0;
|
|
63
|
+
|
|
64
|
+
// Twinkle
|
|
65
|
+
star.opacity += star.twinkleSpeed;
|
|
66
|
+
if (star.opacity > 1 || star.opacity < 0.2) {
|
|
67
|
+
star.twinkleSpeed = -star.twinkleSpeed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Mouse parallax
|
|
71
|
+
const dx = mouseRef.current.x - star.x;
|
|
72
|
+
const dy = mouseRef.current.y - star.y;
|
|
73
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
74
|
+
if (dist < 200) {
|
|
75
|
+
star.x -= dx * 0.0005;
|
|
76
|
+
star.y -= dy * 0.0005;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Draw star
|
|
80
|
+
ctx.beginPath();
|
|
81
|
+
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
|
|
82
|
+
ctx.fillStyle = `rgba(224, 224, 224, ${star.opacity})`;
|
|
83
|
+
ctx.fill();
|
|
84
|
+
|
|
85
|
+
// Glow for larger stars
|
|
86
|
+
if (star.size > 1.5) {
|
|
87
|
+
ctx.beginPath();
|
|
88
|
+
ctx.arc(star.x, star.y, star.size * 3, 0, Math.PI * 2);
|
|
89
|
+
const gradient = ctx.createRadialGradient(
|
|
90
|
+
star.x, star.y, 0,
|
|
91
|
+
star.x, star.y, star.size * 3
|
|
92
|
+
);
|
|
93
|
+
gradient.addColorStop(0, `rgba(201, 162, 39, ${star.opacity * 0.3})`);
|
|
94
|
+
gradient.addColorStop(1, 'rgba(201, 162, 39, 0)');
|
|
95
|
+
ctx.fillStyle = gradient;
|
|
96
|
+
ctx.fill();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
animate();
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
window.removeEventListener('resize', resize);
|
|
107
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
108
|
+
cancelAnimationFrame(animationRef.current);
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<canvas
|
|
114
|
+
ref={canvasRef}
|
|
115
|
+
className="fixed inset-0 pointer-events-none"
|
|
116
|
+
style={{ zIndex: 0 }}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { TarotCard as TarotCardType } from '@/types';
|
|
3
|
+
|
|
4
|
+
interface TarotCardProps {
|
|
5
|
+
card: TarotCardType;
|
|
6
|
+
isReversed?: boolean;
|
|
7
|
+
isFlipped?: boolean;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sizeClasses = {
|
|
14
|
+
sm: 'w-20 h-32',
|
|
15
|
+
md: 'w-32 h-48',
|
|
16
|
+
lg: 'w-44 h-72',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function TarotCard({
|
|
20
|
+
card,
|
|
21
|
+
isReversed = false,
|
|
22
|
+
isFlipped = false,
|
|
23
|
+
onClick,
|
|
24
|
+
size = 'md',
|
|
25
|
+
className = '',
|
|
26
|
+
}: TarotCardProps) {
|
|
27
|
+
const [flipped, setFlipped] = useState(isFlipped);
|
|
28
|
+
|
|
29
|
+
const handleClick = () => {
|
|
30
|
+
if (!flipped) {
|
|
31
|
+
setFlipped(true);
|
|
32
|
+
}
|
|
33
|
+
onClick?.();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const suitSymbol = {
|
|
37
|
+
wands: '⚡',
|
|
38
|
+
cups: '🏆',
|
|
39
|
+
swords: '⚔️',
|
|
40
|
+
pentacles: '💰',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const elementColor = {
|
|
44
|
+
wands: 'text-orange-400',
|
|
45
|
+
cups: 'text-blue-400',
|
|
46
|
+
swords: 'text-gray-300',
|
|
47
|
+
pentacles: 'text-green-400',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={`relative perspective-1000 cursor-pointer ${className}`}
|
|
53
|
+
onClick={handleClick}
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
className={`relative preserve-3d transition-transform duration-700 ${
|
|
57
|
+
flipped ? 'rotate-y-180' : ''
|
|
58
|
+
} ${sizeClasses[size]}`}
|
|
59
|
+
>
|
|
60
|
+
{/* Card Back */}
|
|
61
|
+
<div className="absolute inset-0 backface-hidden rounded-lg border-2 border-ancient/40 bg-abyss overflow-hidden">
|
|
62
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
63
|
+
<div className="w-full h-full p-2">
|
|
64
|
+
<div className="w-full h-full border border-ancient/30 rounded flex items-center justify-center relative">
|
|
65
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
66
|
+
<div className="w-12 h-12 border border-ancient/50 rotate-45" />
|
|
67
|
+
</div>
|
|
68
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
69
|
+
<span className="text-ancient/60 text-2xl">✦</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="absolute top-1 left-1 text-ancient/30 text-xs">⚜</div>
|
|
72
|
+
<div className="absolute top-1 right-1 text-ancient/30 text-xs">⚜</div>
|
|
73
|
+
<div className="absolute bottom-1 left-1 text-ancient/30 text-xs">⚜</div>
|
|
74
|
+
<div className="absolute bottom-1 right-1 text-ancient/30 text-xs">⚜</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Card Front */}
|
|
81
|
+
<div
|
|
82
|
+
className={`absolute inset-0 backface-hidden rotate-y-180 rounded-lg border-2 ${
|
|
83
|
+
isReversed ? 'border-crimson/60' : 'border-ancient/60'
|
|
84
|
+
} bg-abyss overflow-hidden`}
|
|
85
|
+
>
|
|
86
|
+
<div className="absolute inset-0 flex flex-col items-center justify-between p-2">
|
|
87
|
+
{/* Top */}
|
|
88
|
+
<div className="w-full flex justify-between items-start">
|
|
89
|
+
<span className={`text-xs ${card.suit ? elementColor[card.suit] : 'text-ancient'}`}>
|
|
90
|
+
{card.number || (card.arcana === 'major' ? '∞' : '')}
|
|
91
|
+
</span>
|
|
92
|
+
{card.suit && (
|
|
93
|
+
<span className="text-xs">{suitSymbol[card.suit]}</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Center */}
|
|
98
|
+
<div className="flex-1 flex items-center justify-center">
|
|
99
|
+
<div className="text-center">
|
|
100
|
+
<div className="text-3xl mb-1">
|
|
101
|
+
{card.arcana === 'major' ? '☽' : suitSymbol[card.suit as keyof typeof suitSymbol]}
|
|
102
|
+
</div>
|
|
103
|
+
<div className={`text-xs ${isReversed ? 'text-crimson/80' : 'text-ancient'} font-medium`}>
|
|
104
|
+
{card.name}
|
|
105
|
+
</div>
|
|
106
|
+
{isReversed && (
|
|
107
|
+
<div className="text-[10px] text-crimson/60 mt-0.5">逆位</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Bottom */}
|
|
113
|
+
<div className="w-full flex justify-between items-end">
|
|
114
|
+
<span className="text-[10px] text-stardust/40">
|
|
115
|
+
{card.arcana === 'major' ? '大阿卡纳' : card.suit === 'wands' ? '权杖' : card.suit === 'cups' ? '圣杯' : card.suit === 'swords' ? '宝剑' : '星币'}
|
|
116
|
+
</span>
|
|
117
|
+
<span className={`text-xs ${card.suit ? elementColor[card.suit] : 'text-ancient'}`}>
|
|
118
|
+
{card.number || (card.arcana === 'major' ? '∞' : '')}
|
|
119
|
+
</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Decorative border */}
|
|
124
|
+
<div className="absolute inset-1 border border-ancient/10 rounded pointer-events-none" />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// 精确出生时刻输入:time 输入 + 自动匹配时辰(东玄以此时辰排盘,星盘用精确时刻)
|
|
2
|
+
import { timeToShichenLabel, timeToHourIndex } from '@/data/shichen';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value: string; // HH:MM
|
|
6
|
+
onChange: (time: string) => void;
|
|
7
|
+
id?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function TimeShichenInput({ value, onChange, id, label = '出生时刻(精确)' }: Props) {
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<div style={{ display: 'flex', gap: '.5rem', alignItems: 'stretch' }}>
|
|
15
|
+
<input
|
|
16
|
+
className="input-line"
|
|
17
|
+
type="time"
|
|
18
|
+
id={id}
|
|
19
|
+
value={value}
|
|
20
|
+
onChange={e => onChange(e.target.value)}
|
|
21
|
+
style={{ flex: 1 }}
|
|
22
|
+
/>
|
|
23
|
+
<span className="shichen-chip">{timeToShichenLabel(value)}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { timeToHourIndex };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// 星盘:出生日期/时刻/地点(经纬度→LST 上升点)→ 行星落座/相位/十二宫
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { astrologyCalc } from '@core/engine/astrology';
|
|
4
|
+
import { ZODIAC, ZODIAC_MEAN, HOUSES } from '@core/data/zodiac';
|
|
5
|
+
import { mod } from '@core/data/ganzhi';
|
|
6
|
+
import type { GeoLocation } from '@core/types';
|
|
7
|
+
import type { UserProfile } from '@/utils/userStore';
|
|
8
|
+
import LocationPicker from '@/components/LocationPicker';
|
|
9
|
+
import { currentUser } from '@/utils/userStore';
|
|
10
|
+
import { ResultCard } from '@/components/ResultCard';
|
|
11
|
+
import SongSelect from '@/components/SongSelect';
|
|
12
|
+
import DateInput from '@/components/DateInput';
|
|
13
|
+
import ProfilePicker from '@/components/ProfilePicker';
|
|
14
|
+
|
|
15
|
+
interface PanelProps { onDivine: (inputs: unknown, profile?: UserProfile, question?: string, profileId?: string) => void; }
|
|
16
|
+
|
|
17
|
+
export function AstrologyPanel({ onDivine }: PanelProps) {
|
|
18
|
+
const user = currentUser();
|
|
19
|
+
const p = user?.profile;
|
|
20
|
+
const [date, setDate] = useState(p?.birthDate || '1990-06-15');
|
|
21
|
+
const [time, setTime] = useState(() => { const h = (p?.birthHourIndex ?? 4) * 2; return String(h).padStart(2, '0') + ':30'; });
|
|
22
|
+
const [loc, setLoc] = useState<GeoLocation | null>(p?.location || null);
|
|
23
|
+
const [gender, setGender] = useState<'男' | '女'>(p?.gender || '男');
|
|
24
|
+
const [profileSrc, setProfileSrc] = useState<{ id: string; name: string } | null>(null);
|
|
25
|
+
|
|
26
|
+
const filled = p && date === p.birthDate && gender === p.gender;
|
|
27
|
+
const go = () => {
|
|
28
|
+
const [y, m, d] = date.split('-').map(Number);
|
|
29
|
+
const [hh, mm] = time.split(':').map(Number);
|
|
30
|
+
if (!y || !m || !d) return;
|
|
31
|
+
const birthHourIndex = Math.floor(((hh || 0) + 1) % 24 / 2);
|
|
32
|
+
const profile: UserProfile = { birthDate: date, birthTime: time, birthHourIndex, gender, location: loc };
|
|
33
|
+
onDivine({ y, m, d, hour: hh || 0, min: mm || 0, lng: loc?.lng, lat: loc?.lat }, profile, undefined, profileSrc?.id || 'main');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<ProfilePicker onPick={(p, src) => { setDate(p.birthDate); setTime(p.birthTime || '08:30'); setGender(p.gender); setLoc(p.location); if (src) setProfileSrc(src); }} />
|
|
39
|
+
{user && filled && <p className="hint" style={{ color: 'var(--celadon-deep)', marginTop: 'var(--sp-2)' }}>已依主档案预填(可改,改后以所书为准)</p>}
|
|
40
|
+
<div className="field"><label htmlFor="as-date">出生日期(公历 / 农历)</label>
|
|
41
|
+
<DateInput id="as-date" value={date} onChange={setDate} /></div>
|
|
42
|
+
<div className="field-row">
|
|
43
|
+
<div className="field"><label htmlFor="as-hour">出生时刻</label>
|
|
44
|
+
<input className="input-line" type="time" id="as-hour" value={time} onChange={e => setTime(e.target.value)} /></div>
|
|
45
|
+
<div className="field"><label htmlFor="as-gender">性别</label>
|
|
46
|
+
<SongSelect id="as-gender" value={gender} options={[{ value: '男', label: '男' }, { value: '女', label: '女' }]} onChange={v => setGender(v as '男' | '女')} />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="field">
|
|
50
|
+
<label>出生地点(上升点需经纬度)</label>
|
|
51
|
+
<LocationPicker value={loc} onChange={setLoc} />
|
|
52
|
+
</div>
|
|
53
|
+
<button className="btn-divine" onClick={go}>布 星 图<span className="small">推行星黄经 · 布十二宫</span></button>
|
|
54
|
+
<p className="hint" style={{ marginTop: '.8rem' }}>行星位置采用简略天文近似(平均运动),上升点按本地恒星时 LST 推算,宫位取整宫制,仅供怡情。</p>
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function AstrologyResult({ data }: { data: ReturnType<typeof astrologyCalc> }) {
|
|
60
|
+
const r = data;
|
|
61
|
+
const ascSign = Math.floor(mod(r.asc, 360) / 30);
|
|
62
|
+
const sunSign = Math.floor(r.sun / 30);
|
|
63
|
+
const moonSign = Math.floor(r.moon / 30);
|
|
64
|
+
const signRows = r.planets.map(([name, sym, lon]) => {
|
|
65
|
+
const s = Math.floor(mod(lon, 360) / 30);
|
|
66
|
+
const deg = mod(lon, 30);
|
|
67
|
+
return { name, sym, s, deg };
|
|
68
|
+
});
|
|
69
|
+
const detailRows = (r.planetDetails || []).map(p => (
|
|
70
|
+
<div className="result-cell" key={p.cn}>
|
|
71
|
+
<div className="k">{p.cn} {p.sym} {p.retrograde ? '· 逆行' : ''}</div>
|
|
72
|
+
<div className="v">{p.sign} {p.degree.toFixed(1)}°</div>
|
|
73
|
+
<div className="tiny">落{p.house}宫{p.dignity.status ? ' · ' + p.dignity.status : ''}</div>
|
|
74
|
+
{p.dignity.note && <div className="tiny muted">{p.dignity.note}</div>}
|
|
75
|
+
</div>
|
|
76
|
+
));
|
|
77
|
+
const houseRows = (r.houses || []).map(h => (
|
|
78
|
+
<div className="result-cell" key={h.num}>
|
|
79
|
+
<div className="k">{h.num}宫</div>
|
|
80
|
+
<div className="v">{h.sign}</div>
|
|
81
|
+
<div className="tiny">宫主 {h.ruler}</div>
|
|
82
|
+
</div>
|
|
83
|
+
));
|
|
84
|
+
const aspectHtml = r.aspects.length
|
|
85
|
+
? r.aspects.map((a, i) => <p key={i}><strong>{a[0]}</strong> 与 <strong>{a[1]}</strong> 成 <span className="tag-cool">{a[2]}</span> —— {a[3]}</p>)
|
|
86
|
+
: <p>诸星无显著相位,气机平和。</p>;
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
<ResultCard title="命盘之纲">
|
|
90
|
+
<p>上升 <strong>{ZODIAC[ascSign][0]}</strong> {mod(r.asc, 30).toFixed(1)}°(本地恒星时 {r.lstHours.toFixed(2)}h),中天 <strong>{ZODIAC[Math.floor((r.mc ?? 0) / 30)][0]}</strong>,太阳落 <strong>{ZODIAC[sunSign][0]}</strong>,月亮落 <strong>{ZODIAC[moonSign][0]}</strong>。</p>
|
|
91
|
+
<p className="tiny muted">回归黄道(Tropical)· VSOP87 精确星历 · 黄赤交角 {r.epsilon?.toFixed(2)}° · 宫位整宫制</p>
|
|
92
|
+
<p>太阳主自我之核,月亮主情感之底,上升主处世之貌。三者为命盘之纲。</p>
|
|
93
|
+
</ResultCard>
|
|
94
|
+
<ResultCard title="行星经纬"
|
|
95
|
+
cells={signRows.map(p => ({
|
|
96
|
+
k: `${p.name} ${p.sym}`,
|
|
97
|
+
v: `${ZODIAC[p.s][2]} ${ZODIAC[p.s][0]}座 ${p.deg.toFixed(1)}°`,
|
|
98
|
+
}))}>
|
|
99
|
+
{signRows.map(p => <p key={p.name} className="tiny muted">{ZODIAC_MEAN[ZODIAC[p.s][0]]}</p>)}
|
|
100
|
+
</ResultCard>
|
|
101
|
+
<ResultCard title="行星入宫 · 庙旺 · 逆行">
|
|
102
|
+
<div className="result-grid" style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))' }}>
|
|
103
|
+
{detailRows}
|
|
104
|
+
</div>
|
|
105
|
+
<p className="tiny muted" style={{ marginTop: '.6rem' }}>庙=本垣力量最显,旺=擢升助力显著,陷/弱=力量受抑;逆行行星主内省与回炉,其宫位事务易有反复。</p>
|
|
106
|
+
</ResultCard>
|
|
107
|
+
<ResultCard title="相位经纬">{aspectHtml}</ResultCard>
|
|
108
|
+
<ResultCard title="行星落宫参详">
|
|
109
|
+
{r.planets.map((p) => {
|
|
110
|
+
const s = Math.floor(mod(p[2], 360) / 30);
|
|
111
|
+
const house = mod(s - ascSign + 12, 12); // 整宫制:上升起算
|
|
112
|
+
return <p key={p[0]}><strong>{p[0]}</strong>落{house + 1}宫({HOUSES[house]})· {ZODIAC[s][0]}座 —— {houseMeaning(p[0], house)}</p>;
|
|
113
|
+
})}
|
|
114
|
+
<p className="tiny muted">整宫制推演,行星入宫主人生领域之侧重;仅供怡情。</p>
|
|
115
|
+
</ResultCard>
|
|
116
|
+
<ResultCard title="十二宫位(整宫制)">
|
|
117
|
+
<p>{HOUSES.map((hname, i) => <span key={hname}><strong>{hname}</strong>·{ZODIAC[mod(ascSign + i, 12)][0]} </span>)}</p>
|
|
118
|
+
<div className="result-grid" style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(120px,1fr))', marginTop: '.7rem' }}>
|
|
119
|
+
{houseRows}
|
|
120
|
+
</div>
|
|
121
|
+
<p className="mt-1">命宫即上升所落之座,为一生行运之门户;宫主星为该宫头星座之古典守护星(不含三王星)。Placidus 宫位制在迭代计划中。</p>
|
|
122
|
+
</ResultCard>
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
// 行星入宫解读(现代语言)
|
|
127
|
+
function houseMeaning(planet: string, house: number): string {
|
|
128
|
+
const base: string[] = [
|
|
129
|
+
'自我形象与人生方向之所在', '财帛运用与价值感之来源', '思维沟通与手足同侪之域',
|
|
130
|
+
'家宅根基与内在安全之锚', '才艺表现与恋爱子女之场', '日常工作与身体康健之务',
|
|
131
|
+
'姻缘伴侣与合伙共事之缘', '深渊转化与共享资源之秘', '远行求学与信念追寻之路',
|
|
132
|
+
'事业名位与社会形象之台', '交友结社与理想愿景之圈', '潜意识与灵性安顿之隅',
|
|
133
|
+
];
|
|
134
|
+
const p: Record<string, string> = {
|
|
135
|
+
太阳: '光芒所注,此生着力之处', 月亮: '情感所依,心境冷暖之枢',
|
|
136
|
+
水星: '思虑所驰,学习表达之道', 金星: '喜恶所向,审美人际之趣',
|
|
137
|
+
火星: '行动所向,冲劲释放之径', 木星: '幸运所至,成长拓展之门',
|
|
138
|
+
土星: '功课所在,磨砺成就之石',
|
|
139
|
+
};
|
|
140
|
+
return (p[planet] || '此星行运') + ',落于' + base[house] + '。';
|
|
141
|
+
}
|