mirror of
https://github.com/patdelphi/suanming.git
synced 2026-02-27 21:23:12 +08:00
Merge dev branch: Complete UI optimization with Chinese design system
- Implement comprehensive Chinese-style component library - Add unified typography system with semantic font classes - Optimize all pages with responsive design and Chinese aesthetics - Fix button styling and enhance user experience - Add loading states, empty states, and toast notifications - Complete 12 Palaces Details optimization - Establish consistent color scheme and visual hierarchy
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -47,4 +47,6 @@ temp/
|
|||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
numerology.db-shm
|
||||||
|
numerology.db-wal
|
||||||
|
|||||||
265
TYPOGRAPHY_GUIDE.md
Normal file
265
TYPOGRAPHY_GUIDE.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# 神机阁字体规范指南
|
||||||
|
|
||||||
|
## 📖 概述
|
||||||
|
|
||||||
|
本指南定义了神机阁项目的统一字体规范系统,旨在解决项目中字体大小混乱的问题,建立一致的视觉层次和用户体验。
|
||||||
|
|
||||||
|
## 🎯 设计原则
|
||||||
|
|
||||||
|
1. **语义化命名** - 使用语义化的类名,如 `text-heading-lg` 而不是 `text-2xl`
|
||||||
|
2. **响应式优先** - 所有字体规范都考虑了移动端适配
|
||||||
|
3. **可访问性** - 确保足够的对比度和可读性
|
||||||
|
4. **一致性** - 统一的行高、字重和字间距规范
|
||||||
|
5. **中文优化** - 专门为中文内容优化的字体栈
|
||||||
|
|
||||||
|
## 📏 字体规范系统
|
||||||
|
|
||||||
|
### 🏆 显示级标题 (Display)
|
||||||
|
用于首页主标题和重要页面的超大标题
|
||||||
|
|
||||||
|
```css
|
||||||
|
.text-display-xl /* 56px - 首页主标题 */
|
||||||
|
.text-display-lg /* 48px - 重要页面标题 */
|
||||||
|
.text-display-md /* 40px - 次要页面标题 */
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
- 首页 Hero 区域主标题
|
||||||
|
- 重要功能页面的主标题
|
||||||
|
- 营销页面的大标题
|
||||||
|
|
||||||
|
### 📝 标题系列 (Heading)
|
||||||
|
用于页面内容的层次化标题
|
||||||
|
|
||||||
|
```css
|
||||||
|
.text-heading-xl /* 32px - H1 标题 */
|
||||||
|
.text-heading-lg /* 28px - H2 标题 */
|
||||||
|
.text-heading-md /* 24px - H3 标题 */
|
||||||
|
.text-heading-sm /* 20px - H4 标题 */
|
||||||
|
.text-heading-xs /* 18px - H5 标题 */
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
- 页面主标题 (H1)
|
||||||
|
- 章节标题 (H2-H3)
|
||||||
|
- 卡片标题 (H4-H5)
|
||||||
|
- 组件内部标题
|
||||||
|
|
||||||
|
### 📄 正文系列 (Body)
|
||||||
|
用于页面的主要内容文字
|
||||||
|
|
||||||
|
```css
|
||||||
|
.text-body-xl /* 18px - 重要描述文字 */
|
||||||
|
.text-body-lg /* 16px - 标准正文 (默认) */
|
||||||
|
.text-body-md /* 14px - 次要正文 */
|
||||||
|
.text-body-sm /* 12px - 辅助信息 */
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
- 重要的介绍文字 (body-xl)
|
||||||
|
- 标准正文内容 (body-lg)
|
||||||
|
- 卡片描述文字 (body-md)
|
||||||
|
- 提示信息、时间戳 (body-sm)
|
||||||
|
|
||||||
|
### 🏷️ 标签系列 (Label)
|
||||||
|
用于表单标签和小标签
|
||||||
|
|
||||||
|
```css
|
||||||
|
.text-label-lg /* 14px - 表单标签 */
|
||||||
|
.text-label-md /* 12px - 小标签 */
|
||||||
|
.text-label-sm /* 11px - 微小标签 */
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
- 表单字段标签
|
||||||
|
- 状态标签
|
||||||
|
- 分类标签
|
||||||
|
- 徽章文字
|
||||||
|
|
||||||
|
### 🔘 按钮系列 (Button)
|
||||||
|
用于按钮内的文字
|
||||||
|
|
||||||
|
```css
|
||||||
|
.text-button-lg /* 16px - 大按钮 */
|
||||||
|
.text-button-md /* 14px - 标准按钮 */
|
||||||
|
.text-button-sm /* 12px - 小按钮 */
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
- 主要操作按钮 (button-lg)
|
||||||
|
- 标准按钮 (button-md)
|
||||||
|
- 次要按钮和图标按钮 (button-sm)
|
||||||
|
|
||||||
|
## 🎨 字体族规范
|
||||||
|
|
||||||
|
### 中文字体栈
|
||||||
|
```css
|
||||||
|
.font-chinese
|
||||||
|
/* 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', 'Noto Sans SC', 'STHeiti', 'WenQuanYi Micro Hei', sans-serif */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 中文衬线字体
|
||||||
|
```css
|
||||||
|
.font-chinese-serif
|
||||||
|
/* 'Noto Serif SC', 'STSong', 'SimSun', '宋体', serif */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 英文字体
|
||||||
|
```css
|
||||||
|
.font-english
|
||||||
|
/* 'Inter', 'Helvetica Neue', 'Arial', sans-serif */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数字字体
|
||||||
|
```css
|
||||||
|
.font-numeric
|
||||||
|
/* 确保数字对齐的等宽字体 */
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 响应式规范
|
||||||
|
|
||||||
|
所有字体规范都内置了响应式适配:
|
||||||
|
|
||||||
|
- **移动端 (≤768px)**: 字体大小适当缩小
|
||||||
|
- **超小屏 (≤480px)**: 进一步优化字体大小
|
||||||
|
- **桌面端 (>768px)**: 使用标准字体大小
|
||||||
|
|
||||||
|
## 🔧 使用方法
|
||||||
|
|
||||||
|
### 1. HTML 中使用
|
||||||
|
```html
|
||||||
|
<!-- 页面主标题 -->
|
||||||
|
<h1 class="text-heading-xl font-chinese text-red-600">神机阁</h1>
|
||||||
|
|
||||||
|
<!-- 卡片标题 -->
|
||||||
|
<h3 class="text-heading-sm font-chinese">八字命理</h3>
|
||||||
|
|
||||||
|
<!-- 正文内容 -->
|
||||||
|
<p class="text-body-lg font-chinese">这是标准正文内容</p>
|
||||||
|
|
||||||
|
<!-- 按钮文字 -->
|
||||||
|
<button class="text-button-md font-chinese">立即分析</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. React 组件中使用
|
||||||
|
```jsx
|
||||||
|
// 推荐:使用语义化类名
|
||||||
|
<ChineseCardTitle className="text-heading-md font-chinese">
|
||||||
|
十二宫位详解
|
||||||
|
</ChineseCardTitle>
|
||||||
|
|
||||||
|
// 避免:使用数字类名
|
||||||
|
<ChineseCardTitle className="text-xl">
|
||||||
|
十二宫位详解
|
||||||
|
</ChineseCardTitle>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CSS 中使用
|
||||||
|
```css
|
||||||
|
/* 自定义组件样式 */
|
||||||
|
.custom-title {
|
||||||
|
@apply text-heading-lg font-chinese font-semibold;
|
||||||
|
color: var(--cinnabar-500);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 迁移指南
|
||||||
|
|
||||||
|
### 常见替换映射
|
||||||
|
|
||||||
|
| 旧类名 | 新类名 | 用途 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `text-3xl` | `text-display-md` | 页面主标题 |
|
||||||
|
| `text-2xl` | `text-heading-xl` | 大标题 |
|
||||||
|
| `text-xl` | `text-heading-lg` | 中标题 |
|
||||||
|
| `text-lg` | `text-heading-md` | 小标题 |
|
||||||
|
| `text-base` | `text-body-lg` | 标准正文 |
|
||||||
|
| `text-sm` | `text-body-md` | 次要正文 |
|
||||||
|
| `text-xs` | `text-body-sm` | 辅助信息 |
|
||||||
|
|
||||||
|
### 迁移步骤
|
||||||
|
|
||||||
|
1. **识别用途** - 确定文字的语义用途(标题、正文、标签等)
|
||||||
|
2. **选择规范** - 根据用途选择对应的字体规范
|
||||||
|
3. **添加字体族** - 确保添加 `font-chinese` 类
|
||||||
|
4. **测试响应式** - 在不同设备上测试显示效果
|
||||||
|
|
||||||
|
## ✅ 最佳实践
|
||||||
|
|
||||||
|
### ✅ 推荐做法
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 语义化命名 -->
|
||||||
|
<h1 class="text-display-lg font-chinese">神机阁</h1>
|
||||||
|
<h2 class="text-heading-xl font-chinese">命理分析</h2>
|
||||||
|
<p class="text-body-lg font-chinese">专业的命理分析服务</p>
|
||||||
|
<button class="text-button-md font-chinese">开始分析</button>
|
||||||
|
|
||||||
|
<!-- 数字内容使用数字字体 -->
|
||||||
|
<span class="text-body-lg font-numeric">2024</span>
|
||||||
|
|
||||||
|
<!-- 特殊标题使用衬线字体 -->
|
||||||
|
<h1 class="text-display-xl font-chinese-serif">神机阁</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ 避免做法
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 避免:混用数字类名 -->
|
||||||
|
<h1 class="text-4xl">标题</h1>
|
||||||
|
<p class="text-lg">正文</p>
|
||||||
|
|
||||||
|
<!-- 避免:忘记添加字体族 -->
|
||||||
|
<h1 class="text-heading-xl">标题</h1>
|
||||||
|
|
||||||
|
<!-- 避免:不合适的字体大小组合 -->
|
||||||
|
<button class="text-xs">按钮</button> <!-- 太小 -->
|
||||||
|
<p class="text-3xl">正文</p> <!-- 太大 -->
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 常见问题
|
||||||
|
|
||||||
|
### Q: 什么时候使用 display 系列?
|
||||||
|
A: 仅用于首页主标题和重要页面的超大标题,不要在普通内容中使用。
|
||||||
|
|
||||||
|
### Q: 如何选择合适的字体大小?
|
||||||
|
A: 根据内容的重要性和层次选择:
|
||||||
|
- 页面标题 → heading 系列
|
||||||
|
- 正文内容 → body 系列
|
||||||
|
- 表单标签 → label 系列
|
||||||
|
- 按钮文字 → button 系列
|
||||||
|
|
||||||
|
### Q: 移动端需要特殊处理吗?
|
||||||
|
A: 不需要,所有字体规范都内置了响应式适配。
|
||||||
|
|
||||||
|
### Q: 可以自定义字体大小吗?
|
||||||
|
A: 不推荐。如果确实需要,请先考虑是否可以使用现有规范,或者提出新的规范需求。
|
||||||
|
|
||||||
|
## 📊 字体规范对照表
|
||||||
|
|
||||||
|
| 类名 | 桌面端 | 移动端 | 行高 | 字重 | 用途 |
|
||||||
|
|------|--------|--------|------|------|------|
|
||||||
|
| `text-display-xl` | 56px | 40px | 1.1 | 800 | 首页主标题 |
|
||||||
|
| `text-display-lg` | 48px | 36px | 1.1 | 700 | 重要页面标题 |
|
||||||
|
| `text-display-md` | 40px | 32px | 1.2 | 700 | 次要页面标题 |
|
||||||
|
| `text-heading-xl` | 32px | 28px | 1.25 | 600 | H1 标题 |
|
||||||
|
| `text-heading-lg` | 28px | 24px | 1.3 | 600 | H2 标题 |
|
||||||
|
| `text-heading-md` | 24px | 20px | 1.35 | 600 | H3 标题 |
|
||||||
|
| `text-heading-sm` | 20px | 18px | 1.4 | 600 | H4 标题 |
|
||||||
|
| `text-heading-xs` | 18px | 16px | 1.4 | 600 | H5 标题 |
|
||||||
|
| `text-body-xl` | 18px | 16px | 1.6 | 400 | 重要描述 |
|
||||||
|
| `text-body-lg` | 16px | 16px | 1.6 | 400 | 标准正文 |
|
||||||
|
| `text-body-md` | 14px | 14px | 1.6 | 400 | 次要正文 |
|
||||||
|
| `text-body-sm` | 12px | 12px | 1.5 | 400 | 辅助信息 |
|
||||||
|
| `text-label-lg` | 14px | 14px | 1.4 | 500 | 表单标签 |
|
||||||
|
| `text-label-md` | 12px | 12px | 1.4 | 500 | 小标签 |
|
||||||
|
| `text-label-sm` | 11px | 11px | 1.4 | 500 | 微小标签 |
|
||||||
|
| `text-button-lg` | 16px | 16px | 1.4 | 600 | 大按钮 |
|
||||||
|
| `text-button-md` | 14px | 14px | 1.4 | 600 | 标准按钮 |
|
||||||
|
| `text-button-sm` | 12px | 12px | 1.4 | 600 | 小按钮 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**更新日期**: 2024年12月
|
||||||
|
**版本**: 1.0.0
|
||||||
|
**维护者**: 神机阁开发团队
|
||||||
208
UI_OPTIMIZATION_PLAN.md
Normal file
208
UI_OPTIMIZATION_PLAN.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# 神机阁 UI 优化计划
|
||||||
|
|
||||||
|
## 🎯 优化目标
|
||||||
|
|
||||||
|
- 解决移动端顶部字体溢出问题
|
||||||
|
- 建立统一的中式风格设计系统
|
||||||
|
- 实现完美的桌面端与移动端自适应
|
||||||
|
- 创造优雅古典的用户体验
|
||||||
|
- 优化色彩搭配和视觉层次
|
||||||
|
|
||||||
|
## 📋 当前问题分析
|
||||||
|
|
||||||
|
### 1. 移动端问题
|
||||||
|
- 顶部导航字体溢出
|
||||||
|
- 布局在小屏幕上不合理
|
||||||
|
- 触摸交互体验不佳
|
||||||
|
|
||||||
|
### 2. 整体设计问题
|
||||||
|
- 字体大小不统一
|
||||||
|
- 缺乏视觉层次
|
||||||
|
- 色彩搭配不和谐
|
||||||
|
- 缺乏中式设计元素的统一性
|
||||||
|
|
||||||
|
## 🎨 设计系统规划
|
||||||
|
|
||||||
|
### 1. 色彩系统 (中式古典配色)
|
||||||
|
|
||||||
|
#### 主色调
|
||||||
|
- **朱砂红**: `#DC143C` - 主要强调色,用于重要按钮和标题
|
||||||
|
- **金黄色**: `#FFD700` - 辅助色,用于装饰和高亮
|
||||||
|
- **墨黑色**: `#2C2C2C` - 主要文字色
|
||||||
|
- **古纸色**: `#F5F5DC` - 背景色
|
||||||
|
- **青灰色**: `#708090` - 次要文字色
|
||||||
|
|
||||||
|
#### 渐变背景
|
||||||
|
- **主背景**: 从古纸色到淡金色的渐变
|
||||||
|
- **卡片背景**: 半透明白色覆盖
|
||||||
|
- **按钮渐变**: 朱砂红到深红的渐变
|
||||||
|
|
||||||
|
### 2. 字体系统
|
||||||
|
|
||||||
|
#### 字体族
|
||||||
|
```css
|
||||||
|
/* 中文优先字体栈 */
|
||||||
|
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', 'STHeiti', 'WenQuanYi Micro Hei', sans-serif;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字体大小规范
|
||||||
|
- **标题 H1**: 2.5rem (40px) / 移动端 2rem (32px)
|
||||||
|
- **标题 H2**: 2rem (32px) / 移动端 1.75rem (28px)
|
||||||
|
- **标题 H3**: 1.5rem (24px) / 移动端 1.25rem (20px)
|
||||||
|
- **正文**: 1rem (16px) / 移动端 0.875rem (14px)
|
||||||
|
- **小字**: 0.875rem (14px) / 移动端 0.75rem (12px)
|
||||||
|
|
||||||
|
#### 行高规范
|
||||||
|
- **标题**: 1.2
|
||||||
|
- **正文**: 1.6
|
||||||
|
- **按钮文字**: 1.4
|
||||||
|
|
||||||
|
### 3. 间距系统
|
||||||
|
|
||||||
|
#### 基础间距单位 (基于 4px)
|
||||||
|
- **xs**: 0.25rem (4px)
|
||||||
|
- **sm**: 0.5rem (8px)
|
||||||
|
- **md**: 1rem (16px)
|
||||||
|
- **lg**: 1.5rem (24px)
|
||||||
|
- **xl**: 2rem (32px)
|
||||||
|
- **2xl**: 3rem (48px)
|
||||||
|
|
||||||
|
### 4. 组件设计规范
|
||||||
|
|
||||||
|
#### 按钮设计
|
||||||
|
- **主按钮**: 朱砂红背景,金色边框,白色文字
|
||||||
|
- **次按钮**: 透明背景,朱砂红边框,朱砂红文字
|
||||||
|
- **圆角**: 8px
|
||||||
|
- **高度**: 44px (移动端友好)
|
||||||
|
- **内边距**: 16px 24px
|
||||||
|
|
||||||
|
#### 卡片设计
|
||||||
|
- **背景**: 半透明白色 rgba(255,255,255,0.9)
|
||||||
|
- **边框**: 1px solid rgba(220,20,60,0.2)
|
||||||
|
- **圆角**: 12px
|
||||||
|
- **阴影**: 0 4px 20px rgba(0,0,0,0.1)
|
||||||
|
- **内边距**: 24px
|
||||||
|
|
||||||
|
#### 输入框设计
|
||||||
|
- **背景**: 白色
|
||||||
|
- **边框**: 1px solid #E5E5E5
|
||||||
|
- **聚焦边框**: 2px solid #DC143C
|
||||||
|
- **圆角**: 6px
|
||||||
|
- **高度**: 44px
|
||||||
|
- **内边距**: 12px 16px
|
||||||
|
|
||||||
|
## 📱 响应式设计策略
|
||||||
|
|
||||||
|
### 1. 断点设置
|
||||||
|
```css
|
||||||
|
/* 移动端 */
|
||||||
|
@media (max-width: 768px) { ... }
|
||||||
|
|
||||||
|
/* 平板端 */
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) { ... }
|
||||||
|
|
||||||
|
/* 桌面端 */
|
||||||
|
@media (min-width: 1025px) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 移动端优化
|
||||||
|
- **导航栏**: 汉堡菜单,避免文字溢出
|
||||||
|
- **字体缩放**: 移动端字体适当缩小
|
||||||
|
- **触摸目标**: 最小 44px × 44px
|
||||||
|
- **间距调整**: 移动端间距适当减小
|
||||||
|
|
||||||
|
### 3. 布局策略
|
||||||
|
- **桌面端**: 多列布局,侧边栏导航
|
||||||
|
- **移动端**: 单列布局,底部导航
|
||||||
|
- **弹性布局**: 使用 Flexbox 和 Grid
|
||||||
|
|
||||||
|
## 🎭 中式设计元素
|
||||||
|
|
||||||
|
### 1. 装饰元素
|
||||||
|
- **云纹图案**: 作为背景装饰
|
||||||
|
- **回纹边框**: 用于卡片和分割线
|
||||||
|
- **印章样式**: 用于重要标识
|
||||||
|
- **书法笔触**: 用于标题装饰
|
||||||
|
|
||||||
|
### 2. 图标设计
|
||||||
|
- **八卦图标**: 用于导航和功能区分
|
||||||
|
- **中式符号**: 太极、阴阳、五行元素
|
||||||
|
- **传统色彩**: 朱砂、金黄、墨色
|
||||||
|
|
||||||
|
### 3. 动画效果
|
||||||
|
- **淡入淡出**: 页面切换动画
|
||||||
|
- **缓动函数**: cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
- **悬停效果**: 轻微的缩放和阴影变化
|
||||||
|
|
||||||
|
## 🔧 技术实现方案
|
||||||
|
|
||||||
|
### 1. CSS 架构
|
||||||
|
- **原子化 CSS**: 使用 Tailwind CSS
|
||||||
|
- **自定义主题**: 扩展 Tailwind 配置
|
||||||
|
- **组件样式**: CSS Modules 或 Styled Components
|
||||||
|
|
||||||
|
### 2. 响应式实现
|
||||||
|
- **移动优先**: Mobile-first 设计
|
||||||
|
- **弹性单位**: rem, em, vw, vh
|
||||||
|
- **媒体查询**: 断点式响应
|
||||||
|
|
||||||
|
### 3. 性能优化
|
||||||
|
- **字体优化**: 字体子集化,预加载
|
||||||
|
- **图片优化**: WebP 格式,懒加载
|
||||||
|
- **CSS 优化**: 关键 CSS 内联
|
||||||
|
|
||||||
|
## 📋 实施计划
|
||||||
|
|
||||||
|
### 阶段一:基础设施 (1-2天)
|
||||||
|
1. 更新 Tailwind 配置,添加中式主题
|
||||||
|
2. 创建设计系统组件库
|
||||||
|
3. 建立响应式断点系统
|
||||||
|
|
||||||
|
### 阶段二:核心组件 (2-3天)
|
||||||
|
1. 重构导航栏组件 (解决移动端溢出)
|
||||||
|
2. 优化按钮和表单组件
|
||||||
|
3. 重设计卡片和布局组件
|
||||||
|
|
||||||
|
### 阶段三:页面优化 (3-4天)
|
||||||
|
1. 首页重新设计
|
||||||
|
2. 分析页面布局优化
|
||||||
|
3. 历史记录页面改进
|
||||||
|
4. 用户中心页面美化
|
||||||
|
|
||||||
|
### 阶段四:细节完善 (1-2天)
|
||||||
|
1. 动画效果添加
|
||||||
|
2. 交互细节优化
|
||||||
|
3. 跨设备测试
|
||||||
|
4. 性能优化
|
||||||
|
|
||||||
|
## 🎯 预期效果
|
||||||
|
|
||||||
|
### 视觉效果
|
||||||
|
- 统一的中式古典风格
|
||||||
|
- 和谐的色彩搭配
|
||||||
|
- 清晰的视觉层次
|
||||||
|
- 优雅的动画过渡
|
||||||
|
|
||||||
|
### 用户体验
|
||||||
|
- 完美的移动端适配
|
||||||
|
- 直观的导航体验
|
||||||
|
- 舒适的阅读体验
|
||||||
|
- 流畅的交互反馈
|
||||||
|
|
||||||
|
### 技术指标
|
||||||
|
- 移动端适配率 100%
|
||||||
|
- 页面加载速度 < 2s
|
||||||
|
- 交互响应时间 < 100ms
|
||||||
|
- 跨浏览器兼容性 95%+
|
||||||
|
|
||||||
|
## 📝 验收标准
|
||||||
|
|
||||||
|
1. **移动端测试**: 在各种移动设备上无溢出、布局正常
|
||||||
|
2. **响应式测试**: 在不同屏幕尺寸下表现良好
|
||||||
|
3. **视觉一致性**: 所有页面遵循统一设计规范
|
||||||
|
4. **性能测试**: 页面加载和交互性能达标
|
||||||
|
5. **用户体验**: 导航清晰、操作便捷、视觉舒适
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*此计划将分阶段实施,每个阶段完成后进行测试和调整,确保最终效果符合预期。*
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# PowerShell script to create template files for all remaining Edge Functions
|
|
||||||
|
|
||||||
$functions = @(
|
|
||||||
'numerology-analysis',
|
|
||||||
'reading-history',
|
|
||||||
'yijing-analyzer',
|
|
||||||
'bazi-analysis',
|
|
||||||
'ziwei-analysis',
|
|
||||||
'yijing-analysis',
|
|
||||||
'create-user-simple',
|
|
||||||
'create-admin-user',
|
|
||||||
'custom-auth',
|
|
||||||
'profile-manager',
|
|
||||||
'wuxing-analysis',
|
|
||||||
'bazi-detail-analysis',
|
|
||||||
'bazi-wuxing-analysis',
|
|
||||||
'bazi-details'
|
|
||||||
)
|
|
||||||
|
|
||||||
$template = @'
|
|
||||||
// Supabase Edge Function: {0}
|
|
||||||
// TODO: Copy the actual code from Supabase Dashboard
|
|
||||||
|
|
||||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
|
||||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
|
||||||
|
|
||||||
const corsHeaders = {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
||||||
}
|
|
||||||
|
|
||||||
serve(async (req) => {
|
|
||||||
// Handle CORS preflight requests
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
return new Response('ok', { headers: corsHeaders })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO: Replace with actual implementation from Dashboard
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ message: '{0} function - replace with actual code' }),
|
|
||||||
{
|
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: error.message }),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
'@
|
|
||||||
|
|
||||||
foreach ($func in $functions) {
|
|
||||||
$functionName = $func -replace '-', ' ' | ForEach-Object { (Get-Culture).TextInfo.ToTitleCase($_) }
|
|
||||||
$content = $template -f $functionName, $func
|
|
||||||
$filePath = "supabase\functions\$func\index.ts"
|
|
||||||
|
|
||||||
if (!(Test-Path $filePath)) {
|
|
||||||
$content | Out-File -FilePath $filePath -Encoding UTF8
|
|
||||||
Write-Host "Created: $filePath"
|
|
||||||
} else {
|
|
||||||
Write-Host "Already exists: $filePath"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "All function templates created successfully!"
|
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "node server/index.cjs",
|
"start": "node server/index.cjs",
|
||||||
"db:init": "node server/scripts/initDatabase.cjs"
|
"db:init": "node server/scripts/initDatabase.cjs",
|
||||||
|
"test": "node tests/integration.test.cjs",
|
||||||
|
"test:integration": "node tests/integration.test.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
@@ -58,7 +60,6 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.364.0",
|
"lucide-react": "^0.364.0",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"node-fetch": "^2.7.0",
|
|
||||||
"nodemon": "^3.0.2",
|
"nodemon": "^3.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.14",
|
"eslint-plugin-react-refresh": "^0.4.14",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.12.0",
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
"tailwindcss": "v3.4.16",
|
"tailwindcss": "v3.4.16",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -137,9 +137,6 @@ importers:
|
|||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.4
|
specifier: ^0.4.4
|
||||||
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
node-fetch:
|
|
||||||
specifier: ^2.7.0
|
|
||||||
version: 2.7.0
|
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.1.10
|
version: 3.1.10
|
||||||
@@ -225,6 +222,9 @@ importers:
|
|||||||
globals:
|
globals:
|
||||||
specifier: ^15.12.0
|
specifier: ^15.12.0
|
||||||
version: 15.15.0
|
version: 15.15.0
|
||||||
|
node-fetch:
|
||||||
|
specifier: ^2.7.0
|
||||||
|
version: 2.7.0
|
||||||
postcss:
|
postcss:
|
||||||
specifier: 8.4.49
|
specifier: 8.4.49
|
||||||
version: 8.4.49
|
version: 8.4.49
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ const BaziAnalysisDisplay: React.FC<BaziAnalysisDisplayProps> = ({ birthDate })
|
|||||||
{/* 天干信息 */}
|
{/* 天干信息 */}
|
||||||
<div className="bg-gradient-to-r from-red-50 to-yellow-50 rounded-lg p-3">
|
<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>
|
<h4 className="font-bold text-red-700 mb-2">天干:{pillar.tiangan}</h4>
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
|
||||||
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.tianganWuxing]}`}>
|
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.tianganWuxing]}`}>
|
||||||
五行:{pillar.tianganWuxing}
|
五行:{pillar.tianganWuxing}
|
||||||
</div>
|
</div>
|
||||||
@@ -408,7 +408,7 @@ const BaziAnalysisDisplay: React.FC<BaziAnalysisDisplayProps> = ({ birthDate })
|
|||||||
{/* 地支信息 */}
|
{/* 地支信息 */}
|
||||||
<div className="bg-gradient-to-r from-yellow-50 to-red-50 rounded-lg p-3">
|
<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>
|
<h4 className="font-bold text-red-700 mb-2">地支:{pillar.dizhi}</h4>
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
|
||||||
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.dizhiWuxing]}`}>
|
<div className={`px-2 py-1 rounded border ${wuxingColors[pillar.dizhiWuxing]}`}>
|
||||||
五行:{pillar.dizhiWuxing}
|
五行:{pillar.dizhiWuxing}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
|
|||||||
const total = Object.values(elements).reduce((sum: number, count: any) => sum + (typeof count === 'number' ? count : 0), 0) as number;
|
const total = Object.values(elements).reduce((sum: number, count: any) => sum + (typeof count === 'number' ? count : 0), 0) as number;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-5 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 md:gap-4">
|
||||||
{Object.entries(elements).map(([element, count]) => {
|
{Object.entries(elements).map(([element, count]) => {
|
||||||
const numCount = typeof count === 'number' ? count : 0;
|
const numCount = typeof count === 'number' ? count : 0;
|
||||||
const percentage = total > 0 ? Math.round((numCount / total) * 100) : 0;
|
const percentage = total > 0 ? Math.round((numCount / total) * 100) : 0;
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts';
|
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts';
|
||||||
import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2, Clock, Target, Heart, DollarSign, Activity, Crown, Compass, Moon, Sun } from 'lucide-react';
|
import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2, Clock, Target, Heart, DollarSign, Activity, Crown, Compass, Moon, Sun } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||||
|
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from './ui/ChineseCard';
|
||||||
|
import { ChineseLoading } from './ui/ChineseLoading';
|
||||||
import { localApi } from '../lib/localApi';
|
import { localApi } from '../lib/localApi';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface CompleteZiweiAnalysisProps {
|
interface CompleteZiweiAnalysisProps {
|
||||||
birthDate: {
|
birthDate: {
|
||||||
@@ -211,35 +214,35 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 星曜颜色配置
|
// 星曜颜色配置(中式配色)
|
||||||
const starColors: { [key: string]: string } = {
|
const starColors: { [key: string]: string } = {
|
||||||
'紫微': 'bg-purple-100 text-purple-800 border-purple-300',
|
'紫微': 'bg-red-100 text-red-800 border-red-300',
|
||||||
'天机': 'bg-blue-100 text-blue-800 border-blue-300',
|
'天机': 'bg-blue-100 text-blue-800 border-blue-300',
|
||||||
'太阳': 'bg-orange-100 text-orange-800 border-orange-300',
|
'太阳': 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
||||||
'武曲': 'bg-gray-100 text-gray-800 border-gray-300',
|
'武曲': 'bg-gray-100 text-gray-800 border-gray-300',
|
||||||
'天同': 'bg-green-100 text-green-800 border-green-300',
|
'天同': 'bg-green-100 text-green-800 border-green-300',
|
||||||
'廉贞': 'bg-red-100 text-red-800 border-red-300',
|
'廉贞': 'bg-red-100 text-red-800 border-red-300',
|
||||||
'天府': 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
'天府': 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
||||||
'太阴': 'bg-indigo-100 text-indigo-800 border-indigo-300',
|
'太阴': 'bg-blue-100 text-blue-800 border-blue-300',
|
||||||
'贪狼': 'bg-pink-100 text-pink-800 border-pink-300',
|
'贪狼': 'bg-orange-100 text-orange-800 border-orange-300',
|
||||||
'巨门': 'bg-slate-100 text-slate-800 border-slate-300',
|
'巨门': 'bg-gray-100 text-gray-800 border-gray-300',
|
||||||
'天相': 'bg-cyan-100 text-cyan-800 border-cyan-300',
|
'天相': 'bg-green-100 text-green-800 border-green-300',
|
||||||
'天梁': 'bg-emerald-100 text-emerald-800 border-emerald-300',
|
'天梁': 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
||||||
'七杀': 'bg-rose-100 text-rose-800 border-rose-300',
|
'七杀': 'bg-red-100 text-red-800 border-red-300',
|
||||||
'破军': 'bg-amber-100 text-amber-800 border-amber-300'
|
'破军': 'bg-orange-100 text-orange-800 border-orange-300'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 吉星煞星颜色配置
|
// 吉星煞星颜色配置(中式配色)
|
||||||
const luckyStarColors = 'bg-green-50 text-green-700 border-green-200';
|
const luckyStarColors = 'bg-yellow-50 text-yellow-700 border-yellow-200';
|
||||||
const unluckyStarColors = 'bg-red-50 text-red-700 border-red-200';
|
const unluckyStarColors = 'bg-red-50 text-red-700 border-red-200';
|
||||||
|
|
||||||
// 宫位强度颜色
|
// 宫位强度颜色(中式配色)
|
||||||
const strengthColors: { [key: string]: string } = {
|
const strengthColors: { [key: string]: string } = {
|
||||||
'旺': 'text-green-600 bg-green-50',
|
'旺': 'text-red-600 bg-red-50',
|
||||||
'得地': 'text-blue-600 bg-blue-50',
|
'得地': 'text-yellow-600 bg-yellow-50',
|
||||||
'平': 'text-yellow-600 bg-yellow-50',
|
'平': 'text-gray-600 bg-gray-50',
|
||||||
'不得地': 'text-orange-600 bg-orange-50',
|
'不得地': 'text-orange-600 bg-orange-50',
|
||||||
'陷': 'text-red-600 bg-red-50'
|
'陷': 'text-gray-500 bg-gray-100'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 五行局颜色
|
// 五行局颜色
|
||||||
@@ -299,14 +302,19 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
|||||||
// 渲染加载状态
|
// 渲染加载状态
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-indigo-50">
|
<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-purple-400 p-8">
|
<ChineseCard variant="elevated" className="p-8">
|
||||||
<CardContent className="text-center">
|
<ChineseCardContent className="text-center">
|
||||||
<Loader2 className="h-12 w-12 animate-spin text-purple-600 mx-auto mb-4" />
|
<ChineseLoading
|
||||||
<h3 className="text-xl font-bold text-purple-800 mb-2">正在进行专业紫微斗数分析</h3>
|
size="lg"
|
||||||
<p className="text-purple-600">请稍候,正在排盘并生成您的详细命理报告...</p>
|
variant="chinese"
|
||||||
</CardContent>
|
text="正在进行专业紫微斗数分析"
|
||||||
</Card>
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
<h3 className="text-xl font-bold text-red-600 mb-2 font-chinese">排盘分析中</h3>
|
||||||
|
<p className="text-gray-600 font-chinese">请稍候,正在生成您的详细命理报告...</p>
|
||||||
|
</ChineseCardContent>
|
||||||
|
</ChineseCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -346,31 +354,63 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染宫位卡片
|
// 渲染宫位卡片(中式设计)
|
||||||
const renderPalaceCard = (palaceName: string, palace: any) => {
|
const renderPalaceCard = (palaceName: string, palace: any) => {
|
||||||
if (!palace) return null;
|
if (!palace) return null;
|
||||||
|
|
||||||
|
// 宫位图标映射
|
||||||
|
const palaceIcons: { [key: string]: any } = {
|
||||||
|
'命宫': User,
|
||||||
|
'兄弟宫': Heart,
|
||||||
|
'夫妻宫': Heart,
|
||||||
|
'子女宫': Star,
|
||||||
|
'财帛宫': DollarSign,
|
||||||
|
'疾厄宫': Activity,
|
||||||
|
'迁移宫': Compass,
|
||||||
|
'交友宫': Heart,
|
||||||
|
'事业宫': Crown,
|
||||||
|
'田宅宫': Target,
|
||||||
|
'福德宫': Sun,
|
||||||
|
'父母宫': Moon
|
||||||
|
};
|
||||||
|
|
||||||
|
const PalaceIcon = palaceIcons[palaceName] || Star;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={palaceName} className="chinese-card-decoration hover:shadow-xl transition-all duration-300 border-2 border-purple-400 min-h-[280px] w-full">
|
<ChineseCard key={palaceName} variant="bordered" className="hover:shadow-xl transition-all duration-300 min-h-[320px] w-full">
|
||||||
<CardHeader className="text-center pb-2">
|
<ChineseCardHeader className="text-center pb-3">
|
||||||
<CardTitle className="text-purple-800 text-lg font-bold chinese-text-shadow">
|
<div className="flex flex-col items-center space-y-2">
|
||||||
{palaceName}
|
<div className="w-10 h-10 bg-gradient-to-br from-red-500 to-red-600 rounded-full flex items-center justify-center shadow-lg">
|
||||||
</CardTitle>
|
<PalaceIcon className="h-5 w-5 text-white" />
|
||||||
<div className="flex justify-center items-center space-x-2">
|
</div>
|
||||||
<span className="text-purple-600 text-sm">{palace.position}</span>
|
<ChineseCardTitle className="text-red-600 text-heading-lg font-bold font-chinese">
|
||||||
<span className={`px-2 py-1 rounded text-xs font-medium ${strengthColors[palace.strength] || 'text-gray-600 bg-gray-50'}`}>
|
{palaceName}
|
||||||
{palace.strength}
|
</ChineseCardTitle>
|
||||||
</span>
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-gray-600 text-body-md font-chinese">{palace.position || palace.branch}</span>
|
||||||
|
<span className={cn(
|
||||||
|
'px-2 py-1 rounded-full text-label-md font-medium font-chinese',
|
||||||
|
strengthColors[palace.strength] || 'text-gray-600 bg-gray-50'
|
||||||
|
)}>
|
||||||
|
{palace.strength}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
<CardContent className="space-y-3">
|
<ChineseCardContent className="space-y-3">
|
||||||
{/* 主星 */}
|
{/* 主星 */}
|
||||||
{palace.main_stars && palace.main_stars.length > 0 && (
|
{palace.main_stars && palace.main_stars.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-xs font-semibold text-purple-800 mb-2">主星</h5>
|
<h5 className="text-label-lg font-semibold text-red-800 mb-2 font-chinese flex items-center">
|
||||||
|
<Star className="h-4 w-4 mr-1" />
|
||||||
|
主星
|
||||||
|
</h5>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{palace.main_stars.map((star: string, index: number) => (
|
{palace.main_stars.map((star: string, index: number) => (
|
||||||
<span key={index} className={`px-2 py-1 rounded text-xs font-medium border ${starColors[star] || 'bg-gray-100 text-gray-800 border-gray-300'}`}>
|
<span key={index} className={cn(
|
||||||
|
'px-2 py-1 rounded-full text-label-md font-medium border font-chinese',
|
||||||
|
starColors[star] || 'bg-gray-100 text-gray-800 border-gray-300'
|
||||||
|
)}>
|
||||||
{star}
|
{star}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -381,10 +421,16 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
|||||||
{/* 吉星 */}
|
{/* 吉星 */}
|
||||||
{palace.lucky_stars && palace.lucky_stars.length > 0 && (
|
{palace.lucky_stars && palace.lucky_stars.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-xs font-semibold text-green-800 mb-2">吉星</h5>
|
<h5 className="text-label-lg font-semibold text-yellow-800 mb-2 font-chinese flex items-center">
|
||||||
|
<Sparkles className="h-4 w-4 mr-1" />
|
||||||
|
吉星
|
||||||
|
</h5>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{palace.lucky_stars.map((star: string, index: number) => (
|
{palace.lucky_stars.map((star: string, index: number) => (
|
||||||
<span key={index} className={`px-2 py-1 rounded text-xs font-medium border ${luckyStarColors}`}>
|
<span key={index} className={cn(
|
||||||
|
'px-2 py-1 rounded-full text-label-md font-medium border font-chinese',
|
||||||
|
luckyStarColors
|
||||||
|
)}>
|
||||||
{star}
|
{star}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -395,10 +441,16 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
|||||||
{/* 煞星 */}
|
{/* 煞星 */}
|
||||||
{palace.unlucky_stars && palace.unlucky_stars.length > 0 && (
|
{palace.unlucky_stars && palace.unlucky_stars.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-xs font-semibold text-red-800 mb-2">煞星</h5>
|
<h5 className="text-label-lg font-semibold text-red-800 mb-2 font-chinese flex items-center">
|
||||||
|
<Zap className="h-4 w-4 mr-1" />
|
||||||
|
煞星
|
||||||
|
</h5>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{palace.unlucky_stars.map((star: string, index: number) => (
|
{palace.unlucky_stars.map((star: string, index: number) => (
|
||||||
<span key={index} className={`px-2 py-1 rounded text-xs font-medium border ${unluckyStarColors}`}>
|
<span key={index} className={cn(
|
||||||
|
'px-2 py-1 rounded-full text-label-md font-medium border font-chinese',
|
||||||
|
unluckyStarColors
|
||||||
|
)}>
|
||||||
{star}
|
{star}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -408,12 +460,16 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
|||||||
|
|
||||||
{/* 宫位解读 */}
|
{/* 宫位解读 */}
|
||||||
{palace.interpretation && (
|
{palace.interpretation && (
|
||||||
<div className="border-t pt-2">
|
<div className="border-t border-red-100 pt-3 mt-3">
|
||||||
<p className="text-xs text-gray-700 leading-relaxed">{palace.interpretation}</p>
|
<h5 className="text-label-lg font-semibold text-gray-800 mb-2 font-chinese flex items-center">
|
||||||
|
<BookOpen className="h-4 w-4 mr-1" />
|
||||||
|
宫位解读
|
||||||
|
</h5>
|
||||||
|
<p className="text-body-md text-gray-700 leading-relaxed font-chinese">{palace.interpretation}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -601,22 +657,26 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 十二宫位详解 */}
|
{/* 十二宫位详解 */}
|
||||||
<Card className="chinese-card-decoration border-2 border-purple-400">
|
<ChineseCard variant="elevated" className="bg-gradient-to-br from-red-50 to-yellow-50">
|
||||||
<CardHeader>
|
<ChineseCardHeader>
|
||||||
<CardTitle className="text-purple-800 text-2xl font-bold chinese-text-shadow flex items-center space-x-2">
|
<div className="text-center">
|
||||||
<Compass className="h-6 w-6" />
|
<div className="w-12 h-12 bg-gradient-to-br from-red-600 to-red-700 rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg">
|
||||||
<span>十二宫位详解</span>
|
<Compass className="h-6 w-6 text-white" />
|
||||||
</CardTitle>
|
</div>
|
||||||
<p className="text-purple-600 mt-2">紫微斗数将人生分为十二个宫位,每个宫位代表不同的人生领域</p>
|
<ChineseCardTitle className="text-red-600 text-2xl md:text-3xl font-bold font-chinese">
|
||||||
</CardHeader>
|
十二宫位详解
|
||||||
<CardContent>
|
</ChineseCardTitle>
|
||||||
<div className="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1 gap-4">
|
<p className="text-gray-600 mt-2 font-chinese">紫微斗数将人生分为十二个宫位,每个宫位代表不同的人生领域</p>
|
||||||
|
</div>
|
||||||
|
</ChineseCardHeader>
|
||||||
|
<ChineseCardContent>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
|
||||||
{analysisData.ziwei_analysis?.twelve_palaces && Object.entries(analysisData.ziwei_analysis.twelve_palaces).map(([palaceName, palace]) =>
|
{analysisData.ziwei_analysis?.twelve_palaces && Object.entries(analysisData.ziwei_analysis.twelve_palaces).map(([palaceName, palace]) =>
|
||||||
renderPalaceCard(palaceName, palace)
|
renderPalaceCard(palaceName, palace)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
|
|
||||||
{/* 四化飞星 */}
|
{/* 四化飞星 */}
|
||||||
{analysisData.ziwei_analysis?.si_hua && (
|
{analysisData.ziwei_analysis?.si_hua && (
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { Sparkles, User, History, LogOut, Home, Stars } from 'lucide-react';
|
import { Sparkles, User, History, LogOut, Home, Menu, X } from 'lucide-react';
|
||||||
import { Button } from './ui/Button';
|
import { ChineseButton } from './ui/ChineseButton';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -12,11 +13,13 @@ interface LayoutProps {
|
|||||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||||
const { user, signOut } = useAuth();
|
const { user, signOut } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
try {
|
try {
|
||||||
await signOut();
|
await signOut();
|
||||||
toast.success('登出成功');
|
toast.success('登出成功');
|
||||||
|
setIsMobileMenuOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('登出失败');
|
toast.error('登出失败');
|
||||||
}
|
}
|
||||||
@@ -24,32 +27,44 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ path: '/', label: '首页', icon: Home },
|
{ path: '/', label: '首页', icon: Home },
|
||||||
{ path: '/analysis', label: '命理分析', icon: Sparkles, requireAuth: true },
|
{ path: '/analysis', label: '分析', icon: Sparkles, requireAuth: true },
|
||||||
{ path: '/history', label: '历史记录', icon: History, requireAuth: true },
|
{ path: '/history', label: '历史', icon: History, requireAuth: true },
|
||||||
{ path: '/profile', label: '个人档案', icon: User, requireAuth: true },
|
{ path: '/profile', label: '档案', icon: User, requireAuth: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
console.log('Toggle mobile menu:', !isMobileMenuOpen);
|
||||||
|
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMobileMenu = () => {
|
||||||
|
setIsMobileMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen relative">
|
<div className="min-h-screen relative">
|
||||||
{/* 导航栏 */}
|
{/* 导航栏 */}
|
||||||
<nav className="chinese-traditional-bg shadow-2xl border-b-4 border-yellow-400 relative overflow-hidden">
|
<nav className="bg-gradient-to-r from-red-600 to-red-700 shadow-xl border-b-2 border-yellow-500 relative overflow-hidden z-[9998]">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
<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 justify-between items-center h-16">
|
||||||
|
{/* 品牌Logo */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Link to="/" className="flex items-center space-x-3 group">
|
<Link to="/" className="flex items-center space-x-2 group" onClick={closeMobileMenu}>
|
||||||
{/* 品牌图标 */}
|
<div className="w-10 h-10 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center shadow-lg border-2 border-yellow-600 group-hover:scale-110 transition-transform duration-300">
|
||||||
<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
|
<img
|
||||||
src="/traditional-chinese-bagua-eight-trigrams-black-gold.jpg"
|
src="/traditional-chinese-bagua-eight-trigrams-black-gold.jpg"
|
||||||
alt="神机阁"
|
alt="神机阁"
|
||||||
className="w-6 h-6 rounded-full object-cover"
|
className="w-7 h-7 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<span className="text-xl md:text-2xl font-bold text-white font-chinese group-hover:text-gold-100 transition-colors duration-300">
|
||||||
|
神机阁
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-6">
|
{/* 桌面端导航 */}
|
||||||
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
{navigationItems.map((item) => {
|
{navigationItems.map((item) => {
|
||||||
if (item.requireAuth && !user) return null;
|
if (item.requireAuth && !user) return null;
|
||||||
|
|
||||||
@@ -60,38 +75,119 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={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 ${
|
className={cn(
|
||||||
|
'flex items-center space-x-1.5 px-3 py-2 rounded-lg font-medium transition-all duration-300 text-sm',
|
||||||
|
'border border-transparent hover:border-yellow-400',
|
||||||
isActive
|
isActive
|
||||||
? 'text-red-800 chinese-golden-glow border-red-600 shadow-lg transform scale-105'
|
? 'text-yellow-100 bg-white/10 border-yellow-400 shadow-lg'
|
||||||
: 'text-yellow-200 hover:text-red-800 hover:chinese-golden-glow border-transparent hover:border-red-600 hover:shadow-lg hover:scale-105'
|
: 'text-white hover:text-yellow-100 hover:bg-white/10'
|
||||||
}`}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-4 w-4" />
|
||||||
<span>{item.label}</span>
|
<span className="whitespace-nowrap">{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<Button
|
<ChineseButton
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
variant="outline"
|
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"
|
size="sm"
|
||||||
|
className="text-white border-white hover:bg-white hover:text-red-600"
|
||||||
>
|
>
|
||||||
<LogOut className="h-5 w-5" />
|
<LogOut className="h-4 w-4 mr-1" />
|
||||||
<span>登出</span>
|
<span className="hidden lg:inline">登出</span>
|
||||||
</Button>
|
</ChineseButton>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-2">
|
||||||
<Link to="/login">
|
<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">
|
<ChineseButton variant="outline" size="sm" className="text-white border-white hover:bg-white hover:text-red-600">
|
||||||
登录
|
登录
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/register">
|
<Link to="/register">
|
||||||
<Button className="chinese-red-glow text-white border-2 border-yellow-400 hover:shadow-xl transition-all duration-300 font-serif">
|
<ChineseButton variant="secondary" size="sm">
|
||||||
注册
|
注册
|
||||||
</Button>
|
</ChineseButton>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端汉堡菜单按钮 */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
className="p-2 rounded-lg text-white hover:bg-white/10 transition-colors duration-200"
|
||||||
|
aria-label="切换菜单"
|
||||||
|
>
|
||||||
|
{isMobileMenuOpen ? (
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
) : (
|
||||||
|
<Menu className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端菜单面板 */}
|
||||||
|
<div className={cn(
|
||||||
|
'md:hidden fixed top-16 left-0 right-0 z-[9999]',
|
||||||
|
'bg-red-600/95 backdrop-blur-md border-t border-yellow-500/30',
|
||||||
|
'transform transition-all duration-300 ease-in-out',
|
||||||
|
isMobileMenuOpen
|
||||||
|
? 'translate-y-0 opacity-100 visible'
|
||||||
|
: '-translate-y-2 opacity-0 invisible'
|
||||||
|
)}>
|
||||||
|
<div className="px-4 py-4 space-y-2">
|
||||||
|
{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}
|
||||||
|
onClick={closeMobileMenu}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center space-x-3 px-4 py-3 rounded-lg font-medium transition-all duration-200',
|
||||||
|
'border border-transparent',
|
||||||
|
isActive
|
||||||
|
? 'text-yellow-100 bg-white/15 border-yellow-400/50'
|
||||||
|
: 'text-white hover:text-yellow-100 hover:bg-white/10'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-white/20">
|
||||||
|
{user ? (
|
||||||
|
<ChineseButton
|
||||||
|
onClick={handleSignOut}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full text-white border-white hover:bg-white hover:text-red-600"
|
||||||
|
>
|
||||||
|
<LogOut className="h-5 w-5 mr-2" />
|
||||||
|
登出
|
||||||
|
</ChineseButton>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Link to="/login" onClick={closeMobileMenu} className="block">
|
||||||
|
<ChineseButton variant="outline" className="w-full text-white border-white hover:bg-white hover:text-red-600">
|
||||||
|
登录
|
||||||
|
</ChineseButton>
|
||||||
|
</Link>
|
||||||
|
<Link to="/register" onClick={closeMobileMenu} className="block">
|
||||||
|
<ChineseButton variant="secondary" className="w-full">
|
||||||
|
注册
|
||||||
|
</ChineseButton>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -101,16 +197,16 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 relative">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 md:py-8 relative min-h-[calc(100vh-200px)]">
|
||||||
{/* 主内容区装饰元素 */}
|
{/* 装饰元素 - 仅在桌面端显示 */}
|
||||||
<div className="absolute top-0 left-0 w-24 h-24 opacity-10 pointer-events-none">
|
<div className="hidden lg:block absolute top-0 left-0 w-20 h-20 opacity-10 pointer-events-none">
|
||||||
<img
|
<img
|
||||||
src="/chinese_traditional_golden_ornate_frame.png"
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
alt=""
|
alt=""
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-0 right-0 w-24 h-24 opacity-10 pointer-events-none">
|
<div className="hidden lg:block absolute bottom-0 right-0 w-20 h-20 opacity-10 pointer-events-none">
|
||||||
<img
|
<img
|
||||||
src="/chinese_traditional_golden_ornate_frame.png"
|
src="/chinese_traditional_golden_ornate_frame.png"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -118,22 +214,36 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
{/* 点击遮罩层关闭移动端菜单 */}
|
||||||
|
{isMobileMenuOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/20 z-[9997] md:hidden"
|
||||||
|
onClick={closeMobileMenu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* 页脚装饰 */}
|
{/* 页脚 */}
|
||||||
<footer className="mt-16 py-8 border-t-2 border-yellow-400 mystical-gradient">
|
<footer className="mt-auto py-6 md:py-8 border-t border-red-200 bg-gradient-to-br from-yellow-50 to-red-50">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center">
|
<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">
|
<div className="w-10 h-10 md:w-12 md:h-12 mx-auto mb-3 md:mb-4 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center shadow-lg border-2 border-red-500">
|
||||||
<img
|
<img
|
||||||
src="/traditional_chinese_gold_red_dragon_symbol.jpg"
|
src="/traditional_chinese_gold_red_dragon_symbol.jpg"
|
||||||
alt="龙符"
|
alt="龙符"
|
||||||
className="w-8 h-8 rounded-full object-cover"
|
className="w-6 h-6 md:w-8 md:h-8 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-red-700 font-medium font-serif">神机阁 - 传统智慧与现代科技的完美融合</p>
|
<p className="text-red-600 font-medium font-chinese text-sm md:text-base">
|
||||||
<p className="text-red-600 text-sm mt-2">© 2025 AI命理分析平台 - Created by MiniMax Agent</p>
|
神机阁 - 传统智慧与现代科技的完美融合
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-xs md:text-sm mt-1 md:mt-2">
|
||||||
|
© 2025 AI命理分析平台
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
98
src/components/ui/ChineseButton.tsx
Normal file
98
src/components/ui/ChineseButton.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface ChineseButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseButton = React.forwardRef<HTMLButtonElement, ChineseButtonProps>(
|
||||||
|
({ className, variant = 'primary', size = 'md', children, ...props }, ref) => {
|
||||||
|
const baseClasses = [
|
||||||
|
'inline-flex items-center justify-center',
|
||||||
|
'font-chinese font-medium',
|
||||||
|
'transition-all duration-200 ease-in-out',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
'relative overflow-hidden',
|
||||||
|
'active:scale-95 hover-lift',
|
||||||
|
];
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: [
|
||||||
|
'bg-gradient-to-r from-red-600 to-red-700 !text-white',
|
||||||
|
'border border-red-600',
|
||||||
|
'shadow-lg hover:shadow-xl',
|
||||||
|
'hover:scale-105 active:scale-95 hover:!text-white',
|
||||||
|
'focus:ring-red-500',
|
||||||
|
'relative overflow-hidden',
|
||||||
|
'before:absolute before:inset-0',
|
||||||
|
'before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent',
|
||||||
|
'before:translate-x-[-100%] hover:before:translate-x-[100%]',
|
||||||
|
'before:transition-transform before:duration-700',
|
||||||
|
],
|
||||||
|
secondary: [
|
||||||
|
'bg-gradient-to-r from-yellow-400 to-yellow-500 text-gray-900',
|
||||||
|
'border border-yellow-500',
|
||||||
|
'shadow-lg hover:shadow-xl',
|
||||||
|
'hover:scale-105 active:scale-95',
|
||||||
|
'focus:ring-yellow-500',
|
||||||
|
],
|
||||||
|
outline: [
|
||||||
|
'bg-transparent text-red-600',
|
||||||
|
'border-2 border-red-600',
|
||||||
|
'hover:bg-red-600 hover:text-white',
|
||||||
|
'focus:ring-red-500',
|
||||||
|
],
|
||||||
|
ghost: [
|
||||||
|
'bg-transparent text-gray-700',
|
||||||
|
'hover:bg-gray-100 hover:text-red-600',
|
||||||
|
'focus:ring-gray-500',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: [
|
||||||
|
'px-3 py-1.5 text-button-sm rounded-md',
|
||||||
|
'min-h-[36px]', // 移动端友好的最小高度
|
||||||
|
],
|
||||||
|
md: [
|
||||||
|
'px-6 py-2.5 text-button-md rounded-lg',
|
||||||
|
'min-h-[44px]', // 移动端友好的最小高度
|
||||||
|
],
|
||||||
|
lg: [
|
||||||
|
'px-8 py-3 text-button-lg rounded-xl',
|
||||||
|
'min-h-[52px]', // 移动端友好的最小高度
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移动端响应式调整
|
||||||
|
const responsiveClasses = [
|
||||||
|
'md:hover:scale-105', // 只在桌面端启用悬停缩放
|
||||||
|
'active:scale-95', // 所有设备都有点击反馈
|
||||||
|
'touch-manipulation', // 优化触摸体验
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
sizeClasses[size],
|
||||||
|
responsiveClasses,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseButton.displayName = 'ChineseButton';
|
||||||
|
|
||||||
|
export { ChineseButton };
|
||||||
|
export type { ChineseButtonProps };
|
||||||
233
src/components/ui/ChineseCard.tsx
Normal file
233
src/components/ui/ChineseCard.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface ChineseCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
variant?: 'default' | 'elevated' | 'bordered' | 'golden';
|
||||||
|
padding?: 'sm' | 'md' | 'lg';
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseCard = React.forwardRef<HTMLDivElement, ChineseCardProps>(
|
||||||
|
({ className, variant = 'default', padding = 'md', children, ...props }, ref) => {
|
||||||
|
const baseClasses = [
|
||||||
|
'relative',
|
||||||
|
'transition-all duration-300 ease-in-out',
|
||||||
|
'font-chinese hover-lift animate-fade-in-up',
|
||||||
|
];
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
default: [
|
||||||
|
'bg-white/90 backdrop-blur-sm',
|
||||||
|
'border border-paper-300',
|
||||||
|
'rounded-lg',
|
||||||
|
'shadow-chinese-sm hover:shadow-chinese',
|
||||||
|
],
|
||||||
|
elevated: [
|
||||||
|
'bg-white/95 backdrop-blur-md',
|
||||||
|
'border border-cinnabar-200',
|
||||||
|
'rounded-xl',
|
||||||
|
'shadow-chinese hover:shadow-chinese-md',
|
||||||
|
'hover:-translate-y-1',
|
||||||
|
],
|
||||||
|
bordered: [
|
||||||
|
'bg-paper-50/80 backdrop-blur-sm',
|
||||||
|
'border-2 border-cinnabar-300',
|
||||||
|
'rounded-lg',
|
||||||
|
'shadow-paper',
|
||||||
|
// 传统边框装饰
|
||||||
|
'before:absolute before:inset-2',
|
||||||
|
'before:border before:border-gold-300/50',
|
||||||
|
'before:rounded-md before:pointer-events-none',
|
||||||
|
],
|
||||||
|
golden: [
|
||||||
|
'bg-gold-gradient',
|
||||||
|
'border-2 border-gold-600',
|
||||||
|
'rounded-xl',
|
||||||
|
'shadow-gold hover:shadow-gold',
|
||||||
|
'text-ink-900',
|
||||||
|
// 金色光晕效果
|
||||||
|
'before:absolute before:inset-0',
|
||||||
|
'before:bg-gradient-to-br before:from-white/20 before:to-transparent',
|
||||||
|
'before:rounded-xl before:pointer-events-none',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingClasses = {
|
||||||
|
sm: 'p-4',
|
||||||
|
md: 'p-6',
|
||||||
|
lg: 'p-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移动端响应式调整
|
||||||
|
const responsiveClasses = [
|
||||||
|
// 移动端减少内边距
|
||||||
|
'max-md:p-4',
|
||||||
|
// 移动端优化圆角
|
||||||
|
'max-md:rounded-lg',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
paddingClasses[padding],
|
||||||
|
responsiveClasses,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseCard.displayName = 'ChineseCard';
|
||||||
|
|
||||||
|
// 卡片标题组件
|
||||||
|
interface ChineseCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseCardHeader = React.forwardRef<HTMLDivElement, ChineseCardHeaderProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col space-y-1.5',
|
||||||
|
'pb-4 mb-4',
|
||||||
|
'border-b border-cinnabar-200',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseCardHeader.displayName = 'ChineseCardHeader';
|
||||||
|
|
||||||
|
// 卡片标题文字组件
|
||||||
|
interface ChineseCardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseCardTitle = React.forwardRef<HTMLParagraphElement, ChineseCardTitleProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
className={cn(
|
||||||
|
'text-heading-md font-semibold leading-none tracking-tight',
|
||||||
|
'text-cinnabar-500',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseCardTitle.displayName = 'ChineseCardTitle';
|
||||||
|
|
||||||
|
// 卡片描述组件
|
||||||
|
interface ChineseCardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseCardDescription = React.forwardRef<HTMLParagraphElement, ChineseCardDescriptionProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-body-md text-ink-500',
|
||||||
|
'font-chinese',
|
||||||
|
'leading-relaxed',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseCardDescription.displayName = 'ChineseCardDescription';
|
||||||
|
|
||||||
|
// 卡片内容组件
|
||||||
|
interface ChineseCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseCardContent = React.forwardRef<HTMLDivElement, ChineseCardContentProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-ink-900',
|
||||||
|
'leading-relaxed',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseCardContent.displayName = 'ChineseCardContent';
|
||||||
|
|
||||||
|
// 卡片底部组件
|
||||||
|
interface ChineseCardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseCardFooter = React.forwardRef<HTMLDivElement, ChineseCardFooterProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center',
|
||||||
|
'pt-4 mt-4',
|
||||||
|
'border-t border-paper-300',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseCardFooter.displayName = 'ChineseCardFooter';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChineseCard,
|
||||||
|
ChineseCardHeader,
|
||||||
|
ChineseCardTitle,
|
||||||
|
ChineseCardDescription,
|
||||||
|
ChineseCardContent,
|
||||||
|
ChineseCardFooter,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ChineseCardProps,
|
||||||
|
ChineseCardHeaderProps,
|
||||||
|
ChineseCardTitleProps,
|
||||||
|
ChineseCardDescriptionProps,
|
||||||
|
ChineseCardContentProps,
|
||||||
|
ChineseCardFooterProps,
|
||||||
|
};
|
||||||
91
src/components/ui/ChineseEmpty.tsx
Normal file
91
src/components/ui/ChineseEmpty.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { FileX, Search, Inbox, AlertCircle } from 'lucide-react';
|
||||||
|
import { ChineseButton } from './ChineseButton';
|
||||||
|
|
||||||
|
interface ChineseEmptyProps {
|
||||||
|
type?: 'default' | 'search' | 'data' | 'error';
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
action?: {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseEmpty: React.FC<ChineseEmptyProps> = ({
|
||||||
|
type = 'default',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
const iconMap = {
|
||||||
|
default: Inbox,
|
||||||
|
search: Search,
|
||||||
|
data: FileX,
|
||||||
|
error: AlertCircle
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTitles = {
|
||||||
|
default: '暂无数据',
|
||||||
|
search: '未找到相关内容',
|
||||||
|
data: '暂无记录',
|
||||||
|
error: '加载失败'
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultDescriptions = {
|
||||||
|
default: '这里还没有任何内容',
|
||||||
|
search: '请尝试其他关键词或调整筛选条件',
|
||||||
|
data: '您还没有创建任何记录',
|
||||||
|
error: '数据加载出现问题,请稍后重试'
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
default: 'text-gray-400',
|
||||||
|
search: 'text-blue-400',
|
||||||
|
data: 'text-yellow-400',
|
||||||
|
error: 'text-red-400'
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = iconMap[type];
|
||||||
|
const displayTitle = title || defaultTitles[type];
|
||||||
|
const displayDescription = description || defaultDescriptions[type];
|
||||||
|
const iconColor = colorMap[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
'flex flex-col items-center justify-center py-12 px-4 text-center',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{/* 图标 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<Icon className={cn('h-16 w-16', iconColor)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2 font-chinese">
|
||||||
|
{displayTitle}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
|
<p className="text-gray-600 mb-6 max-w-sm font-chinese leading-relaxed">
|
||||||
|
{displayDescription}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
{action && (
|
||||||
|
<ChineseButton
|
||||||
|
variant={type === 'error' ? 'primary' : 'secondary'}
|
||||||
|
onClick={action.onClick}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</ChineseButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ChineseEmpty };
|
||||||
|
export type { ChineseEmptyProps };
|
||||||
115
src/components/ui/ChineseInput.tsx
Normal file
115
src/components/ui/ChineseInput.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface ChineseInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
helperText?: string;
|
||||||
|
variant?: 'default' | 'bordered' | 'filled';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseInput = React.forwardRef<HTMLInputElement, ChineseInputProps>(
|
||||||
|
({ className, label, error, helperText, variant = 'default', size = 'md', ...props }, ref) => {
|
||||||
|
const baseClasses = [
|
||||||
|
'w-full font-chinese transition-all duration-200 ease-in-out',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-offset-1',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
'placeholder:text-gray-400',
|
||||||
|
];
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
default: [
|
||||||
|
'bg-white border border-gray-300',
|
||||||
|
'hover:border-red-400 focus:border-red-500 focus:ring-red-500/20',
|
||||||
|
error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||||
|
],
|
||||||
|
bordered: [
|
||||||
|
'bg-transparent border-2 border-red-300',
|
||||||
|
'hover:border-red-500 focus:border-red-600 focus:ring-red-500/20',
|
||||||
|
error ? 'border-red-500 focus:border-red-600 focus:ring-red-500/20' : '',
|
||||||
|
],
|
||||||
|
filled: [
|
||||||
|
'bg-red-50 border border-red-200',
|
||||||
|
'hover:bg-red-100 hover:border-red-300',
|
||||||
|
'focus:bg-white focus:border-red-500 focus:ring-red-500/20',
|
||||||
|
error ? 'bg-red-100 border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: [
|
||||||
|
'px-3 py-2 text-body-md rounded-md',
|
||||||
|
'min-h-[36px]', // 移动端友好
|
||||||
|
],
|
||||||
|
md: [
|
||||||
|
'px-4 py-2.5 text-body-lg rounded-lg',
|
||||||
|
'min-h-[44px]', // 移动端友好
|
||||||
|
],
|
||||||
|
lg: [
|
||||||
|
'px-5 py-3 text-body-xl rounded-xl',
|
||||||
|
'min-h-[52px]', // 移动端友好
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移动端响应式调整
|
||||||
|
const responsiveClasses = [
|
||||||
|
'touch-manipulation', // 优化触摸体验
|
||||||
|
'max-md:text-base', // 移动端字体调整
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* 标签 */}
|
||||||
|
{label && (
|
||||||
|
<label className="block text-label-lg font-medium text-gray-700 mb-2 font-chinese">
|
||||||
|
{label}
|
||||||
|
{props.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 输入框 */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
className={cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
sizeClasses[size],
|
||||||
|
responsiveClasses,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 错误状态图标 */}
|
||||||
|
{error && (
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误信息或帮助文本 */}
|
||||||
|
{(error || helperText) && (
|
||||||
|
<div className="mt-1.5">
|
||||||
|
{error ? (
|
||||||
|
<p className="text-body-sm text-red-600 font-chinese">{error}</p>
|
||||||
|
) : (
|
||||||
|
helperText && (
|
||||||
|
<p className="text-body-sm text-gray-500 font-chinese">{helperText}</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseInput.displayName = 'ChineseInput';
|
||||||
|
|
||||||
|
export { ChineseInput };
|
||||||
|
export type { ChineseInputProps };
|
||||||
99
src/components/ui/ChineseLoading.tsx
Normal file
99
src/components/ui/ChineseLoading.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Loader2, Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ChineseLoadingProps {
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
variant?: 'spinner' | 'dots' | 'chinese';
|
||||||
|
text?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseLoading: React.FC<ChineseLoadingProps> = ({
|
||||||
|
size = 'md',
|
||||||
|
variant = 'chinese',
|
||||||
|
text,
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'h-4 w-4',
|
||||||
|
md: 'h-8 w-8',
|
||||||
|
lg: 'h-12 w-12'
|
||||||
|
};
|
||||||
|
|
||||||
|
const textSizeClasses = {
|
||||||
|
sm: 'text-sm',
|
||||||
|
md: 'text-base',
|
||||||
|
lg: 'text-lg'
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSpinner = () => (
|
||||||
|
<Loader2 className={cn(
|
||||||
|
'animate-spin text-red-600',
|
||||||
|
sizeClasses[size]
|
||||||
|
)} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDots = () => (
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
'bg-red-600 rounded-full animate-pulse',
|
||||||
|
size === 'sm' ? 'h-2 w-2' : size === 'md' ? 'h-3 w-3' : 'h-4 w-4'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${i * 0.2}s`,
|
||||||
|
animationDuration: '1s'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderChinese = () => (
|
||||||
|
<div className="relative">
|
||||||
|
<div className={cn(
|
||||||
|
'border-4 border-red-200 border-t-red-600 rounded-full animate-spin',
|
||||||
|
sizeClasses[size]
|
||||||
|
)} />
|
||||||
|
<Sparkles className={cn(
|
||||||
|
'absolute inset-0 m-auto text-yellow-500 animate-pulse',
|
||||||
|
size === 'sm' ? 'h-2 w-2' : size === 'md' ? 'h-4 w-4' : 'h-6 w-6'
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderVariant = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'spinner':
|
||||||
|
return renderSpinner();
|
||||||
|
case 'dots':
|
||||||
|
return renderDots();
|
||||||
|
case 'chinese':
|
||||||
|
default:
|
||||||
|
return renderChinese();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
'flex flex-col items-center justify-center space-y-3',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{renderVariant()}
|
||||||
|
{text && (
|
||||||
|
<p className={cn(
|
||||||
|
'text-gray-600 font-chinese',
|
||||||
|
textSizeClasses[size]
|
||||||
|
)}>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ChineseLoading };
|
||||||
|
export type { ChineseLoadingProps };
|
||||||
160
src/components/ui/ChineseSelect.tsx
Normal file
160
src/components/ui/ChineseSelect.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ChineseSelectOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChineseSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
helperText?: string;
|
||||||
|
variant?: 'default' | 'bordered' | 'filled';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
options: ChineseSelectOption[];
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseSelect = React.forwardRef<HTMLSelectElement, ChineseSelectProps>(
|
||||||
|
({ className, label, error, helperText, variant = 'default', size = 'md', options, placeholder, ...props }, ref) => {
|
||||||
|
const baseClasses = [
|
||||||
|
'w-full font-chinese transition-all duration-200 ease-in-out',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-offset-1',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
'appearance-none cursor-pointer',
|
||||||
|
'bg-no-repeat bg-right',
|
||||||
|
];
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
default: [
|
||||||
|
'bg-white border border-gray-300',
|
||||||
|
'hover:border-red-400 focus:border-red-500 focus:ring-red-500/20',
|
||||||
|
error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||||
|
],
|
||||||
|
bordered: [
|
||||||
|
'bg-transparent border-2 border-red-300',
|
||||||
|
'hover:border-red-500 focus:border-red-600 focus:ring-red-500/20',
|
||||||
|
error ? 'border-red-500 focus:border-red-600 focus:ring-red-500/20' : '',
|
||||||
|
],
|
||||||
|
filled: [
|
||||||
|
'bg-red-50 border border-red-200',
|
||||||
|
'hover:bg-red-100 hover:border-red-300',
|
||||||
|
'focus:bg-white focus:border-red-500 focus:ring-red-500/20',
|
||||||
|
error ? 'bg-red-100 border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: [
|
||||||
|
'px-3 py-2 pr-8 text-sm rounded-md',
|
||||||
|
'min-h-[36px]', // 移动端友好
|
||||||
|
],
|
||||||
|
md: [
|
||||||
|
'px-4 py-2.5 pr-10 text-base rounded-lg',
|
||||||
|
'min-h-[44px]', // 移动端友好
|
||||||
|
],
|
||||||
|
lg: [
|
||||||
|
'px-5 py-3 pr-12 text-lg rounded-xl',
|
||||||
|
'min-h-[52px]', // 移动端友好
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移动端响应式调整
|
||||||
|
const responsiveClasses = [
|
||||||
|
'touch-manipulation', // 优化触摸体验
|
||||||
|
'max-md:text-base', // 移动端字体调整
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* 标签 */}
|
||||||
|
{label && (
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2 font-chinese">
|
||||||
|
{label}
|
||||||
|
{props.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 选择器容器 */}
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
className={cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
sizeClasses[size],
|
||||||
|
responsiveClasses,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* 占位符选项 */}
|
||||||
|
{placeholder && (
|
||||||
|
<option value="" disabled>
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 选项列表 */}
|
||||||
|
{options.map((option) => (
|
||||||
|
<option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
disabled={option.disabled}
|
||||||
|
className="font-chinese"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* 下拉箭头 */}
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pointer-events-none">
|
||||||
|
<div className={cn(
|
||||||
|
'pr-2',
|
||||||
|
size === 'sm' ? 'pr-2' : size === 'md' ? 'pr-3' : 'pr-4'
|
||||||
|
)}>
|
||||||
|
<ChevronDown className={cn(
|
||||||
|
'text-gray-400',
|
||||||
|
size === 'sm' ? 'h-4 w-4' : size === 'md' ? 'h-5 w-5' : 'h-6 w-6'
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误状态图标 */}
|
||||||
|
{error && (
|
||||||
|
<div className={cn(
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||||
|
size === 'sm' ? 'pr-7' : size === 'md' ? 'pr-9' : 'pr-11'
|
||||||
|
)}>
|
||||||
|
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误信息或帮助文本 */}
|
||||||
|
{(error || helperText) && (
|
||||||
|
<div className="mt-1.5">
|
||||||
|
{error ? (
|
||||||
|
<p className="text-sm text-red-600 font-chinese">{error}</p>
|
||||||
|
) : (
|
||||||
|
helperText && (
|
||||||
|
<p className="text-sm text-gray-500 font-chinese">{helperText}</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ChineseSelect.displayName = 'ChineseSelect';
|
||||||
|
|
||||||
|
export { ChineseSelect };
|
||||||
|
export type { ChineseSelectProps, ChineseSelectOption };
|
||||||
109
src/components/ui/ChineseToast.tsx
Normal file
109
src/components/ui/ChineseToast.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ChineseToastProps {
|
||||||
|
type?: 'success' | 'error' | 'warning' | 'info';
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChineseToast: React.FC<ChineseToastProps> = ({
|
||||||
|
type = 'info',
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
onClose,
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
const iconMap = {
|
||||||
|
success: CheckCircle,
|
||||||
|
error: XCircle,
|
||||||
|
warning: AlertCircle,
|
||||||
|
info: Info
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
success: {
|
||||||
|
bg: 'bg-green-50',
|
||||||
|
border: 'border-green-200',
|
||||||
|
icon: 'text-green-600',
|
||||||
|
title: 'text-green-800',
|
||||||
|
message: 'text-green-700'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
bg: 'bg-red-50',
|
||||||
|
border: 'border-red-200',
|
||||||
|
icon: 'text-red-600',
|
||||||
|
title: 'text-red-800',
|
||||||
|
message: 'text-red-700'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
bg: 'bg-yellow-50',
|
||||||
|
border: 'border-yellow-200',
|
||||||
|
icon: 'text-yellow-600',
|
||||||
|
title: 'text-yellow-800',
|
||||||
|
message: 'text-yellow-700'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
bg: 'bg-blue-50',
|
||||||
|
border: 'border-blue-200',
|
||||||
|
icon: 'text-blue-600',
|
||||||
|
title: 'text-blue-800',
|
||||||
|
message: 'text-blue-700'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = iconMap[type];
|
||||||
|
const colors = colorMap[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
'flex items-start p-4 rounded-lg border shadow-lg',
|
||||||
|
'animate-in slide-in-from-top-2 duration-300',
|
||||||
|
colors.bg,
|
||||||
|
colors.border,
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{/* 图标 */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Icon className={cn('h-5 w-5', colors.icon)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
{title && (
|
||||||
|
<h4 className={cn(
|
||||||
|
'text-sm font-semibold font-chinese mb-1',
|
||||||
|
colors.title
|
||||||
|
)}>
|
||||||
|
{title}
|
||||||
|
</h4>
|
||||||
|
)}
|
||||||
|
<p className={cn(
|
||||||
|
'text-sm font-chinese leading-relaxed',
|
||||||
|
colors.message
|
||||||
|
)}>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 关闭按钮 */}
|
||||||
|
{onClose && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className={cn(
|
||||||
|
'flex-shrink-0 ml-4 p-1 rounded-md hover:bg-white/50 transition-colors',
|
||||||
|
colors.icon
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ChineseToast };
|
||||||
|
export type { ChineseToastProps };
|
||||||
227
src/index.css
227
src/index.css
@@ -1,11 +1,31 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;500;600;700;900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;500;600;700;900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* 导入字体规范系统 */
|
||||||
|
@import './styles/typography.css';
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* 中式设计系统颜色变量 */
|
||||||
|
--cinnabar-50: #fef2f2;
|
||||||
|
--cinnabar-500: #DC143C;
|
||||||
|
--cinnabar-900: #7c1420;
|
||||||
|
--gold-50: #fffbeb;
|
||||||
|
--gold-500: #FFD700;
|
||||||
|
--gold-900: #78350f;
|
||||||
|
--ink-50: #f8fafc;
|
||||||
|
--ink-500: #64748b;
|
||||||
|
--ink-900: #2C2C2C;
|
||||||
|
--paper-50: #fefefe;
|
||||||
|
--paper-500: #F5F5DC;
|
||||||
|
--paper-900: #a8a88a;
|
||||||
|
|
||||||
|
/* 传统组件颜色 */
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 0 0% 98%;
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
@@ -14,11 +34,27 @@
|
|||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 220 13% 91%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
/* 传统中式颜色 */
|
|
||||||
--chinese-red: #dc2626;
|
/* 兼容性颜色变量 */
|
||||||
--chinese-gold: #facc15;
|
--background: 0 0% 100%;
|
||||||
--chinese-dark-red: #991b1b;
|
--foreground: 240 10% 3.9%;
|
||||||
--chinese-deep-gold: #d97706;
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 346 77% 49.8%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 346 77% 49.8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -33,10 +69,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Noto Serif SC', serif;
|
font-family: var(--font-chinese);
|
||||||
background: linear-gradient(135deg, #fef7cd 0%, #fed7aa 25%, #fecaca 50%, #fed7aa 75%, #fef7cd 100%);
|
background: linear-gradient(135deg, var(--paper-500) 0%, var(--gold-50) 100%);
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
color: var(--ink-900);
|
||||||
|
/* 使用标准正文字体规范 */
|
||||||
|
@apply text-body-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题默认样式 - 使用新的字体规范 */
|
||||||
|
h1 {
|
||||||
|
@apply text-heading-xl font-chinese font-semibold;
|
||||||
|
color: var(--cinnabar-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-heading-lg font-chinese font-semibold;
|
||||||
|
color: var(--cinnabar-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-heading-md font-chinese font-semibold;
|
||||||
|
color: var(--ink-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@apply text-heading-sm font-chinese font-semibold;
|
||||||
|
color: var(--ink-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
@apply text-heading-xs font-chinese font-semibold;
|
||||||
|
color: var(--ink-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
@apply text-body-xl font-chinese font-semibold;
|
||||||
|
color: var(--ink-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮基础样式 */
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框基础样式 */
|
||||||
|
input, textarea, select {
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::before {
|
body::before {
|
||||||
@@ -206,6 +288,137 @@ img {
|
|||||||
object-position: top;
|
object-position: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 中式动画效果 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInFromTop {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slideInRight 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scale-in {
|
||||||
|
animation: scaleIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-from-top {
|
||||||
|
animation: slideInFromTop 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面切换动画 */
|
||||||
|
.page-transition-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-transition-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-transition-exit {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-transition-exit-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停效果增强 */
|
||||||
|
.hover-lift {
|
||||||
|
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中式装饰动画 */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画优化 */
|
||||||
|
@keyframes spin-slow {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin-slow {
|
||||||
|
animation: spin-slow 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.fixed {
|
.fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { localApi } from '../lib/localApi';
|
import { localApi } from '../lib/localApi';
|
||||||
import { Button } from '../components/ui/Button';
|
import { ChineseButton } from '../components/ui/ChineseButton';
|
||||||
import { Input } from '../components/ui/Input';
|
import { ChineseInput } from '../components/ui/ChineseInput';
|
||||||
import { Select } from '../components/ui/Select';
|
import { ChineseSelect } from '../components/ui/ChineseSelect';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from '../components/ui/ChineseCard';
|
||||||
import AnalysisResultDisplay from '../components/AnalysisResultDisplay';
|
import AnalysisResultDisplay from '../components/AnalysisResultDisplay';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Sparkles, Star, Compass, Calendar, MapPin, User, Loader2 } from 'lucide-react';
|
import { Sparkles, Star, Compass, Calendar, MapPin, User, Loader2 } from 'lucide-react';
|
||||||
import { UserProfile, AnalysisRequest, NumerologyReading } from '../types';
|
import { UserProfile, AnalysisRequest, NumerologyReading } from '../types';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
type AnalysisType = 'bazi' | 'ziwei' | 'yijing';
|
type AnalysisType = 'bazi' | 'ziwei' | 'yijing';
|
||||||
|
|
||||||
@@ -173,40 +174,46 @@ const AnalysisPage: React.FC = () => {
|
|||||||
title: '八字命理',
|
title: '八字命理',
|
||||||
description: '基于传统八字学说,分析五行平衡、格局特点、四柱信息',
|
description: '基于传统八字学说,分析五行平衡、格局特点、四柱信息',
|
||||||
icon: Sparkles,
|
icon: Sparkles,
|
||||||
color: 'text-purple-600',
|
color: 'text-red-600',
|
||||||
bgColor: 'bg-purple-50',
|
bgColor: 'bg-red-50',
|
||||||
borderColor: 'border-purple-200'
|
borderColor: 'border-red-300'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ziwei' as AnalysisType,
|
type: 'ziwei' as AnalysisType,
|
||||||
title: '紫微斗数',
|
title: '紫微斗数',
|
||||||
description: '通过星曜排布和十二宫位分析性格命运',
|
description: '通过星曜排布和十二宫位分析性格命运',
|
||||||
icon: Star,
|
icon: Star,
|
||||||
color: 'text-blue-600',
|
color: 'text-yellow-600',
|
||||||
bgColor: 'bg-blue-50',
|
bgColor: 'bg-yellow-50',
|
||||||
borderColor: 'border-blue-200'
|
borderColor: 'border-yellow-300'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'yijing' as AnalysisType,
|
type: 'yijing' as AnalysisType,
|
||||||
title: '易经占卜',
|
title: '易经占卜',
|
||||||
description: '运用梅花易数起卦法,解读卦象含义,指导人生决策',
|
description: '运用梅花易数起卦法,解读卦象含义,指导人生决策',
|
||||||
icon: Compass,
|
icon: Compass,
|
||||||
color: 'text-amber-600',
|
color: 'text-orange-600',
|
||||||
bgColor: 'bg-amber-50',
|
bgColor: 'bg-orange-50',
|
||||||
borderColor: 'border-amber-200'
|
borderColor: 'border-orange-300'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto px-4 space-y-8">
|
<div className="max-w-7xl mx-auto px-4 py-6 space-y-6 md:space-y-8">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl md:text-3xl font-bold text-red-600 font-chinese mb-2">命理分析</h1>
|
||||||
|
<p className="text-gray-600 font-chinese">选择分析方式,探索您的命运密码</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 分析类型选择 */}
|
{/* 分析类型选择 */}
|
||||||
<Card>
|
<ChineseCard variant="elevated">
|
||||||
<CardHeader>
|
<ChineseCardHeader>
|
||||||
<CardTitle>选择分析类型</CardTitle>
|
<ChineseCardTitle className="text-red-600 font-chinese">选择分析类型</ChineseCardTitle>
|
||||||
<p className="text-gray-600">选择您感兴趣的命理分析方式</p>
|
<p className="text-gray-600 font-chinese">选择您感兴趣的命理分析方式</p>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
<CardContent>
|
<ChineseCardContent>
|
||||||
<div className="grid md:grid-cols-3 gap-4">
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{analysisTypes.map((type) => {
|
{analysisTypes.map((type) => {
|
||||||
const Icon = type.icon;
|
const Icon = type.icon;
|
||||||
const isSelected = analysisType === type.type;
|
const isSelected = analysisType === type.type;
|
||||||
@@ -214,66 +221,80 @@ const AnalysisPage: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={type.type}
|
key={type.type}
|
||||||
onClick={() => setAnalysisType(type.type)}
|
onClick={() => setAnalysisType(type.type)}
|
||||||
className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
|
className={cn(
|
||||||
|
'p-4 md:p-5 rounded-lg border-2 cursor-pointer transition-all duration-200',
|
||||||
|
'hover:shadow-md active:scale-95',
|
||||||
isSelected
|
isSelected
|
||||||
? `${type.borderColor} ${type.bgColor}`
|
? `${type.borderColor} ${type.bgColor} shadow-md`
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300 bg-white'
|
||||||
}`}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<div className="flex items-center space-x-3 mb-3">
|
||||||
<Icon className={`h-6 w-6 ${isSelected ? type.color : 'text-gray-400'}`} />
|
<div className={cn(
|
||||||
<h3 className={`font-medium ${isSelected ? type.color : 'text-gray-700'}`}>
|
'w-10 h-10 rounded-full flex items-center justify-center',
|
||||||
|
isSelected ? type.bgColor : 'bg-gray-100'
|
||||||
|
)}>
|
||||||
|
<Icon className={cn(
|
||||||
|
'h-5 w-5',
|
||||||
|
isSelected ? type.color : 'text-gray-400'
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
<h3 className={cn(
|
||||||
|
'font-semibold font-chinese text-lg',
|
||||||
|
isSelected ? type.color : 'text-gray-700'
|
||||||
|
)}>
|
||||||
{type.title}
|
{type.title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{type.description}</p>
|
<p className="text-sm text-gray-600 font-chinese leading-relaxed">{type.description}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
|
|
||||||
{/* 分析表单 */}
|
{/* 分析表单 */}
|
||||||
<Card>
|
<ChineseCard variant="bordered">
|
||||||
<CardHeader>
|
<ChineseCardHeader>
|
||||||
<CardTitle>填写分析信息</CardTitle>
|
<ChineseCardTitle className="text-red-600 font-chinese">填写分析信息</ChineseCardTitle>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600 font-chinese">
|
||||||
{profile ? '已从您的档案中自动填充,您可以修改' : '请填写以下信息进行分析'}
|
{profile ? '已从您的档案中自动填充,您可以修改' : '请填写以下信息进行分析'}
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
<CardContent>
|
<ChineseCardContent>
|
||||||
{analysisType === 'yijing' ? (
|
{analysisType === 'yijing' ? (
|
||||||
// 易经占卜表单
|
// 易经占卜表单
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Input
|
<ChineseInput
|
||||||
label="占卜问题 *"
|
label="占卜问题"
|
||||||
value={formData.question}
|
value={formData.question}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, question: e.target.value }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, question: e.target.value }))}
|
||||||
placeholder="请输入您希望占卜的具体问题,如:我的事业发展如何?"
|
placeholder="请输入您希望占卜的具体问题,如:我的事业发展如何?"
|
||||||
required
|
required
|
||||||
|
variant="filled"
|
||||||
|
helperText="💡 提示:问题越具体,占卜结果越准确。可以询问事业、感情、财运、健康等方面的问题。"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
|
||||||
💡 提示:问题越具体,占卜结果越准确。可以询问事业、感情、财运、健康等方面的问题。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 八字和紫微表单
|
// 八字和紫微表单
|
||||||
<>
|
<>
|
||||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
<div className="grid md:grid-cols-2 gap-4 md:gap-6 mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
label="姓名 *"
|
label="姓名"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
required
|
required
|
||||||
placeholder="请输入真实姓名"
|
placeholder="请输入真实姓名"
|
||||||
|
variant="filled"
|
||||||
|
className="pr-10"
|
||||||
/>
|
/>
|
||||||
<User className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
<User className="absolute right-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select
|
<ChineseSelect
|
||||||
label="性别 *"
|
label="性别"
|
||||||
value={formData.gender}
|
value={formData.gender}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, gender: e.target.value as 'male' | 'female' }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, gender: e.target.value as 'male' | 'female' }))}
|
||||||
options={[
|
options={[
|
||||||
@@ -281,14 +302,15 @@ const AnalysisPage: React.FC = () => {
|
|||||||
{ value: 'female', label: '女性' }
|
{ value: 'female', label: '女性' }
|
||||||
]}
|
]}
|
||||||
required
|
required
|
||||||
|
variant="filled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
<div className="grid md:grid-cols-2 gap-4 md:gap-6 mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
type="date"
|
type="date"
|
||||||
label="出生日期 *"
|
label="出生日期"
|
||||||
value={formData.birth_date}
|
value={formData.birth_date}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
@@ -301,40 +323,47 @@ const AnalysisPage: React.FC = () => {
|
|||||||
min="1900-01-01"
|
min="1900-01-01"
|
||||||
max="2100-12-31"
|
max="2100-12-31"
|
||||||
required
|
required
|
||||||
|
variant="filled"
|
||||||
|
className="pr-10"
|
||||||
/>
|
/>
|
||||||
<Calendar className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
<Calendar className="absolute right-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Input
|
<ChineseInput
|
||||||
type="time"
|
type="time"
|
||||||
label="出生时间"
|
label="出生时间"
|
||||||
value={formData.birth_time}
|
value={formData.birth_time}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, birth_time: e.target.value }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, birth_time: e.target.value }))}
|
||||||
placeholder="选填,但强烈建议填写"
|
helperText="选填,但强烈建议填写以提高准确性"
|
||||||
|
variant="filled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{analysisType !== 'ziwei' && (
|
{analysisType !== 'ziwei' && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
label="出生地点"
|
label="出生地点"
|
||||||
value={formData.birth_place}
|
value={formData.birth_place}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, birth_place: e.target.value }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, birth_place: e.target.value }))}
|
||||||
placeholder="如:北京市朝阳区(选填)"
|
placeholder="如:北京市朝阳区(选填)"
|
||||||
|
variant="filled"
|
||||||
|
className="pr-10"
|
||||||
|
helperText="选填,用于更精确的地理位置分析"
|
||||||
/>
|
/>
|
||||||
<MapPin className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
<MapPin className="absolute right-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<ChineseButton
|
||||||
onClick={handleAnalysis}
|
onClick={handleAnalysis}
|
||||||
disabled={loading || (analysisType === 'yijing' ? !formData.question : (!formData.name || !formData.birth_date))}
|
disabled={loading || (analysisType === 'yijing' ? !formData.question : (!formData.name || !formData.birth_date))}
|
||||||
className="w-full"
|
className="w-full mt-6"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
variant="primary"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
@@ -347,9 +376,9 @@ const AnalysisPage: React.FC = () => {
|
|||||||
开始{analysisTypes.find(t => t.type === analysisType)?.title}分析
|
开始{analysisTypes.find(t => t.type === analysisType)?.title}分析
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
|
|
||||||
{/* 分析结果 */}
|
{/* 分析结果 */}
|
||||||
{analysisResult && (
|
{analysisResult && (
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { localApi } from '../lib/localApi';
|
import { localApi } from '../lib/localApi';
|
||||||
import { Button } from '../components/ui/Button';
|
import { ChineseButton } from '../components/ui/ChineseButton';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from '../components/ui/ChineseCard';
|
||||||
|
import { ChineseEmpty } from '../components/ui/ChineseEmpty';
|
||||||
|
import { ChineseLoading } from '../components/ui/ChineseLoading';
|
||||||
import AnalysisResultDisplay from '../components/AnalysisResultDisplay';
|
import AnalysisResultDisplay from '../components/AnalysisResultDisplay';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { History, Calendar, User, Sparkles, Star, Compass, Eye, Trash2 } from 'lucide-react';
|
import { History, Calendar, User, Sparkles, Star, Compass, Eye, Trash2 } from 'lucide-react';
|
||||||
import { NumerologyReading } from '../types';
|
import { NumerologyReading } from '../types';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
const HistoryPage: React.FC = () => {
|
const HistoryPage: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -132,9 +135,9 @@ const HistoryPage: React.FC = () => {
|
|||||||
|
|
||||||
const getAnalysisTypeColor = (type: string) => {
|
const getAnalysisTypeColor = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'bazi': return 'text-purple-600 bg-purple-50';
|
case 'bazi': return 'text-red-600 bg-red-50';
|
||||||
case 'ziwei': return 'text-blue-600 bg-blue-50';
|
case 'ziwei': return 'text-yellow-600 bg-yellow-50';
|
||||||
case 'yijing': return 'text-green-600 bg-green-50';
|
case 'yijing': return 'text-orange-600 bg-orange-50';
|
||||||
default: return 'text-gray-600 bg-gray-50';
|
default: return 'text-gray-600 bg-gray-50';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -152,15 +155,15 @@ const HistoryPage: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<ChineseButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setViewingResult(false)}
|
onClick={() => setViewingResult(false)}
|
||||||
>
|
>
|
||||||
← 返回列表
|
← 返回列表
|
||||||
</Button>
|
</ChineseButton>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<h2 className="text-xl font-semibold">{selectedReading.name} 的{getAnalysisTypeName(selectedReading.reading_type)}</h2>
|
<h2 className="text-xl font-semibold font-chinese text-red-600">{selectedReading.name} 的{getAnalysisTypeName(selectedReading.reading_type)}</h2>
|
||||||
<p className="text-gray-600">{new Date(selectedReading.created_at).toLocaleString('zh-CN')}</p>
|
<p className="text-gray-600 font-chinese">{new Date(selectedReading.created_at).toLocaleString('zh-CN')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,97 +188,104 @@ const HistoryPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="max-w-7xl mx-auto px-4 py-6 space-y-6">
|
||||||
<Card>
|
<div className="text-center">
|
||||||
<CardHeader>
|
<h1 className="text-2xl md:text-3xl font-bold text-red-600 font-chinese mb-2">历史记录</h1>
|
||||||
|
<p className="text-gray-600 font-chinese">查看您之前的所有命理分析记录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChineseCard variant="elevated">
|
||||||
|
<ChineseCardHeader>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
|
||||||
<History className="h-6 w-6 text-purple-600" />
|
<History className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>历史记录</CardTitle>
|
<ChineseCardTitle className="text-red-600 font-chinese">分析记录</ChineseCardTitle>
|
||||||
<p className="text-gray-600">查看您之前的所有命理分析记录</p>
|
<p className="text-gray-600 font-chinese">您的命理分析历史</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
</Card>
|
<ChineseCardContent>
|
||||||
|
{loading ? (
|
||||||
{loading ? (
|
<ChineseLoading
|
||||||
<div className="flex items-center justify-center py-16">
|
size="lg"
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-purple-600"></div>
|
variant="chinese"
|
||||||
</div>
|
text="正在加载历史记录..."
|
||||||
) : readings.length === 0 ? (
|
className="py-16"
|
||||||
<Card>
|
/>
|
||||||
<CardContent className="text-center py-16">
|
) : readings.length === 0 ? (
|
||||||
<History className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
<ChineseEmpty
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无分析记录</h3>
|
type="data"
|
||||||
<p className="text-gray-600 mb-6">您还没有进行过任何命理分析</p>
|
title="暂无分析记录"
|
||||||
<Button onClick={() => window.location.href = '/analysis'}>
|
description="您还没有进行过任何命理分析"
|
||||||
<Sparkles className="mr-2 h-4 w-4" />
|
action={{
|
||||||
立即开始分析
|
label: '立即开始分析',
|
||||||
</Button>
|
onClick: () => window.location.href = '/analysis'
|
||||||
</CardContent>
|
}}
|
||||||
</Card>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{readings.map((reading) => {
|
{readings.map((reading) => {
|
||||||
const Icon = getAnalysisTypeIcon(reading.reading_type);
|
const Icon = getAnalysisTypeIcon(reading.reading_type);
|
||||||
const colorClass = getAnalysisTypeColor(reading.reading_type);
|
const colorClass = getAnalysisTypeColor(reading.reading_type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={reading.id} className="hover:shadow-lg transition-shadow">
|
<ChineseCard key={reading.id} variant="bordered" className="hover:shadow-lg transition-all duration-200">
|
||||||
<CardContent className="p-6">
|
<ChineseCardContent className="p-4 md:p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${colorClass}`}>
|
<div className={cn('w-10 h-10 rounded-full flex items-center justify-center', colorClass)}>
|
||||||
<Icon className="h-5 w-5" />
|
<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).toLocaleString('zh-CN')}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex-1">
|
||||||
<User className="h-3 w-3" />
|
<h3 className="font-semibold text-gray-900 font-chinese">
|
||||||
<span>
|
{reading.name || '未知姓名'} - {getAnalysisTypeName(reading.reading_type)}
|
||||||
{reading.reading_type === 'yijing'
|
</h3>
|
||||||
? `问题:${getInputDataValue(reading.input_data, 'question', '综合运势').substring(0, 20)}${getInputDataValue(reading.input_data, 'question', '').length > 20 ? '...' : ''}`
|
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-4 text-sm text-gray-600 mt-1 space-y-1 sm:space-y-0">
|
||||||
: reading.birth_date}
|
<div className="flex items-center space-x-1">
|
||||||
</span>
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span className="font-chinese">{new Date(reading.created_at).toLocaleString('zh-CN')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span className="font-chinese">
|
||||||
|
{reading.reading_type === 'yijing'
|
||||||
|
? `问题:${getInputDataValue(reading.input_data, 'question', '综合运势').substring(0, 20)}${getInputDataValue(reading.input_data, 'question', '').length > 20 ? '...' : ''}`
|
||||||
|
: reading.birth_date}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 self-end sm:self-center">
|
||||||
|
<ChineseButton
|
||||||
|
variant="outline"
|
||||||
|
size="md"
|
||||||
|
onClick={() => handleViewReading(reading)}
|
||||||
|
>
|
||||||
|
<Eye className="mr-1 h-4 w-4" />
|
||||||
|
查看
|
||||||
|
</ChineseButton>
|
||||||
|
<ChineseButton
|
||||||
|
variant="ghost"
|
||||||
|
size="md"
|
||||||
|
onClick={() => handleDeleteReading(reading.id)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</ChineseButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ChineseCardContent>
|
||||||
|
</ChineseCard>
|
||||||
<div className="flex items-center space-x-2">
|
);
|
||||||
<Button
|
})}
|
||||||
variant="outline"
|
</div>
|
||||||
size="sm"
|
)}
|
||||||
onClick={() => handleViewReading(reading)}
|
</ChineseCardContent>
|
||||||
>
|
</ChineseCard>
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Sparkles, Star, Compass, Heart, BarChart3, BookOpen } from 'lucide-react';
|
import { Sparkles, Star, Compass, Heart, BarChart3, BookOpen } from 'lucide-react';
|
||||||
import { Button } from '../components/ui/Button';
|
import { ChineseButton } from '../components/ui/ChineseButton';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from '../components/ui/ChineseCard';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
const HomePage: React.FC = () => {
|
const HomePage: React.FC = () => {
|
||||||
@@ -57,58 +57,58 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="text-center space-y-8 relative">
|
<div className="text-center space-y-6 md:space-y-8 relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* 传统中式背景装饰 */}
|
{/* 传统中式背景装饰 */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<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 className="w-60 h-60 md:w-80 md:h-80 bg-gradient-to-r from-red-500/30 to-red-600/30 rounded-full blur-3xl"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<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 className="w-80 h-80 md:w-96 md:h-96 bg-gradient-to-r from-yellow-400/20 to-yellow-500/20 rounded-full blur-3xl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10">
|
<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">
|
<div className="w-12 h-12 md:w-14 md:h-14 mx-auto mb-4 md:mb-6 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center shadow-lg border-2 border-red-600">
|
||||||
<img
|
<img
|
||||||
src="/traditional-chinese-bagua-eight-trigrams-black-gold.jpg"
|
src="/traditional-chinese-bagua-eight-trigrams-black-gold.jpg"
|
||||||
alt="太极八卦"
|
alt="太极八卦"
|
||||||
className="w-10 h-10 rounded-full object-cover"
|
className="w-8 h-8 md:w-10 md:h-10 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-5xl md:text-6xl font-bold text-red-800 mb-6 chinese-text-shadow font-serif">
|
<h1 className="text-display-xl font-bold text-red-600 mb-4 md:mb-6 font-chinese">
|
||||||
神机阁
|
神机阁
|
||||||
<span className="block text-3xl md:text-4xl text-yellow-600 mt-2 chinese-text-shadow">
|
<span className="block text-display-md text-yellow-600 mt-2">
|
||||||
AI智能命理分析
|
专业命理分析平台
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-red-700 max-w-3xl mx-auto leading-relaxed font-medium">
|
<p className="text-body-xl text-gray-700 max-w-2xl lg:max-w-3xl mx-auto leading-relaxed font-chinese px-4">
|
||||||
融合传统命理智慧与现代AI技术,为您提供个性化、专业化的命理解读和人生指导
|
融合传统命理智慧与现代AI技术,为您提供个性化、专业化的命理解读和人生指导
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center relative z-10">
|
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center relative z-10 px-4">
|
||||||
{user ? (
|
{user ? (
|
||||||
<Link to="/analysis">
|
<Link to="/analysis" className="w-full sm:w-auto">
|
||||||
<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">
|
<ChineseButton size="lg" className="w-full">
|
||||||
<Sparkles className="mr-2 h-5 w-5" />
|
<Sparkles className="mr-2 h-5 w-5" />
|
||||||
开始分析
|
开始分析
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link to="/register">
|
<Link to="/register" className="w-full sm:w-auto">
|
||||||
<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">
|
<ChineseButton variant="secondary" size="lg" className="w-full">
|
||||||
<Heart className="mr-2 h-5 w-5" />
|
<Heart className="mr-2 h-5 w-5" />
|
||||||
免费注册
|
免费注册
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/login">
|
<Link to="/login" className="w-full sm:w-auto">
|
||||||
<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">
|
<ChineseButton variant="outline" size="lg" className="w-full">
|
||||||
登录账户
|
登录账户
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -116,16 +116,16 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<div className="grid md:grid-cols-3 gap-6 relative justify-center max-w-6xl mx-auto">
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 relative max-w-6xl mx-auto px-4">
|
||||||
{/* 装饰元素 - 调整为更适合3列布局的位置 */}
|
{/* 装饰元素 - 仅在大屏幕显示 */}
|
||||||
<div className="absolute -left-12 top-1/4 w-20 h-20 opacity-20 pointer-events-none hidden md:block">
|
<div className="absolute -left-12 top-1/4 w-16 h-16 opacity-15 pointer-events-none hidden xl:block">
|
||||||
<img
|
<img
|
||||||
src="/chinese_traditional_red_gold_auspicious_cloud_pattern.jpg"
|
src="/chinese_traditional_red_gold_auspicious_cloud_pattern.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
className="w-full h-full object-cover rounded-lg"
|
className="w-full h-full object-cover rounded-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -right-12 bottom-1/4 w-20 h-20 opacity-20 pointer-events-none hidden md:block">
|
<div className="absolute -right-12 bottom-1/4 w-16 h-16 opacity-15 pointer-events-none hidden xl:block">
|
||||||
<img
|
<img
|
||||||
src="/chinese_traditional_red_gold_auspicious_cloud_pattern.jpg"
|
src="/chinese_traditional_red_gold_auspicious_cloud_pattern.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -136,48 +136,48 @@ const HomePage: React.FC = () => {
|
|||||||
{features.map((feature, index) => {
|
{features.map((feature, index) => {
|
||||||
const Icon = feature.icon;
|
const Icon = feature.icon;
|
||||||
return (
|
return (
|
||||||
<Card key={index} className="text-center hover:shadow-2xl transition-all duration-300 chinese-card-decoration dragon-corner transform hover:scale-105">
|
<ChineseCard key={index} variant="elevated" className="text-center sm:col-span-1 lg:col-span-1 last:sm:col-span-2 last:lg:col-span-1">
|
||||||
<CardHeader>
|
<ChineseCardHeader>
|
||||||
<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`}>
|
<div className="w-12 h-12 md:w-14 md:h-14 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center mx-auto mb-3 md:mb-4 shadow-lg border-2 border-red-600">
|
||||||
<Icon className={`h-6 w-6 text-red-800`} />
|
<Icon className="h-6 w-6 md:h-7 md:w-7 text-red-800" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className={`${feature.color} text-2xl font-bold font-serif chinese-text-shadow`}>{feature.title}</CardTitle>
|
<ChineseCardTitle className="text-red-600 text-heading-md font-bold font-chinese">{feature.title}</ChineseCardTitle>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
<CardContent>
|
<ChineseCardContent>
|
||||||
<p className="text-red-700 leading-relaxed font-medium mb-4">{feature.description}</p>
|
<p className="text-gray-700 leading-relaxed font-chinese mb-4 text-body-md">{feature.description}</p>
|
||||||
{user && (
|
{user && (
|
||||||
<Link to={feature.link}>
|
<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">
|
<ChineseButton variant="secondary" className="w-full">
|
||||||
立即体验
|
立即体验
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<Card className="chinese-traditional-bg text-white text-center dragon-corner relative overflow-hidden">
|
<ChineseCard variant="golden" className="text-center relative overflow-hidden mx-4">
|
||||||
<CardContent className="py-12 relative z-10">
|
<ChineseCardContent className="py-8 md: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">
|
<div className="w-14 h-14 md:w-16 md:h-16 mx-auto mb-4 md:mb-6 bg-gradient-to-br from-red-600 to-red-700 rounded-full flex items-center justify-center shadow-2xl border-2 border-red-800">
|
||||||
<Sparkles className="w-8 h-8 text-red-800" />
|
<Sparkles className="w-7 h-7 md:w-8 md:h-8 text-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-4xl font-bold mb-4 chinese-text-shadow font-serif">探索您的命运密码</h2>
|
<h2 className="text-display-md font-bold mb-3 md:mb-4 font-chinese text-red-800">探索您的命运密码</h2>
|
||||||
<p className="text-red-100 mb-8 text-lg font-medium leading-relaxed">
|
<p className="text-red-700 mb-6 md:mb-8 text-body-lg font-chinese leading-relaxed px-4">
|
||||||
加入数万用户的选择,让AI帮您解读人生密码
|
加入数万用户的选择,让AI帮您解读人生密码
|
||||||
</p>
|
</p>
|
||||||
{!user && (
|
{!user && (
|
||||||
<Link to="/register">
|
<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">
|
<ChineseButton variant="primary" size="lg" className="shadow-xl">
|
||||||
立即开始
|
立即开始
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { Button } from '../components/ui/Button';
|
import { ChineseButton } from '../components/ui/ChineseButton';
|
||||||
import { Input } from '../components/ui/Input';
|
import { ChineseInput } from '../components/ui/ChineseInput';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from '../components/ui/ChineseCard';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Mail, Lock, LogIn } from 'lucide-react';
|
import { Mail, Lock, LogIn } from 'lucide-react';
|
||||||
|
|
||||||
@@ -34,62 +34,71 @@ const LoginPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[80vh] flex items-center justify-center">
|
<div className="min-h-[80vh] flex items-center justify-center px-4 py-8">
|
||||||
<Card className="w-full max-w-md">
|
{/* 背景装饰 */}
|
||||||
<CardHeader className="text-center">
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div className="absolute top-1/4 left-1/4 w-32 h-32 bg-gradient-to-r from-red-500/10 to-yellow-500/10 rounded-full blur-3xl"></div>
|
||||||
<LogIn className="h-6 w-6 text-purple-600" />
|
<div className="absolute bottom-1/4 right-1/4 w-40 h-40 bg-gradient-to-r from-yellow-500/10 to-red-500/10 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChineseCard variant="elevated" className="w-full max-w-md relative z-10">
|
||||||
|
<ChineseCardHeader className="text-center">
|
||||||
|
<div className="w-14 h-14 bg-gradient-to-br from-red-600 to-red-700 rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg border-2 border-yellow-500">
|
||||||
|
<LogIn className="h-7 w-7 text-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl">登录账户</CardTitle>
|
<ChineseCardTitle className="text-2xl md:text-3xl text-red-600 font-chinese">登录账户</ChineseCardTitle>
|
||||||
<p className="text-gray-600">欢迎回到神机阁</p>
|
<p className="text-gray-600 font-chinese mt-2">欢迎回到神机阁</p>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
<CardContent>
|
<ChineseCardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
label="邮箱地址"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
required
|
|
||||||
placeholder="请输入您的邮箱"
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Mail className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
<ChineseInput
|
||||||
|
type="email"
|
||||||
|
label="邮箱地址"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入您的邮箱"
|
||||||
|
variant="bordered"
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<Mail className="absolute left-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
label="密码"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
placeholder="请输入您的密码"
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
<ChineseInput
|
||||||
|
type="password"
|
||||||
|
label="密码"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入您的密码"
|
||||||
|
variant="bordered"
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<Lock className="absolute left-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<ChineseButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
size="lg"
|
||||||
|
className="w-full mt-6"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? '登录中...' : '登录'}
|
{loading ? '登录中...' : '登录'}
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600 font-chinese">
|
||||||
还没有账户?
|
还没有账户?
|
||||||
<Link to="/register" className="text-purple-600 hover:text-purple-700 font-medium ml-1">
|
<Link to="/register" className="text-red-600 hover:text-red-700 font-medium ml-1 transition-colors duration-200">
|
||||||
立即注册
|
立即注册
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { localApi } from '../lib/localApi';
|
import { localApi } from '../lib/localApi';
|
||||||
import { Button } from '../components/ui/Button';
|
import { ChineseButton } from '../components/ui/ChineseButton';
|
||||||
import { Input } from '../components/ui/Input';
|
import { ChineseInput } from '../components/ui/ChineseInput';
|
||||||
import { Select } from '../components/ui/Select';
|
import { ChineseSelect } from '../components/ui/ChineseSelect';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from '../components/ui/ChineseCard';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { User, Calendar, MapPin, Save } from 'lucide-react';
|
import { User, Calendar, MapPin, Save } from 'lucide-react';
|
||||||
import { UserProfile } from '../types';
|
import { UserProfile } from '../types';
|
||||||
@@ -88,62 +88,72 @@ const ProfilePage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||||
<Card>
|
<div className="text-center mb-6">
|
||||||
<CardHeader>
|
<h1 className="text-2xl md:text-3xl font-bold text-red-600 font-chinese mb-2">个人档案</h1>
|
||||||
|
<p className="text-gray-600 font-chinese">完善您的个人信息,获得更精准的命理分析</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChineseCard variant="elevated">
|
||||||
|
<ChineseCardHeader>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
|
||||||
<User className="h-6 w-6 text-purple-600" />
|
<User className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>个人档案</CardTitle>
|
<ChineseCardTitle className="text-red-600 font-chinese">基本信息</ChineseCardTitle>
|
||||||
<p className="text-gray-600">完善您的个人信息,获得更精准的命理分析</p>
|
<p className="text-gray-600 font-chinese">请填写准确的个人信息</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
<CardContent>
|
<ChineseCardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4 md:gap-6">
|
||||||
<Input
|
<ChineseInput
|
||||||
label="姓名 *"
|
label="姓名"
|
||||||
value={formData.full_name}
|
value={formData.full_name}
|
||||||
onChange={(e) => handleInputChange('full_name', e.target.value)}
|
onChange={(e) => handleInputChange('full_name', e.target.value)}
|
||||||
required
|
required
|
||||||
placeholder="请输入您的真实姓名"
|
placeholder="请输入您的真实姓名"
|
||||||
|
variant="filled"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<ChineseInput
|
||||||
label="用户名"
|
label="用户名"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||||
placeholder="请输入用户名(可选)"
|
placeholder="请输入用户名(可选)"
|
||||||
|
variant="filled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4 md:gap-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
type="date"
|
type="date"
|
||||||
label="出生日期 *"
|
label="出生日期"
|
||||||
value={formData.birth_date}
|
value={formData.birth_date}
|
||||||
onChange={(e) => handleInputChange('birth_date', e.target.value)}
|
onChange={(e) => handleInputChange('birth_date', e.target.value)}
|
||||||
required
|
required
|
||||||
|
variant="filled"
|
||||||
|
className="pr-10"
|
||||||
/>
|
/>
|
||||||
<Calendar className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
<Calendar className="absolute right-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Input
|
<ChineseInput
|
||||||
type="time"
|
type="time"
|
||||||
label="出生时间"
|
label="出生时间"
|
||||||
value={formData.birth_time}
|
value={formData.birth_time}
|
||||||
onChange={(e) => handleInputChange('birth_time', e.target.value)}
|
onChange={(e) => handleInputChange('birth_time', e.target.value)}
|
||||||
placeholder="选填,但强烈建议填写"
|
helperText="选填,但强烈建议填写以提高分析准确性"
|
||||||
|
variant="filled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4 md:gap-6">
|
||||||
<Select
|
<ChineseSelect
|
||||||
label="性别 *"
|
label="性别"
|
||||||
value={formData.gender}
|
value={formData.gender}
|
||||||
onChange={(e) => handleInputChange('gender', e.target.value)}
|
onChange={(e) => handleInputChange('gender', e.target.value)}
|
||||||
options={[
|
options={[
|
||||||
@@ -151,47 +161,52 @@ const ProfilePage: React.FC = () => {
|
|||||||
{ value: 'female', label: '女性' }
|
{ value: 'female', label: '女性' }
|
||||||
]}
|
]}
|
||||||
required
|
required
|
||||||
|
variant="filled"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
label="出生地点"
|
label="出生地点"
|
||||||
value={formData.birth_location}
|
value={formData.birth_location}
|
||||||
onChange={(e) => handleInputChange('birth_location', e.target.value)}
|
onChange={(e) => handleInputChange('birth_location', e.target.value)}
|
||||||
placeholder="如:北京市朝阳区"
|
placeholder="如:北京市朝阳区"
|
||||||
|
variant="filled"
|
||||||
|
className="pr-10"
|
||||||
|
helperText="选填,用于更精确的地理位置分析"
|
||||||
/>
|
/>
|
||||||
<MapPin className="absolute right-3 top-8 h-4 w-4 text-gray-400 pointer-events-none" />
|
<MapPin className="absolute right-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg">
|
<div className="bg-red-50 p-4 rounded-lg border border-red-200">
|
||||||
<h4 className="font-medium text-blue-800 mb-2">温馨提示</h4>
|
<h4 className="font-semibold text-red-800 mb-2 font-chinese">温馨提示</h4>
|
||||||
<ul className="text-sm text-blue-700 space-y-1">
|
<ul className="text-sm text-red-700 space-y-1 font-chinese">
|
||||||
<li>• 姓名和出生日期是必填项,对命理分析至关重要</li>
|
<li>• 姓名和出生日期是必填项,对命理分析至关重要</li>
|
||||||
<li>• 出生时间越精确,分析结果越准确</li>
|
<li>• 出生时间越精确,分析结果越准确</li>
|
||||||
<li>• 出生地点有助于更精准的时间校正</li>
|
<li>• 出生地点有助于更精准的时间校正</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<ChineseButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full mt-6"
|
||||||
|
size="lg"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Save className="mr-2 h-4 w-4" />
|
<Save className="mr-2 h-4 w-4" />
|
||||||
{loading ? '保存中...' : '保存档案'}
|
{loading ? '保存中...' : '保存档案'}
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{profile && (
|
{profile && (
|
||||||
<div className="mt-6 pt-6 border-t border-gray-200">
|
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 font-chinese">
|
||||||
最后更新:{new Date(profile.updated_at).toLocaleString('zh-CN')}
|
最后更新:{new Date(profile.updated_at).toLocaleString('zh-CN')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { Button } from '../components/ui/Button';
|
import { ChineseButton } from '../components/ui/ChineseButton';
|
||||||
import { Input } from '../components/ui/Input';
|
import { ChineseInput } from '../components/ui/ChineseInput';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from '../components/ui/ChineseCard';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Mail, Lock, UserPlus } from 'lucide-react';
|
import { Mail, Lock, UserPlus } from 'lucide-react';
|
||||||
|
|
||||||
@@ -46,75 +46,88 @@ const RegisterPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[80vh] flex items-center justify-center">
|
<div className="min-h-[80vh] flex items-center justify-center px-4 py-8">
|
||||||
<Card className="w-full max-w-md">
|
{/* 背景装饰 */}
|
||||||
<CardHeader className="text-center">
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div className="absolute top-1/3 left-1/3 w-36 h-36 bg-gradient-to-r from-yellow-500/10 to-red-500/10 rounded-full blur-3xl"></div>
|
||||||
<UserPlus className="h-6 w-6 text-purple-600" />
|
<div className="absolute bottom-1/3 right-1/3 w-44 h-44 bg-gradient-to-r from-red-500/10 to-yellow-500/10 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChineseCard variant="elevated" className="w-full max-w-md relative z-10">
|
||||||
|
<ChineseCardHeader className="text-center">
|
||||||
|
<div className="w-14 h-14 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg border-2 border-red-600">
|
||||||
|
<UserPlus className="h-7 w-7 text-red-800" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl">创建账户</CardTitle>
|
<ChineseCardTitle className="text-2xl md:text-3xl text-red-600 font-chinese">创建账户</ChineseCardTitle>
|
||||||
<p className="text-gray-600">加入神机阁,开启您的命理之旅</p>
|
<p className="text-gray-600 font-chinese mt-2">加入神机阁,开启您的命理之旅</p>
|
||||||
</CardHeader>
|
</ChineseCardHeader>
|
||||||
<CardContent>
|
<ChineseCardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
type="email"
|
type="email"
|
||||||
label="邮箱地址"
|
label="邮箱地址"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
placeholder="请输入您的邮箱"
|
placeholder="请输入您的邮箱"
|
||||||
|
variant="bordered"
|
||||||
className="pl-10"
|
className="pl-10"
|
||||||
/>
|
/>
|
||||||
<Mail className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
<Mail className="absolute left-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
type="password"
|
type="password"
|
||||||
label="密码"
|
label="密码"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
placeholder="请输入您的密码(不少于6位)"
|
placeholder="请输入您的密码(不少于6位)"
|
||||||
|
variant="bordered"
|
||||||
className="pl-10"
|
className="pl-10"
|
||||||
|
helperText="密码长度不能少于6位"
|
||||||
/>
|
/>
|
||||||
<Lock className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
<Lock className="absolute left-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<ChineseInput
|
||||||
type="password"
|
type="password"
|
||||||
label="确认密码"
|
label="确认密码"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
placeholder="请再次输入密码"
|
placeholder="请再次输入密码"
|
||||||
|
variant="bordered"
|
||||||
className="pl-10"
|
className="pl-10"
|
||||||
|
error={confirmPassword && password !== confirmPassword ? '两次输入的密码不一致' : undefined}
|
||||||
/>
|
/>
|
||||||
<Lock className="absolute left-3 top-8 h-4 w-4 text-gray-400" />
|
<Lock className="absolute left-3 top-9 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<ChineseButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full mt-6"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? '注册中...' : '注册账户'}
|
{loading ? '注册中...' : '注册账户'}
|
||||||
</Button>
|
</ChineseButton>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600 font-chinese">
|
||||||
已有账户?
|
已有账户?
|
||||||
<Link to="/login" className="text-purple-600 hover:text-purple-700 font-medium ml-1">
|
<Link to="/login" className="text-red-600 hover:text-red-700 font-medium ml-1 transition-colors duration-200">
|
||||||
立即登录
|
立即登录
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</ChineseCardContent>
|
||||||
</Card>
|
</ChineseCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
274
src/styles/typography.css
Normal file
274
src/styles/typography.css
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/* 神机阁字体规范系统 */
|
||||||
|
|
||||||
|
/* ===== 字体族定义 ===== */
|
||||||
|
:root {
|
||||||
|
/* 中文字体栈 - 优先级从高到低 */
|
||||||
|
--font-chinese: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', 'Noto Sans SC', 'STHeiti', 'WenQuanYi Micro Hei', sans-serif;
|
||||||
|
|
||||||
|
/* 中文衬线字体 - 用于特殊标题 */
|
||||||
|
--font-chinese-serif: 'Noto Serif SC', 'STSong', 'SimSun', '宋体', serif;
|
||||||
|
|
||||||
|
/* 英文字体 - 用于数字和英文内容 */
|
||||||
|
--font-english: 'Inter', 'Helvetica Neue', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 字体大小规范 ===== */
|
||||||
|
/* 基于 16px 基准,使用 rem 单位确保可访问性 */
|
||||||
|
|
||||||
|
/* 超大标题 - 用于首页主标题 */
|
||||||
|
.text-display-xl {
|
||||||
|
font-size: 3.5rem; /* 56px */
|
||||||
|
line-height: 1.1;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-display-lg {
|
||||||
|
font-size: 3rem; /* 48px */
|
||||||
|
line-height: 1.1;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-display-md {
|
||||||
|
font-size: 2.5rem; /* 40px */
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题系列 */
|
||||||
|
.text-heading-xl {
|
||||||
|
font-size: 2rem; /* 32px */
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-lg {
|
||||||
|
font-size: 1.75rem; /* 28px */
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-md {
|
||||||
|
font-size: 1.5rem; /* 24px */
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-sm {
|
||||||
|
font-size: 1.25rem; /* 20px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-xs {
|
||||||
|
font-size: 1.125rem; /* 18px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正文系列 */
|
||||||
|
.text-body-xl {
|
||||||
|
font-size: 1.125rem; /* 18px */
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-lg {
|
||||||
|
font-size: 1rem; /* 16px */
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-md {
|
||||||
|
font-size: 0.875rem; /* 14px */
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-sm {
|
||||||
|
font-size: 0.75rem; /* 12px */
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签和辅助文字 */
|
||||||
|
.text-label-lg {
|
||||||
|
font-size: 0.875rem; /* 14px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-label-md {
|
||||||
|
font-size: 0.75rem; /* 12px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-label-sm {
|
||||||
|
font-size: 0.6875rem; /* 11px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮文字 */
|
||||||
|
.text-button-lg {
|
||||||
|
font-size: 1rem; /* 16px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-button-md {
|
||||||
|
font-size: 0.875rem; /* 14px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-button-sm {
|
||||||
|
font-size: 0.75rem; /* 12px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 响应式字体规范 ===== */
|
||||||
|
/* 移动端优化 - 在小屏幕上适当缩小字体 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.text-display-xl {
|
||||||
|
font-size: 2.5rem; /* 40px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-display-lg {
|
||||||
|
font-size: 2.25rem; /* 36px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-display-md {
|
||||||
|
font-size: 2rem; /* 32px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-xl {
|
||||||
|
font-size: 1.75rem; /* 28px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-lg {
|
||||||
|
font-size: 1.5rem; /* 24px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-md {
|
||||||
|
font-size: 1.25rem; /* 20px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-xl {
|
||||||
|
font-size: 1rem; /* 16px */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕进一步优化 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.text-display-xl {
|
||||||
|
font-size: 2rem; /* 32px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-display-lg {
|
||||||
|
font-size: 1.875rem; /* 30px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-display-md {
|
||||||
|
font-size: 1.75rem; /* 28px */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 字体族应用类 ===== */
|
||||||
|
.font-chinese {
|
||||||
|
font-family: var(--font-chinese);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-chinese-serif {
|
||||||
|
font-family: var(--font-chinese-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-english {
|
||||||
|
font-family: var(--font-english);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 特殊用途字体类 ===== */
|
||||||
|
/* 数字专用 - 确保数字对齐 */
|
||||||
|
.font-numeric {
|
||||||
|
font-family: var(--font-english);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码字体 */
|
||||||
|
.font-mono {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 字重规范 ===== */
|
||||||
|
.font-thin { font-weight: 100; }
|
||||||
|
.font-extralight { font-weight: 200; }
|
||||||
|
.font-light { font-weight: 300; }
|
||||||
|
.font-normal { font-weight: 400; }
|
||||||
|
.font-medium { font-weight: 500; }
|
||||||
|
.font-semibold { font-weight: 600; }
|
||||||
|
.font-bold { font-weight: 700; }
|
||||||
|
.font-extrabold { font-weight: 800; }
|
||||||
|
.font-black { font-weight: 900; }
|
||||||
|
|
||||||
|
/* ===== 行高规范 ===== */
|
||||||
|
.leading-none { line-height: 1; }
|
||||||
|
.leading-tight { line-height: 1.25; }
|
||||||
|
.leading-snug { line-height: 1.375; }
|
||||||
|
.leading-normal { line-height: 1.5; }
|
||||||
|
.leading-relaxed { line-height: 1.625; }
|
||||||
|
.leading-loose { line-height: 2; }
|
||||||
|
|
||||||
|
/* ===== 字间距规范 ===== */
|
||||||
|
.tracking-tighter { letter-spacing: -0.05em; }
|
||||||
|
.tracking-tight { letter-spacing: -0.025em; }
|
||||||
|
.tracking-normal { letter-spacing: 0em; }
|
||||||
|
.tracking-wide { letter-spacing: 0.025em; }
|
||||||
|
.tracking-wider { letter-spacing: 0.05em; }
|
||||||
|
.tracking-widest { letter-spacing: 0.1em; }
|
||||||
|
|
||||||
|
/* ===== 使用指南注释 ===== */
|
||||||
|
/*
|
||||||
|
字体使用指南:
|
||||||
|
|
||||||
|
1. 显示级标题 (Display):
|
||||||
|
- text-display-xl: 首页主标题
|
||||||
|
- text-display-lg: 重要页面标题
|
||||||
|
- text-display-md: 次要页面标题
|
||||||
|
|
||||||
|
2. 标题 (Heading):
|
||||||
|
- text-heading-xl: H1 标题
|
||||||
|
- text-heading-lg: H2 标题
|
||||||
|
- text-heading-md: H3 标题
|
||||||
|
- text-heading-sm: H4 标题
|
||||||
|
- text-heading-xs: H5 标题
|
||||||
|
|
||||||
|
3. 正文 (Body):
|
||||||
|
- text-body-xl: 重要描述文字
|
||||||
|
- text-body-lg: 标准正文 (默认)
|
||||||
|
- text-body-md: 次要正文
|
||||||
|
- text-body-sm: 辅助信息
|
||||||
|
|
||||||
|
4. 标签 (Label):
|
||||||
|
- text-label-lg: 表单标签
|
||||||
|
- text-label-md: 小标签
|
||||||
|
- text-label-sm: 微小标签
|
||||||
|
|
||||||
|
5. 按钮 (Button):
|
||||||
|
- text-button-lg: 大按钮
|
||||||
|
- text-button-md: 标准按钮
|
||||||
|
- text-button-sm: 小按钮
|
||||||
|
|
||||||
|
字体族使用:
|
||||||
|
- font-chinese: 中文内容 (默认)
|
||||||
|
- font-chinese-serif: 特殊标题
|
||||||
|
- font-english: 英文和数字
|
||||||
|
- font-numeric: 数字对齐
|
||||||
|
- font-mono: 代码
|
||||||
|
*/
|
||||||
@@ -10,29 +10,132 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: '2rem',
|
padding: '1rem',
|
||||||
screens: {
|
screens: {
|
||||||
|
sm: '640px',
|
||||||
|
md: '768px',
|
||||||
|
lg: '1024px',
|
||||||
|
xl: '1280px',
|
||||||
'2xl': '1400px',
|
'2xl': '1400px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 中式字体系统
|
||||||
|
fontFamily: {
|
||||||
|
'chinese': ['PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', 'STHeiti', 'WenQuanYi Micro Hei', 'sans-serif'],
|
||||||
|
'serif-chinese': ['STSong', 'SimSun', '宋体', 'serif'],
|
||||||
|
},
|
||||||
|
// 统一字体规范系统
|
||||||
|
fontSize: {
|
||||||
|
// 保留原有尺寸以兼容现有代码
|
||||||
|
'xs': ['0.75rem', { lineHeight: '1.5' }],
|
||||||
|
'sm': ['0.875rem', { lineHeight: '1.6' }],
|
||||||
|
'base': ['1rem', { lineHeight: '1.6' }],
|
||||||
|
'lg': ['1.125rem', { lineHeight: '1.5' }],
|
||||||
|
'xl': ['1.25rem', { lineHeight: '1.4' }],
|
||||||
|
'2xl': ['1.5rem', { lineHeight: '1.3' }],
|
||||||
|
'3xl': ['1.875rem', { lineHeight: '1.2' }],
|
||||||
|
'4xl': ['2.25rem', { lineHeight: '1.2' }],
|
||||||
|
'5xl': ['3rem', { lineHeight: '1.1' }],
|
||||||
|
'6xl': ['3.5rem', { lineHeight: '1.1' }],
|
||||||
|
|
||||||
|
// 新的语义化字体规范
|
||||||
|
// 显示级标题
|
||||||
|
'display-xl': ['3.5rem', { lineHeight: '1.1', fontWeight: '800', letterSpacing: '-0.02em' }],
|
||||||
|
'display-lg': ['3rem', { lineHeight: '1.1', fontWeight: '700', letterSpacing: '-0.02em' }],
|
||||||
|
'display-md': ['2.5rem', { lineHeight: '1.2', fontWeight: '700', letterSpacing: '-0.01em' }],
|
||||||
|
|
||||||
|
// 标题系列
|
||||||
|
'heading-xl': ['2rem', { lineHeight: '1.25', fontWeight: '600' }],
|
||||||
|
'heading-lg': ['1.75rem', { lineHeight: '1.3', fontWeight: '600' }],
|
||||||
|
'heading-md': ['1.5rem', { lineHeight: '1.35', fontWeight: '600' }],
|
||||||
|
'heading-sm': ['1.25rem', { lineHeight: '1.4', fontWeight: '600' }],
|
||||||
|
'heading-xs': ['1.125rem', { lineHeight: '1.4', fontWeight: '600' }],
|
||||||
|
|
||||||
|
// 正文系列
|
||||||
|
'body-xl': ['1.125rem', { lineHeight: '1.6', fontWeight: '400' }],
|
||||||
|
'body-lg': ['1rem', { lineHeight: '1.6', fontWeight: '400' }],
|
||||||
|
'body-md': ['0.875rem', { lineHeight: '1.6', fontWeight: '400' }],
|
||||||
|
'body-sm': ['0.75rem', { lineHeight: '1.5', fontWeight: '400' }],
|
||||||
|
|
||||||
|
// 标签系列
|
||||||
|
'label-lg': ['0.875rem', { lineHeight: '1.4', fontWeight: '500' }],
|
||||||
|
'label-md': ['0.75rem', { lineHeight: '1.4', fontWeight: '500' }],
|
||||||
|
'label-sm': ['0.6875rem', { lineHeight: '1.4', fontWeight: '500' }],
|
||||||
|
|
||||||
|
// 按钮系列
|
||||||
|
'button-lg': ['1rem', { lineHeight: '1.4', fontWeight: '600', letterSpacing: '0.01em' }],
|
||||||
|
'button-md': ['0.875rem', { lineHeight: '1.4', fontWeight: '600', letterSpacing: '0.01em' }],
|
||||||
|
'button-sm': ['0.75rem', { lineHeight: '1.4', fontWeight: '600', letterSpacing: '0.01em' }],
|
||||||
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
// 中式配色系统
|
||||||
colors: {
|
colors: {
|
||||||
|
// 传统中式颜色
|
||||||
|
'cinnabar': {
|
||||||
|
50: '#fef2f2',
|
||||||
|
100: '#fee2e2',
|
||||||
|
200: '#fecaca',
|
||||||
|
300: '#fca5a5',
|
||||||
|
400: '#f87171',
|
||||||
|
500: '#DC143C', // 朱砂红主色
|
||||||
|
600: '#c41e3a',
|
||||||
|
700: '#a91b2e',
|
||||||
|
800: '#8f1725',
|
||||||
|
900: '#7c1420',
|
||||||
|
},
|
||||||
|
'gold': {
|
||||||
|
50: '#fffbeb',
|
||||||
|
100: '#fef3c7',
|
||||||
|
200: '#fde68a',
|
||||||
|
300: '#fcd34d',
|
||||||
|
400: '#fbbf24',
|
||||||
|
500: '#FFD700', // 金黄色主色
|
||||||
|
600: '#d97706',
|
||||||
|
700: '#b45309',
|
||||||
|
800: '#92400e',
|
||||||
|
900: '#78350f',
|
||||||
|
},
|
||||||
|
'ink': {
|
||||||
|
50: '#f8fafc',
|
||||||
|
100: '#f1f5f9',
|
||||||
|
200: '#e2e8f0',
|
||||||
|
300: '#cbd5e1',
|
||||||
|
400: '#94a3b8',
|
||||||
|
500: '#64748b',
|
||||||
|
600: '#475569',
|
||||||
|
700: '#334155',
|
||||||
|
800: '#1e293b',
|
||||||
|
900: '#2C2C2C', // 墨黑色主色
|
||||||
|
},
|
||||||
|
'paper': {
|
||||||
|
50: '#fefefe',
|
||||||
|
100: '#fdfdfd',
|
||||||
|
200: '#fafafa',
|
||||||
|
300: '#f7f7f7',
|
||||||
|
400: '#f3f3f3',
|
||||||
|
500: '#F5F5DC', // 古纸色主色
|
||||||
|
600: '#e8e8cd',
|
||||||
|
700: '#d4d4b8',
|
||||||
|
800: '#c0c0a3',
|
||||||
|
900: '#a8a88a',
|
||||||
|
},
|
||||||
|
// 保留原有颜色以兼容现有组件
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
input: 'hsl(var(--input))',
|
input: 'hsl(var(--input))',
|
||||||
ring: 'hsl(var(--ring))',
|
ring: 'hsl(var(--ring))',
|
||||||
background: 'hsl(var(--background))',
|
background: 'hsl(var(--background))',
|
||||||
foreground: 'hsl(var(--foreground))',
|
foreground: 'hsl(var(--foreground))',
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: '#2B5D3A',
|
DEFAULT: '#DC143C', // 更新为朱砂红
|
||||||
foreground: 'hsl(var(--primary-foreground))',
|
foreground: '#ffffff',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: '#4A90E2',
|
DEFAULT: '#FFD700', // 更新为金黄色
|
||||||
foreground: 'hsl(var(--secondary-foreground))',
|
foreground: '#2C2C2C',
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: '#F5A623',
|
DEFAULT: '#708090', // 青灰色
|
||||||
foreground: 'hsl(var(--accent-foreground))',
|
foreground: '#ffffff',
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
@@ -51,12 +154,63 @@ module.exports = {
|
|||||||
foreground: 'hsl(var(--card-foreground))',
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
// 中式间距系统 (基于4px)
|
||||||
lg: 'var(--radius)',
|
spacing: {
|
||||||
md: 'calc(var(--radius) - 2px)',
|
'xs': '0.25rem', // 4px
|
||||||
sm: 'calc(var(--radius) - 4px)',
|
'sm': '0.5rem', // 8px
|
||||||
|
'md': '1rem', // 16px
|
||||||
|
'lg': '1.5rem', // 24px
|
||||||
|
'xl': '2rem', // 32px
|
||||||
|
'2xl': '3rem', // 48px
|
||||||
|
'3xl': '4rem', // 64px
|
||||||
},
|
},
|
||||||
|
// 中式圆角系统
|
||||||
|
borderRadius: {
|
||||||
|
'none': '0',
|
||||||
|
'sm': '0.25rem', // 4px
|
||||||
|
'DEFAULT': '0.375rem', // 6px
|
||||||
|
'md': '0.5rem', // 8px
|
||||||
|
'lg': '0.75rem', // 12px
|
||||||
|
'xl': '1rem', // 16px
|
||||||
|
'2xl': '1.5rem', // 24px
|
||||||
|
'full': '9999px',
|
||||||
|
// 保留原有变量以兼容
|
||||||
|
'radius-lg': 'var(--radius)',
|
||||||
|
'radius-md': 'calc(var(--radius) - 2px)',
|
||||||
|
'radius-sm': 'calc(var(--radius) - 4px)',
|
||||||
|
},
|
||||||
|
// 中式阴影系统
|
||||||
|
boxShadow: {
|
||||||
|
'chinese-sm': '0 1px 3px rgba(220, 20, 60, 0.1)',
|
||||||
|
'chinese': '0 4px 20px rgba(220, 20, 60, 0.15)',
|
||||||
|
'chinese-md': '0 8px 25px rgba(220, 20, 60, 0.15)',
|
||||||
|
'chinese-lg': '0 15px 35px rgba(220, 20, 60, 0.2)',
|
||||||
|
'chinese-xl': '0 25px 50px rgba(220, 20, 60, 0.25)',
|
||||||
|
'gold': '0 4px 20px rgba(255, 215, 0, 0.3)',
|
||||||
|
'paper': '0 2px 10px rgba(245, 245, 220, 0.5)',
|
||||||
|
},
|
||||||
|
// 中式渐变背景
|
||||||
|
backgroundImage: {
|
||||||
|
'chinese-gradient': 'linear-gradient(135deg, #F5F5DC 0%, #FFD700 100%)',
|
||||||
|
'cinnabar-gradient': 'linear-gradient(135deg, #DC143C 0%, #8f1725 100%)',
|
||||||
|
'gold-gradient': 'linear-gradient(135deg, #FFD700 0%, #b45309 100%)',
|
||||||
|
'paper-gradient': 'linear-gradient(135deg, #fefefe 0%, #F5F5DC 100%)',
|
||||||
|
},
|
||||||
|
// 中式动画
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
'fade-in': {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
'slide-in': {
|
||||||
|
'0%': { transform: 'translateX(-100%)' },
|
||||||
|
'100%': { transform: 'translateX(0)' },
|
||||||
|
},
|
||||||
|
'glow': {
|
||||||
|
'0%, 100%': { boxShadow: '0 0 5px rgba(255, 215, 0, 0.5)' },
|
||||||
|
'50%': { boxShadow: '0 0 20px rgba(255, 215, 0, 0.8)' },
|
||||||
|
},
|
||||||
|
// 保留原有动画
|
||||||
'accordion-down': {
|
'accordion-down': {
|
||||||
from: { height: 0 },
|
from: { height: 0 },
|
||||||
to: { height: 'var(--radix-accordion-content-height)' },
|
to: { height: 'var(--radix-accordion-content-height)' },
|
||||||
@@ -67,6 +221,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
|
'fade-in': 'fade-in 0.5s ease-out',
|
||||||
|
'slide-in': 'slide-in 0.3s ease-out',
|
||||||
|
'glow': 'glow 2s ease-in-out infinite',
|
||||||
|
// 保留原有动画
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
},
|
},
|
||||||
|
|||||||
308
tests/integration.test.cjs
Normal file
308
tests/integration.test.cjs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// 集成测试 - 测试前后端主要功能
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
// 测试配置
|
||||||
|
const BASE_URL = 'http://localhost:3001';
|
||||||
|
const API_URL = `${BASE_URL}/api`;
|
||||||
|
|
||||||
|
// 测试数据 - 使用随机邮箱避免冲突
|
||||||
|
const testUser = {
|
||||||
|
email: `test${Date.now()}@example.com`,
|
||||||
|
password: 'test123456',
|
||||||
|
full_name: '测试用户'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('使用测试邮箱:', testUser.email);
|
||||||
|
|
||||||
|
const testBirthData = {
|
||||||
|
name: '张三',
|
||||||
|
birth_date: '1990-05-15',
|
||||||
|
birth_time: '14:30',
|
||||||
|
gender: 'male',
|
||||||
|
location: '北京'
|
||||||
|
};
|
||||||
|
|
||||||
|
let authToken = null;
|
||||||
|
let userId = null;
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
async function makeRequest(endpoint, options = {}) {
|
||||||
|
const url = `${API_URL}${endpoint}`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authToken) {
|
||||||
|
headers.Authorization = `Bearer ${authToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { response, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试套件
|
||||||
|
async function runTests() {
|
||||||
|
console.log('🚀 开始集成测试...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 测试健康检查
|
||||||
|
await testHealthCheck();
|
||||||
|
|
||||||
|
// 2. 测试用户注册
|
||||||
|
await testUserRegistration();
|
||||||
|
|
||||||
|
// 3. 测试用户登录
|
||||||
|
await testUserLogin();
|
||||||
|
|
||||||
|
// 4. 测试获取用户信息
|
||||||
|
await testGetUserInfo();
|
||||||
|
|
||||||
|
// 5. 测试八字分析
|
||||||
|
await testBaziAnalysis();
|
||||||
|
|
||||||
|
// 6. 测试紫微分析
|
||||||
|
await testZiweiAnalysis();
|
||||||
|
|
||||||
|
// 7. 测试易经分析
|
||||||
|
await testYijingAnalysis();
|
||||||
|
|
||||||
|
// 8. 测试历史记录
|
||||||
|
await testHistoryRecords();
|
||||||
|
|
||||||
|
// 9. 测试用户档案
|
||||||
|
await testUserProfile();
|
||||||
|
|
||||||
|
// 10. 测试用户登出
|
||||||
|
await testUserLogout();
|
||||||
|
|
||||||
|
console.log('✅ 所有测试通过!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 测试失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试健康检查
|
||||||
|
async function testHealthCheck() {
|
||||||
|
console.log('📋 测试健康检查...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/health');
|
||||||
|
|
||||||
|
assert.strictEqual(response.status, 200, '健康检查应该返回 200');
|
||||||
|
assert.strictEqual(data.status, 'healthy', '健康状态应该为 healthy');
|
||||||
|
|
||||||
|
console.log('✅ 健康检查通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试用户注册
|
||||||
|
async function testUserRegistration() {
|
||||||
|
console.log('📋 测试用户注册...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(testUser)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 409 || (response.status === 400 && data.error && data.error.code === 'EMAIL_EXISTS')) {
|
||||||
|
console.log('⚠️ 用户已存在,跳过注册测试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status !== 200 && response.status !== 201) {
|
||||||
|
console.error('注册失败,状态码:', response.status);
|
||||||
|
console.error('错误信息:', data);
|
||||||
|
throw new Error(`注册应该成功,但返回状态码 ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data.data && data.data.user, '应该返回用户信息');
|
||||||
|
assert(data.data && data.data.token, '应该返回认证令牌');
|
||||||
|
|
||||||
|
authToken = data.data.token;
|
||||||
|
userId = data.data.user.id;
|
||||||
|
|
||||||
|
console.log('✅ 用户注册通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试用户登录
|
||||||
|
async function testUserLogin() {
|
||||||
|
console.log('📋 测试用户登录...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: testUser.email,
|
||||||
|
password: testUser.password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('登录失败,状态码:', response.status);
|
||||||
|
console.error('错误信息:', data);
|
||||||
|
throw new Error(`登录应该成功,但返回状态码 ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data.data && data.data.user, '应该返回用户信息');
|
||||||
|
assert(data.data && data.data.token, '应该返回认证令牌');
|
||||||
|
|
||||||
|
authToken = data.data.token;
|
||||||
|
userId = data.data.user.id;
|
||||||
|
|
||||||
|
console.log('✅ 用户登录通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试获取用户信息
|
||||||
|
async function testGetUserInfo() {
|
||||||
|
console.log('📋 测试获取用户信息...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/auth/me');
|
||||||
|
|
||||||
|
assert.strictEqual(response.status, 200, '获取用户信息应该成功');
|
||||||
|
assert(data.data.user, '应该返回用户信息');
|
||||||
|
assert.strictEqual(data.data.user.email, testUser.email, '邮箱应该匹配');
|
||||||
|
|
||||||
|
console.log('✅ 获取用户信息通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试八字分析
|
||||||
|
async function testBaziAnalysis() {
|
||||||
|
console.log('📋 测试八字分析...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/analysis/bazi', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ birth_data: testBirthData })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('八字分析失败,状态码:', response.status);
|
||||||
|
console.error('错误信息:', data);
|
||||||
|
throw new Error(`八字分析应该成功,但返回状态码 ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data.data && data.data.analysis, '应该返回分析结果');
|
||||||
|
// 注意:八字分析不存储历史记录,所以没有 record_id
|
||||||
|
|
||||||
|
console.log('✅ 八字分析通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试紫微分析
|
||||||
|
async function testZiweiAnalysis() {
|
||||||
|
console.log('📋 测试紫微分析...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/analysis/ziwei', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ birth_data: testBirthData })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('紫微分析失败,状态码:', response.status);
|
||||||
|
console.error('错误信息:', data);
|
||||||
|
throw new Error(`紫微分析应该成功,但返回状态码 ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data.data && data.data.analysis, '应该返回分析结果');
|
||||||
|
// 注意:紫微分析不存储历史记录,所以没有 record_id
|
||||||
|
|
||||||
|
console.log('✅ 紫微分析通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试易经分析
|
||||||
|
async function testYijingAnalysis() {
|
||||||
|
console.log('📋 测试易经分析...');
|
||||||
|
|
||||||
|
const yijingData = {
|
||||||
|
question: '今年运势如何?',
|
||||||
|
method: 'coin',
|
||||||
|
hexagram: '111111'
|
||||||
|
};
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/analysis/yijing', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(yijingData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('易经分析失败,状态码:', response.status);
|
||||||
|
console.error('错误信息:', data);
|
||||||
|
throw new Error(`易经分析应该成功,但返回状态码 ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data.data && data.data.analysis, '应该返回分析结果');
|
||||||
|
// 注意:易经分析不存储历史记录,所以没有 record_id
|
||||||
|
|
||||||
|
console.log('✅ 易经分析通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试历史记录
|
||||||
|
async function testHistoryRecords() {
|
||||||
|
console.log('📋 测试历史记录...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/history');
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('获取历史记录失败,状态码:', response.status);
|
||||||
|
console.error('错误信息:', data);
|
||||||
|
throw new Error(`获取历史记录应该成功,但返回状态码 ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(Array.isArray(data.data), '应该返回数组');
|
||||||
|
// 注意:由于分析功能不自动存储历史记录,可能没有历史记录
|
||||||
|
console.log('历史记录数量:', data.data.length);
|
||||||
|
|
||||||
|
console.log('✅ 历史记录通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试用户档案
|
||||||
|
async function testUserProfile() {
|
||||||
|
console.log('📋 测试用户档案...');
|
||||||
|
|
||||||
|
// 获取档案
|
||||||
|
const { response: getResponse, data: getData } = await makeRequest('/profile');
|
||||||
|
|
||||||
|
assert.strictEqual(getResponse.status, 200, '获取用户档案应该成功');
|
||||||
|
|
||||||
|
// 更新档案
|
||||||
|
const profileUpdateData = {
|
||||||
|
full_name: '更新的测试用户',
|
||||||
|
bio: '这是一个测试用户的简介'
|
||||||
|
};
|
||||||
|
|
||||||
|
const { response: updateResponse, data: updateResult } = await makeRequest('/profile', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(profileUpdateData)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(updateResponse.status, 200, '更新用户档案应该成功');
|
||||||
|
|
||||||
|
console.log('✅ 用户档案通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试用户登出
|
||||||
|
async function testUserLogout() {
|
||||||
|
console.log('📋 测试用户登出...');
|
||||||
|
|
||||||
|
const { response, data } = await makeRequest('/auth/logout', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(response.status, 200, '登出应该成功');
|
||||||
|
|
||||||
|
// 清除认证令牌
|
||||||
|
authToken = null;
|
||||||
|
|
||||||
|
console.log('✅ 用户登出通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
if (require.main === module) {
|
||||||
|
runTests().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runTests };
|
||||||
Reference in New Issue
Block a user