feat: 优化首页布局和Docker配置

- 调整首页四个模块为4个一排的响应式布局
- 优化立即体验按钮高度对齐,使用flex布局确保视觉统一
- 升级Dockerfile支持Node 20和Puppeteer,优化构建性能
- 修复AI解读404错误处理,支持奇门遁甲类型
- 优化奇门盘移动端显示,解决重叠问题
- 完善Docker配置文件,提升部署成功率
This commit is contained in:
patdelphi
2025-08-25 22:27:48 +08:00
parent 0f3e1f406f
commit 5319c91d0d
7 changed files with 1680 additions and 111 deletions

View File

@@ -46,9 +46,8 @@ tests/
# Development tools
.eslintrc*
.prettierrc*
tailwind.config.js
vite.config.ts
tsconfig*.json
components.json
# Logs

View File

@@ -1,12 +1,33 @@
# 使用官方Node.js运行时作为基础镜像
FROM node:18-alpine
FROM node:20-alpine
# 更换Alpine镜像源为清华大学
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
# Install python and build tools for native modules
RUN apk add --no-cache python3 make g++
# 设置工作目录
WORKDIR /app
# --- Puppeteer 相关的配置和安装 ---
# 阻止 puppeteer 自动下载 Chromium
ENV PUPPETEER_SKIP_DOWNLOAD=true
# 安装 Chromium 浏览器
RUN apk add --no-cache chromium
# 设置 puppeteer 查找浏览器的路径
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# --- Puppeteer 相关的配置和安装 ---
# 复制package.json和package-lock.json
COPY package.json package-lock.json ./
# 设置npm镜像源
RUN npm config set registry https://registry.npmmirror.com
# 强制清理npm缓存
RUN npm cache clean --force
# 安装所有依赖(包括开发依赖用于构建前端)
RUN npm ci
@@ -14,10 +35,10 @@ RUN npm ci
COPY . .
# 构建前端
RUN npm run build
RUN npm run build:prod
# 清理开发依赖,只保留生产依赖
RUN npm ci --only=production
RUN npm ci --omit=dev
# 创建数据目录用于SQLite数据库
RUN mkdir -p /app/data

1434
docs/qimen_theory.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -292,7 +292,7 @@ const CompleteQimenAnalysis: React.FC<QimenAnalysisProps> = ({ analysis, classNa
return String(value);
};
// 渲染奇门盘九宫格(参考传统样式)
// 渲染专业奇门盘九宫格(参考传统专业样式)
const renderQimenPan = () => {
if (!qimenPan || !qimenPan.dipan) return null;
@@ -304,80 +304,164 @@ const CompleteQimenAnalysis: React.FC<QimenAnalysisProps> = ({ analysis, classNa
];
const palaceNames = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离'];
const palacePositions = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
const palaceNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
const palaceElements = ['水', '土', '木', '木', '土', '金', '金', '土', '火'];
// 地支对应
const palaceZhi = ['子', '未', '卯', '辰', '戊', '戌', '酉', '丑', '午'];
// 获取宫位颜色
const getPalaceColor = (palaceIndex: number, palace: any) => {
const isCenter = palaceIndex === 4;
if (isCenter) return 'bg-yellow-100 border-yellow-400';
// 根据九星设置颜色
const starColors = {
'天蓬': 'bg-blue-50 border-blue-300',
'天任': 'bg-green-50 border-green-300',
'天冲': 'bg-red-50 border-red-300',
'天辅': 'bg-purple-50 border-purple-300',
'天英': 'bg-orange-50 border-orange-300',
'天芮': 'bg-gray-50 border-gray-300',
'天柱': 'bg-indigo-50 border-indigo-300',
'天心': 'bg-pink-50 border-pink-300',
'天禽': 'bg-yellow-50 border-yellow-300'
};
return starColors[palace?.star] || 'bg-gray-50 border-gray-300';
};
// 获取门的颜色
const getDoorColor = (door: string) => {
const doorColors = {
'休门': 'text-blue-600',
'生门': 'text-green-600',
'伤门': 'text-red-600',
'杜门': 'text-gray-600',
'景门': 'text-orange-600',
'死门': 'text-black',
'惊门': 'text-purple-600',
'开门': 'text-yellow-600'
};
return doorColors[door] || 'text-gray-600';
};
// 获取神的颜色
const getGodColor = (god: string) => {
const godColors = {
'值符': 'text-red-700',
'腾蛇': 'text-red-500',
'太阴': 'text-blue-700',
'六合': 'text-green-700',
'白虎': 'text-gray-700',
'玄武': 'text-black',
'九地': 'text-yellow-700',
'九天': 'text-purple-700'
};
return godColors[god] || 'text-gray-600';
};
return (
<div className="space-y-6">
{/* 四柱信息 */}
{/* 四柱信息和基本信息 */}
{timeInfo?.ganzhi && (
<div className="text-center mb-6">
<div className="text-lg font-bold text-red-800 mb-2">
{timeInfo.ganzhi.year?.gan}{timeInfo.ganzhi.year?.zhi} {timeInfo.ganzhi.month?.gan}{timeInfo.ganzhi.month?.zhi} {timeInfo.ganzhi.day?.gan}{timeInfo.ganzhi.day?.zhi} {timeInfo.ganzhi.hour?.gan}{timeInfo.ganzhi.hour?.zhi}
<div className="bg-gradient-to-r from-red-50 to-yellow-50 p-4 rounded-lg border border-red-200">
<div className="text-center space-y-2">
<div className="text-lg font-bold text-red-800">
{timeInfo.ganzhi.year?.gan}{timeInfo.ganzhi.year?.zhi} {timeInfo.ganzhi.month?.gan}{timeInfo.ganzhi.month?.zhi} {timeInfo.ganzhi.day?.gan}{timeInfo.ganzhi.day?.zhi} {timeInfo.ganzhi.hour?.gan}{timeInfo.ganzhi.hour?.zhi}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-red-700">
<div>{timeInfo.jieqi || '未知'}</div>
<div>{timeInfo.yuan || '未知'}</div>
<div>{qimenPan?.yindun ? '阴遁' : '阳遁'}{qimenPan?.jushu || ''}</div>
<div></div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm text-red-700">
<div>{timeInfo.zhifu || '未知'}</div>
<div>使{timeInfo.zhishi || '未知'}</div>
<div></div>
</div>
<div className="text-sm text-red-700 space-y-1">
<div>{timeInfo.jieqi || '未知'} ~ {timeInfo.yuan || '未知'} {qimenPan?.yindun ? '阴遁' : '阳遁'}{qimenPan?.jushu || ''}</div>
<div>{timeInfo.zhifu || '未知'} 使{timeInfo.zhishi || '未知'}</div>
<div> </div>
</div>
</div>
)}
{/* 传统九宫格布局 */}
<div className="max-w-lg mx-auto">
<div className="grid grid-cols-3 gap-0 border-2 border-black">
{/* 专业九宫格奇门盘 */}
<div className="w-full max-w-sm mx-auto p-4">
<div className="grid grid-cols-3 gap-3 w-full">
{gridPositions.map((row, rowIndex) =>
row.map((palaceIndex, colIndex) => {
const palace = qimenPan.dipan[palaceIndex];
const isCenter = rowIndex === 1 && colIndex === 1;
const isCenter = palaceIndex === 4;
const colorClass = getPalaceColor(palaceIndex, palace);
return (
<div
key={`${rowIndex}-${colIndex}`}
className={cn(
'aspect-square border border-black p-2 text-xs bg-yellow-50 relative',
isCenter && 'bg-yellow-100'
'aspect-square border-2 relative flex flex-col justify-between p-1',
'min-h-[80px] min-w-[80px]',
colorClass,
isCenter && 'border-4 border-yellow-500 bg-yellow-200'
)}
>
{/* 宫位标识 */}
<div className="absolute top-0 left-0 text-xs text-gray-600 font-bold">
{palaceNames[palaceIndex]}
{/* 顶部行:宫位信息 */}
<div className="flex justify-between items-start text-xs leading-none">
<div className="text-red-800 font-bold">
<div>{palaceNames[palaceIndex]}</div>
<div className="text-gray-600">{palaceNumbers[palaceIndex]}</div>
</div>
<div className="text-center">
<div className="text-blue-700 font-medium">{palaceElements[palaceIndex]}</div>
<div className="text-gray-700">{palaceZhi[palaceIndex]}</div>
</div>
</div>
{/* 宫位内容 */}
<div className="h-full flex flex-col justify-center items-center space-y-1">
{/* 天干 */}
{/* 中心区域:主要信息 */}
<div className="flex-1 flex flex-col justify-center items-center">
{/* 天干 - 最大最显眼 */}
{palace?.gan && (
<div className="text-black font-bold text-lg">
<div className="text-black font-bold text-lg sm:text-xl md:text-2xl">
{palace.gan}
</div>
)}
{/* 九星 */}
{palace?.star && (
<div className="text-blue-700 font-medium text-sm">
<div className="text-blue-700 font-bold text-xs">
{palace.star}
</div>
)}
</div>
{/* 八门 */}
{/* 底部行:门神信息 */}
<div className="flex justify-between items-end text-xs font-bold">
{/* 左下角:八门 */}
<div>
{palace?.door && (
<div className="text-green-700 font-medium text-sm">
<div className={getDoorColor(palace.door)}>
{palace.door}
</div>
)}
</div>
{/* 八神 */}
{/* 右下角:八神 */}
<div>
{palace?.god && (
<div className="text-purple-700 font-medium text-sm">
<div className={getGodColor(palace.god)}>
{palace.god}
</div>
)}
</div>
{/* 宫位编号 */}
<div className="absolute bottom-0 right-0 text-xs text-gray-600">
{palacePositions[palaceIndex]}
</div>
{/* 特殊标记 */}
{palace?.special && (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="w-4 h-4 rounded-full bg-red-500 text-white text-xs flex items-center justify-center font-bold">
{palace.special}
</div>
</div>
)}
</div>
);
})
@@ -385,36 +469,64 @@ const CompleteQimenAnalysis: React.FC<QimenAnalysisProps> = ({ analysis, classNa
</div>
</div>
{/* 四害颜色说明 */}
<div className="text-center text-sm text-red-700">
<span className="text-red-600"></span><span className="text-green-600"></span><span className="text-purple-600"></span> <span className="text-red-600"></span> <span className="text-gray-600"></span>
</div>
{/* 奇门盘构成要素详解 */}
<div className="bg-white p-4 rounded-lg border-l-4 border-indigo-500">
<h4 className="font-bold text-red-800 mb-3"></h4>
{/* 颜色和符号说明 */}
<div className="bg-white p-4 rounded-lg border border-gray-200">
<h4 className="font-bold text-red-800 mb-3 text-center"></h4>
<div className="grid md:grid-cols-2 gap-4 text-sm">
<div className="space-y-2">
<div className="flex items-center">
<span className="w-3 h-3 bg-blue-500 rounded mr-2"></span>
<span className="text-red-700"><strong></strong></span>
</div>
<div className="flex items-center">
<span className="w-3 h-3 bg-green-500 rounded mr-2"></span>
<span className="text-red-700"><strong></strong></span>
<div className="font-semibold text-red-700 mb-2"></div>
<div className="grid grid-cols-2 gap-1 text-xs">
<div className="flex items-center"><span className="w-3 h-3 bg-blue-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-green-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-red-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-purple-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-orange-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-gray-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-indigo-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-pink-200 rounded mr-1"></span></div>
<div className="flex items-center"><span className="w-3 h-3 bg-yellow-200 rounded mr-1"></span></div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center">
<span className="w-3 h-3 bg-purple-500 rounded mr-2"></span>
<span className="text-red-700"><strong></strong></span>
</div>
<div className="flex items-center">
<span className="w-3 h-3 bg-orange-500 rounded mr-2"></span>
<span className="text-red-700"><strong></strong></span>
<div className="font-semibold text-red-700 mb-2"></div>
<div className="grid grid-cols-2 gap-1 text-xs">
<div className="text-blue-600"></div>
<div className="text-green-600"></div>
<div className="text-red-600"></div>
<div className="text-gray-600"></div>
<div className="text-orange-600"></div>
<div className="text-black"></div>
<div className="text-purple-600"></div>
<div className="text-yellow-600"></div>
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="font-semibold text-red-700 mb-2"></div>
<div className="grid grid-cols-4 gap-2 text-xs">
<div className="text-red-700"></div>
<div className="text-red-500"></div>
<div className="text-blue-700"></div>
<div className="text-green-700"></div>
<div className="text-gray-700"></div>
<div className="text-black"></div>
<div className="text-yellow-700"></div>
<div className="text-purple-700"></div>
</div>
</div>
</div>
{/* 奇门盘解读要点 */}
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-lg border border-blue-200">
<h4 className="font-bold text-red-800 mb-3"></h4>
<div className="text-sm text-red-700 space-y-2">
<div> <strong></strong></div>
<div> <strong></strong></div>
<div> <strong></strong></div>
<div> <strong></strong></div>
<div> <strong></strong></div>
<div> <strong></strong></div>
</div>
</div>
</div>
);

View File

@@ -19,7 +19,7 @@ import { toast } from 'sonner';
interface AIInterpretationButtonProps {
analysisData?: any; // 分析数据对象(可选)
analysisMarkdown?: string; // 直接传递的MD内容可选
analysisType: 'bazi' | 'ziwei' | 'yijing';
analysisType: 'bazi' | 'ziwei' | 'yijing' | 'qimen';
recordId?: number; // 分析记录ID用于AI解读
className?: string;
variant?: 'default' | 'outline' | 'ghost';

View File

@@ -165,7 +165,7 @@ const HomePage: React.FC = () => {
</div>
{/* Features Section */}
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 relative max-w-6xl mx-auto px-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 relative max-w-7xl mx-auto px-4">
{/* 装饰元素 - 仅在大屏幕显示 */}
<div className="absolute -left-12 top-1/4 w-16 h-16 opacity-15 pointer-events-none hidden xl:block">
<img
@@ -185,15 +185,15 @@ const HomePage: React.FC = () => {
{features.map((feature, index) => {
const Icon = feature.icon;
return (
<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">
<ChineseCard key={index} variant="elevated" className="text-center h-full flex flex-col">
<ChineseCardHeader>
<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 md:h-7 md:w-7 text-red-800" />
</div>
<ChineseCardTitle className="text-red-600 text-heading-md font-bold font-chinese">{feature.title}</ChineseCardTitle>
</ChineseCardHeader>
<ChineseCardContent>
<p className="text-gray-700 leading-relaxed font-chinese mb-4 text-body-md">{feature.description}</p>
<ChineseCardContent className="flex-1 flex flex-col">
<p className="text-gray-700 leading-relaxed font-chinese mb-4 text-body-md flex-1">{feature.description}</p>
{user && (
<Link to={feature.link}>
<ChineseButton variant="secondary" className="w-full">

View File

@@ -1052,6 +1052,9 @@ export const getAIInterpretation = async (readingId: number): Promise<AIInterpre
tokensUsed: data.data.tokens_used
};
}
} else if (response.status === 404) {
// 404是正常情况表示还没有AI解读记录
return null;
}
}