feat: 初始化天选公司 MVP

- 首页、信息录入页、结果页完整流程
- 命理分析规则引擎(五行计算)
- 公司类型与岗位方向匹配
- 响应式东方命理风格 UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jowe
2026-03-23 23:47:44 +08:00
parent fe531af56c
commit 9dfa2a9532
23 changed files with 3655 additions and 2 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
# 预留后续环境变量
# VITE_API_BASE_URL=

9
.gitignore vendored Normal file
View 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
View 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. 推荐文案模板设计

View File

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

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

File diff suppressed because one or more lines are too long

2
vite.config.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare const _default: import("vite").UserConfig;
export default _default;

5
vite.config.js Normal file
View 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
View File

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});