feat: 初始化天选公司 MVP
- 首页、信息录入页、结果页完整流程 - 命理分析规则引擎(五行计算) - 公司类型与岗位方向匹配 - 响应式东方命理风格 UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# 预留后续环境变量
|
||||
# VITE_API_BASE_URL=
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
npm-debug.log*
|
||||
*.tsbuildinfo
|
||||
vite.config.d.ts
|
||||
88
PRD.md
Normal file
88
PRD.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 天选公司 PRD v1
|
||||
|
||||
## 产品定位
|
||||
|
||||
天选公司是一款结合东方命理逻辑与职业规划思路的 Web 产品。用户输入出生年月日、时辰、性别、出生地后,系统基于通用八字/五行逻辑生成职业命理画像,并给出适合的公司类型、岗位方向和当前阶段择业建议。
|
||||
|
||||
## 产品目标
|
||||
|
||||
- 帮助用户理解自己更适合什么类型的公司
|
||||
- 帮助用户理解自己更适合什么岗位方向
|
||||
- 提供当前阶段择业建议
|
||||
- 将命理兴趣转化为职业决策参考
|
||||
|
||||
## 目标用户
|
||||
|
||||
- 应届生
|
||||
- 1-5 年职场人
|
||||
- 5 年以上职场人
|
||||
|
||||
## MVP 功能
|
||||
|
||||
1. 出生信息输入
|
||||
2. 命理职业画像生成
|
||||
3. 公司类型匹配
|
||||
4. 岗位类型匹配
|
||||
5. 当前阶段择业建议
|
||||
6. 结果页先展示简版,再展开详版
|
||||
|
||||
## 输入项
|
||||
|
||||
- 出生年月日
|
||||
- 出生时辰
|
||||
- 性别
|
||||
- 出生地
|
||||
|
||||
## 公司类型范围
|
||||
|
||||
- 一线大厂
|
||||
- 上市公司
|
||||
- 独角兽公司
|
||||
- 高增长创业公司
|
||||
- 成熟型中大型企业
|
||||
- 强技术驱动型公司
|
||||
- 强产品驱动型公司
|
||||
- 强销售驱动型公司
|
||||
- 稳定流程型组织
|
||||
- 高压竞争型组织
|
||||
|
||||
## 岗位类型范围
|
||||
|
||||
- 前端开发
|
||||
- 后端开发
|
||||
- 算法 / AI
|
||||
- 测试 / QA
|
||||
- 产品经理
|
||||
- 设计师
|
||||
- 内容运营
|
||||
- 用户运营
|
||||
- 增长运营
|
||||
- 销售
|
||||
- 商务拓展
|
||||
- HR
|
||||
- 财务
|
||||
- 项目管理
|
||||
- 管理岗
|
||||
|
||||
## 产品风格
|
||||
|
||||
- 专业
|
||||
- 克制
|
||||
- 东方命理气质
|
||||
- 有神秘感但不过度玄化
|
||||
|
||||
## 首版不做
|
||||
|
||||
- 登录注册
|
||||
- 报告保存
|
||||
- 付费能力
|
||||
- 企业真实岗位抓取
|
||||
- 精准推荐具体公司
|
||||
|
||||
## 下一步开发重点
|
||||
|
||||
1. 首页信息架构细化
|
||||
2. 表单页设计
|
||||
3. 结果页结构设计
|
||||
4. 命理标签映射设计
|
||||
5. 推荐文案模板设计
|
||||
65
README.md
65
README.md
@@ -1,3 +1,64 @@
|
||||
# tianxuan-company
|
||||
# 天选公司
|
||||
|
||||
天选公司 - 基于命理推理的职业匹配产品
|
||||
一个基于出生信息、天干地支与命理推理的职业匹配 Web 产品。
|
||||
|
||||
## 当前阶段
|
||||
|
||||
当前仓库为项目初始化版本,已完成:
|
||||
|
||||
- 独立项目目录创建
|
||||
- Web 前端基础骨架搭建
|
||||
- README 与 PRD 文档落地
|
||||
- 首页占位页搭建
|
||||
|
||||
暂未包含:
|
||||
|
||||
- 登录/注册
|
||||
- 报告保存
|
||||
- 付费能力
|
||||
- 真实命理引擎细节
|
||||
- 具体公司库接入
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vite
|
||||
- React
|
||||
- TypeScript
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
tianxuan-company/
|
||||
├── README.md
|
||||
├── PRD.md
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── tsconfig.app.json
|
||||
├── tsconfig.node.json
|
||||
├── vite.config.ts
|
||||
├── index.html
|
||||
└── src/
|
||||
├── App.tsx
|
||||
├── main.tsx
|
||||
├── pages/
|
||||
├── components/
|
||||
├── data/
|
||||
├── lib/
|
||||
├── styles/
|
||||
└── types/
|
||||
```
|
||||
|
||||
## 本地启动
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 设计信息填写页
|
||||
2. 设计结果页结构
|
||||
3. 建立命理标签与职业映射规则
|
||||
4. 补充页面交互流
|
||||
5. 在确认后创建 Gitea 仓库并推送
|
||||
|
||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>天选公司</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="基于出生信息、天干地支与命理推理,为用户生成公司类型与岗位方向匹配建议。"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
1820
package-lock.json
generated
Normal file
1820
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "tianxuan-company",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.3"
|
||||
}
|
||||
}
|
||||
39
src/App.tsx
Normal file
39
src/App.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { HomePage } from './pages/HomePage';
|
||||
import { FormPage } from './pages/FormPage';
|
||||
import { ResultPage } from './pages/ResultPage';
|
||||
import type { UserInput, FortuneResult } from './types';
|
||||
|
||||
type Page = 'home' | 'form' | 'result';
|
||||
|
||||
export default function App() {
|
||||
const [currentPage, setCurrentPage] = useState<Page>('home');
|
||||
const [userInput, setUserInput] = useState<UserInput | null>(null);
|
||||
const [fortuneResult, setFortuneResult] = useState<FortuneResult | null>(null);
|
||||
|
||||
const goToForm = () => {
|
||||
setCurrentPage('form');
|
||||
};
|
||||
|
||||
const goToResult = (input: UserInput, result: FortuneResult) => {
|
||||
setUserInput(input);
|
||||
setFortuneResult(result);
|
||||
setCurrentPage('result');
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
setCurrentPage('home');
|
||||
setUserInput(null);
|
||||
setFortuneResult(null);
|
||||
};
|
||||
|
||||
if (currentPage === 'form') {
|
||||
return <FormPage onSubmit={goToResult} onBack={goHome} />;
|
||||
}
|
||||
|
||||
if (currentPage === 'result' && userInput && fortuneResult) {
|
||||
return <ResultPage input={userInput} result={fortuneResult} onBack={goHome} />;
|
||||
}
|
||||
|
||||
return <HomePage onStart={goToForm} />;
|
||||
}
|
||||
279
src/lib/fortune.ts
Normal file
279
src/lib/fortune.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import type { UserInput, FortuneResult, FortuneProfile, CompanyRecommendation, JobRecommendation, StageAdvice } from '../types';
|
||||
|
||||
// 简化的八字五行计算 - 基于出生年份
|
||||
function calculateWuxing(birthDate: string): { wuxing: FortuneProfile['wuxing'], wuxingLabel: string } {
|
||||
const date = new Date(birthDate);
|
||||
const year = date.getFullYear();
|
||||
|
||||
// 简化版:根据年份天干映射五行
|
||||
const tiangan = year % 10;
|
||||
const tianganMap: Record<number, string> = {
|
||||
0: 'metal', 1: 'metal',
|
||||
2: 'water', 3: 'water',
|
||||
4: 'wood', 5: 'wood',
|
||||
6: 'fire', 7: 'fire',
|
||||
8: 'earth', 9: 'earth'
|
||||
};
|
||||
|
||||
const wuxingKey = tianganMap[tiangan] || 'earth';
|
||||
const labelMap: Record<string, string> = {
|
||||
wood: '木',
|
||||
fire: '火',
|
||||
earth: '土',
|
||||
metal: '金',
|
||||
water: '水'
|
||||
};
|
||||
|
||||
return {
|
||||
wuxing: wuxingKey as FortuneProfile['wuxing'],
|
||||
wuxingLabel: labelMap[wuxingKey] || '土'
|
||||
};
|
||||
}
|
||||
|
||||
// 根据五行生成命理画像
|
||||
function generateProfile(wuxing: FortuneProfile['wuxing'], wuxingLabel: string): FortuneProfile {
|
||||
const profiles: Record<string, Partial<FortuneProfile>> = {
|
||||
wood: {
|
||||
personality: ['有生发之力', '善于规划与成长', '追求目标坚定'],
|
||||
strengths: ['领导力', '规划能力', '适应力'],
|
||||
weaknesses: ['有时过于刚硬', '易忽略细节'],
|
||||
developmentRhythm: 'growth'
|
||||
},
|
||||
fire: {
|
||||
personality: ['热情洋溢', '执行力强', '喜欢表达'],
|
||||
strengths: ['创造力', '感染力', '行动力'],
|
||||
weaknesses: ['易急躁', '缺乏耐心'],
|
||||
developmentRhythm: 'breakthrough'
|
||||
},
|
||||
earth: {
|
||||
personality: ['稳重务实', '注重规则', '善于协调'],
|
||||
strengths: ['稳定性', '执行力', '资源整合'],
|
||||
weaknesses: ['过于保守', '创新不足'],
|
||||
developmentRhythm: 'stable'
|
||||
},
|
||||
metal: {
|
||||
personality: ['刚毅果断', '追求效率', '原则性强'],
|
||||
strengths: ['决断力', '专业度', '抗压能力'],
|
||||
weaknesses: ['过于严厉', '不够灵活'],
|
||||
developmentRhythm: 'accumulation'
|
||||
},
|
||||
water: {
|
||||
personality: ['灵活变通', '善于沟通', '适应力强'],
|
||||
strengths: ['沟通力', '学习力', '洞察力'],
|
||||
weaknesses: ['易动摇', '缺乏坚持'],
|
||||
developmentRhythm: 'growth'
|
||||
}
|
||||
};
|
||||
|
||||
const profile = profiles[wuxing] || profiles.earth;
|
||||
|
||||
return {
|
||||
wuxing,
|
||||
wuxingLabel,
|
||||
personality: profile.personality || [],
|
||||
strengths: profile.strengths || [],
|
||||
weaknesses: profile.weaknesses || [],
|
||||
developmentRhythm: profile.developmentRhythm || 'stable',
|
||||
};
|
||||
}
|
||||
|
||||
// 公司类型推荐
|
||||
function generateCompanyRecommendations(profile: FortuneProfile): CompanyRecommendation[] {
|
||||
const { developmentRhythm } = profile;
|
||||
|
||||
const companyTypes: CompanyRecommendation[] = [
|
||||
{
|
||||
type: '一线大厂',
|
||||
matchScore: 85,
|
||||
reasons: ['资源丰富,平台稳定', '制度完善,适合长期发展', '能发挥你的专业能力'],
|
||||
suitableFor: ['追求稳定发展', '希望积累大平台经验'],
|
||||
notSuitableFor: ['渴望快速晋升', '喜欢灵活工作方式'],
|
||||
stageAdvice: developmentRhythm === 'stable' ? '适合进入积累期' : '可考虑内部晋升机会'
|
||||
},
|
||||
{
|
||||
type: '独角兽公司',
|
||||
matchScore: 78,
|
||||
reasons: ['成长空间大', '能接触核心业务', '晋升机会多'],
|
||||
suitableFor: ['渴望快速成长', '愿意承担风险'],
|
||||
notSuitableFor: ['追求极度稳定', '不喜欢不确定性'],
|
||||
stageAdvice: developmentRhythm === 'growth' ? '非常适合当前阶段' : '需评估团队稳定性'
|
||||
},
|
||||
{
|
||||
type: '高增长创业公司',
|
||||
matchScore: 72,
|
||||
reasons: ['决策灵活', '成长速度快', '能全面锻炼能力'],
|
||||
suitableFor: ['敢于冒险', '希望快速晋升'],
|
||||
notSuitableFor: ['喜欢按部就班', '需要明确指引'],
|
||||
stageAdvice: '适合寻求突破的年轻人'
|
||||
},
|
||||
{
|
||||
type: '强技术驱动型公司',
|
||||
matchScore: 80,
|
||||
reasons: ['重视技术能力', '氛围开放', '创新机会多'],
|
||||
suitableFor: ['技术导向', '喜欢钻研'],
|
||||
notSuitableFor: ['销售导向', '不喜欢深度工作'],
|
||||
stageAdvice: '技术路线发展的理想选择'
|
||||
},
|
||||
{
|
||||
type: '强产品驱动型公司',
|
||||
matchScore: 76,
|
||||
reasons: ['用户体验为核心', '重视创新', '成长空间大'],
|
||||
suitableFor: ['产品思维', '用户导向'],
|
||||
notSuitableFor: ['纯执行导向', '不喜欢思考'],
|
||||
stageAdvice: '产品路线发展的好平台'
|
||||
},
|
||||
{
|
||||
type: '稳定流程型组织',
|
||||
matchScore: 68,
|
||||
reasons: ['工作稳定', '流程规范', '压力适中'],
|
||||
suitableFor: ['追求工作生活平衡', '喜欢清晰职责'],
|
||||
notSuitableFor: ['渴望快速变化', '喜欢创新'],
|
||||
stageAdvice: developmentRhythm === 'stable' ? '适合当前阶段' : '可能限制发展'
|
||||
}
|
||||
];
|
||||
|
||||
return companyTypes
|
||||
.sort((a, b) => b.matchScore - a.matchScore)
|
||||
.slice(0, 4);
|
||||
}
|
||||
|
||||
// 岗位类型推荐
|
||||
function generateJobRecommendations(profile: FortuneProfile): JobRecommendation[] {
|
||||
|
||||
const jobTypes: JobRecommendation[] = [
|
||||
{
|
||||
category: '技术研发',
|
||||
matchScore: 88,
|
||||
reasons: ['你的特质适合深耕技术', '需要专业能力和持续学习'],
|
||||
suitableWorkStyle: ['独立钻研', '解决复杂问题'],
|
||||
notSuitableFor: ['频繁社交', '简单重复工作']
|
||||
},
|
||||
{
|
||||
category: '产品经理',
|
||||
matchScore: 82,
|
||||
reasons: ['需要平衡多方需求', '注重规划和沟通'],
|
||||
suitableWorkStyle: ['协调资源', '用户洞察'],
|
||||
notSuitableFor: ['纯技术执行', '不喜欢沟通']
|
||||
},
|
||||
{
|
||||
category: '运营',
|
||||
matchScore: 79,
|
||||
reasons: ['需要数据分析能力', '注重用户增长'],
|
||||
suitableWorkStyle: ['数据分析', '活动策划'],
|
||||
notSuitableFor: ['极度内向', '不喜欢变化']
|
||||
},
|
||||
{
|
||||
category: '销售商务',
|
||||
matchScore: 75,
|
||||
reasons: ['需要人际沟通能力', '结果导向'],
|
||||
suitableWorkStyle: ['人际交往', '资源整合'],
|
||||
notSuitableFor: ['不喜欢竞争', '害怕被拒绝']
|
||||
},
|
||||
{
|
||||
category: '设计',
|
||||
matchScore: 77,
|
||||
reasons: ['需要审美和创造力', '注重用户体验'],
|
||||
suitableWorkStyle: ['视觉表达', '创意工作'],
|
||||
notSuitableFor: ['不喜欢抽象思维', '追求确定答案']
|
||||
},
|
||||
{
|
||||
category: '管理岗',
|
||||
matchScore: 73,
|
||||
reasons: ['需要领导力和规划能力', '注重团队发展'],
|
||||
suitableWorkStyle: ['团队管理', '战略规划'],
|
||||
notSuitableFor: ['喜欢独立工作', '不喜欢处理人事']
|
||||
}
|
||||
];
|
||||
|
||||
// 根据五行微调分数
|
||||
if (profile.wuxing === 'wood') {
|
||||
jobTypes.find(j => j.category === '管理岗')!.matchScore += 5;
|
||||
jobTypes.find(j => j.category === '产品经理')!.matchScore += 3;
|
||||
} else if (profile.wuxing === 'fire') {
|
||||
jobTypes.find(j => j.category === '销售商务')!.matchScore += 5;
|
||||
jobTypes.find(j => j.category === '运营')!.matchScore += 3;
|
||||
} else if (profile.wuxing === 'water') {
|
||||
jobTypes.find(j => j.category === '运营')!.matchScore += 5;
|
||||
jobTypes.find(j => j.category === '产品经理')!.matchScore += 3;
|
||||
} else if (profile.wuxing === 'metal') {
|
||||
jobTypes.find(j => j.category === '技术研发')!.matchScore += 5;
|
||||
jobTypes.find(j => j.category === '管理岗')!.matchScore += 3;
|
||||
} else if (profile.wuxing === 'earth') {
|
||||
jobTypes.find(j => j.category === '运营')!.matchScore += 3;
|
||||
jobTypes.find(j => j.category === '管理岗')!.matchScore += 5;
|
||||
}
|
||||
|
||||
return jobTypes
|
||||
.sort((a, b) => b.matchScore - a.matchScore)
|
||||
.slice(0, 4);
|
||||
}
|
||||
|
||||
// 生成当前阶段建议
|
||||
function generateStageAdvice(profile: FortuneProfile): StageAdvice {
|
||||
const { developmentRhythm, wuxingLabel } = profile;
|
||||
|
||||
const adviceMap: Record<string, StageAdvice> = {
|
||||
stable: {
|
||||
overall: `你目前处于稳定积累期,五行属${wuxingLabel},适合稳扎稳打。`,
|
||||
careerDirection: '建议在现有岗位上深耕,积累专业能力。',
|
||||
jobChangeTiming: '当前不建议频繁变动,保持稳定是更好的选择。',
|
||||
platformPreference: '优先选择成熟稳定的大平台。',
|
||||
keyObservations: [
|
||||
'关注岗位的成长空间而非只看薪资',
|
||||
'建立专业壁垒是关键',
|
||||
'可以开始培养管理能力'
|
||||
]
|
||||
},
|
||||
growth: {
|
||||
overall: `你目前处于成长期,五行属${wuxingLabel},有较强的上升动力。`,
|
||||
careerDirection: '适合寻求晋升机会或承担更大责任。',
|
||||
jobChangeTiming: '可以开始关注外部机会,但不要急于变动。',
|
||||
platformPreference: '独角兽或高速成长公司可能更适合你。',
|
||||
keyObservations: [
|
||||
'主动争取项目机会',
|
||||
'扩大影响力范围',
|
||||
'开始建立个人品牌'
|
||||
]
|
||||
},
|
||||
breakthrough: {
|
||||
overall: `你目前处于突破期,五行属${wuxingLabel},适合做出重大改变。`,
|
||||
careerDirection: '可以考虑转型或创业的机会。',
|
||||
jobChangeTiming: '现在是尝试新方向的好时机。',
|
||||
platformPreference: '创业公司或新业务部门可能更适合你。',
|
||||
keyObservations: [
|
||||
'勇敢尝试新领域',
|
||||
'不要害怕失败',
|
||||
'整合资源是关键'
|
||||
]
|
||||
},
|
||||
accumulation: {
|
||||
overall: `你目前处于沉淀期,五行属${wuxingLabel},需要耐心积累。`,
|
||||
careerDirection: '适合深耕现有领域,等待时机。',
|
||||
jobChangeTiming: '建议先完成当前阶段的积累。',
|
||||
platformPreference: '稳定平台更适合当前状态。',
|
||||
keyObservations: [
|
||||
'专注提升专业深度',
|
||||
'建立行业人脉',
|
||||
'等待合适的机会'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
return adviceMap[developmentRhythm] || adviceMap.stable;
|
||||
}
|
||||
|
||||
// 主分析函数
|
||||
export function analyzeFortune(input: UserInput): FortuneResult {
|
||||
const { wuxing, wuxingLabel } = calculateWuxing(input.birthDate);
|
||||
const profile = generateProfile(wuxing, wuxingLabel);
|
||||
const companyRecommendations = generateCompanyRecommendations(profile);
|
||||
const jobRecommendations = generateJobRecommendations(profile);
|
||||
const stageAdvice = generateStageAdvice(profile);
|
||||
|
||||
return {
|
||||
profile,
|
||||
companyRecommendations,
|
||||
jobRecommendations,
|
||||
stageAdvice
|
||||
};
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './styles/index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
160
src/pages/FormPage.tsx
Normal file
160
src/pages/FormPage.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useState } from 'react';
|
||||
import type { UserInput, FortuneResult } from '../types';
|
||||
import { analyzeFortune } from '../lib/fortune';
|
||||
|
||||
interface FormPageProps {
|
||||
onSubmit: (input: UserInput, result: FortuneResult) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function FormPage({ onSubmit, onBack }: FormPageProps) {
|
||||
const [birthDate, setBirthDate] = useState('');
|
||||
const [birthTime, setBirthTime] = useState('');
|
||||
const [gender, setGender] = useState<'male' | 'female'>('male');
|
||||
const [birthPlace, setBirthPlace] = useState('');
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!birthDate || !birthTime || !birthPlace) return;
|
||||
|
||||
setIsAnalyzing(true);
|
||||
|
||||
// 模拟分析过程
|
||||
setTimeout(() => {
|
||||
const input: UserInput = {
|
||||
birthDate,
|
||||
birthTime,
|
||||
gender,
|
||||
birthPlace
|
||||
};
|
||||
const result = analyzeFortune(input);
|
||||
onSubmit(input, result);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const isValid = birthDate && birthTime && birthPlace;
|
||||
|
||||
return (
|
||||
<main className="page-shell">
|
||||
<div className="form-section">
|
||||
<header className="form-header">
|
||||
<button type="button" className="back-btn" onClick={onBack}>
|
||||
← 返回
|
||||
</button>
|
||||
<h1 className="form-title">输入你的信息</h1>
|
||||
<p className="form-subtitle">
|
||||
我们会根据你的出生信息,推算你的职业命理画像
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form className="form-card" onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="birthDate" className="form-label">
|
||||
出生日期 <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="birthDate"
|
||||
className="form-input"
|
||||
value={birthDate}
|
||||
onChange={(e) => setBirthDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="birthTime" className="form-label">
|
||||
出生时辰 <span className="required">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="birthTime"
|
||||
className="form-input form-select"
|
||||
value={birthTime}
|
||||
onChange={(e) => setBirthTime(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">请选择时辰</option>
|
||||
<option value="00:00">子时 (23:00-01:00)</option>
|
||||
<option value="02:00">丑时 (01:00-03:00)</option>
|
||||
<option value="04:00">寅时 (03:00-05:00)</option>
|
||||
<option value="06:00">卯时 (05:00-07:00)</option>
|
||||
<option value="08:00">辰时 (07:00-09:00)</option>
|
||||
<option value="10:00">巳时 (09:00-11:00)</option>
|
||||
<option value="12:00">午时 (11:00-13:00)</option>
|
||||
<option value="14:00">未时 (13:00-15:00)</option>
|
||||
<option value="16:00">申时 (15:00-17:00)</option>
|
||||
<option value="18:00">酉时 (17:00-19:00)</option>
|
||||
<option value="20:00">戌时 (19:00-21:00)</option>
|
||||
<option value="22:00">亥时 (21:00-23:00)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
性别 <span className="required">*</span>
|
||||
</label>
|
||||
<div className="radio-group">
|
||||
<label className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="gender"
|
||||
value="male"
|
||||
checked={gender === 'male'}
|
||||
onChange={() => setGender('male')}
|
||||
/>
|
||||
<span className="radio-custom"></span>
|
||||
男
|
||||
</label>
|
||||
<label className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="gender"
|
||||
value="female"
|
||||
checked={gender === 'female'}
|
||||
onChange={() => setGender('female')}
|
||||
/>
|
||||
<span className="radio-custom"></span>
|
||||
女
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="birthPlace" className="form-label">
|
||||
出生地 <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="birthPlace"
|
||||
className="form-input"
|
||||
placeholder="例如:上海、北京、广州"
|
||||
value={birthPlace}
|
||||
onChange={(e) => setBirthPlace(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-notice">
|
||||
<p>※ 您的信息仅用于本次命理分析,不会保存</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-full"
|
||||
disabled={!isValid || isAnalyzing}
|
||||
>
|
||||
{isAnalyzing ? (
|
||||
<span className="loading-state">
|
||||
<span className="loading-dot"></span>
|
||||
分析中...
|
||||
</span>
|
||||
) : (
|
||||
'开始分析'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
94
src/pages/HomePage.tsx
Normal file
94
src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface HomePageProps {
|
||||
onStart: () => void;
|
||||
}
|
||||
|
||||
export function HomePage({ onStart }: HomePageProps) {
|
||||
const [showIntro, setShowIntro] = useState(false);
|
||||
|
||||
return (
|
||||
<main className="page-shell">
|
||||
<div className="hero-section">
|
||||
<header className="hero-header">
|
||||
<span className="brand-tag">东方命理 × 职业选择</span>
|
||||
<h1 className="hero-title">
|
||||
<span className="title-main">天选公司</span>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<p className="hero-description">
|
||||
输入你的出生年月日、时辰、性别与出生地,生成你的职业命理画像,
|
||||
获得更适合你的公司类型与岗位方向建议。
|
||||
</p>
|
||||
|
||||
<div className="hero-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={onStart}
|
||||
>
|
||||
开始测试
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setShowIntro(!showIntro)}
|
||||
>
|
||||
{showIntro ? '收起说明' : '查看产品说明'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showIntro && (
|
||||
<section className="intro-section">
|
||||
<div className="intro-card">
|
||||
<h3>首版能力</h3>
|
||||
<ul className="feature-list">
|
||||
<li>
|
||||
<span className="feature-icon">✦</span>
|
||||
命理职业画像
|
||||
</li>
|
||||
<li>
|
||||
<span className="feature-icon">✦</span>
|
||||
公司类型匹配
|
||||
</li>
|
||||
<li>
|
||||
<span className="feature-icon">✦</span>
|
||||
岗位方向推荐
|
||||
</li>
|
||||
<li>
|
||||
<span className="feature-icon">✦</span>
|
||||
当前阶段择业建议
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="intro-card">
|
||||
<h3>输入信息</h3>
|
||||
<ul className="feature-list">
|
||||
<li>出生年月日</li>
|
||||
<li>出生时辰</li>
|
||||
<li>性别</li>
|
||||
<li>出生地</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="intro-card">
|
||||
<h3>适合人群</h3>
|
||||
<ul className="feature-list">
|
||||
<li>正在找工作的应届生</li>
|
||||
<li>考虑跳槽的职场人</li>
|
||||
<li>对职业方向迷茫的人</li>
|
||||
<li>对命理与职业规划感兴趣的人</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="page-footer">
|
||||
<p>本产品仅供娱乐参考,不构成职业决策依据</p>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
185
src/pages/ResultPage.tsx
Normal file
185
src/pages/ResultPage.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useState } from 'react';
|
||||
import type { UserInput, FortuneResult } from '../types';
|
||||
|
||||
interface ResultPageProps {
|
||||
input: UserInput;
|
||||
result: FortuneResult;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ResultPage({ result, onBack }: ResultPageProps) {
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const { profile, companyRecommendations, jobRecommendations, stageAdvice } = result;
|
||||
|
||||
return (
|
||||
<main className="page-shell">
|
||||
<div className="result-section">
|
||||
<header className="result-header">
|
||||
<button type="button" className="back-btn" onClick={onBack}>
|
||||
← 重新测试
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* 简版结果 */}
|
||||
<section className="result-card summary-card">
|
||||
<div className="summary-header">
|
||||
<span className="summary-label">你的职业命理画像</span>
|
||||
<span className="wuxing-badge">{profile.wuxingLabel} 行</span>
|
||||
</div>
|
||||
|
||||
<div className="summary-content">
|
||||
<p className="summary-text">
|
||||
{profile.personality.join(',')}。
|
||||
你的优势是{profile.strengths.slice(0, 3).join('、')},
|
||||
需要注意{profile.weaknesses[0]}。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="recommendation-grid">
|
||||
<div className="recommendation-item">
|
||||
<span className="rec-label">推荐公司类型</span>
|
||||
<span className="rec-value">{companyRecommendations[0]?.type}</span>
|
||||
</div>
|
||||
<div className="recommendation-item">
|
||||
<span className="rec-label">推荐岗位方向</span>
|
||||
<span className="rec-value">{jobRecommendations[0]?.category}</span>
|
||||
</div>
|
||||
<div className="recommendation-item">
|
||||
<span className="rec-label">当前阶段</span>
|
||||
<span className="rec-value">
|
||||
{profile.developmentRhythm === 'stable' && '稳定积累期'}
|
||||
{profile.developmentRhythm === 'growth' && '成长期'}
|
||||
{profile.developmentRhythm === 'breakthrough' && '突破期'}
|
||||
{profile.developmentRhythm === 'accumulation' && '沉淀期'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 展开详细解读按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-full toggle-detail"
|
||||
onClick={() => setShowDetail(!showDetail)}
|
||||
>
|
||||
{showDetail ? '收起详细解读' : '展开详细解读'}
|
||||
</button>
|
||||
|
||||
{/* 详细解读 */}
|
||||
{showDetail && (
|
||||
<div className="detail-section">
|
||||
{/* 命理画像详情 */}
|
||||
<section className="result-card">
|
||||
<h3 className="section-title">命理职业画像</h3>
|
||||
<div className="detail-grid">
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">五行属性</span>
|
||||
<span className="detail-value">{profile.wuxingLabel} 行</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">性格特征</span>
|
||||
<span className="detail-value">{profile.personality.join('、')}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">核心优势</span>
|
||||
<span className="detail-value">{profile.strengths.join('、')}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">注意短板</span>
|
||||
<span className="detail-value">{profile.weaknesses.join('、')}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">发展节奏</span>
|
||||
<span className="detail-value">
|
||||
{profile.developmentRhythm === 'stable' && '稳定积累期 - 适合稳扎稳打'}
|
||||
{profile.developmentRhythm === 'growth' && '成长期 - 适合寻求突破'}
|
||||
{profile.developmentRhythm === 'breakthrough' && '突破期 - 适合重大改变'}
|
||||
{profile.developmentRhythm === 'accumulation' && '沉淀期 - 需要耐心积累'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 公司推荐详情 */}
|
||||
<section className="result-card">
|
||||
<h3 className="section-title">公司类型推荐</h3>
|
||||
<div className="recommendation-list">
|
||||
{companyRecommendations.map((company, index) => (
|
||||
<div key={index} className="recommendation-row">
|
||||
<div className="rec-rank">{index + 1}</div>
|
||||
<div className="rec-content">
|
||||
<div className="rec-header">
|
||||
<span className="rec-name">{company.type}</span>
|
||||
<span className="rec-score">{company.matchScore}% 匹配</span>
|
||||
</div>
|
||||
<p className="rec-reason">{company.reasons.join(',')}</p>
|
||||
<div className="rec-tags">
|
||||
<span className="tag">适合:{company.suitableFor.join('、')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 岗位推荐详情 */}
|
||||
<section className="result-card">
|
||||
<h3 className="section-title">岗位方向推荐</h3>
|
||||
<div className="recommendation-list">
|
||||
{jobRecommendations.map((job, index) => (
|
||||
<div key={index} className="recommendation-row">
|
||||
<div className="rec-rank">{index + 1}</div>
|
||||
<div className="rec-content">
|
||||
<div className="rec-header">
|
||||
<span className="rec-name">{job.category}</span>
|
||||
<span className="rec-score">{job.matchScore}% 匹配</span>
|
||||
</div>
|
||||
<p className="rec-reason">{job.reasons.join(',')}</p>
|
||||
<div className="rec-tags">
|
||||
<span className="tag">适合:{job.suitableWorkStyle.join('、')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 当前阶段建议 */}
|
||||
<section className="result-card">
|
||||
<h3 className="section-title">当前阶段建议</h3>
|
||||
<div className="stage-advice">
|
||||
<p className="advice-overview">{stageAdvice.overall}</p>
|
||||
<div className="advice-items">
|
||||
<div className="advice-item">
|
||||
<span className="advice-label">职业方向</span>
|
||||
<span className="advice-value">{stageAdvice.careerDirection}</span>
|
||||
</div>
|
||||
<div className="advice-item">
|
||||
<span className="advice-label">变动时机</span>
|
||||
<span className="advice-value">{stageAdvice.jobChangeTiming}</span>
|
||||
</div>
|
||||
<div className="advice-item">
|
||||
<span className="advice-label">平台偏好</span>
|
||||
<span className="advice-value">{stageAdvice.platformPreference}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="key-observations">
|
||||
<h4>重点观察</h4>
|
||||
<ul>
|
||||
{stageAdvice.keyObservations.map((obs, i) => (
|
||||
<li key={i}>{obs}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="result-footer">
|
||||
<p>本结果仅供娱乐参考,不构成职业决策依据</p>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
768
src/styles/index.css
Normal file
768
src/styles/index.css
Normal file
@@ -0,0 +1,768 @@
|
||||
:root {
|
||||
/* 字体系统 */
|
||||
--font-display: "Noto Serif SC", "Source Han Serif SC", "Songti SC", serif;
|
||||
--font-body: "Noto Sans SC", "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
|
||||
/* 色彩体系 - 东方命理气质 */
|
||||
--color-bg-deep: #08090c;
|
||||
--color-bg-primary: #0c0f14;
|
||||
--color-bg-card: rgba(18, 22, 28, 0.92);
|
||||
--color-bg-elevated: rgba(26, 31, 40, 0.88);
|
||||
|
||||
--color-gold-primary: #d6b47a;
|
||||
--color-gold-light: #e8cda0;
|
||||
--color-gold-dark: #a67c3d;
|
||||
--color-gold-muted: rgba(214, 180, 122, 0.18);
|
||||
|
||||
--color-text-primary: #f4efe6;
|
||||
--color-text-secondary: rgba(244, 239, 230, 0.78);
|
||||
--color-text-muted: rgba(244, 239, 230, 0.52);
|
||||
|
||||
--color-border: rgba(214, 180, 122, 0.14);
|
||||
--color-border-hover: rgba(214, 180, 122, 0.32);
|
||||
|
||||
/* 间距系统 */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
--space-2xl: 48px;
|
||||
--space-3xl: 64px;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 20px;
|
||||
--radius-xl: 28px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-card: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
--shadow-elevated: 0 16px 48px rgba(0, 0, 0, 0.5), 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* 动画 */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-base: 0.25s ease;
|
||||
--transition-slow: 0.4s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background: var(--color-bg-deep);
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 背景氛围 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(166, 116, 53, 0.12) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 85% 80%, rgba(139, 90, 43, 0.08) 0%, transparent 50%),
|
||||
linear-gradient(180deg, var(--color-bg-deep) 0%, var(--color-bg-primary) 100%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.page-shell {
|
||||
width: min(960px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: var(--space-2xl) 0 var(--space-3xl);
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 通用按钮 */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
padding: 14px 28px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-gold-primary) 0%, var(--color-gold-dark) 100%);
|
||||
color: #0a0b0e;
|
||||
box-shadow: 0 4px 16px rgba(214, 180, 122, 0.25);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 24px rgba(214, 180, 122, 0.35);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
border-color: var(--color-border-hover);
|
||||
background: var(--color-gold-muted);
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 首页样式 */
|
||||
.hero-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
.hero-header {
|
||||
text-align: center;
|
||||
padding: var(--space-xl) 0;
|
||||
}
|
||||
|
||||
.brand-tag {
|
||||
display: inline-block;
|
||||
color: var(--color-gold-primary);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(48px, 10vw, 84px);
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.title-main {
|
||||
background: linear-gradient(135deg, var(--color-gold-light) 0%, var(--color-gold-primary) 50%, var(--color-gold-dark) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 17px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 产品说明区域 */
|
||||
.intro-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: var(--space-md);
|
||||
margin-top: var(--space-lg);
|
||||
animation: slideUp 0.5s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.intro-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-lg);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.intro-card h3 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 18px;
|
||||
color: var(--color-gold-primary);
|
||||
margin-bottom: var(--space-md);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
color: var(--color-gold-primary);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* 页脚 */
|
||||
.page-footer {
|
||||
text-align: center;
|
||||
margin-top: var(--space-3xl);
|
||||
padding-top: var(--space-xl);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.page-footer p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ==== 表单页样式 ==== */
|
||||
.form-section {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
border-radius: var(--radius-full);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: var(--color-gold-primary);
|
||||
background: var(--color-gold-muted);
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--color-gold-primary);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 14px 16px;
|
||||
font-size: 15px;
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-gold-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-gold-muted);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23d6b47a' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 14px center;
|
||||
padding-right: 44px;
|
||||
}
|
||||
|
||||
/* 单选按钮 */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.radio-label input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.radio-custom {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.radio-label input:checked + .radio-custom {
|
||||
border-color: var(--color-gold-primary);
|
||||
}
|
||||
|
||||
.radio-label input:checked + .radio-custom::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
background: var(--color-gold-primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.form-notice {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-notice p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
||||
50% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* ==== 结果页样式 ==== */
|
||||
.result-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* 简版结果卡片 */
|
||||
.summary-card {
|
||||
background: linear-gradient(135deg, rgba(214, 180, 122, 0.08) 0%, rgba(18, 22, 28, 0.92) 100%);
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-gold-primary);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wuxing-badge {
|
||||
background: var(--color-gold-muted);
|
||||
color: var(--color-gold-primary);
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 推荐网格 */
|
||||
.recommendation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.recommendation-item {
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rec-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.rec-value {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-gold-primary);
|
||||
}
|
||||
|
||||
/* 展开按钮 */
|
||||
.toggle-detail {
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
/* 详细解读区域 */
|
||||
.detail-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
animation: slideUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--color-gold-primary);
|
||||
margin-bottom: var(--space-lg);
|
||||
padding-bottom: var(--space-sm);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 详情网格 */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 推荐列表 */
|
||||
.recommendation-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.recommendation-row {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.recommendation-row:hover {
|
||||
background: rgba(214, 180, 122, 0.06);
|
||||
}
|
||||
|
||||
.rec-rank {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-gold-muted);
|
||||
color: var(--color-gold-primary);
|
||||
border-radius: 50%;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rec-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rec-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.rec-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rec-score {
|
||||
color: var(--color-gold-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rec-reason {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.rec-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--color-gold-muted);
|
||||
color: var(--color-text-secondary);
|
||||
padding: 3px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 阶段建议 */
|
||||
.stage-advice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.advice-overview {
|
||||
font-size: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.7;
|
||||
padding: var(--space-md);
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 3px solid var(--color-gold-primary);
|
||||
}
|
||||
|
||||
.advice-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.advice-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-md);
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.advice-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.advice-value {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.key-observations h4 {
|
||||
font-size: 14px;
|
||||
color: var(--color-gold-primary);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.key-observations ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.key-observations li {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
padding-left: var(--space-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.key-observations li::before {
|
||||
content: '→';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-gold-primary);
|
||||
}
|
||||
|
||||
/* 结果页脚 */
|
||||
.result-footer {
|
||||
text-align: center;
|
||||
margin-top: var(--space-lg);
|
||||
padding-top: var(--space-lg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.result-footer p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.page-shell {
|
||||
padding: var(--space-lg) 0 var(--space-2xl);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 42px;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.intro-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recommendation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.result-card {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
47
src/types/index.ts
Normal file
47
src/types/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface UserInput {
|
||||
birthDate: string; // YYYY-MM-DD
|
||||
birthTime: string; // HH:mm
|
||||
gender: 'male' | 'female';
|
||||
birthPlace: string;
|
||||
}
|
||||
|
||||
export interface FortuneProfile {
|
||||
wuxing: 'wood' | 'fire' | 'earth' | 'metal' | 'water';
|
||||
wuxingLabel: string;
|
||||
personality: string[];
|
||||
strengths: string[];
|
||||
weaknesses: string[];
|
||||
developmentRhythm: 'stable' | 'growth' | 'breakthrough' | 'accumulation';
|
||||
}
|
||||
|
||||
export interface CompanyRecommendation {
|
||||
type: string;
|
||||
matchScore: number;
|
||||
reasons: string[];
|
||||
suitableFor: string[];
|
||||
notSuitableFor: string[];
|
||||
stageAdvice: string;
|
||||
}
|
||||
|
||||
export interface JobRecommendation {
|
||||
category: string;
|
||||
matchScore: number;
|
||||
reasons: string[];
|
||||
suitableWorkStyle: string[];
|
||||
notSuitableFor: string[];
|
||||
}
|
||||
|
||||
export interface StageAdvice {
|
||||
overall: string;
|
||||
careerDirection: string;
|
||||
jobChangeTiming: string;
|
||||
platformPreference: string;
|
||||
keyObservations: string[];
|
||||
}
|
||||
|
||||
export interface FortuneResult {
|
||||
profile: FortuneProfile;
|
||||
companyRecommendations: CompanyRecommendation[];
|
||||
jobRecommendations: JobRecommendation[];
|
||||
stageAdvice: StageAdvice;
|
||||
}
|
||||
21
tsconfig.app.json
Normal file
21
tsconfig.app.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
1
tsconfig.app.tsbuildinfo
Normal file
1
tsconfig.app.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/lib/fortune.ts","./src/pages/formpage.tsx","./src/pages/homepage.tsx","./src/pages/resultpage.tsx","./src/types/index.ts"],"version":"5.9.3"}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
1
tsconfig.node.tsbuildinfo
Normal file
1
tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
2
vite.config.d.ts
vendored
Normal file
2
vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vite").UserConfig;
|
||||
export default _default;
|
||||
5
vite.config.js
Normal file
5
vite.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
Reference in New Issue
Block a user