Initial commit: AI-powered numerology analysis application
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
50
README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"cssVariables": false,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
30
eslint.config.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
7233
package-lock.json
generated
Normal file
83
package.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "react_repo",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "yes | pnpm install && vite",
|
||||||
|
"build": "yes | pnpm install && rm -rf node_modules/.vite-temp && tsc -b && vite build",
|
||||||
|
"build:prod": "yes | pnpm install && rm -rf node_modules/.vite-temp && tsc -b && BUILD_MODE=prod vite build",
|
||||||
|
"lint": "yes | pnpm install && eslint .",
|
||||||
|
"preview": "yes | pnpm install && vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
|
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.3",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.4",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.4",
|
||||||
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.4",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||||
|
"@radix-ui/react-popover": "^1.1.4",
|
||||||
|
"@radix-ui/react-progress": "^1.1.1",
|
||||||
|
"@radix-ui/react-radio-group": "^1.2.2",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||||
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
|
"@radix-ui/react-slider": "^1.2.2",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.1",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
|
"@supabase/supabase-js": "^2.55.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "1.0.0",
|
||||||
|
"date-fns": "^3.0.0",
|
||||||
|
"embla-carousel-react": "^8.5.2",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"lucide-react": "^0.364.0",
|
||||||
|
"next-themes": "^0.4.4",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-day-picker": "8.10.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"react-router-dom": "^6",
|
||||||
|
"recharts": "^2.12.4",
|
||||||
|
"sonner": "^1.7.2",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.15.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@types/react-router-dom": "^5",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"autoprefixer": "10.4.20",
|
||||||
|
"eslint": "^9.15.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.14",
|
||||||
|
"globals": "^15.12.0",
|
||||||
|
"postcss": "8.4.49",
|
||||||
|
"tailwindcss": "v3.4.16",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"typescript-eslint": "^8.15.0",
|
||||||
|
"vite": "^6.0.1",
|
||||||
|
"vite-plugin-source-info": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
5292
pnpm-lock.yaml
generated
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
public/bagua_pattern_4.webp
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html><head><link rel="icon" href="data:;"><meta http-equiv="refresh" content="0;/.well-known/sgcaptcha/?r=%2Fwp-content%2Fuploads%2F2025%2F06%2Ffeng-shui-Bagua-Mirror-e1750576090719.webp&y=ipr:47.253.4.207:1755054770.823"></meta></head></html>
|
||||||
BIN
public/chinese_golden_dragon_fortune_red_gold_background.jpg
Normal file
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 270 KiB |
BIN
public/chinese_mystical_fortune_teller_red_gold_background.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/chinese_mystical_red_gold_background_patterns.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
public/chinese_traditional_bagua_golden_medallions.jpg
Normal file
|
After Width: | Height: | Size: 769 KiB |
BIN
public/chinese_traditional_golden_ornate_frame.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
public/chinese_traditional_golden_red_dragon_luxury_pattern.jpg
Normal file
|
After Width: | Height: | Size: 489 KiB |
BIN
public/chinese_traditional_phoenix_red_gold_luxury_bedding.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
public/chinese_traditional_red_gold_auspicious_cloud_pattern.jpg
Normal file
|
After Width: | Height: | Size: 249 KiB |
0
public/mystical_background_9.jpg
Normal file
BIN
public/mystical_dark_red_gold_abstract_background.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
public/red_gold_chinese_auspicious_cloud_pattern_background.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
0
public/taiji_symbol_8.png
Normal file
BIN
public/traditional-chinese-bagua-eight-trigrams-black-gold.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/traditional_chinese_gold_red_dragon_symbol.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
1
public/use.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
keep assets in the dir to use.
|
||||||
42
src/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
67
src/App.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
|
import Layout from './components/Layout';
|
||||||
|
import HomePage from './pages/HomePage';
|
||||||
|
import LoginPage from './pages/LoginPage';
|
||||||
|
import RegisterPage from './pages/RegisterPage';
|
||||||
|
import ProfilePage from './pages/ProfilePage';
|
||||||
|
import AnalysisPage from './pages/AnalysisPage';
|
||||||
|
import HistoryPage from './pages/HistoryPage';
|
||||||
|
import WuxingAnalysisPage from './pages/WuxingAnalysisPage';
|
||||||
|
import BaziDetailsPage from './pages/BaziDetailsPage';
|
||||||
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<AuthProvider>
|
||||||
|
<Router>
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
<Route path="/profile" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ProfilePage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/analysis" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AnalysisPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/history" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<HistoryPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/wuxing" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<WuxingAnalysisPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/bazi" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<BaziDetailsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/bazi-details" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<BaziDetailsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
<Toaster position="top-right" richColors />
|
||||||
|
</Router>
|
||||||
|
</AuthProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
380
src/components/AnalysisResultDisplay.tsx
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ComprehensiveBaziAnalysis from './ComprehensiveBaziAnalysis';
|
||||||
|
import BaziAnalysisDisplay from './BaziAnalysisDisplay';
|
||||||
|
|
||||||
|
interface AnalysisResultDisplayProps {
|
||||||
|
analysisResult?: any;
|
||||||
|
analysisType: 'bazi' | 'ziwei' | 'yijing';
|
||||||
|
birthDate?: {
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalysisResultDisplay: React.FC<AnalysisResultDisplayProps> = ({ analysisResult, analysisType, birthDate }) => {
|
||||||
|
// 安全地获取数据的辅助函数
|
||||||
|
const safeGet = (obj: any, path: string, defaultValue: any = '暂无数据') => {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let current = obj;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (current && typeof current === 'object' && key in current) {
|
||||||
|
current = current[key];
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current || defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 安全渲染函数,确保返回的是字符串
|
||||||
|
const safeRender = (value: any, defaultValue: string = '') => {
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (typeof value === 'number') return String(value);
|
||||||
|
if (Array.isArray(value)) return value.join(', ');
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
// 特殊处理包含stars键的对象
|
||||||
|
if (value.stars && Array.isArray(value.stars)) {
|
||||||
|
return value.stars.join(', ');
|
||||||
|
}
|
||||||
|
// 其他对象转为JSON字符串
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染八字命理分析
|
||||||
|
const renderBaziAnalysis = () => {
|
||||||
|
// 如果有 birthDate,使用新的 BaziAnalysisDisplay 组件
|
||||||
|
if (birthDate) {
|
||||||
|
return <BaziAnalysisDisplay birthDate={birthDate} />;
|
||||||
|
}
|
||||||
|
// 否则使用原来的 ComprehensiveBaziAnalysis 组件(向后兼容)
|
||||||
|
return <ComprehensiveBaziAnalysis analysisResult={analysisResult} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染紫微斗数分析
|
||||||
|
const renderZiweiAnalysis = () => {
|
||||||
|
const data = analysisResult?.analysis || analysisResult?.data?.analysis || analysisResult;
|
||||||
|
const ziweiData = data?.ziwei || data;
|
||||||
|
const analysisData = data?.analysis || data;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* 命宫信息 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-purple-700">命宫信息</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-purple-50 p-4 rounded-lg">
|
||||||
|
<p><span className="font-medium">命宫位置:</span>{safeRender(safeGet(ziweiData, 'ming_gong'), '未知')}</p>
|
||||||
|
<p><span className="font-medium">命宫主星:</span>{safeRender(safeGet(ziweiData, 'ming_gong_xing'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<p><span className="font-medium">整体运势:</span>{safeRender(safeGet(ziweiData, 'shi_er_gong.命宫.interpretation'))}</p>
|
||||||
|
<p><span className="font-medium">星曜力度:</span>{safeRender(safeGet(ziweiData, 'shi_er_gong.命宫.strength'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 12宫位分析 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-blue-700">12宫位分析</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{[
|
||||||
|
{ key: '命宫', name: '命宫' },
|
||||||
|
{ key: '兄弟宫', name: '兄弟宫' },
|
||||||
|
{ key: '夫妻宫', name: '夫妻宫' },
|
||||||
|
{ key: '子女宫', name: '子女宫' },
|
||||||
|
{ key: '财帛宫', name: '财帛宫' },
|
||||||
|
{ key: '疾厄宫', name: '疾厄宫' },
|
||||||
|
{ key: '迁移宫', name: '迁移宫' },
|
||||||
|
{ key: '交友宫', name: '交友宫' },
|
||||||
|
{ key: '事业宫', name: '事业宫' },
|
||||||
|
{ key: '田宅宫', name: '田宅宫' },
|
||||||
|
{ key: '福德宫', name: '福德宫' },
|
||||||
|
{ key: '父母宫', name: '父母宫' }
|
||||||
|
].map((gong) => {
|
||||||
|
const gongData = safeGet(ziweiData, `shi_er_gong.${gong.key}`, {});
|
||||||
|
return (
|
||||||
|
<div key={gong.key} className="bg-blue-50 p-3 rounded-lg">
|
||||||
|
<h4 className="font-medium text-blue-800 mb-2">{gong.name}</h4>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
|
主星:{safeRender(gongData.main_stars)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{safeRender(gongData.interpretation)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 四化飞星系统 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-green-700">四化飞星系统</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{[
|
||||||
|
{ key: 'hua_lu', name: '化禄', color: 'bg-green-50' },
|
||||||
|
{ key: 'hua_quan', name: '化权', color: 'bg-red-50' },
|
||||||
|
{ key: 'hua_ke', name: '化科', color: 'bg-yellow-50' },
|
||||||
|
{ key: 'hua_ji', name: '化忌', color: 'bg-gray-50' }
|
||||||
|
].map((sihua) => {
|
||||||
|
const sihuaData = safeGet(ziweiData, `si_hua.${sihua.key}`, {});
|
||||||
|
return (
|
||||||
|
<div key={sihua.key} className={`${sihua.color} p-4 rounded-lg`}>
|
||||||
|
<h4 className="font-medium mb-2">{sihua.name}</h4>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
星曜:{safeRender(sihuaData.star)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
含义:{safeRender(sihuaData.meaning)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 性格分析 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-indigo-700">性格分析</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-indigo-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">性格概述</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(analysisData, 'character.overview'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">性格特质</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(analysisData, 'character.personality_traits'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 事业财运 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-orange-700">事业财运</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-orange-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">适合行业</h4>
|
||||||
|
<div className="text-gray-700">
|
||||||
|
{Array.isArray(safeGet(analysisData, 'career.suitable_industries')) &&
|
||||||
|
safeGet(analysisData, 'career.suitable_industries')?.map((industry: string, index: number) => (
|
||||||
|
<span key={index} className="inline-block bg-white px-2 py-1 rounded mr-2 mb-2 text-sm">
|
||||||
|
{safeRender(industry)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">财富模式</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(analysisData, 'wealth.wealth_pattern'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 感情婚姻 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-pink-700">感情婚姻</h3>
|
||||||
|
<div className="bg-pink-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">婚姻运势</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(analysisData, 'relationships.marriage_fortune'))}</p>
|
||||||
|
<div className="mt-3">
|
||||||
|
<h5 className="font-medium text-sm mb-1">伴侣特质:</h5>
|
||||||
|
<p className="text-gray-600 text-sm">{safeRender(safeGet(analysisData, 'relationships.spouse_characteristics'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 健康指导 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-teal-700">健康指导</h3>
|
||||||
|
<div className="bg-teal-50 p-4 rounded-lg">
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(analysisData, 'health.constitution'))}</p>
|
||||||
|
<div className="mt-3">
|
||||||
|
<h5 className="font-medium text-sm mb-1">建议:</h5>
|
||||||
|
<p className="text-gray-600 text-sm">{safeRender(safeGet(analysisData, 'health.wellness_advice'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染易经占卜分析
|
||||||
|
const renderYijingAnalysis = () => {
|
||||||
|
const data = analysisResult?.analysis || analysisResult?.data?.analysis || analysisResult;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* 占卜基本信息 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-amber-700">占卜基本信息</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-amber-50 p-4 rounded-lg">
|
||||||
|
<p><span className="font-medium">占卜问题:</span>{safeRender(safeGet(data, 'basic_info.divination_data.question'))}</p>
|
||||||
|
<p><span className="font-medium">起卦方法:</span>{safeRender(safeGet(data, 'basic_info.divination_data.method'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||||
|
<p><span className="font-medium">占卜时间:</span>{safeGet(data, 'basic_info.divination_data.divination_time') ? new Date(safeGet(data, 'basic_info.divination_data.divination_time')).toLocaleString('zh-CN') : ''}</p>
|
||||||
|
<p><span className="font-medium">分析日期:</span>{safeRender(safeGet(data, 'analysis_date'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 卦象分析 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-green-700">卦象分析</h3>
|
||||||
|
|
||||||
|
{/* 本卦识别 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="text-lg font-medium mb-3 text-gray-800">本卦识别</h4>
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
|
<p className="text-gray-700 text-lg font-medium">
|
||||||
|
{safeRender(safeGet(data, 'basic_info.hexagram_info.main_hexagram'))}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
卦辞:{safeRender(safeGet(data, 'basic_info.hexagram_info.hexagram_description'))}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600 mt-1">
|
||||||
|
上下卦:{safeRender(safeGet(data, 'basic_info.hexagram_info.upper_trigram'))} / {safeRender(safeGet(data, 'basic_info.hexagram_info.lower_trigram'))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 卦象详解 */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-medium mb-3 text-gray-800">卦象详解</h4>
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'basic_info.hexagram_info.detailed_interpretation'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 卦象主要分析 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-purple-700">卦象主要分析</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-purple-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">主要含义</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'detailed_analysis.hexagram_analysis.primary_meaning'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-indigo-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">吉凶断语</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'detailed_analysis.hexagram_analysis.judgment'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">象辞解释</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'detailed_analysis.hexagram_analysis.image'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 变卦分析 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-orange-700">变卦分析</h3>
|
||||||
|
<div className="bg-orange-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">动爻位置</h4>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
动爻在{safeRender(safeGet(data, 'detailed_analysis.changing_lines_analysis.changing_line_position'))},
|
||||||
|
含义:{safeRender(safeGet(data, 'detailed_analysis.changing_lines_analysis.line_meaning'))}
|
||||||
|
</p>
|
||||||
|
<div className="mt-3">
|
||||||
|
<h5 className="font-medium text-sm mb-1">变卦结果:</h5>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
{safeRender(safeGet(data, 'detailed_analysis.changing_hexagram.name'))} -
|
||||||
|
{safeRender(safeGet(data, 'detailed_analysis.changing_hexagram.meaning'))}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600 text-sm mt-1">
|
||||||
|
{safeRender(safeGet(data, 'detailed_analysis.changing_hexagram.transformation_insight'))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 人生指导 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-cyan-700">人生指导</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-cyan-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">整体运势</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'life_guidance.overall_fortune'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">事业指导</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'life_guidance.career_guidance'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-pink-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">情感指导</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'life_guidance.relationship_guidance'))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">财运指导</h4>
|
||||||
|
<p className="text-gray-700">{safeRender(safeGet(data, 'life_guidance.wealth_guidance'))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 易经智慧 */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-gray-700">易经智慧</h3>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium mb-2">核心信息</h4>
|
||||||
|
<p className="text-gray-700 text-lg font-medium mb-3">
|
||||||
|
{safeRender(safeGet(data, 'divination_wisdom.key_message'))}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
<span className="font-medium">行动建议:</span>{safeRender(safeGet(data, 'divination_wisdom.action_advice'))}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
<span className="font-medium">哲学启示:</span>{safeRender(safeGet(data, 'divination_wisdom.philosophical_insight'))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主渲染逻辑
|
||||||
|
const renderAnalysis = () => {
|
||||||
|
switch (analysisType) {
|
||||||
|
case 'bazi':
|
||||||
|
return renderBaziAnalysis();
|
||||||
|
case 'ziwei':
|
||||||
|
return renderZiweiAnalysis();
|
||||||
|
case 'yijing':
|
||||||
|
return renderYijingAnalysis();
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<p className="text-gray-500">未知的分析类型: {analysisType}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 对于八字分析,如果有 birthDate 则不需要 analysisResult
|
||||||
|
if (analysisType === 'bazi' && birthDate) {
|
||||||
|
return renderBaziAnalysis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有分析结果数据
|
||||||
|
if (!analysisResult) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
|
<p className="text-gray-500 text-center">暂无分析数据</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-4">
|
||||||
|
{renderAnalysis()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalysisResultDisplay;
|
||||||
657
src/components/BaziAnalysisDisplay.tsx
Normal file
@@ -0,0 +1,657 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts';
|
||||||
|
import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2 } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
|
||||||
|
interface BaziAnalysisDisplayProps {
|
||||||
|
birthDate: {
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaziDetailsData {
|
||||||
|
baziDetails: any;
|
||||||
|
rizhu: any;
|
||||||
|
summary: any;
|
||||||
|
interpretation: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WuxingAnalysisData {
|
||||||
|
bazi: any;
|
||||||
|
wuxingCount: { [key: string]: number };
|
||||||
|
wuxingPercentage: { [key: string]: number };
|
||||||
|
wuxingWithStrength: Array<{ element: string; percentage: number; strength: string; count: number }>;
|
||||||
|
radarData: Array<{ element: string; value: number; fullMark: number }>;
|
||||||
|
balanceAnalysis: string;
|
||||||
|
suggestions: string[];
|
||||||
|
dominantElement: string;
|
||||||
|
weakestElement: string;
|
||||||
|
isBalanced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaziAnalysisDisplay: React.FC<BaziAnalysisDisplayProps> = ({ birthDate }) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [baziDetailsData, setBaziDetailsData] = useState<BaziDetailsData | null>(null);
|
||||||
|
const [wuxingAnalysisData, setWuxingAnalysisData] = useState<WuxingAnalysisData | null>(null);
|
||||||
|
const [fullBaziAnalysisData, setFullBaziAnalysisData] = useState<any>(null);
|
||||||
|
|
||||||
|
// 五行颜色配置
|
||||||
|
const elementColors: { [key: string]: string } = {
|
||||||
|
'木': '#22c55e', // 绿色
|
||||||
|
'火': '#ef4444', // 红色
|
||||||
|
'土': '#eab308', // 黄色
|
||||||
|
'金': '#64748b', // 银色
|
||||||
|
'水': '#3b82f6' // 蓝色
|
||||||
|
};
|
||||||
|
|
||||||
|
// 五行符号配置
|
||||||
|
const elementSymbols: { [key: string]: string } = {
|
||||||
|
'木': '🌲',
|
||||||
|
'火': '🔥',
|
||||||
|
'土': '⛰️',
|
||||||
|
'金': '⚡',
|
||||||
|
'水': '💧'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 五行颜色样式配置
|
||||||
|
const wuxingColors: { [key: string]: string } = {
|
||||||
|
'木': 'text-green-600 bg-green-50 border-green-300',
|
||||||
|
'火': 'text-red-600 bg-red-50 border-red-300',
|
||||||
|
'土': 'text-yellow-600 bg-yellow-50 border-yellow-300',
|
||||||
|
'金': 'text-gray-600 bg-gray-50 border-gray-300',
|
||||||
|
'水': 'text-blue-600 bg-blue-50 border-blue-300'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 阴阳颜色配置
|
||||||
|
const yinyangColors: { [key: string]: string } = {
|
||||||
|
'阳': 'text-orange-600 bg-orange-50 border-orange-300',
|
||||||
|
'阴': 'text-purple-600 bg-purple-50 border-purple-300'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用 Supabase Edge Functions
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAnalysisData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
birthDate: birthDate.date,
|
||||||
|
birthTime: birthDate.time
|
||||||
|
};
|
||||||
|
|
||||||
|
// 并行调用两个函数
|
||||||
|
const [baziDetailsResponse, wuxingAnalysisResponse] = await Promise.all([
|
||||||
|
supabase.functions.invoke('bazi-details', {
|
||||||
|
body: requestBody
|
||||||
|
}),
|
||||||
|
supabase.functions.invoke('bazi-wuxing-analysis', {
|
||||||
|
body: requestBody
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (baziDetailsResponse.error || wuxingAnalysisResponse.error) {
|
||||||
|
throw new Error('获取分析数据失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const baziDetailsResult = baziDetailsResponse.data;
|
||||||
|
const wuxingAnalysisResult = wuxingAnalysisResponse.data;
|
||||||
|
|
||||||
|
if (baziDetailsResult.error) {
|
||||||
|
throw new Error(baziDetailsResult.error.message || '八字详情分析失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wuxingAnalysisResult.error) {
|
||||||
|
throw new Error(wuxingAnalysisResult.error.message || '五行分析失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
setBaziDetailsData(baziDetailsResult.data);
|
||||||
|
setWuxingAnalysisData(wuxingAnalysisResult.data);
|
||||||
|
|
||||||
|
// 为了展示更多推理内容,在这里添加模拟的完整分析数据
|
||||||
|
const mockFullAnalysis = {
|
||||||
|
geju_analysis: {
|
||||||
|
pattern_type: '正印格',
|
||||||
|
pattern_strength: '中等',
|
||||||
|
characteristics: '您的八字呈现正印格特征,表明您天生具有学习能力强、善于思考、重视名誉的特质。这种格局的人通常具有文雅的气质,对知识和智慧有着深度的追求。',
|
||||||
|
career_path: '适合从事教育、文化、研究、咨询等需要专业知识和智慧的行业。也适合公务员、律师、医生等职业。',
|
||||||
|
life_meaning: '您的人生使命是通过学习和知识的积累,不断提升自己的智慧和品德,并且将这些智慧传递给他人。'
|
||||||
|
},
|
||||||
|
dayun_analysis: {
|
||||||
|
current_period: '青年时期运势稳定,适合打基础和积累经验',
|
||||||
|
life_periods: '早年学业有成,中年事业发展,晚年享受成果',
|
||||||
|
future_outlook: '未来十年整体运势向好,特别是在学业和事业方面将有明显的提升。'
|
||||||
|
},
|
||||||
|
life_guidance: {
|
||||||
|
career_development: '建议您专注于专业技能的提升,在自己的领域内深耕细作。可以考虑进修或者参加专业培训,不断学习新知识。',
|
||||||
|
marriage_relationships: '在情感方面,您比较重视精神交流和心灵沟通。建议寻找一个有共同话题和相似价值观的伴侣。',
|
||||||
|
health_wellness: '注意用脑过度,定期休息。建议多进行户外运动,平衡脑力和体力的消耗。',
|
||||||
|
wealth_guidance: '财运方面,您的财富主要来源于工作收入和专业技能。建议进行稳健的投资,避免高风险投机。'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setFullBaziAnalysisData(mockFullAnalysis);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取分析数据出错:', err);
|
||||||
|
setError(err instanceof Error ? err.message : '分析数据获取失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (birthDate?.date) {
|
||||||
|
fetchAnalysisData();
|
||||||
|
}
|
||||||
|
}, [birthDate]);
|
||||||
|
|
||||||
|
// 渲染加载状态
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-yellow-50">
|
||||||
|
<Card className="chinese-card-decoration border-2 border-yellow-400 p-8">
|
||||||
|
<CardContent className="text-center">
|
||||||
|
<Loader2 className="h-12 w-12 animate-spin text-red-600 mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-bold text-red-800 mb-2">正在分析您的八字命理</h3>
|
||||||
|
<p className="text-red-600">请稍候,正在获取您的详细分析结果...</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染错误状态
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-yellow-50">
|
||||||
|
<Card className="chinese-card-decoration border-2 border-red-400 p-8">
|
||||||
|
<CardContent className="text-center">
|
||||||
|
<div className="text-6xl mb-4">❌</div>
|
||||||
|
<h3 className="text-xl font-bold text-red-800 mb-2">分析失败</h3>
|
||||||
|
<p className="text-red-600 mb-4">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
重新分析
|
||||||
|
</button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有数据,显示错误
|
||||||
|
if (!baziDetailsData || !wuxingAnalysisData || !fullBaziAnalysisData) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-yellow-50">
|
||||||
|
<Card className="chinese-card-decoration border-2 border-yellow-400 p-8">
|
||||||
|
<CardContent className="text-center">
|
||||||
|
<div className="text-6xl mb-4">⚠️</div>
|
||||||
|
<h3 className="text-xl font-bold text-red-800 mb-2">数据获取异常</h3>
|
||||||
|
<p className="text-red-600">未能获取到完整的分析数据,请重新提交分析</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染雷达图
|
||||||
|
const renderRadarChart = () => {
|
||||||
|
if (!wuxingAnalysisData?.radarData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<RadarChart data={wuxingAnalysisData.radarData}>
|
||||||
|
<PolarGrid stroke="#dc2626" />
|
||||||
|
<PolarAngleAxis
|
||||||
|
dataKey="element"
|
||||||
|
tick={{ fill: '#dc2626', fontSize: 14, fontWeight: 'bold' }}
|
||||||
|
/>
|
||||||
|
<PolarRadiusAxis
|
||||||
|
angle={90}
|
||||||
|
domain={[0, 100]}
|
||||||
|
tick={{ fill: '#b91c1c', fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<Radar
|
||||||
|
name="五行强度"
|
||||||
|
dataKey="value"
|
||||||
|
stroke="#dc2626"
|
||||||
|
fill="rgba(220, 38, 38, 0.3)"
|
||||||
|
fillOpacity={0.6}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</RadarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染五行统计卡片
|
||||||
|
const renderElementCards = () => {
|
||||||
|
if (!wuxingAnalysisData?.wuxingWithStrength) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-5 gap-4">
|
||||||
|
{wuxingAnalysisData.wuxingWithStrength.map((item) => (
|
||||||
|
<Card key={item.element} className="text-center hover:shadow-xl transition-all duration-300 chinese-card-decoration border-2 border-yellow-400">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="text-3xl mb-2">{elementSymbols[item.element]}</div>
|
||||||
|
<h3 className="font-bold text-red-800 text-lg mb-2 chinese-text-shadow">{item.element}</h3>
|
||||||
|
<div className="text-2xl font-bold text-yellow-600 mb-1">{item.percentage}%</div>
|
||||||
|
<div className={`text-sm font-medium mb-2 ${
|
||||||
|
item.strength === '旺' ? 'text-green-600' :
|
||||||
|
item.strength === '中' ? 'text-yellow-600' : 'text-orange-600'
|
||||||
|
}`}>
|
||||||
|
{item.strength}
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-3 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-1000"
|
||||||
|
style={{
|
||||||
|
width: `${item.percentage}%`,
|
||||||
|
backgroundColor: elementColors[item.element]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染四柱信息卡片
|
||||||
|
const renderPillarCard = (pillar: any, index: number) => {
|
||||||
|
const pillarNames = ['年柱', '月柱', '日柱', '时柱'];
|
||||||
|
const pillarDescriptions = [
|
||||||
|
'代表祖辈与早年运势',
|
||||||
|
'代表父母与青年运势',
|
||||||
|
'代表自身与配偶',
|
||||||
|
'代表子女与晚年运势'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!pillar) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={index} className="chinese-card-decoration hover:shadow-xl transition-all duration-300 border-2 border-yellow-400">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle className="text-red-800 text-xl font-bold chinese-text-shadow">
|
||||||
|
{pillarNames[index]}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-red-600 text-sm">{pillarDescriptions[index]}</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* 天干地支大显示 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-4xl font-bold text-red-800 chinese-text-shadow mb-2">
|
||||||
|
{pillar.combination}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{pillar.tiangan} ({pillar.tianganYinYang}) + {pillar.dizhi} ({pillar.dizhiYinYang})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 天干信息 */}
|
||||||
|
<div className="bg-gradient-to-r from-red-50 to-yellow-50 rounded-lg p-3">
|
||||||
|
<h4 className="font-bold text-red-700 mb-2">天干:{pillar.tiangan}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.tianganWuxing]}`}>
|
||||||
|
五行:{pillar.tianganWuxing}
|
||||||
|
</div>
|
||||||
|
<div className={`px-2 py-1 rounded border ${yinyangColors[pillar.tianganYinYang]}`}>
|
||||||
|
阴阳:{pillar.tianganYinYang}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地支信息 */}
|
||||||
|
<div className="bg-gradient-to-r from-yellow-50 to-red-50 rounded-lg p-3">
|
||||||
|
<h4 className="font-bold text-red-700 mb-2">地支:{pillar.dizhi}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.dizhiWuxing]}`}>
|
||||||
|
五行:{pillar.dizhiWuxing}
|
||||||
|
</div>
|
||||||
|
<div className={`px-2 py-1 rounded border ${yinyangColors[pillar.dizhiYinYang]}`}>
|
||||||
|
阴阳:{pillar.dizhiYinYang}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 relative bg-gradient-to-br from-red-50 to-yellow-50 min-h-screen p-4">
|
||||||
|
{/* 页面装饰背景 */}
|
||||||
|
<div className="absolute top-0 left-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-20 right-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain rotate-180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* 八字概览 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
八字命理综合分析
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-3xl font-bold text-red-800 chinese-text-shadow mb-4">
|
||||||
|
{baziDetailsData?.summary?.fullBazi || '八字排盘'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-red-600 text-lg mb-4">
|
||||||
|
出生日期:{birthDate.date} {birthDate.time}
|
||||||
|
</p>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{baziDetailsData?.interpretation?.overall || '根据您的八字,显示出独特的命理特征。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 日主信息 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<User className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
日主信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-6xl font-bold text-red-800 chinese-text-shadow mb-4">
|
||||||
|
{baziDetailsData?.rizhu?.tiangan || '未知'}
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div className={`px-4 py-2 rounded-lg border-2 ${wuxingColors[baziDetailsData?.rizhu?.wuxing || '土']}`}>
|
||||||
|
<span className="font-bold">五行:{baziDetailsData?.rizhu?.wuxing || '未知'}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`px-4 py-2 rounded-lg border-2 ${yinyangColors[baziDetailsData?.rizhu?.yinyang || '阳']}`}>
|
||||||
|
<span className="font-bold">阴阳:{baziDetailsData?.rizhu?.yinyang || '未知'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-2 rounded-lg border-2 bg-indigo-50 border-indigo-300 text-indigo-700">
|
||||||
|
<span className="font-bold">日主</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{baziDetailsData?.rizhu?.description || '日主特征体现了您的核心性格。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 四柱详细信息 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
四柱详细信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||||
|
{baziDetailsData?.summary?.pillars?.map((pillar: any, index: number) =>
|
||||||
|
renderPillarCard(pillar, index)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-blue-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">年柱解读</h4>
|
||||||
|
<p className="text-red-700">{baziDetailsData?.interpretation?.yearPillar || '年柱代表祖辈与早年运势。'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-green-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">月柱解读</h4>
|
||||||
|
<p className="text-red-700">{baziDetailsData?.interpretation?.monthPillar || '月柱代表父母与青年运势。'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-yellow-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">日柱解读</h4>
|
||||||
|
<p className="text-red-700">{baziDetailsData?.interpretation?.dayPillar || '日柱代表自身与配偶。'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-purple-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">时柱解读</h4>
|
||||||
|
<p className="text-red-700">{baziDetailsData?.interpretation?.hourPillar || '时柱代表子女与晚年运势。'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 五行能量分布 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
五行能量分布
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{renderElementCards()}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 五行平衡雷达图 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
五行平衡雷达图
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
{renderRadarChart()}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 五行平衡分析 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Zap className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
五行平衡分析
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-yellow-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">五行平衡状况</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{wuxingAnalysisData?.balanceAnalysis || '您的五行分布显示了独特的能量特征。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-green-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">调和建议</h4>
|
||||||
|
<div className="text-red-700 leading-relaxed">
|
||||||
|
{wuxingAnalysisData?.suggestions?.map((suggestion: string, index: number) => (
|
||||||
|
<p key={index} className="mb-2">• {suggestion}</p>
|
||||||
|
)) || <p>建议通过特定的方式来平衡五行能量。</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-blue-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">五行特征总结</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
您的主导元素是 <span className="font-bold">{wuxingAnalysisData?.dominantElement}</span>,
|
||||||
|
最弱元素是 <span className="font-bold">{wuxingAnalysisData?.weakestElement}</span>。
|
||||||
|
五行平衡状态:{wuxingAnalysisData?.isBalanced ? '较为均衡' : '需要调节'}。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 格局分析 */}
|
||||||
|
{fullBaziAnalysisData && (
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Star className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
格局分析
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-indigo-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">格局类型:{fullBaziAnalysisData.geju_analysis?.pattern_type}</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{fullBaziAnalysisData.geju_analysis?.characteristics}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-green-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">适宜发展路径</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{fullBaziAnalysisData.geju_analysis?.career_path}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-purple-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">人生含义</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{fullBaziAnalysisData.geju_analysis?.life_meaning}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 大运流年分析 */}
|
||||||
|
{fullBaziAnalysisData && (
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<TrendingUp className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
大运流年分析
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-red-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">当前运势</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{fullBaziAnalysisData.dayun_analysis?.current_period}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-blue-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">人生阶段</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{fullBaziAnalysisData.dayun_analysis?.life_periods}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-orange-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">未来展望</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{fullBaziAnalysisData.dayun_analysis?.future_outlook}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 专业人生指导 */}
|
||||||
|
{fullBaziAnalysisData && (
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<BookOpen className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
专业人生指导
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-blue-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">事业发展</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed text-sm">
|
||||||
|
{fullBaziAnalysisData.life_guidance?.career_development}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-pink-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">感情婚姻</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed text-sm">
|
||||||
|
{fullBaziAnalysisData.life_guidance?.marriage_relationships}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-green-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">健康养生</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed text-sm">
|
||||||
|
{fullBaziAnalysisData.life_guidance?.health_wellness}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-yellow-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">财富管理</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed text-sm">
|
||||||
|
{fullBaziAnalysisData.life_guidance?.wealth_guidance}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 人生指导建议 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Sparkles className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
人生指导建议
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-purple-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">性格特征</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{baziDetailsData?.rizhu?.meaning || '您的性格特征体现在日主的特质中。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-blue-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">整体运势</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
根据您的八字排盘分析,建议您在人生的不同阶段关注相应的发展重点。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-orange-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">平衡发展</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
结合五行分析和八字特征,建议您在生活中注重五行的平衡发展,
|
||||||
|
以达到身心健康和事业顺利的最佳状态。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaziAnalysisDisplay;
|
||||||
480
src/components/ComprehensiveBaziAnalysis.tsx
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts';
|
||||||
|
import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||||
|
|
||||||
|
interface ComprehensiveBaziAnalysisProps {
|
||||||
|
analysisResult: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComprehensiveBaziAnalysis: React.FC<ComprehensiveBaziAnalysisProps> = ({ analysisResult }) => {
|
||||||
|
// 安全获取数据的辅助函数
|
||||||
|
const safeGet = (obj: any, path: string, defaultValue: any = '暂无数据') => {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let current = obj;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (current && typeof current === 'object' && key in current) {
|
||||||
|
current = current[key];
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current || defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = analysisResult?.analysis || analysisResult?.data?.analysis || analysisResult;
|
||||||
|
|
||||||
|
// 五行颜色配置
|
||||||
|
const elementColors: { [key: string]: string } = {
|
||||||
|
'木': '#22c55e', // 绿色
|
||||||
|
'火': '#ef4444', // 红色
|
||||||
|
'土': '#eab308', // 黄色
|
||||||
|
'金': '#64748b', // 银色
|
||||||
|
'水': '#3b82f6' // 蓝色
|
||||||
|
};
|
||||||
|
|
||||||
|
// 五行符号配置
|
||||||
|
const elementSymbols: { [key: string]: string } = {
|
||||||
|
'木': '🌲',
|
||||||
|
'火': '🔥',
|
||||||
|
'土': '⛰️',
|
||||||
|
'金': '⚡',
|
||||||
|
'水': '💧'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 五行颜色样式配置
|
||||||
|
const wuxingColors: { [key: string]: string } = {
|
||||||
|
'木': 'text-green-600 bg-green-50 border-green-300',
|
||||||
|
'火': 'text-red-600 bg-red-50 border-red-300',
|
||||||
|
'土': 'text-yellow-600 bg-yellow-50 border-yellow-300',
|
||||||
|
'金': 'text-gray-600 bg-gray-50 border-gray-300',
|
||||||
|
'水': 'text-blue-600 bg-blue-50 border-blue-300'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 阴阳颜色配置
|
||||||
|
const yinyangColors: { [key: string]: string } = {
|
||||||
|
'阳': 'text-orange-600 bg-orange-50 border-orange-300',
|
||||||
|
'阴': 'text-purple-600 bg-purple-50 border-purple-300'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成五行雷达图数据
|
||||||
|
const generateRadarData = () => {
|
||||||
|
const elementDistribution = safeGet(data, 'wuxing_analysis.element_distribution', { '木': 1, '火': 1, '土': 2, '金': 2, '水': 2 }) as Record<string, number>;
|
||||||
|
const total = Object.values(elementDistribution).reduce((a, b) => a + (Number(b) || 0), 0);
|
||||||
|
|
||||||
|
return Object.entries(elementDistribution).map(([element, count]) => ({
|
||||||
|
element,
|
||||||
|
value: total > 0 ? Math.round(((Number(count) || 0) / total) * 100) : 20,
|
||||||
|
fullMark: 100
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成五行统计卡片数据
|
||||||
|
const generateElementCards = () => {
|
||||||
|
const elementDistribution = safeGet(data, 'wuxing_analysis.element_distribution', { '木': 1, '火': 1, '土': 2, '金': 2, '水': 2 }) as Record<string, number>;
|
||||||
|
const total = Object.values(elementDistribution).reduce((a, b) => a + (Number(b) || 0), 0);
|
||||||
|
|
||||||
|
return Object.entries(elementDistribution).map(([element, count]) => {
|
||||||
|
const percentage = total > 0 ? Math.round(((Number(count) || 0) / total) * 100) : 20;
|
||||||
|
let strength = '中';
|
||||||
|
if (percentage >= 30) strength = '旺';
|
||||||
|
else if (percentage <= 10) strength = '弱';
|
||||||
|
|
||||||
|
return {
|
||||||
|
element,
|
||||||
|
count: Number(count) || 0,
|
||||||
|
percentage,
|
||||||
|
strength
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成四柱信息
|
||||||
|
const generatePillarInfo = () => {
|
||||||
|
const baziChart = safeGet(data, 'basic_info.bazi_chart', {});
|
||||||
|
return {
|
||||||
|
year: {
|
||||||
|
tiangan: safeGet(baziChart, 'year_pillar.stem', '甲'),
|
||||||
|
dizhi: safeGet(baziChart, 'year_pillar.branch', '子'),
|
||||||
|
tianganWuxing: getElementFromStem(safeGet(baziChart, 'year_pillar.stem', '甲')),
|
||||||
|
dizhiWuxing: getBranchElement(safeGet(baziChart, 'year_pillar.branch', '子')),
|
||||||
|
tianganYinYang: getYinYangFromStem(safeGet(baziChart, 'year_pillar.stem', '甲')),
|
||||||
|
dizhiYinYang: getYinYangFromBranch(safeGet(baziChart, 'year_pillar.branch', '子')),
|
||||||
|
combination: safeGet(baziChart, 'year_pillar.stem', '甲') + safeGet(baziChart, 'year_pillar.branch', '子')
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
tiangan: safeGet(baziChart, 'month_pillar.stem', '乙'),
|
||||||
|
dizhi: safeGet(baziChart, 'month_pillar.branch', '丑'),
|
||||||
|
tianganWuxing: getElementFromStem(safeGet(baziChart, 'month_pillar.stem', '乙')),
|
||||||
|
dizhiWuxing: getBranchElement(safeGet(baziChart, 'month_pillar.branch', '丑')),
|
||||||
|
tianganYinYang: getYinYangFromStem(safeGet(baziChart, 'month_pillar.stem', '乙')),
|
||||||
|
dizhiYinYang: getYinYangFromBranch(safeGet(baziChart, 'month_pillar.branch', '丑')),
|
||||||
|
combination: safeGet(baziChart, 'month_pillar.stem', '乙') + safeGet(baziChart, 'month_pillar.branch', '丑')
|
||||||
|
},
|
||||||
|
day: {
|
||||||
|
tiangan: safeGet(baziChart, 'day_pillar.stem', '丙'),
|
||||||
|
dizhi: safeGet(baziChart, 'day_pillar.branch', '寅'),
|
||||||
|
tianganWuxing: getElementFromStem(safeGet(baziChart, 'day_pillar.stem', '丙')),
|
||||||
|
dizhiWuxing: getBranchElement(safeGet(baziChart, 'day_pillar.branch', '寅')),
|
||||||
|
tianganYinYang: getYinYangFromStem(safeGet(baziChart, 'day_pillar.stem', '丙')),
|
||||||
|
dizhiYinYang: getYinYangFromBranch(safeGet(baziChart, 'day_pillar.branch', '寅')),
|
||||||
|
combination: safeGet(baziChart, 'day_pillar.stem', '丙') + safeGet(baziChart, 'day_pillar.branch', '寅')
|
||||||
|
},
|
||||||
|
hour: {
|
||||||
|
tiangan: safeGet(baziChart, 'hour_pillar.stem', '丁'),
|
||||||
|
dizhi: safeGet(baziChart, 'hour_pillar.branch', '卯'),
|
||||||
|
tianganWuxing: getElementFromStem(safeGet(baziChart, 'hour_pillar.stem', '丁')),
|
||||||
|
dizhiWuxing: getBranchElement(safeGet(baziChart, 'hour_pillar.branch', '卯')),
|
||||||
|
tianganYinYang: getYinYangFromStem(safeGet(baziChart, 'hour_pillar.stem', '丁')),
|
||||||
|
dizhiYinYang: getYinYangFromBranch(safeGet(baziChart, 'hour_pillar.branch', '卯')),
|
||||||
|
combination: safeGet(baziChart, 'hour_pillar.stem', '丁') + safeGet(baziChart, 'hour_pillar.branch', '卯')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:获取天干五行
|
||||||
|
const getElementFromStem = (stem: string): string => {
|
||||||
|
const stemElements: { [key: string]: string } = {
|
||||||
|
'甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土',
|
||||||
|
'己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水'
|
||||||
|
};
|
||||||
|
return stemElements[stem] || '土';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:获取地支五行
|
||||||
|
const getBranchElement = (branch: string): string => {
|
||||||
|
const branchElements: { [key: string]: string } = {
|
||||||
|
'子': '水', '丑': '土', '寅': '木', '卯': '木', '辰': '土', '巳': '火',
|
||||||
|
'午': '火', '未': '土', '申': '金', '酉': '金', '戌': '土', '亥': '水'
|
||||||
|
};
|
||||||
|
return branchElements[branch] || '土';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:获取天干阴阳
|
||||||
|
const getYinYangFromStem = (stem: string): string => {
|
||||||
|
const yangStems = ['甲', '丙', '戊', '庚', '壬'];
|
||||||
|
return yangStems.includes(stem) ? '阳' : '阴';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:获取地支阴阳
|
||||||
|
const getYinYangFromBranch = (branch: string): string => {
|
||||||
|
const yangBranches = ['子', '寅', '辰', '午', '申', '戌'];
|
||||||
|
return yangBranches.includes(branch) ? '阳' : '阴';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染雷达图
|
||||||
|
const renderRadarChart = () => {
|
||||||
|
const radarData = generateRadarData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<RadarChart data={radarData}>
|
||||||
|
<PolarGrid stroke="#dc2626" />
|
||||||
|
<PolarAngleAxis
|
||||||
|
dataKey="element"
|
||||||
|
tick={{ fill: '#dc2626', fontSize: 14, fontWeight: 'bold' }}
|
||||||
|
/>
|
||||||
|
<PolarRadiusAxis
|
||||||
|
angle={90}
|
||||||
|
domain={[0, 100]}
|
||||||
|
tick={{ fill: '#b91c1c', fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<Radar
|
||||||
|
name="五行强度"
|
||||||
|
dataKey="value"
|
||||||
|
stroke="#dc2626"
|
||||||
|
fill="rgba(220, 38, 38, 0.3)"
|
||||||
|
fillOpacity={0.6}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</RadarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染五行统计卡片
|
||||||
|
const renderElementCards = () => {
|
||||||
|
const elementData = generateElementCards();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-5 gap-4">
|
||||||
|
{elementData.map((item) => (
|
||||||
|
<Card key={item.element} className="text-center hover:shadow-xl transition-all duration-300 chinese-card-decoration border-2 border-yellow-400">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="text-3xl mb-2">{elementSymbols[item.element]}</div>
|
||||||
|
<h3 className="font-bold text-red-800 text-lg mb-2 chinese-text-shadow">{item.element}</h3>
|
||||||
|
<div className="text-2xl font-bold text-yellow-600 mb-1">{item.percentage}%</div>
|
||||||
|
<div className={`text-sm font-medium mb-2 ${
|
||||||
|
item.strength === '旺' ? 'text-green-600' :
|
||||||
|
item.strength === '中' ? 'text-yellow-600' : 'text-orange-600'
|
||||||
|
}`}>
|
||||||
|
{item.strength}
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-3 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-1000"
|
||||||
|
style={{
|
||||||
|
width: `${item.percentage}%`,
|
||||||
|
backgroundColor: elementColors[item.element]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染四柱信息卡片
|
||||||
|
const renderPillarCard = (pillar: any, index: number) => {
|
||||||
|
const pillarNames = ['年柱', '月柱', '日柱', '时柱'];
|
||||||
|
const pillarDescriptions = [
|
||||||
|
'代表祖辈与早年运势',
|
||||||
|
'代表父母与青年运势',
|
||||||
|
'代表自身与配偶',
|
||||||
|
'代表子女与晚年运势'
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={index} className="chinese-card-decoration hover:shadow-xl transition-all duration-300 border-2 border-yellow-400">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle className="text-red-800 text-xl font-bold chinese-text-shadow">
|
||||||
|
{pillarNames[index]}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-red-600 text-sm">{pillarDescriptions[index]}</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* 天干地支大显示 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-4xl font-bold text-red-800 chinese-text-shadow mb-2">
|
||||||
|
{pillar.combination}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{pillar.tiangan} ({pillar.tianganYinYang}) + {pillar.dizhi} ({pillar.dizhiYinYang})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 天干信息 */}
|
||||||
|
<div className="bg-gradient-to-r from-red-50 to-yellow-50 rounded-lg p-3">
|
||||||
|
<h4 className="font-bold text-red-700 mb-2">天干:{pillar.tiangan}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.tianganWuxing]}`}>
|
||||||
|
五行:{pillar.tianganWuxing}
|
||||||
|
</div>
|
||||||
|
<div className={`px-2 py-1 rounded border ${yinyangColors[pillar.tianganYinYang]}`}>
|
||||||
|
阴阳:{pillar.tianganYinYang}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地支信息 */}
|
||||||
|
<div className="bg-gradient-to-r from-yellow-50 to-red-50 rounded-lg p-3">
|
||||||
|
<h4 className="font-bold text-red-700 mb-2">地支:{pillar.dizhi}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.dizhiWuxing]}`}>
|
||||||
|
五行:{pillar.dizhiWuxing}
|
||||||
|
</div>
|
||||||
|
<div className={`px-2 py-1 rounded border ${yinyangColors[pillar.dizhiYinYang]}`}>
|
||||||
|
阴阳:{pillar.dizhiYinYang}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const personalData = safeGet(data, 'basic_info.personal_data', {});
|
||||||
|
const baziChart = safeGet(data, 'basic_info.bazi_chart', {});
|
||||||
|
const pillarInfo = generatePillarInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 relative">
|
||||||
|
{/* 页面装饰背景 */}
|
||||||
|
<div className="absolute top-0 left-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-20 right-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain rotate-180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 八字概览 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
八字概览
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-3xl font-bold text-red-800 chinese-text-shadow mb-4">
|
||||||
|
{safeGet(baziChart, 'complete_chart', '甲子 乙丑 丙寅 丁卯')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-red-600 text-lg mb-4">
|
||||||
|
{personalData.name ? `${personalData.name} ` : ''}出生日期:{personalData.birth_date || '未知'} {personalData.birth_time || '未知'}
|
||||||
|
</p>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'life_guidance.overall_summary', '根据您的八字,显示出独特的命理特征...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 日主信息 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<User className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
日主信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-6xl font-bold text-red-800 chinese-text-shadow mb-4">
|
||||||
|
{safeGet(baziChart, 'day_master', '丙')}
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div className={`px-4 py-2 rounded-lg border-2 ${wuxingColors[getElementFromStem(safeGet(baziChart, 'day_master', '丙'))]}`}>
|
||||||
|
<span className="font-bold">五行:{getElementFromStem(safeGet(baziChart, 'day_master', '丙'))}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`px-4 py-2 rounded-lg border-2 ${yinyangColors[getYinYangFromStem(safeGet(baziChart, 'day_master', '丙'))]}`}>
|
||||||
|
<span className="font-bold">阴阳:{getYinYangFromStem(safeGet(baziChart, 'day_master', '丙'))}</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-2 rounded-lg border-2 bg-indigo-50 border-indigo-300 text-indigo-700">
|
||||||
|
<span className="font-bold">格局:{safeGet(data, 'geju_analysis.pattern_type', '正格')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'wuxing_analysis.personal_traits', '您的日主特征体现了独特的性格魅力...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 四柱详细信息 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
四柱详细信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||||
|
{[pillarInfo.year, pillarInfo.month, pillarInfo.day, pillarInfo.hour].map((pillar, index) =>
|
||||||
|
renderPillarCard(pillar, index)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 五行能量分布 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
五行能量分布
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{renderElementCards()}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 五行平衡雷达图 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
五行平衡雷达图
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
{renderRadarChart()}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 五行平衡分析 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Zap className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
五行平衡分析
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-yellow-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">五行平衡状况</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'wuxing_analysis.balance_analysis', '您的五行分布显示了独特的能量特征...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-green-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">调和建议</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'wuxing_analysis.suggestions', '建议通过特定的方式来平衡五行能量...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 格局分析与建议 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Sparkles className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
格局分析与人生指导
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-purple-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">格局特征</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'geju_analysis.characteristics', '您的八字格局显示了独特的命理特征...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-blue-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">事业发展</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'life_guidance.career_development', '在事业发展方面,您适合...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-pink-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">感情婚姻</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'life_guidance.marriage_relationships', '在感情方面,您的特点是...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg border-l-4 border-orange-500">
|
||||||
|
<h4 className="font-bold text-red-800 mb-2">健康养生</h4>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{safeGet(data, 'life_guidance.health_wellness', '健康方面需要注意...')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComprehensiveBaziAnalysis;
|
||||||
35
src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const searilizeError = (error: any) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message + '\n' + error.stack;
|
||||||
|
}
|
||||||
|
return JSON.stringify(error, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<
|
||||||
|
{ children: React.ReactNode },
|
||||||
|
{ hasError: boolean; error: any }
|
||||||
|
> {
|
||||||
|
constructor(props: { children: React.ReactNode }) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: any) {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-red-500 rounded">
|
||||||
|
<h2 className="text-red-500">Something went wrong.</h2>
|
||||||
|
<pre className="mt-2 text-sm">{searilizeError(this.state.error)}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/components/Layout.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { Sparkles, User, History, LogOut, Home, Stars } from 'lucide-react';
|
||||||
|
import { Button } from './ui/Button';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||||
|
const { user, signOut } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
try {
|
||||||
|
await signOut();
|
||||||
|
toast.success('登出成功');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('登出失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigationItems = [
|
||||||
|
{ path: '/', label: '首页', icon: Home },
|
||||||
|
{ path: '/analysis', label: '命理分析', icon: Sparkles, requireAuth: true },
|
||||||
|
{ path: '/history', label: '历史记录', icon: History, requireAuth: true },
|
||||||
|
{ path: '/profile', label: '个人档案', icon: User, requireAuth: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen relative">
|
||||||
|
{/* 导航栏 */}
|
||||||
|
<nav className="chinese-traditional-bg shadow-2xl border-b-4 border-yellow-400 relative overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link to="/" className="flex items-center space-x-3 group">
|
||||||
|
{/* 品牌图标 */}
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-yellow-400 to-amber-600 rounded-full flex items-center justify-center shadow-lg border-2 border-red-300 group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<img
|
||||||
|
src="/traditional-chinese-bagua-eight-trigrams-black-gold.jpg"
|
||||||
|
alt="神机阁"
|
||||||
|
className="w-6 h-6 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-bold text-yellow-200 font-serif chinese-text-shadow group-hover:text-yellow-100 transition-colors duration-300">神机阁</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-6">
|
||||||
|
{navigationItems.map((item) => {
|
||||||
|
if (item.requireAuth && !user) return null;
|
||||||
|
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = location.pathname === item.path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-all duration-300 border-2 font-serif ${
|
||||||
|
isActive
|
||||||
|
? 'text-red-800 chinese-golden-glow border-red-600 shadow-lg transform scale-105'
|
||||||
|
: 'text-yellow-200 hover:text-red-800 hover:chinese-golden-glow border-transparent hover:border-red-600 hover:shadow-lg hover:scale-105'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{user ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleSignOut}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center space-x-2 chinese-golden-glow text-red-800 border-2 border-red-600 hover:shadow-xl transition-all duration-300 font-serif"
|
||||||
|
>
|
||||||
|
<LogOut className="h-5 w-5" />
|
||||||
|
<span>登出</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Link to="/login">
|
||||||
|
<Button variant="outline" className="chinese-golden-glow text-red-800 border-2 border-red-600 hover:shadow-xl transition-all duration-300 font-serif">
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/register">
|
||||||
|
<Button className="chinese-red-glow text-white border-2 border-yellow-400 hover:shadow-xl transition-all duration-300 font-serif">
|
||||||
|
注册
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* 主内容区域 */}
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 relative">
|
||||||
|
{/* 主内容区装饰元素 */}
|
||||||
|
<div className="absolute top-0 left-0 w-24 h-24 opacity-10 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 right-0 w-24 h-24 opacity-10 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain rotate-180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* 页脚装饰 */}
|
||||||
|
<footer className="mt-16 py-8 border-t-2 border-yellow-400 mystical-gradient">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 mx-auto mb-4 bg-gradient-to-br from-yellow-400 to-amber-600 rounded-full flex items-center justify-center shadow-lg border-2 border-red-600">
|
||||||
|
<img
|
||||||
|
src="/traditional_chinese_gold_red_dragon_symbol.jpg"
|
||||||
|
alt="龙符"
|
||||||
|
className="w-8 h-8 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-700 font-medium font-serif">神机阁 - 传统智慧与现代科技的完美融合</p>
|
||||||
|
<p className="text-red-600 text-sm mt-2">© 2025 AI命理分析平台 - Created by MiniMax Agent</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
27
src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||||
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-purple-600"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
36
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { ButtonHTMLAttributes } from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: 'default' | 'outline' | 'secondary' | 'destructive';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'md',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none';
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
default: 'bg-purple-600 text-white hover:bg-purple-700',
|
||||||
|
outline: 'border border-purple-300 text-purple-600 hover:bg-purple-50',
|
||||||
|
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
|
||||||
|
destructive: 'bg-red-600 text-white hover:bg-red-700',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: 'h-8 px-3 text-sm',
|
||||||
|
md: 'h-10 px-4',
|
||||||
|
lg: 'h-12 px-6 text-lg',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn(baseStyles, variants[variant], sizes[size], className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Card: React.FC<CardProps> = ({ children, className }) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('bg-white rounded-lg shadow-lg p-6', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CardHeaderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardHeader: React.FC<CardHeaderProps> = ({ children, className }) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('mb-4', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CardTitleProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardTitle: React.FC<CardTitleProps> = ({ children, className }) => {
|
||||||
|
return (
|
||||||
|
<h3 className={cn('text-xl font-semibold text-gray-900', className)}>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CardContentProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardContent: React.FC<CardContentProps> = ({ children, className }) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('text-gray-700', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
35
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { InputHTMLAttributes } from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Input: React.FC<InputProps> = ({
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{label && (
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
className={cn(
|
||||||
|
'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500',
|
||||||
|
error && 'border-red-300 focus:border-red-500 focus:ring-red-500',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-600">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
src/components/ui/Select.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { SelectHTMLAttributes } from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select: React.FC<SelectProps> = ({
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
options,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{label && (
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
className={cn(
|
||||||
|
'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 bg-white',
|
||||||
|
error && 'border-red-300 focus:border-red-500 focus:ring-red-500',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-600">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
76
src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||||
|
import { User } from '@supabase/supabase-js';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User | null;
|
||||||
|
loading: boolean;
|
||||||
|
signIn: (email: string, password: string) => Promise<any>;
|
||||||
|
signUp: (email: string, password: string) => Promise<any>;
|
||||||
|
signOut: () => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface AuthProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: AuthProviderProps) {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Load user on mount (one-time check)
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadUser() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
setUser(user);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadUser();
|
||||||
|
|
||||||
|
// Set up auth listener - KEEP SIMPLE, avoid any async operations in callback
|
||||||
|
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||||
|
(_event, session) => {
|
||||||
|
// NEVER use any async operations in callback
|
||||||
|
setUser(session?.user || null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auth methods
|
||||||
|
async function signIn(email: string, password: string) {
|
||||||
|
return await supabase.auth.signInWithPassword({ email, password });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signUp(email: string, password: string) {
|
||||||
|
return await supabase.auth.signUp({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signOut() {
|
||||||
|
return await supabase.auth.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, loading, signIn, signUp, signOut }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
19
src/hooks/use-mobile.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||||
|
const onChange = () => {
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||||
|
}
|
||||||
|
mql.addEventListener("change", onChange)
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||||
|
return () => mql.removeEventListener("change", onChange)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return !!isMobile
|
||||||
|
}
|
||||||
211
src/index.css
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;500;600;700;900&display=swap');
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--sidebar-background: 0 0% 98%;
|
||||||
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
|
--sidebar-border: 220 13% 91%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
/* 传统中式颜色 */
|
||||||
|
--chinese-red: #dc2626;
|
||||||
|
--chinese-gold: #facc15;
|
||||||
|
--chinese-dark-red: #991b1b;
|
||||||
|
--chinese-deep-gold: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--sidebar-background: 240 5.9% 10%;
|
||||||
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Serif SC', serif;
|
||||||
|
background: linear-gradient(135deg, #fef7cd 0%, #fed7aa 25%, #fecaca 50%, #fed7aa 75%, #fef7cd 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('/chinese_golden_red_auspicious_cloud_pattern_background.jpg');
|
||||||
|
background-size: 400px 400px;
|
||||||
|
background-repeat: repeat;
|
||||||
|
opacity: 0.08;
|
||||||
|
z-index: -2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(circle at 20% 20%, rgba(220, 38, 38, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(250, 204, 21, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(220, 38, 38, 0.05) 0%, transparent 70%);
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 传统中式装饰类 */
|
||||||
|
.chinese-traditional-bg {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #991b1b 50%, #dc2626 100%);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-traditional-bg::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-image: url('/red_gold_chinese_auspicious_cloud_pattern_background.jpg');
|
||||||
|
background-size: 300px 300px;
|
||||||
|
background-repeat: repeat;
|
||||||
|
opacity: 0.3;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-traditional-bg > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-golden-frame {
|
||||||
|
position: relative;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
background: linear-gradient(#fef7cd, #fef7cd) padding-box,
|
||||||
|
linear-gradient(45deg, #facc15, #d97706, #facc15) border-box;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-golden-frame::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: -8px;
|
||||||
|
right: -8px;
|
||||||
|
bottom: -8px;
|
||||||
|
background: linear-gradient(45deg, #facc15, #d97706, #facc15);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-card-decoration {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(254, 247, 205, 0.95);
|
||||||
|
border: 2px solid #facc15;
|
||||||
|
box-shadow: 0 8px 32px rgba(220, 38, 38, 0.15),
|
||||||
|
inset 0 1px 0 rgba(250, 204, 21, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-card-decoration::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
border: 1px solid rgba(250, 204, 21, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-text-shadow {
|
||||||
|
text-shadow: 2px 2px 4px rgba(220, 38, 38, 0.3),
|
||||||
|
0 0 8px rgba(250, 204, 21, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-golden-glow {
|
||||||
|
background: linear-gradient(135deg, #facc15 0%, #d97706 50%, #facc15 100%);
|
||||||
|
box-shadow: 0 4px 20px rgba(250, 204, 21, 0.4),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-red-glow {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #991b1b 50%, #dc2626 100%);
|
||||||
|
box-shadow: 0 4px 20px rgba(220, 38, 38, 0.4),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.traditional-border {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: linear-gradient(white, white) padding-box,
|
||||||
|
linear-gradient(45deg, #facc15, #dc2626, #facc15, #dc2626) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragon-corner {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragon-corner::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -10px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-image: url('/traditional_chinese_gold_red_dragon_symbol.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.7;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragon-corner::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
right: -10px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-image: url('/traditional_chinese_gold_red_dragon_symbol.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.7;
|
||||||
|
z-index: 1;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-gradient {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(220, 38, 38, 0.1) 0%,
|
||||||
|
rgba(250, 204, 21, 0.1) 25%,
|
||||||
|
rgba(220, 38, 38, 0.1) 50%,
|
||||||
|
rgba(250, 204, 21, 0.1) 75%,
|
||||||
|
rgba(220, 38, 38, 0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-position: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
10
src/lib/supabase.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||||
|
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
|
throw new Error('Missing Supabase environment variables')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||||
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
13
src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary.tsx'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
299
src/pages/AnalysisPage.tsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Input } from '../components/ui/Input';
|
||||||
|
import { Select } from '../components/ui/Select';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import AnalysisResultDisplay from '../components/AnalysisResultDisplay';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Sparkles, Star, Compass, Calendar, MapPin, User, Loader2 } from 'lucide-react';
|
||||||
|
import { UserProfile, AnalysisRequest, NumerologyReading } from '../types';
|
||||||
|
|
||||||
|
type AnalysisType = 'bazi' | 'ziwei' | 'yijing';
|
||||||
|
|
||||||
|
const AnalysisPage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
|
const [analysisType, setAnalysisType] = useState<AnalysisType>('bazi');
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
birth_date: '',
|
||||||
|
birth_time: '',
|
||||||
|
gender: 'male' as 'male' | 'female',
|
||||||
|
birth_place: '',
|
||||||
|
question: ''
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [analysisResult, setAnalysisResult] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProfile();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const loadProfile = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('user_profiles')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setProfile(data);
|
||||||
|
setFormData({
|
||||||
|
name: data.full_name || '',
|
||||||
|
birth_date: data.birth_date || '',
|
||||||
|
birth_time: data.birth_time || '',
|
||||||
|
gender: data.gender || 'male',
|
||||||
|
birth_place: data.birth_location || '',
|
||||||
|
question: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载档案失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAnalysis = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
if (!formData.name || !formData.birth_date) {
|
||||||
|
toast.error('请填写姓名和出生日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setAnalysisResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 对于八字分析,直接显示结果,不需要调用 Edge Function
|
||||||
|
if (analysisType === 'bazi') {
|
||||||
|
const birthData = {
|
||||||
|
date: formData.birth_date,
|
||||||
|
time: formData.birth_time || '12:00'
|
||||||
|
};
|
||||||
|
setAnalysisResult({ type: 'bazi', birthDate: birthData });
|
||||||
|
toast.success('分析完成!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于其他分析类型,保持原有逻辑
|
||||||
|
const analysisRequest: AnalysisRequest = {
|
||||||
|
user_id: user.id,
|
||||||
|
reading_type: analysisType,
|
||||||
|
birth_data: {
|
||||||
|
name: formData.name,
|
||||||
|
birth_date: formData.birth_date,
|
||||||
|
birth_time: formData.birth_time,
|
||||||
|
gender: formData.gender,
|
||||||
|
birth_place: formData.birth_place,
|
||||||
|
...(analysisType === 'yijing' && { question: formData.question })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const functionName = `${analysisType}-analyzer?_t=${new Date().getTime()}`;
|
||||||
|
const { data, error } = await supabase.functions.invoke(functionName, {
|
||||||
|
body: analysisRequest
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.error) {
|
||||||
|
throw new Error(data.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnalysisResult(data.data);
|
||||||
|
toast.success('分析完成!');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('分析失败:', error);
|
||||||
|
toast.error('分析失败:' + (error.message || '未知错误'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const analysisTypes = [
|
||||||
|
{
|
||||||
|
type: 'bazi' as AnalysisType,
|
||||||
|
title: '八字命理',
|
||||||
|
description: '基于传统八字学说,分析五行平衡、格局特点、四柱信息',
|
||||||
|
icon: Sparkles,
|
||||||
|
color: 'text-purple-600',
|
||||||
|
bgColor: 'bg-purple-50',
|
||||||
|
borderColor: 'border-purple-200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ziwei' as AnalysisType,
|
||||||
|
title: '紫微斗数',
|
||||||
|
description: '通过星曜排布和十二宫位分析性格命运',
|
||||||
|
icon: Star,
|
||||||
|
color: 'text-blue-600',
|
||||||
|
bgColor: 'bg-blue-50',
|
||||||
|
borderColor: 'border-blue-200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'yijing' as AnalysisType,
|
||||||
|
title: '易经占卜',
|
||||||
|
description: '运用梅花易数起卦法,解读卦象含义,指导人生决策',
|
||||||
|
icon: Compass,
|
||||||
|
color: 'text-amber-600',
|
||||||
|
bgColor: 'bg-amber-50',
|
||||||
|
borderColor: 'border-amber-200'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* 分析类型选择 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>选择分析类型</CardTitle>
|
||||||
|
<p className="text-gray-600">选择您感兴趣的命理分析方式</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
|
{analysisTypes.map((type) => {
|
||||||
|
const Icon = type.icon;
|
||||||
|
const isSelected = analysisType === type.type;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={type.type}
|
||||||
|
onClick={() => setAnalysisType(type.type)}
|
||||||
|
className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
|
||||||
|
isSelected
|
||||||
|
? `${type.borderColor} ${type.bgColor}`
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
|
<Icon className={`h-6 w-6 ${isSelected ? type.color : 'text-gray-400'}`} />
|
||||||
|
<h3 className={`font-medium ${isSelected ? type.color : 'text-gray-700'}`}>
|
||||||
|
{type.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600">{type.description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 分析表单 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>填写分析信息</CardTitle>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{profile ? '已从您的档案中自动填充,您可以修改' : '请填写以下信息进行分析'}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
label="姓名 *"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
required
|
||||||
|
placeholder="请输入真实姓名"
|
||||||
|
/>
|
||||||
|
<User className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="性别 *"
|
||||||
|
value={formData.gender}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, gender: e.target.value as 'male' | 'female' }))}
|
||||||
|
options={[
|
||||||
|
{ value: 'male', label: '男性' },
|
||||||
|
{ value: 'female', label: '女性' }
|
||||||
|
]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
label="出生日期 *"
|
||||||
|
value={formData.birth_date}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, birth_date: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Calendar className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
label="出生时间"
|
||||||
|
value={formData.birth_time}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, birth_time: e.target.value }))}
|
||||||
|
placeholder="选填,但强烈建议填写"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{analysisType === 'yijing' && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<Input
|
||||||
|
label="占卜问题"
|
||||||
|
value={formData.question}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, question: e.target.value }))}
|
||||||
|
placeholder="请输入您希望占卜的具体问题(可选)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{analysisType !== 'ziwei' && analysisType !== 'yijing' && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
label="出生地点"
|
||||||
|
value={formData.birth_place}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, birth_place: e.target.value }))}
|
||||||
|
placeholder="如:北京市朝阳区(选填)"
|
||||||
|
/>
|
||||||
|
<MapPin className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleAnalysis}
|
||||||
|
disabled={loading || !formData.name || !formData.birth_date}
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
分析中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="mr-2 h-4 w-4" />
|
||||||
|
开始{analysisTypes.find(t => t.type === analysisType)?.title}分析
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 分析结果 */}
|
||||||
|
{analysisResult && (
|
||||||
|
<AnalysisResultDisplay
|
||||||
|
analysisResult={analysisResult.type !== 'bazi' ? analysisResult : undefined}
|
||||||
|
analysisType={analysisType}
|
||||||
|
birthDate={analysisResult.type === 'bazi' ? analysisResult.birthDate : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalysisPage;
|
||||||
418
src/pages/BaziDetailsPage.tsx
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Calendar, Clock, Star, BookOpen, Sparkles, User } from 'lucide-react';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 生辰八字详情数据接口 - 匹配后端返回结构
|
||||||
|
interface PillarInfo {
|
||||||
|
tiangan: string;
|
||||||
|
dizhi: string;
|
||||||
|
tianganWuxing: string;
|
||||||
|
dizhiWuxing: string;
|
||||||
|
tianganYinYang: string;
|
||||||
|
dizhiYinYang: string;
|
||||||
|
combination: string;
|
||||||
|
pillarName: string;
|
||||||
|
shengxiao?: string;
|
||||||
|
tianganMeaning?: string;
|
||||||
|
dizhiMeaning?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaziApiResponse {
|
||||||
|
baziDetails: {
|
||||||
|
year: PillarInfo;
|
||||||
|
month: PillarInfo;
|
||||||
|
day: PillarInfo;
|
||||||
|
hour: PillarInfo;
|
||||||
|
};
|
||||||
|
rizhu: {
|
||||||
|
tiangan: string;
|
||||||
|
wuxing: string;
|
||||||
|
yinyang: string;
|
||||||
|
description: string;
|
||||||
|
meaning?: string;
|
||||||
|
};
|
||||||
|
summary: {
|
||||||
|
fullBazi: string;
|
||||||
|
birthInfo: {
|
||||||
|
solarDate: string;
|
||||||
|
birthTime: string;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
hour: number;
|
||||||
|
};
|
||||||
|
pillars: PillarInfo[];
|
||||||
|
};
|
||||||
|
interpretation: {
|
||||||
|
overall: string;
|
||||||
|
yearPillar: string;
|
||||||
|
monthPillar: string;
|
||||||
|
dayPillar: string;
|
||||||
|
hourPillar: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaziDetailsPage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [birthDate, setBirthDate] = useState('');
|
||||||
|
const [birthTime, setBirthTime] = useState('12:00');
|
||||||
|
const [baziData, setBaziData] = useState<BaziApiResponse | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 五行颜色配置
|
||||||
|
const wuxingColors: { [key: string]: string } = {
|
||||||
|
'木': 'text-green-600 bg-green-50 border-green-300',
|
||||||
|
'火': 'text-red-600 bg-red-50 border-red-300',
|
||||||
|
'土': 'text-yellow-600 bg-yellow-50 border-yellow-300',
|
||||||
|
'金': 'text-gray-600 bg-gray-50 border-gray-300',
|
||||||
|
'水': 'text-blue-600 bg-blue-50 border-blue-300'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 阴阳颜色配置
|
||||||
|
const yinyangColors: { [key: string]: string } = {
|
||||||
|
'阳': 'text-orange-600 bg-orange-50 border-orange-300',
|
||||||
|
'阴': 'text-purple-600 bg-purple-50 border-purple-300'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用Supabase Edge Function获取八字详细信息
|
||||||
|
const fetchBaziDetails = async () => {
|
||||||
|
if (!birthDate) {
|
||||||
|
toast.error('请选择您的出生日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用Supabase Edge Function
|
||||||
|
const { data, error } = await supabase.functions.invoke('bazi-details', {
|
||||||
|
body: {
|
||||||
|
birthDate,
|
||||||
|
birthTime
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (data?.data) {
|
||||||
|
setBaziData(data.data);
|
||||||
|
toast.success('八字详情分析完成!');
|
||||||
|
} else {
|
||||||
|
throw new Error('排盘结果为空');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('八字排盘错误:', err);
|
||||||
|
setError(err.message || '分析失败,请稍后重试');
|
||||||
|
toast.error('分析失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染四柱信息卡片
|
||||||
|
const renderPillarCard = (pillar: PillarInfo | null | undefined, index: number) => {
|
||||||
|
// 防护性检查:确保 pillar 对象存在
|
||||||
|
if (!pillar) {
|
||||||
|
return (
|
||||||
|
<Card key={index} className="chinese-card-decoration hover:shadow-xl transition-all duration-300 border-2 border-yellow-400">
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<p className="text-red-600">柱信息加载中...</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pillarNames = ['年柱', '月柱', '日柱', '时柱'];
|
||||||
|
const pillarDescriptions = [
|
||||||
|
'代表祖辈与早年运势',
|
||||||
|
'代表父母与青年运势',
|
||||||
|
'代表自身与配偶',
|
||||||
|
'代表子女与晚年运势'
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={index} className="chinese-card-decoration hover:shadow-xl transition-all duration-300 border-2 border-yellow-400">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle className="text-red-800 text-xl font-bold chinese-text-shadow">
|
||||||
|
{pillarNames[index] || '未知柱'}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-red-600 text-sm">{pillarDescriptions[index] || '描述加载中'}</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* 天干地支大显示 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-4xl font-bold text-red-800 chinese-text-shadow mb-2">
|
||||||
|
{pillar?.combination || '未知'}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{pillar?.tiangan || '未知'} ({pillar?.tianganYinYang || '未知'}) + {pillar?.dizhi || '未知'} ({pillar?.dizhiYinYang || '未知'})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 天干信息 */}
|
||||||
|
<div className="bg-gradient-to-r from-red-50 to-yellow-50 rounded-lg p-3">
|
||||||
|
<h4 className="font-bold text-red-700 mb-2">天干:{pillar?.tiangan || '未知'}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className={`px-2 py-1 rounded border ${pillar?.tianganWuxing && wuxingColors[pillar.tianganWuxing] ? wuxingColors[pillar.tianganWuxing] : 'bg-gray-50 border-gray-300 text-gray-600'}`}>
|
||||||
|
五行:{pillar?.tianganWuxing || '未知'}
|
||||||
|
</div>
|
||||||
|
<div className={`px-2 py-1 rounded border ${pillar?.tianganYinYang && yinyangColors[pillar.tianganYinYang] ? yinyangColors[pillar.tianganYinYang] : 'bg-gray-50 border-gray-300 text-gray-600'}`}>
|
||||||
|
阴阳:{pillar?.tianganYinYang || '未知'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地支信息 */}
|
||||||
|
<div className="bg-gradient-to-r from-yellow-50 to-red-50 rounded-lg p-3">
|
||||||
|
<h4 className="font-bold text-red-700 mb-2">地支:{pillar?.dizhi || '未知'}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className={`px-2 py-1 rounded border ${pillar?.dizhiWuxing && wuxingColors[pillar.dizhiWuxing] ? wuxingColors[pillar.dizhiWuxing] : 'bg-gray-50 border-gray-300 text-gray-600'}`}>
|
||||||
|
五行:{pillar?.dizhiWuxing || '未知'}
|
||||||
|
</div>
|
||||||
|
<div className={`px-2 py-1 rounded border ${pillar?.dizhiYinYang && yinyangColors[pillar.dizhiYinYang] ? yinyangColors[pillar.dizhiYinYang] : 'bg-gray-50 border-gray-300 text-gray-600'}`}>
|
||||||
|
阴阳:{pillar?.dizhiYinYang || '未知'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 relative">
|
||||||
|
{/* 页面装饰背景 */}
|
||||||
|
<div className="absolute top-0 left-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-20 right-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain rotate-180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题区域 */}
|
||||||
|
<div className="text-center space-y-4 relative z-10">
|
||||||
|
<div className="w-16 h-16 mx-auto bg-gradient-to-br from-yellow-400 to-amber-600 rounded-full flex items-center justify-center shadow-2xl border-3 border-red-600">
|
||||||
|
<BookOpen className="w-8 h-8 text-red-800" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-red-800 chinese-text-shadow font-serif">
|
||||||
|
生辰八字
|
||||||
|
<span className="block text-lg text-yellow-600 mt-2 font-normal">
|
||||||
|
详细展示您的四柱信息与命理特征
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输入区域 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Calendar className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
输入您的出生信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-red-700 mb-2">
|
||||||
|
出生日期 *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={birthDate}
|
||||||
|
onChange={(e) => setBirthDate(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 border-2 border-yellow-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 bg-white text-red-800"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-red-700 mb-2">
|
||||||
|
出生时间
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={birthTime}
|
||||||
|
onChange={(e) => setBirthTime(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 border-2 border-yellow-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 bg-white text-red-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={fetchBaziDetails}
|
||||||
|
disabled={isLoading || !birthDate}
|
||||||
|
size="lg"
|
||||||
|
className="w-full chinese-red-glow text-white hover:shadow-xl transition-all duration-300 border-2 border-yellow-400"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>加载中...</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Star className="mr-2 h-5 w-5" />
|
||||||
|
开始八字详情
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<Card className="border-red-400 bg-red-50">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<p className="text-red-700 text-center">{error}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 加载状态 */}
|
||||||
|
{isLoading && (
|
||||||
|
<Card className="chinese-card-decoration border-2 border-yellow-400">
|
||||||
|
<CardContent className="p-8">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<div className="w-16 h-16 mx-auto border-4 border-red-600 border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
<p className="text-red-700 text-lg font-medium">正在进行八字排盘分析...</p>
|
||||||
|
<p className="text-red-600 text-sm">请稍候,这需要一些时间来计算您的详细八字信息</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 八字详情结果 */}
|
||||||
|
{baziData && !isLoading && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* 八字概览 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
八字概览
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-3xl font-bold text-red-800 chinese-text-shadow mb-4">
|
||||||
|
{baziData.summary?.fullBazi || '未知'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-red-600 text-lg mb-4">
|
||||||
|
出生日期:{baziData.summary?.birthInfo?.solarDate || '未知'} {baziData.summary?.birthInfo?.birthTime || '未知'}
|
||||||
|
</p>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{baziData.interpretation?.overall || '暂无详细分析'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 日主信息 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<User className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
日主信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-6xl font-bold text-red-800 chinese-text-shadow mb-4">
|
||||||
|
{baziData.rizhu?.tiangan || '未知'}
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div className={`px-4 py-2 rounded-lg border-2 ${baziData.rizhu?.wuxing ? wuxingColors[baziData.rizhu.wuxing] || 'bg-gray-50 border-gray-300' : 'bg-gray-50 border-gray-300'}`}>
|
||||||
|
<span className="font-bold">五行:{baziData.rizhu?.wuxing || '未知'}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`px-4 py-2 rounded-lg border-2 ${baziData.rizhu?.yinyang ? yinyangColors[baziData.rizhu.yinyang] || 'bg-gray-50 border-gray-300' : 'bg-gray-50 border-gray-300'}`}>
|
||||||
|
<span className="font-bold">阴阳:{baziData.rizhu?.yinyang || '未知'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-700 leading-relaxed">
|
||||||
|
{baziData.rizhu?.description || '暂无详细描述'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 四柱详细信息 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
四柱详细信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||||
|
{baziData?.baziDetails ? [
|
||||||
|
baziData.baziDetails.year,
|
||||||
|
baziData.baziDetails.month,
|
||||||
|
baziData.baziDetails.day,
|
||||||
|
baziData.baziDetails.hour
|
||||||
|
].map((pillar, index) =>
|
||||||
|
renderPillarCard(pillar, index)
|
||||||
|
) : (
|
||||||
|
<div className="col-span-full text-center text-red-600 py-8">
|
||||||
|
四柱数据加载中...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 命理解释 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Sparkles className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
命理解释
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{baziData.interpretation && Object.entries(baziData.interpretation).map(([key, value], index) => {
|
||||||
|
if (key === 'overall') return null; // 已在概览中显示
|
||||||
|
const titles: { [key: string]: string } = {
|
||||||
|
yearPillar: '年柱解释',
|
||||||
|
monthPillar: '月柱解释',
|
||||||
|
dayPillar: '日柱解释',
|
||||||
|
hourPillar: '时柱解释'
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div key={key} className="flex items-start space-x-3 p-4 bg-white rounded-lg border-l-4 border-yellow-500">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-yellow-400 to-amber-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<span className="text-red-800 font-bold text-sm">{index}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-bold text-red-800 mb-1">{titles[key] || '说明'}</h4>
|
||||||
|
<p className="text-red-700 font-medium">{value}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaziDetailsPage;
|
||||||
258
src/pages/HistoryPage.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import AnalysisResultDisplay from '../components/AnalysisResultDisplay';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { History, Calendar, User, Sparkles, Star, Compass, Eye, Trash2 } from 'lucide-react';
|
||||||
|
import { NumerologyReading } from '../types';
|
||||||
|
|
||||||
|
const HistoryPage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [readings, setReadings] = useState<NumerologyReading[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedReading, setSelectedReading] = useState<NumerologyReading | null>(null);
|
||||||
|
const [viewingResult, setViewingResult] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadHistory();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const loadHistory = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase.functions.invoke('reading-history', {
|
||||||
|
body: {
|
||||||
|
action: 'get_history',
|
||||||
|
user_id: user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.error) {
|
||||||
|
throw new Error(data.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyData = data.data || [];
|
||||||
|
|
||||||
|
// 数据转换适配器:将旧格式转换为新格式
|
||||||
|
const processedData = historyData.map((reading: any) => {
|
||||||
|
// 如果有 analysis 字段,直接使用
|
||||||
|
if (reading.analysis) {
|
||||||
|
return reading;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只有 results 字段,转换为新格式
|
||||||
|
if (reading.results) {
|
||||||
|
return {
|
||||||
|
...reading,
|
||||||
|
analysis: {
|
||||||
|
[reading.reading_type]: {
|
||||||
|
[`${reading.reading_type}_analysis`]: reading.results
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
analysis_time: reading.created_at,
|
||||||
|
version: '1.0',
|
||||||
|
analysis_type: reading.reading_type,
|
||||||
|
migrated_from_results: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return reading;
|
||||||
|
});
|
||||||
|
|
||||||
|
setReadings(processedData);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载历史记录失败:', error);
|
||||||
|
toast.error('加载历史记录失败:' + (error.message || '未知错误'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteReading = async (readingId: string) => {
|
||||||
|
if (!confirm('确定要删除这条分析记录吗?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await supabase.functions.invoke('reading-history', {
|
||||||
|
body: {
|
||||||
|
action: 'delete_reading',
|
||||||
|
reading_id: readingId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReadings(prev => prev.filter(r => r.id !== readingId));
|
||||||
|
if (selectedReading?.id === readingId) {
|
||||||
|
setSelectedReading(null);
|
||||||
|
setViewingResult(false);
|
||||||
|
}
|
||||||
|
toast.success('删除成功');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
toast.error('删除失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewReading = (reading: NumerologyReading) => {
|
||||||
|
setSelectedReading(reading);
|
||||||
|
setViewingResult(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnalysisTypeIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'bazi': return Sparkles;
|
||||||
|
case 'ziwei': return Star;
|
||||||
|
case 'yijing': return Compass;
|
||||||
|
default: return History;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnalysisTypeColor = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'bazi': return 'text-purple-600 bg-purple-50';
|
||||||
|
case 'ziwei': return 'text-blue-600 bg-blue-50';
|
||||||
|
case 'yijing': return 'text-green-600 bg-green-50';
|
||||||
|
default: return 'text-gray-600 bg-gray-50';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnalysisTypeName = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'bazi': return '八字命理';
|
||||||
|
case 'ziwei': return '紫微斗数';
|
||||||
|
case 'yijing': return '易经占卜';
|
||||||
|
default: return '未知类型';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (viewingResult && selectedReading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setViewingResult(false)}
|
||||||
|
>
|
||||||
|
← 返回列表
|
||||||
|
</Button>
|
||||||
|
<div className="text-right">
|
||||||
|
<h2 className="text-xl font-semibold">{selectedReading.name} 的{getAnalysisTypeName(selectedReading.reading_type)}</h2>
|
||||||
|
<p className="text-gray-600">{new Date(selectedReading.created_at).toLocaleString('zh-CN')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnalysisResultDisplay
|
||||||
|
analysisResult={selectedReading.analysis}
|
||||||
|
analysisType={selectedReading.reading_type as 'bazi' | 'ziwei' | 'yijing'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||||
|
<History className="h-6 w-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle>历史记录</CardTitle>
|
||||||
|
<p className="text-gray-600">查看您之前的所有命理分析记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-16">
|
||||||
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-purple-600"></div>
|
||||||
|
</div>
|
||||||
|
) : readings.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="text-center py-16">
|
||||||
|
<History className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无分析记录</h3>
|
||||||
|
<p className="text-gray-600 mb-6">您还没有进行过任何命理分析</p>
|
||||||
|
<Button onClick={() => window.location.href = '/analysis'}>
|
||||||
|
<Sparkles className="mr-2 h-4 w-4" />
|
||||||
|
立即开始分析
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{readings.map((reading) => {
|
||||||
|
const Icon = getAnalysisTypeIcon(reading.reading_type);
|
||||||
|
const colorClass = getAnalysisTypeColor(reading.reading_type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={reading.id} className="hover:shadow-lg transition-shadow">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${colorClass}`}>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-900">
|
||||||
|
{reading.name || '未知姓名'} 的{getAnalysisTypeName(reading.reading_type)}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center space-x-4 text-sm text-gray-600 mt-1">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span>{new Date(reading.created_at).toLocaleDateString('zh-CN')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span>{reading.birth_date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewReading(reading)}
|
||||||
|
>
|
||||||
|
<Eye className="mr-1 h-3 w-3" />
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteReading(reading.id)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryPage;
|
||||||
185
src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Sparkles, Star, Compass, Heart, BarChart3, BookOpen } from 'lucide-react';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
|
const HomePage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: Sparkles,
|
||||||
|
title: '八字命理',
|
||||||
|
description: '基于传统八字学说,深度分析您的五行平衡、格局特点、四柱信息和人生走向',
|
||||||
|
color: 'text-red-700',
|
||||||
|
bgColor: 'chinese-golden-glow',
|
||||||
|
iconBg: 'bg-gradient-to-br from-yellow-400 to-amber-500',
|
||||||
|
link: '/analysis'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Star,
|
||||||
|
title: '紫微斗数',
|
||||||
|
description: '通过星曜排布和十二宫位分析,揭示您的性格特质和命运走向',
|
||||||
|
color: 'text-red-700',
|
||||||
|
bgColor: 'chinese-golden-glow',
|
||||||
|
iconBg: 'bg-gradient-to-br from-yellow-400 to-amber-500',
|
||||||
|
link: '/analysis'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Compass,
|
||||||
|
title: '易经占卜',
|
||||||
|
description: '运用梅花易数起卦法,解读卦象含义,为您的人生决策提供智慧指引',
|
||||||
|
color: 'text-red-700',
|
||||||
|
bgColor: 'chinese-golden-glow',
|
||||||
|
iconBg: 'bg-gradient-to-br from-yellow-400 to-amber-500',
|
||||||
|
link: '/analysis'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-16 relative">
|
||||||
|
{/* 页面装饰性背景元素 */}
|
||||||
|
<div className="absolute top-0 left-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-20 right-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain rotate-90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="text-center space-y-8 relative">
|
||||||
|
<div className="relative">
|
||||||
|
{/* 传统中式背景装饰 */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-80 h-80 chinese-red-glow rounded-full opacity-30 blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-96 h-96 chinese-golden-glow rounded-full opacity-20 blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
{/* 太极符号装饰 */}
|
||||||
|
<div className="w-14 h-14 mx-auto mb-6 bg-gradient-to-br from-yellow-400 to-amber-600 rounded-full flex items-center justify-center shadow-lg border-2 border-red-600">
|
||||||
|
<img
|
||||||
|
src="/traditional-chinese-bagua-eight-trigrams-black-gold.jpg"
|
||||||
|
alt="太极八卦"
|
||||||
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-5xl md:text-6xl font-bold text-red-800 mb-6 chinese-text-shadow font-serif">
|
||||||
|
神机阁
|
||||||
|
<span className="block text-3xl md:text-4xl text-yellow-600 mt-2 chinese-text-shadow">
|
||||||
|
AI智能命理分析
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-red-700 max-w-3xl mx-auto leading-relaxed font-medium">
|
||||||
|
融合传统命理智慧与现代AI技术,为您提供个性化、专业化的命理解读和人生指导
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center relative z-10">
|
||||||
|
{user ? (
|
||||||
|
<Link to="/analysis">
|
||||||
|
<Button size="lg" className="w-full sm:w-auto chinese-red-glow text-white hover:shadow-xl transition-all duration-300 border-2 border-yellow-400">
|
||||||
|
<Sparkles className="mr-2 h-5 w-5" />
|
||||||
|
开始分析
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link to="/register">
|
||||||
|
<Button size="lg" className="w-full sm:w-auto chinese-golden-glow text-red-800 hover:shadow-xl transition-all duration-300 border-2 border-red-600">
|
||||||
|
<Heart className="mr-2 h-5 w-5" />
|
||||||
|
免费注册
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/login">
|
||||||
|
<Button variant="outline" size="lg" className="w-full sm:w-auto border-2 border-yellow-500 text-red-700 hover:bg-yellow-50 hover:shadow-lg transition-all duration-300">
|
||||||
|
登录账户
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 relative justify-center max-w-6xl mx-auto">
|
||||||
|
{/* 装饰元素 - 调整为更适合3列布局的位置 */}
|
||||||
|
<div className="absolute -left-12 top-1/4 w-20 h-20 opacity-20 pointer-events-none hidden md:block">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_red_gold_auspicious_cloud_pattern.jpg"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute -right-12 bottom-1/4 w-20 h-20 opacity-20 pointer-events-none hidden md:block">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_red_gold_auspicious_cloud_pattern.jpg"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{features.map((feature, index) => {
|
||||||
|
const Icon = feature.icon;
|
||||||
|
return (
|
||||||
|
<Card key={index} className="text-center hover:shadow-2xl transition-all duration-300 chinese-card-decoration dragon-corner transform hover:scale-105">
|
||||||
|
<CardHeader>
|
||||||
|
<div className={`w-12 h-12 ${feature.iconBg} rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg border-2 border-red-600`}>
|
||||||
|
<Icon className={`h-6 w-6 text-red-800`} />
|
||||||
|
</div>
|
||||||
|
<CardTitle className={`${feature.color} text-2xl font-bold font-serif chinese-text-shadow`}>{feature.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-red-700 leading-relaxed font-medium mb-4">{feature.description}</p>
|
||||||
|
{user && (
|
||||||
|
<Link to={feature.link}>
|
||||||
|
<Button className="w-full chinese-golden-glow text-red-800 hover:shadow-lg transition-all duration-300 border border-red-600">
|
||||||
|
立即体验
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<Card className="chinese-traditional-bg text-white text-center dragon-corner relative overflow-hidden">
|
||||||
|
<CardContent className="py-12 relative z-10">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-6 bg-gradient-to-br from-yellow-400 to-amber-600 rounded-full flex items-center justify-center shadow-2xl border-3 border-yellow-300">
|
||||||
|
<Sparkles className="w-8 h-8 text-red-800" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-4xl font-bold mb-4 chinese-text-shadow font-serif">探索您的命运密码</h2>
|
||||||
|
<p className="text-red-100 mb-8 text-lg font-medium leading-relaxed">
|
||||||
|
加入数万用户的选择,让AI帮您解读人生密码
|
||||||
|
</p>
|
||||||
|
{!user && (
|
||||||
|
<Link to="/register">
|
||||||
|
<Button variant="outline" size="lg" className="chinese-golden-glow text-red-800 border-3 border-yellow-300 hover:shadow-2xl transition-all duration-300 transform hover:scale-105">
|
||||||
|
立即开始
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
97
src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Input } from '../components/ui/Input';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Mail, Lock, LogIn } from 'lucide-react';
|
||||||
|
|
||||||
|
const LoginPage: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { signIn } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await signIn(email, password);
|
||||||
|
if (error) {
|
||||||
|
toast.error('登录失败:' + error.message);
|
||||||
|
} else {
|
||||||
|
toast.success('登录成功!');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('登录过程中发生错误');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[80vh] flex items-center justify-center">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<LogIn className="h-6 w-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-2xl">登录账户</CardTitle>
|
||||||
|
<p className="text-gray-600">欢迎回到神机阁</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
label="邮箱地址"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入您的邮箱"
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label="密码"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入您的密码"
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '登录中...' : '登录'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
还没有账户?
|
||||||
|
<Link to="/register" className="text-purple-600 hover:text-purple-700 font-medium ml-1">
|
||||||
|
立即注册
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
221
src/pages/ProfilePage.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Input } from '../components/ui/Input';
|
||||||
|
import { Select } from '../components/ui/Select';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { User, Calendar, MapPin, Save } from 'lucide-react';
|
||||||
|
import { UserProfile } from '../types';
|
||||||
|
|
||||||
|
const ProfilePage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
full_name: '',
|
||||||
|
birth_date: '',
|
||||||
|
birth_time: '',
|
||||||
|
birth_location: '',
|
||||||
|
gender: 'male' as 'male' | 'female',
|
||||||
|
username: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProfile();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const loadProfile = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('user_profiles')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error && error.code !== 'PGRST116') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setProfile(data);
|
||||||
|
setFormData({
|
||||||
|
full_name: data.full_name || '',
|
||||||
|
birth_date: data.birth_date || '',
|
||||||
|
birth_time: data.birth_time || '',
|
||||||
|
birth_location: data.birth_location || '',
|
||||||
|
gender: data.gender || 'male',
|
||||||
|
username: data.username || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载档案失败:', error);
|
||||||
|
toast.error('加载档案失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profileData = {
|
||||||
|
user_id: user.id,
|
||||||
|
...formData,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (profile) {
|
||||||
|
// 更新现有档案
|
||||||
|
result = await supabase
|
||||||
|
.from('user_profiles')
|
||||||
|
.update(profileData)
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.select()
|
||||||
|
.maybeSingle();
|
||||||
|
} else {
|
||||||
|
// 创建新档案
|
||||||
|
result = await supabase
|
||||||
|
.from('user_profiles')
|
||||||
|
.insert([{
|
||||||
|
...profileData,
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
}])
|
||||||
|
.select()
|
||||||
|
.maybeSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProfile(result.data);
|
||||||
|
toast.success('档案保存成功!');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('保存档案失败:', error);
|
||||||
|
toast.error('保存档案失败:' + error.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||||
|
<User className="h-6 w-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle>个人档案</CardTitle>
|
||||||
|
<p className="text-gray-600">完善您的个人信息,获得更精准的命理分析</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<Input
|
||||||
|
label="姓名 *"
|
||||||
|
value={formData.full_name}
|
||||||
|
onChange={(e) => handleInputChange('full_name', e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入您的真实姓名"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="用户名"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||||
|
placeholder="请输入用户名(可选)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
label="出生日期 *"
|
||||||
|
value={formData.birth_date}
|
||||||
|
onChange={(e) => handleInputChange('birth_date', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Calendar className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
label="出生时间"
|
||||||
|
value={formData.birth_time}
|
||||||
|
onChange={(e) => handleInputChange('birth_time', e.target.value)}
|
||||||
|
placeholder="选填,但强烈建议填写"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<Select
|
||||||
|
label="性别 *"
|
||||||
|
value={formData.gender}
|
||||||
|
onChange={(e) => handleInputChange('gender', e.target.value)}
|
||||||
|
options={[
|
||||||
|
{ value: 'male', label: '男性' },
|
||||||
|
{ value: 'female', label: '女性' }
|
||||||
|
]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
label="出生地点"
|
||||||
|
value={formData.birth_location}
|
||||||
|
onChange={(e) => handleInputChange('birth_location', e.target.value)}
|
||||||
|
placeholder="如:北京市朝阳区"
|
||||||
|
/>
|
||||||
|
<MapPin className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h4 className="font-medium text-blue-800 mb-2">温馨提示</h4>
|
||||||
|
<ul className="text-sm text-blue-700 space-y-1">
|
||||||
|
<li>• 姓名和出生日期是必填项,对命理分析至关重要</li>
|
||||||
|
<li>• 出生时间越精确,分析结果越准确</li>
|
||||||
|
<li>• 出生地点有助于更精准的时间校正</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
{loading ? '保存中...' : '保存档案'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{profile && (
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
最后更新:{new Date(profile.updated_at).toLocaleString('zh-CN')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePage;
|
||||||
122
src/pages/RegisterPage.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Input } from '../components/ui/Input';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Mail, Lock, UserPlus } from 'lucide-react';
|
||||||
|
|
||||||
|
const RegisterPage: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { signUp } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
toast.error('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
toast.error('密码长度不能少于6位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await signUp(email, password);
|
||||||
|
if (error) {
|
||||||
|
toast.error('注册失败:' + error.message);
|
||||||
|
} else {
|
||||||
|
toast.success('注册成功!欢迎加入神机阁');
|
||||||
|
navigate('/profile'); // 引导到个人档案页面完善信息
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('注册过程中发生错误');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[80vh] flex items-center justify-center">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UserPlus className="h-6 w-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-2xl">创建账户</CardTitle>
|
||||||
|
<p className="text-gray-600">加入神机阁,开启您的命理之旅</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
label="邮箱地址"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入您的邮箱"
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<Mail className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label="密码"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入您的密码(不少于6位)"
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<Lock className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label="确认密码"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<Lock className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '注册中...' : '注册账户'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
已有账户?
|
||||||
|
<Link to="/login" className="text-purple-600 hover:text-purple-700 font-medium ml-1">
|
||||||
|
立即登录
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterPage;
|
||||||
345
src/pages/WuxingAnalysisPage.tsx
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts';
|
||||||
|
import { Calendar, Clock, Zap, BarChart3, Sparkles, TrendingUp } from 'lucide-react';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 五行分析数据接口
|
||||||
|
interface WuxingElement {
|
||||||
|
element: string;
|
||||||
|
percentage: number;
|
||||||
|
strength: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WuxingData {
|
||||||
|
bazi: {
|
||||||
|
year: { tiangan: string; dizhi: string };
|
||||||
|
month: { tiangan: string; dizhi: string };
|
||||||
|
day: { tiangan: string; dizhi: string };
|
||||||
|
hour: { tiangan: string; dizhi: string };
|
||||||
|
};
|
||||||
|
wuxingCount: { [key: string]: number };
|
||||||
|
wuxingPercentage: { [key: string]: number };
|
||||||
|
wuxingWithStrength: WuxingElement[];
|
||||||
|
radarData: Array<{ element: string; value: number; fullMark: number }>;
|
||||||
|
balanceAnalysis: string;
|
||||||
|
suggestions: string[];
|
||||||
|
dominantElement: string;
|
||||||
|
weakestElement: string;
|
||||||
|
isBalanced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WuxingAnalysisPage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [birthDate, setBirthDate] = useState('');
|
||||||
|
const [birthTime, setBirthTime] = useState('12:00');
|
||||||
|
const [analysisData, setAnalysisData] = useState<WuxingData | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 五行颜色配置
|
||||||
|
const elementColors: { [key: string]: string } = {
|
||||||
|
'木': '#22c55e', // 绿色
|
||||||
|
'火': '#ef4444', // 红色
|
||||||
|
'土': '#eab308', // 黄色
|
||||||
|
'金': '#64748b', // 银色
|
||||||
|
'水': '#3b82f6' // 蓝色
|
||||||
|
};
|
||||||
|
|
||||||
|
// 五行符号配置
|
||||||
|
const elementSymbols: { [key: string]: string } = {
|
||||||
|
'木': '🌲',
|
||||||
|
'火': '🔥',
|
||||||
|
'土': '⛰️',
|
||||||
|
'金': '⚡',
|
||||||
|
'水': '💧'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用Supabase Edge Function进行五行分析
|
||||||
|
const fetchWuxingAnalysis = async () => {
|
||||||
|
if (!birthDate) {
|
||||||
|
toast.error('请选择您的出生日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用Supabase Edge Function
|
||||||
|
const { data, error } = await supabase.functions.invoke('bazi-wuxing-analysis', {
|
||||||
|
body: {
|
||||||
|
birthDate,
|
||||||
|
birthTime
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (data?.data) {
|
||||||
|
setAnalysisData(data.data);
|
||||||
|
toast.success('五行分析完成!');
|
||||||
|
} else {
|
||||||
|
throw new Error('分析结果为空');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('五行分析错误:', err);
|
||||||
|
setError(err.message || '分析失败,请稍后重试');
|
||||||
|
toast.error('分析失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染雷达图
|
||||||
|
const renderRadarChart = () => {
|
||||||
|
if (!analysisData?.radarData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<RadarChart data={analysisData.radarData}>
|
||||||
|
<PolarGrid stroke="#dc2626" />
|
||||||
|
<PolarAngleAxis
|
||||||
|
dataKey="element"
|
||||||
|
tick={{ fill: '#dc2626', fontSize: 14, fontWeight: 'bold' }}
|
||||||
|
/>
|
||||||
|
<PolarRadiusAxis
|
||||||
|
angle={90}
|
||||||
|
domain={[0, 100]}
|
||||||
|
tick={{ fill: '#b91c1c', fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<Radar
|
||||||
|
name="五行强度"
|
||||||
|
dataKey="value"
|
||||||
|
stroke="#dc2626"
|
||||||
|
fill="rgba(220, 38, 38, 0.3)"
|
||||||
|
fillOpacity={0.6}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</RadarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染五行统计卡片
|
||||||
|
const renderElementCards = () => {
|
||||||
|
if (!analysisData?.wuxingWithStrength) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-5 gap-4">
|
||||||
|
{analysisData.wuxingWithStrength.map((item) => (
|
||||||
|
<Card key={item.element} className="text-center hover:shadow-xl transition-all duration-300 chinese-card-decoration border-2 border-yellow-400">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="text-3xl mb-2">{elementSymbols[item.element]}</div>
|
||||||
|
<h3 className="font-bold text-red-800 text-lg mb-2 chinese-text-shadow">{item.element}</h3>
|
||||||
|
<div className="text-2xl font-bold text-yellow-600 mb-1">{item.percentage}%</div>
|
||||||
|
<div className={`text-sm font-medium mb-2 ${
|
||||||
|
item.strength === '旺' ? 'text-green-600' :
|
||||||
|
item.strength === '中' ? 'text-yellow-600' : 'text-orange-600'
|
||||||
|
}`}>
|
||||||
|
{item.strength}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="w-full h-3 bg-gray-200 rounded-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-1000"
|
||||||
|
style={{
|
||||||
|
width: `${item.percentage}%`,
|
||||||
|
backgroundColor: elementColors[item.element]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 relative">
|
||||||
|
{/* 页面装饰背景 */}
|
||||||
|
<div className="absolute top-0 left-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-20 right-0 w-32 h-32 opacity-20 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-contain rotate-180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题区域 */}
|
||||||
|
<div className="text-center space-y-4 relative z-10">
|
||||||
|
<div className="w-16 h-16 mx-auto bg-gradient-to-br from-yellow-400 to-amber-600 rounded-full flex items-center justify-center shadow-2xl border-3 border-red-600">
|
||||||
|
<BarChart3 className="w-8 h-8 text-red-800" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-red-800 chinese-text-shadow font-serif">
|
||||||
|
五行分析
|
||||||
|
<span className="block text-lg text-yellow-600 mt-2 font-normal">
|
||||||
|
深度解析您的五行构成与能量平衡
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输入区域 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Calendar className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
输入您的出生信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-red-700 mb-2">
|
||||||
|
出生日期 *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={birthDate}
|
||||||
|
onChange={(e) => setBirthDate(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 border-2 border-yellow-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 bg-white text-red-800"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-red-700 mb-2">
|
||||||
|
出生时间
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={birthTime}
|
||||||
|
onChange={(e) => setBirthTime(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 border-2 border-yellow-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 bg-white text-red-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={fetchWuxingAnalysis}
|
||||||
|
disabled={isLoading || !birthDate}
|
||||||
|
size="lg"
|
||||||
|
className="w-full chinese-red-glow text-white hover:shadow-xl transition-all duration-300 border-2 border-yellow-400"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>加载中...</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="mr-2 h-5 w-5" />
|
||||||
|
开始五行分析
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<Card className="border-red-400 bg-red-50">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<p className="text-red-700 text-center">{error}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分析结果 */}
|
||||||
|
{analysisData && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* 五行统计卡片 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
五行能量分布
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{renderElementCards()}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 雷达图 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow text-center">
|
||||||
|
五行平衡雷达图
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
{renderRadarChart()}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 平衡分析 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<Zap className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
五行平衡分析
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<p className="text-red-700 leading-relaxed text-lg font-medium whitespace-pre-line">
|
||||||
|
{analysisData.balanceAnalysis}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="text-center p-4 bg-white rounded-lg border-2 border-green-300">
|
||||||
|
<div className="text-2xl mb-2">{elementSymbols[analysisData.dominantElement]}</div>
|
||||||
|
<h4 className="font-bold text-green-700">最强元素</h4>
|
||||||
|
<p className="text-green-600">{analysisData.dominantElement}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-white rounded-lg border-2 border-orange-300">
|
||||||
|
<div className="text-2xl mb-2">{elementSymbols[analysisData.weakestElement]}</div>
|
||||||
|
<h4 className="font-bold text-orange-700">最弱元素</h4>
|
||||||
|
<p className="text-orange-600">{analysisData.weakestElement}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 改善建议 */}
|
||||||
|
<Card className="chinese-card-decoration dragon-corner border-2 border-yellow-400">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-red-800 text-2xl font-bold chinese-text-shadow flex items-center">
|
||||||
|
<TrendingUp className="mr-2 h-6 w-6 text-yellow-600" />
|
||||||
|
五行调和建议
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-yellow-50 rounded-lg p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{analysisData.suggestions.map((suggestion, index) => (
|
||||||
|
<div key={index} className="flex items-start space-x-3 p-4 bg-white rounded-lg border-l-4 border-yellow-500">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-yellow-400 to-amber-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<span className="text-red-800 font-bold text-sm">{index + 1}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-700 font-medium">{suggestion}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WuxingAnalysisPage;
|
||||||
76
src/types/index.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
export interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
username?: string;
|
||||||
|
full_name: string;
|
||||||
|
birth_date: string;
|
||||||
|
birth_time?: string;
|
||||||
|
birth_location?: string;
|
||||||
|
gender: 'male' | 'female';
|
||||||
|
avatar_url?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalysisRecord {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
analysis_type: 'bazi' | 'ziwei' | 'yijing';
|
||||||
|
name: string;
|
||||||
|
birth_date: string;
|
||||||
|
birth_time?: string;
|
||||||
|
birth_place?: string;
|
||||||
|
gender: string;
|
||||||
|
input_data: any;
|
||||||
|
analysis_results: any;
|
||||||
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NumerologyReading {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
profile_id?: string;
|
||||||
|
reading_type: 'bazi' | 'ziwei' | 'yijing' | 'comprehensive';
|
||||||
|
name: string;
|
||||||
|
birth_date: string;
|
||||||
|
birth_time?: string;
|
||||||
|
gender: string;
|
||||||
|
birth_place?: string;
|
||||||
|
input_data: any;
|
||||||
|
analysis: {
|
||||||
|
bazi?: { bazi_analysis: any };
|
||||||
|
ziwei?: { ziwei_analysis: any };
|
||||||
|
yijing?: { yijing_analysis: any };
|
||||||
|
metadata: {
|
||||||
|
analysis_time: string;
|
||||||
|
version: string;
|
||||||
|
analysis_type: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
results: any; // 保持向后兼容
|
||||||
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalysisRequest {
|
||||||
|
user_id: string;
|
||||||
|
birth_data: {
|
||||||
|
name: string;
|
||||||
|
birth_date: string;
|
||||||
|
birth_time?: string;
|
||||||
|
gender: 'male' | 'female';
|
||||||
|
birth_place?: string;
|
||||||
|
question?: string; // for yijing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
data?: T;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
76
tailwind.config.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ['class'],
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx}',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '2rem',
|
||||||
|
screens: {
|
||||||
|
'2xl': '1400px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: '#2B5D3A',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: '#4A90E2',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: '#F5A623',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
'accordion-down': {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
},
|
||||||
|
'accordion-up': {
|
||||||
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('tailwindcss-animate')],
|
||||||
|
}
|
||||||
42
tsconfig.app.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
/* Tailwind stuff */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
/* Linting */
|
||||||
|
"strict": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"noUncheckedIndexedAccess": false,
|
||||||
|
"noImplicitReturns": false,
|
||||||
|
"noImplicitThis": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"noUncheckedSideEffectImports": false
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
24
tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
22
vite.config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import path from "path"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import sourceIdentifierPlugin from 'vite-plugin-source-info'
|
||||||
|
|
||||||
|
const isProd = process.env.BUILD_MODE === 'prod'
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
sourceIdentifierPlugin({
|
||||||
|
enabled: !isProd,
|
||||||
|
attributePrefix: 'data-matrix',
|
||||||
|
includeProps: true,
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||