Initial commit: AI-powered numerology analysis application

This commit is contained in:
patdelphi
2025-08-18 09:13:18 +08:00
commit db343a096e
59 changed files with 17320 additions and 0 deletions

50
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

83
package.json Normal file
View 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

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

1
public/use.txt Normal file
View File

@@ -0,0 +1 @@
keep assets in the dir to use.

42
src/App.css Normal file
View 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
View 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;

View 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;

View 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;

View 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;

View 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
View 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;

View 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;

View 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}
/>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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
View 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
View 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
View 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
View 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
View 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;

View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

76
tailwind.config.js Normal file
View 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
View 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
View 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
View 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
View 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"),
},
},
})