feat: 祈福小助手万年历小程序第一期
@@ -0,0 +1,26 @@
|
||||
// app.js
|
||||
App({
|
||||
onLaunch() {
|
||||
// 初始化本地存储
|
||||
const settings = wx.getStorageSync('lunar-settings');
|
||||
if (!settings) {
|
||||
wx.setStorageSync('lunar-settings', {
|
||||
theme: 'light',
|
||||
weekStartDay: 0,
|
||||
ziHourSect: 'lateZiNextDay',
|
||||
solarTime: true,
|
||||
});
|
||||
}
|
||||
const profiles = wx.getStorageSync('lunar-user-profiles');
|
||||
if (!profiles) {
|
||||
wx.setStorageSync('lunar-user-profiles', { profiles: [], activeIndex: 0 });
|
||||
}
|
||||
const bookmarks = wx.getStorageSync('lunar-bookmarks');
|
||||
if (!bookmarks) {
|
||||
wx.setStorageSync('lunar-bookmarks', []);
|
||||
}
|
||||
},
|
||||
globalData: {
|
||||
// 全局共享数据
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/home/home",
|
||||
"pages/calendar/calendar",
|
||||
"pages/day-detail/day-detail",
|
||||
"pages/bazi/bazi",
|
||||
"pages/fortune/fortune",
|
||||
"pages/divination/divination",
|
||||
"pages/solar-terms/solar-terms",
|
||||
"pages/settings/settings"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#C41E3A",
|
||||
"navigationBarTitleText": "万年历",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#FFFBF5"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#9E8E7E",
|
||||
"selectedColor": "#C41E3A",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/home",
|
||||
"text": "首页",
|
||||
"iconPath": "images/home.png",
|
||||
"selectedIconPath": "images/home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/calendar/calendar",
|
||||
"text": "日历",
|
||||
"iconPath": "images/calendar.png",
|
||||
"selectedIconPath": "images/calendar-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/bazi/bazi",
|
||||
"text": "八字",
|
||||
"iconPath": "images/bazi.png",
|
||||
"selectedIconPath": "images/bazi-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/fortune/fortune",
|
||||
"text": "运势",
|
||||
"iconPath": "images/fortune.png",
|
||||
"selectedIconPath": "images/fortune-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/settings/settings",
|
||||
"text": "设置",
|
||||
"iconPath": "images/settings.png",
|
||||
"selectedIconPath": "images/settings-active.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/* app.wxss — 全局样式 */
|
||||
page {
|
||||
background-color: #FFFBF5;
|
||||
color: #1A1A1A;
|
||||
font-family: 'PingFang SC', 'Noto Sans SC', 'Hiragino Sans GB', system-ui, -apple-system, sans-serif;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 中文衬线字体 */
|
||||
.font-chinese {
|
||||
font-family: 'Noto Serif SC', 'STSong', 'SimSun', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
/* 卡片基础样式 */
|
||||
.card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
/* 徽章 */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.badge-lucky {
|
||||
background-color: #FEF2F2;
|
||||
color: #C41E3A;
|
||||
}
|
||||
.badge-unlucky {
|
||||
background-color: #F1F5F9;
|
||||
color: #546E7A;
|
||||
}
|
||||
.badge-outline {
|
||||
border: 1rpx solid #E8D5C4;
|
||||
color: #6B5E53;
|
||||
background: transparent;
|
||||
}
|
||||
.badge-festival {
|
||||
background-color: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.btn-primary {
|
||||
background-color: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
.btn-primary:active {
|
||||
background-color: #8B1A2B;
|
||||
}
|
||||
.btn-outline {
|
||||
background-color: transparent;
|
||||
border: 2rpx solid #E8D5C4;
|
||||
color: #1A1A1A;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.btn-ghost {
|
||||
background-color: transparent;
|
||||
color: #1A1A1A;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 文本颜色 */
|
||||
.text-primary { color: #C41E3A; }
|
||||
.text-secondary { color: #6B5E53; }
|
||||
.text-muted { color: #9E8E7E; }
|
||||
.text-lucky { color: #C41E3A; }
|
||||
.text-unlucky { color: #546E7A; }
|
||||
|
||||
/* 背景色 */
|
||||
.bg-card { background-color: #FFFFFF; }
|
||||
.bg-page { background-color: #FFFBF5; }
|
||||
|
||||
/* 边框 */
|
||||
.border { border: 1rpx solid #E8D5C4; }
|
||||
.border-b { border-bottom: 1rpx solid #E8D5C4; }
|
||||
|
||||
/* 布局 */
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.flex-1 { flex: 1; }
|
||||
.text-center { text-align: center; }
|
||||
|
||||
/* 间距 */
|
||||
.mt-1 { margin-top: 8rpx; }
|
||||
.mt-2 { margin-top: 16rpx; }
|
||||
.mb-1 { margin-bottom: 8rpx; }
|
||||
.mb-2 { margin-bottom: 16rpx; }
|
||||
.p-2 { padding: 16rpx; }
|
||||
.p-3 { padding: 24rpx; }
|
||||
|
||||
/* 圆角 */
|
||||
.rounded { border-radius: 16rpx; }
|
||||
.rounded-lg { border-radius: 24rpx; }
|
||||
.rounded-full { border-radius: 999rpx; }
|
||||
|
||||
/* 字号 */
|
||||
.text-xs { font-size: 20rpx; }
|
||||
.text-sm { font-size: 24rpx; }
|
||||
.text-base { font-size: 28rpx; }
|
||||
.text-lg { font-size: 32rpx; }
|
||||
.text-xl { font-size: 36rpx; }
|
||||
.text-2xl { font-size: 48rpx; }
|
||||
.text-3xl { font-size: 60rpx; }
|
||||
|
||||
/* 字重 */
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-bold { font-weight: 700; }
|
||||
|
||||
/* 网格 */
|
||||
.grid { display: grid; }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
.grid-cols-7 { grid-template-columns: repeat(7, 1fr); }
|
||||
.gap-1 { gap: 8rpx; }
|
||||
.gap-2 { gap: 16rpx; }
|
||||
.gap-3 { gap: 24rpx; }
|
||||
|
||||
/* 隐藏 */
|
||||
.hidden { display: none; }
|
||||
|
||||
/* 滚动 */
|
||||
.scroll-x {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 安全区域 */
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
|
After Width: | Height: | Size: 67 B |
@@ -0,0 +1,78 @@
|
||||
const { birthInfoToBazi, analyzeElementBalance } = require('../../utils/core/index.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
gender: 'male',
|
||||
birthDate: '1990-01-01',
|
||||
birthTime: '12:00',
|
||||
ziSect: 'lateZiNextDay',
|
||||
bazi: null,
|
||||
pillars: [],
|
||||
elements: [],
|
||||
elementAnalysis: {}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 尝试从本地存储恢复
|
||||
const saved = wx.getStorageSync('lunar-bazi-input');
|
||||
if (saved) {
|
||||
this.setData(saved);
|
||||
}
|
||||
},
|
||||
|
||||
setGender(e) {
|
||||
this.setData({ gender: e.currentTarget.dataset.g });
|
||||
},
|
||||
|
||||
setZiSect(e) {
|
||||
this.setData({ ziSect: e.currentTarget.dataset.s });
|
||||
},
|
||||
|
||||
onDateChange(e) {
|
||||
this.setData({ birthDate: e.detail.value });
|
||||
},
|
||||
|
||||
onTimeChange(e) {
|
||||
this.setData({ birthTime: e.detail.value });
|
||||
},
|
||||
|
||||
calculate() {
|
||||
const { gender, birthDate, birthTime, ziSect } = this.data;
|
||||
const [year, month, day] = birthDate.split('-').map(Number);
|
||||
const [hour, minute] = birthTime.split(':').map(Number);
|
||||
|
||||
const bazi = birthInfoToBazi({ year, month, day, hour, minute, gender, ziSect });
|
||||
const elementAnalysis = analyzeElementBalance(bazi.eightChar);
|
||||
|
||||
const pillars = [
|
||||
{ label: '年柱', ...bazi.eightChar.yearPillar },
|
||||
{ label: '月柱', ...bazi.eightChar.monthPillar },
|
||||
{ label: '日柱', ...bazi.eightChar.dayPillar },
|
||||
{ label: '时柱', ...bazi.eightChar.hourPillar }
|
||||
];
|
||||
|
||||
const total = elementAnalysis.total || 1;
|
||||
const elements = [
|
||||
{ name: '木', count: elementAnalysis.wood, percent: Math.round(elementAnalysis.wood / total * 100) },
|
||||
{ name: '火', count: elementAnalysis.fire, percent: Math.round(elementAnalysis.fire / total * 100) },
|
||||
{ name: '土', count: elementAnalysis.earth, percent: Math.round(elementAnalysis.earth / total * 100) },
|
||||
{ name: '金', count: elementAnalysis.metal, percent: Math.round(elementAnalysis.metal / total * 100) },
|
||||
{ name: '水', count: elementAnalysis.water, percent: Math.round(elementAnalysis.water / total * 100) }
|
||||
];
|
||||
|
||||
this.setData({ bazi, pillars, elements, elementAnalysis });
|
||||
|
||||
// 保存输入
|
||||
wx.setStorageSync('lunar-bazi-input', { gender, birthDate, birthTime, ziSect });
|
||||
// 保存八字供运势页使用
|
||||
wx.setStorageSync('lunar-user-bazi', bazi);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.setData({ bazi: null });
|
||||
},
|
||||
|
||||
goFortune() {
|
||||
wx.switchTab({ url: '/pages/fortune/fortune' });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "八字排盘",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<view class="container">
|
||||
<!-- 输入表单 -->
|
||||
<view class="card" wx:if="{{!bazi}}">
|
||||
<view class="section-title">输入出生信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">性别</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{gender === 'male' ? 'active' : ''}}" bindtap="setGender" data-g="male">男</view>
|
||||
<view class="btn-outline {{gender === 'female' ? 'active' : ''}}" bindtap="setGender" data-g="female">女</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">出生日期</text>
|
||||
<picker mode="date" value="{{birthDate}}" bindchange="onDateChange">
|
||||
<view class="picker-input">{{birthDate}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">出生时间</text>
|
||||
<picker mode="time" value="{{birthTime}}" bindchange="onTimeChange">
|
||||
<view class="picker-input">{{birthTime}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">子时流派</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{ziSect === 'lateZiNextDay' ? 'active' : ''}}" bindtap="setZiSect" data-s="lateZiNextDay">晚子时日柱算次日</view>
|
||||
<view class="btn-outline {{ziSect === 'lateZiSameDay' ? 'active' : ''}}" bindtap="setZiSect" data-s="lateZiSameDay">晚子时日柱算当日</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary mt-2" bindtap="calculate">排盘</view>
|
||||
</view>
|
||||
|
||||
<!-- 八字结果 -->
|
||||
<block wx:if="{{bazi}}">
|
||||
<!-- 四柱 -->
|
||||
<view class="card">
|
||||
<view class="section-title">四柱八字</view>
|
||||
<view class="grid grid-cols-4 gap-2">
|
||||
<view class="pillar" wx:for="{{pillars}}" wx:key="label">
|
||||
<text class="pillar-label">{{item.label}}</text>
|
||||
<text class="pillar-ganzhi">{{item.ganzhi}}</text>
|
||||
<text class="pillar-ten">{{item.tenStar || '日主'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 五行分析 -->
|
||||
<view class="card">
|
||||
<view class="section-title">五行分析</view>
|
||||
<view class="element-bar">
|
||||
<view class="element-item" wx:for="{{elements}}" wx:key="name">
|
||||
<text class="element-name">{{item.name}}</text>
|
||||
<view class="element-track">
|
||||
<view class="element-fill" style="width: {{item.percent}}%"></view>
|
||||
</view>
|
||||
<text class="element-count">{{item.count}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-2 text-sm text-muted">
|
||||
日主:{{bazi.eightChar.dayMaster}} | dominant: {{elementAnalysis.dominant}} | weakest: {{elementAnalysis.weakest}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 大运 -->
|
||||
<view class="card">
|
||||
<view class="section-title">大运</view>
|
||||
<scroll-view scroll-x class="scroll-x">
|
||||
<view class="flex gap-2">
|
||||
<view class="fortune-item" wx:for="{{bazi.decadeFortunes}}" wx:key="index">
|
||||
<text class="text-sm text-muted">{{item.startAge}}-{{item.endAge}}岁</text>
|
||||
<text class="text-base font-medium">{{item.ganzhi}}</text>
|
||||
<text class="text-xs text-muted">{{item.startYear}}-{{item.endYear}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 流年 -->
|
||||
<view class="card">
|
||||
<view class="section-title">流年</view>
|
||||
<scroll-view scroll-x class="scroll-x">
|
||||
<view class="flex gap-2">
|
||||
<view class="fortune-item" wx:for="{{bazi.annualFortunes}}" wx:key="year">
|
||||
<text class="text-sm text-muted">{{item.age}}岁</text>
|
||||
<text class="text-base font-medium">{{item.ganzhi}}</text>
|
||||
<text class="text-xs text-muted">{{item.year}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 操作 -->
|
||||
<view class="flex gap-2 mt-2">
|
||||
<view class="btn-outline flex-1 text-center" bindtap="reset">重新排盘</view>
|
||||
<view class="btn-primary flex-1 text-center" bindtap="goFortune">看运势</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -0,0 +1,107 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 24rpx;
|
||||
color: #6B5E53;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.picker-input {
|
||||
padding: 20rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.btn-outline.active {
|
||||
background: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
border-color: #C41E3A;
|
||||
}
|
||||
|
||||
.pillar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.pillar-label {
|
||||
font-size: 20rpx;
|
||||
color: #9E8E7E;
|
||||
}
|
||||
|
||||
.pillar-ganzhi {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #C41E3A;
|
||||
margin: 8rpx 0;
|
||||
}
|
||||
|
||||
.pillar-ten {
|
||||
font-size: 20rpx;
|
||||
color: #6B5E53;
|
||||
}
|
||||
|
||||
.element-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.element-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.element-name {
|
||||
width: 60rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.element-track {
|
||||
flex: 1;
|
||||
height: 16rpx;
|
||||
background: #E8D5C4;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.element-fill {
|
||||
height: 100%;
|
||||
background: #C41E3A;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.element-count {
|
||||
width: 60rpx;
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.fortune-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
const { getMonthCalendar } = require('../../utils/core/index.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
year: new Date().getFullYear(),
|
||||
month: new Date().getMonth() + 1,
|
||||
weekHeaders: ['日', '一', '二', '三', '四', '五', '六'],
|
||||
calendarDays: [],
|
||||
selectedDay: null,
|
||||
showMonthPicker: false,
|
||||
years: [],
|
||||
months: [1,2,3,4,5,6,7,8,9,10,11,12],
|
||||
pickerValue: [0, 0]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const years = [];
|
||||
for (let y = 1900; y <= 2100; y++) years.push(y);
|
||||
this.setData({ years });
|
||||
this.loadCalendar();
|
||||
},
|
||||
|
||||
loadCalendar() {
|
||||
const { year, month } = this.data;
|
||||
const weeks = getMonthCalendar(year, month);
|
||||
const calendarDays = [];
|
||||
weeks.forEach(week => {
|
||||
week.forEach(day => calendarDays.push(day));
|
||||
});
|
||||
this.setData({ calendarDays });
|
||||
},
|
||||
|
||||
prevMonth() {
|
||||
let { year, month } = this.data;
|
||||
month--;
|
||||
if (month < 1) { month = 12; year--; }
|
||||
this.setData({ year, month }, () => this.loadCalendar());
|
||||
},
|
||||
|
||||
nextMonth() {
|
||||
let { year, month } = this.data;
|
||||
month++;
|
||||
if (month > 12) { month = 1; year++; }
|
||||
this.setData({ year, month }, () => this.loadCalendar());
|
||||
},
|
||||
|
||||
showPicker() {
|
||||
const { year, month } = this.data;
|
||||
this.setData({
|
||||
showMonthPicker: true,
|
||||
pickerValue: [year - 1900, month - 1]
|
||||
});
|
||||
},
|
||||
|
||||
hidePicker() {
|
||||
this.setData({ showMonthPicker: false });
|
||||
},
|
||||
|
||||
pickerChange(e) {
|
||||
this.setData({ pickerValue: e.detail.value });
|
||||
},
|
||||
|
||||
confirmPicker() {
|
||||
const { pickerValue, years, months } = this.data;
|
||||
this.setData({
|
||||
year: years[pickerValue[0]],
|
||||
month: months[pickerValue[1]],
|
||||
showMonthPicker: false
|
||||
}, () => this.loadCalendar());
|
||||
},
|
||||
|
||||
selectDay(e) {
|
||||
const date = e.currentTarget.dataset.date;
|
||||
const day = this.data.calendarDays.find(d => d.solarDate === date);
|
||||
this.setData({ selectedDay: day });
|
||||
},
|
||||
|
||||
goDetail() {
|
||||
const { selectedDay } = this.data;
|
||||
if (selectedDay) {
|
||||
wx.navigateTo({ url: `/pages/day-detail/day-detail?date=${selectedDay.solarDate}` });
|
||||
}
|
||||
},
|
||||
|
||||
noop() {}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "日历",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<view class="container">
|
||||
<!-- 年月选择器 -->
|
||||
<view class="card flex justify-between items-center">
|
||||
<view class="btn-ghost" bindtap="prevMonth">‹</view>
|
||||
<view class="text-xl font-bold" bindtap="showPicker">{{year}}年{{month}}月</view>
|
||||
<view class="btn-ghost" bindtap="nextMonth">›</view>
|
||||
</view>
|
||||
|
||||
<!-- 星期头 -->
|
||||
<view class="grid grid-cols-7 gap-1 mb-1">
|
||||
<view class="text-center text-sm text-muted" wx:for="{{weekHeaders}}" wx:key="*this">{{item}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历网格 -->
|
||||
<view class="grid grid-cols-7 gap-1">
|
||||
<view
|
||||
class="day-cell {{item.isToday ? 'today' : ''}} {{item.solarMonth !== month ? 'other-month' : ''}}"
|
||||
wx:for="{{calendarDays}}"
|
||||
wx:key="solarDate"
|
||||
bindtap="selectDay"
|
||||
data-date="{{item.solarDate}}"
|
||||
>
|
||||
<text class="day-number">{{item.solarDay}}</text>
|
||||
<text class="day-lunar {{item.lunarFestival || item.solarFestival ? 'festival' : ''}}">
|
||||
{{item.lunarFestival || item.solarFestival || item.lunarDayName}}
|
||||
</text>
|
||||
<view class="day-dot" wx:if="{{item.isTermDay}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选中日期详情 -->
|
||||
<view class="card mt-2" wx:if="{{selectedDay}}">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<text class="text-lg font-bold">{{selectedDay.solarDate}}</text>
|
||||
<text class="text-sm text-muted block">{{selectedDay.lunarMonthName}}{{selectedDay.lunarDayName}}</text>
|
||||
</view>
|
||||
<view class="btn-primary" bindtap="goDetail">详情</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 年月选择弹窗 -->
|
||||
<view class="picker-mask {{showMonthPicker ? 'show' : ''}}" bindtap="hidePicker">
|
||||
<view class="picker-panel" catchtap="noop">
|
||||
<view class="picker-header">
|
||||
<text bindtap="hidePicker">取消</text>
|
||||
<text class="font-bold">选择年月</text>
|
||||
<text bindtap="confirmPicker">确定</text>
|
||||
</view>
|
||||
<picker-view class="picker-view" value="{{pickerValue}}" bindchange="pickerChange">
|
||||
<picker-view-column>
|
||||
<view wx:for="{{years}}" wx:key="*this" class="picker-item">{{item}}年</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view wx:for="{{months}}" wx:key="*this" class="picker-item">{{item}}月</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,92 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12rpx 4rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 12rpx;
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
.day-cell.today {
|
||||
background: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.day-cell.today .day-lunar {
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.day-cell.other-month {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day-lunar {
|
||||
font-size: 20rpx;
|
||||
color: #9E8E7E;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.day-lunar.festival {
|
||||
color: #C41E3A;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day-dot {
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 50%;
|
||||
background: #C41E3A;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.picker-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.picker-mask.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.picker-panel {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
height: 400rpx;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
text-align: center;
|
||||
line-height: 68rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
const { getDayInfo, getAlmanacInfo } = require('../../utils/core/index.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
date: '',
|
||||
dayInfo: {},
|
||||
almanac: {}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const date = options.date || new Date().toISOString().split('T')[0];
|
||||
this.setData({ date });
|
||||
this.loadData(date);
|
||||
},
|
||||
|
||||
loadData(dateStr) {
|
||||
const [year, month, day] = dateStr.split('-').map(Number);
|
||||
const dayInfo = getDayInfo(year, month, day);
|
||||
const almanac = getAlmanacInfo(year, month, day);
|
||||
this.setData({ dayInfo, almanac });
|
||||
},
|
||||
|
||||
addBookmark() {
|
||||
const bookmarks = wx.getStorageSync('lunar-bookmarks') || [];
|
||||
const { dayInfo } = this.data;
|
||||
const exists = bookmarks.some(b => b.date === dayInfo.solarDate);
|
||||
if (!exists) {
|
||||
bookmarks.push({
|
||||
date: dayInfo.solarDate,
|
||||
lunarDate: `${dayInfo.lunarMonthName}${dayInfo.lunarDayName}`,
|
||||
note: ''
|
||||
});
|
||||
wx.setStorageSync('lunar-bookmarks', bookmarks);
|
||||
wx.showToast({ title: '已收藏', icon: 'success' });
|
||||
} else {
|
||||
wx.showToast({ title: '已收藏过', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
goBazi() {
|
||||
wx.navigateTo({ url: '/pages/bazi/bazi' });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "日详情",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<view class="container">
|
||||
<!-- 日期头部 -->
|
||||
<view class="card header-card">
|
||||
<view class="text-center">
|
||||
<text class="text-3xl font-bold">{{dayInfo.solarDay}}</text>
|
||||
<text class="text-lg text-muted block">{{dayInfo.solarYear}}年{{dayInfo.solarMonth}}月 {{dayInfo.weekDay}}</text>
|
||||
</view>
|
||||
<view class="flex justify-center gap-2 mt-2">
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.lunarFestival}}">{{dayInfo.lunarFestival}}</text>
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.solarFestival}}">{{dayInfo.solarFestival}}</text>
|
||||
<text class="badge badge-outline" wx:if="{{dayInfo.solarTerm}}">{{dayInfo.solarTerm}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 农历信息 -->
|
||||
<view class="card">
|
||||
<view class="section-title">农历</view>
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">农历日期</text>
|
||||
<text class="text-base font-medium">{{dayInfo.lunarYear}}年{{dayInfo.lunarMonthName}}{{dayInfo.lunarDayName}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">生肖</text>
|
||||
<text class="text-base font-medium">{{dayInfo.zodiac}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">干支</text>
|
||||
<text class="text-base font-medium">{{dayInfo.lunarYearGanzhi}}年 {{dayInfo.lunarMonthGanzhi}}月 {{dayInfo.lunarDayGanzhi}}日</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">月相</text>
|
||||
<text class="text-base font-medium">{{dayInfo.moonPhase}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 宜忌 -->
|
||||
<view class="card">
|
||||
<view class="section-title">宜忌</view>
|
||||
<view class="flex gap-2">
|
||||
<view class="flex-1">
|
||||
<text class="text-lucky font-bold">宜</text>
|
||||
<view class="flex flex-wrap gap-1 mt-1">
|
||||
<text class="badge badge-lucky" wx:for="{{almanac.recommends}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="text-unlucky font-bold">忌</text>
|
||||
<view class="flex flex-wrap gap-1 mt-1">
|
||||
<text class="badge badge-unlucky" wx:for="{{almanac.avoids}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 黄历详情 -->
|
||||
<view class="card">
|
||||
<view class="section-title">黄历</view>
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">值神</text>
|
||||
<text class="text-base font-medium">{{almanac.duty}}({{almanac.dutyLuck === 'good' ? '吉' : almanac.dutyLuck === 'bad' ? '凶' : '平'}})</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">黄道黑道</text>
|
||||
<text class="text-base font-medium">{{almanac.twelveStar.name}}({{almanac.twelveStar.ecliptic}})</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">二十八宿</text>
|
||||
<text class="text-base font-medium">{{almanac.twentyEightStar.name}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">九星</text>
|
||||
<text class="text-base font-medium">{{almanac.nineStar.name}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">六曜</text>
|
||||
<text class="text-base font-medium">{{almanac.sixStar}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">小六壬</text>
|
||||
<text class="text-base font-medium">{{almanac.minorRen.name}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">纳音</text>
|
||||
<text class="text-base font-medium">{{almanac.nayin}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">胎神</text>
|
||||
<text class="text-base font-medium">{{almanac.fetus.position}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 冲煞 -->
|
||||
<view class="card">
|
||||
<view class="section-title">冲煞</view>
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">冲</text>
|
||||
<text class="text-base font-medium">{{almanac.clash}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">害</text>
|
||||
<text class="text-base font-medium">{{almanac.harm}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">合</text>
|
||||
<text class="text-base font-medium">{{almanac.combine}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">煞方</text>
|
||||
<text class="text-base font-medium">{{almanac.evilDirection}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 彭祖百忌 -->
|
||||
<view class="card">
|
||||
<view class="section-title">彭祖百忌</view>
|
||||
<text class="text-base">{{almanac.pengZu}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 时辰黄历 -->
|
||||
<view class="card">
|
||||
<view class="section-title">时辰黄历</view>
|
||||
<view class="hour-list">
|
||||
<view class="hour-item" wx:for="{{almanac.hourDetails}}" wx:key="name">
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="font-medium">{{item.name}}({{item.range}})</text>
|
||||
<text class="text-sm text-muted">{{item.ganzhi}}</text>
|
||||
</view>
|
||||
<view class="flex gap-2 mt-1">
|
||||
<text class="text-xs text-muted">值神:{{item.twelveStar}}</text>
|
||||
<text class="text-xs text-muted">九星:{{item.nineStar}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="flex gap-2 mt-2">
|
||||
<view class="btn-outline flex-1 text-center" bindtap="addBookmark">收藏</view>
|
||||
<view class="btn-primary flex-1 text-center" bindtap="goBazi">排八字</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,41 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
background: linear-gradient(135deg, #C41E3A 0%, #8B1A2B 100%);
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
padding: 40rpx 24rpx;
|
||||
}
|
||||
|
||||
.header-card .text-muted {
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 16rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.hour-list {
|
||||
max-height: 600rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.hour-item {
|
||||
padding: 16rpx;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.hour-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
const { calculatePlumBlossom, calculateBoneWeight, HEAVEN_STEMS, EARTH_BRANCHES, LUNAR_MONTH_NAMES, LUNAR_DAY_NAMES, HOUR_NAMES } = require('../../utils/core/index.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
plumDate: new Date().toISOString().split('T')[0],
|
||||
plumTime: '12:00',
|
||||
plumResult: null,
|
||||
yearGanzhiList: [],
|
||||
monthList: LUNAR_MONTH_NAMES,
|
||||
dayList: LUNAR_DAY_NAMES,
|
||||
hourList: HOUR_NAMES,
|
||||
boneYearIndex: 0,
|
||||
boneMonthIndex: 0,
|
||||
boneDayIndex: 0,
|
||||
boneHourIndex: 0,
|
||||
boneResult: null
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const yearGanzhiList = [];
|
||||
for (let i = 0; i < 60; i++) {
|
||||
yearGanzhiList.push(HEAVEN_STEMS[i % 10] + EARTH_BRANCHES[i % 12]);
|
||||
}
|
||||
this.setData({ yearGanzhiList });
|
||||
},
|
||||
|
||||
onPlumDateChange(e) {
|
||||
this.setData({ plumDate: e.detail.value });
|
||||
},
|
||||
|
||||
onPlumTimeChange(e) {
|
||||
this.setData({ plumTime: e.detail.value });
|
||||
},
|
||||
|
||||
calcPlum() {
|
||||
const { plumDate, plumTime } = this.data;
|
||||
const [year, month, day] = plumDate.split('-').map(Number);
|
||||
const [hour] = plumTime.split(':').map(Number);
|
||||
const result = calculatePlumBlossom(year, month, day, hour);
|
||||
this.setData({ plumResult: result });
|
||||
},
|
||||
|
||||
onBoneYearChange(e) {
|
||||
this.setData({ boneYearIndex: e.detail.value });
|
||||
},
|
||||
|
||||
onBoneMonthChange(e) {
|
||||
this.setData({ boneMonthIndex: e.detail.value });
|
||||
},
|
||||
|
||||
onBoneDayChange(e) {
|
||||
this.setData({ boneDayIndex: e.detail.value });
|
||||
},
|
||||
|
||||
onBoneHourChange(e) {
|
||||
this.setData({ boneHourIndex: e.detail.value });
|
||||
},
|
||||
|
||||
calcBone() {
|
||||
const { boneYearIndex, boneMonthIndex, boneDayIndex, boneHourIndex } = this.data;
|
||||
const result = calculateBoneWeight(boneYearIndex, boneMonthIndex + 1, boneDayIndex + 1, boneHourIndex);
|
||||
this.setData({ boneResult: result });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "占卜",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<view class="container">
|
||||
<!-- 梅花易数 -->
|
||||
<view class="card">
|
||||
<view class="section-title">梅花易数</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">起卦时间</text>
|
||||
<picker mode="date" value="{{plumDate}}" bindchange="onPlumDateChange">
|
||||
<view class="picker-input">{{plumDate}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">时辰</text>
|
||||
<picker mode="time" value="{{plumTime}}" bindchange="onPlumTimeChange">
|
||||
<view class="picker-input">{{plumTime}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="btn-primary" bindtap="calcPlum">起卦</view>
|
||||
</view>
|
||||
|
||||
<!-- 梅花易数结果 -->
|
||||
<view class="card" wx:if="{{plumResult}}">
|
||||
<view class="section-title">卦象</view>
|
||||
<view class="hexagram-display">
|
||||
<view class="trigram-row">
|
||||
<text class="trigram-symbol">{{plumResult.upperTrigram.symbol}}</text>
|
||||
<text class="trigram-name">{{plumResult.upperTrigram.name}}({{plumResult.upperTrigram.element}})</text>
|
||||
</view>
|
||||
<view class="trigram-row">
|
||||
<text class="trigram-symbol">{{plumResult.lowerTrigram.symbol}}</text>
|
||||
<text class="trigram-name">{{plumResult.lowerTrigram.name}}({{plumResult.lowerTrigram.element}})</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-2">
|
||||
<text class="text-lg font-bold">{{plumResult.originalHexagram.name}}</text>
|
||||
<text class="text-sm text-muted block mt-1">第{{plumResult.changingLine}}爻动</text>
|
||||
</view>
|
||||
<view class="mt-2 p-2 bg-page rounded">
|
||||
<text class="text-sm">{{plumResult.originalHexagram.judgment}}</text>
|
||||
</view>
|
||||
<view class="mt-2 p-2 bg-page rounded">
|
||||
<text class="text-sm">{{plumResult.originalHexagram.image}}</text>
|
||||
</view>
|
||||
<view class="mt-2" wx:if="{{plumResult.transformedHexagram}}">
|
||||
<text class="text-base font-medium">变卦:{{plumResult.transformedHexagram.name}}</text>
|
||||
</view>
|
||||
<view class="mt-2 text-sm text-muted">
|
||||
体用关系:{{plumResult.relationship}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 称骨算命 -->
|
||||
<view class="card mt-2">
|
||||
<view class="section-title">称骨算命</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">出生年份(干支)</text>
|
||||
<picker mode="selector" range="{{yearGanzhiList}}" value="{{boneYearIndex}}" bindchange="onBoneYearChange">
|
||||
<view class="picker-input">{{yearGanzhiList[boneYearIndex]}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">农历月份</text>
|
||||
<picker mode="selector" range="{{monthList}}" value="{{boneMonthIndex}}" bindchange="onBoneMonthChange">
|
||||
<view class="picker-input">{{monthList[boneMonthIndex]}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">农历日期</text>
|
||||
<picker mode="selector" range="{{dayList}}" value="{{boneDayIndex}}" bindchange="onBoneDayChange">
|
||||
<view class="picker-input">{{dayList[boneDayIndex]}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">时辰</text>
|
||||
<picker mode="selector" range="{{hourList}}" value="{{boneHourIndex}}" bindchange="onBoneHourChange">
|
||||
<view class="picker-input">{{hourList[boneHourIndex]}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="btn-primary" bindtap="calcBone">算命</view>
|
||||
</view>
|
||||
|
||||
<!-- 称骨结果 -->
|
||||
<view class="card" wx:if="{{boneResult}}">
|
||||
<view class="section-title">称骨结果</view>
|
||||
<view class="text-center">
|
||||
<text class="text-3xl font-bold">{{boneResult.totalLiang}}两{{boneResult.totalQian}}钱</text>
|
||||
<text class="badge {{boneResult.fortune.includes('上') ? 'badge-lucky' : boneResult.fortune.includes('下') ? 'badge-unlucky' : 'badge-outline'}} ml-2">{{boneResult.fortune}}</text>
|
||||
</view>
|
||||
<view class="mt-2 p-3 bg-page rounded">
|
||||
<text class="text-base">{{boneResult.interpretation}}</text>
|
||||
</view>
|
||||
<view class="grid grid-cols-4 gap-2 mt-2">
|
||||
<view class="text-center">
|
||||
<text class="text-xs text-muted">年</text>
|
||||
<text class="text-base font-medium block">{{boneResult.yearWeight}}钱</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-xs text-muted">月</text>
|
||||
<text class="text-base font-medium block">{{boneResult.monthWeight}}钱</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-xs text-muted">日</text>
|
||||
<text class="text-base font-medium block">{{boneResult.dayWeight}}钱</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-xs text-muted">时</text>
|
||||
<text class="text-base font-medium block">{{boneResult.hourWeight}}钱</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,52 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 24rpx;
|
||||
color: #6B5E53;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.picker-input {
|
||||
padding: 20rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.hexagram-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.trigram-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin: 8rpx 0;
|
||||
}
|
||||
|
||||
.trigram-symbol {
|
||||
font-size: 64rpx;
|
||||
}
|
||||
|
||||
.trigram-name {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
const { calculateDailyFortune } = require('../../utils/core/index.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
hasBazi: false,
|
||||
fortune: null,
|
||||
scoreLevelText: ''
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadFortune();
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadFortune();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
loadFortune() {
|
||||
const bazi = wx.getStorageSync('lunar-user-bazi');
|
||||
if (!bazi) {
|
||||
this.setData({ hasBazi: false, fortune: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const fortune = calculateDailyFortune(bazi.eightChar, today);
|
||||
|
||||
const levelMap = {
|
||||
great: '大吉',
|
||||
good: '吉',
|
||||
fair: '平',
|
||||
poor: '凶',
|
||||
bad: '大凶'
|
||||
};
|
||||
|
||||
this.setData({
|
||||
hasBazi: true,
|
||||
fortune,
|
||||
scoreLevelText: levelMap[fortune.scoreLevel] || '平'
|
||||
});
|
||||
},
|
||||
|
||||
goBazi() {
|
||||
wx.switchTab({ url: '/pages/bazi/bazi' });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "每日运势",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<view class="container">
|
||||
<!-- 无八字提示 -->
|
||||
<view class="card text-center" wx:if="{{!hasBazi}}">
|
||||
<text class="text-lg text-muted">请先排八字以查看每日运势</text>
|
||||
<view class="btn-primary mt-2" bindtap="goBazi">去排八字</view>
|
||||
</view>
|
||||
|
||||
<!-- 运势内容 -->
|
||||
<block wx:if="{{hasBazi && fortune}}">
|
||||
<!-- 总分卡片 -->
|
||||
<view class="card score-card">
|
||||
<view class="text-center">
|
||||
<text class="score-value">{{fortune.overallScore}}</text>
|
||||
<text class="score-level">{{scoreLevelText}}</text>
|
||||
</view>
|
||||
<view class="text-sm text-muted text-center mt-1">{{fortune.date}} {{fortune.lunarDate}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 幸运元素 -->
|
||||
<view class="card">
|
||||
<view class="section-title">幸运元素</view>
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">幸运色</text>
|
||||
<text class="text-base font-medium">{{fortune.luckyMeta.colors.join('、')}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">幸运数字</text>
|
||||
<text class="text-base font-medium">{{fortune.luckyMeta.numbers.join('、')}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">幸运方位</text>
|
||||
<text class="text-base font-medium">{{fortune.luckyMeta.direction}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="text-xs text-muted">五行</text>
|
||||
<text class="text-base font-medium">{{fortune.luckyMeta.element}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 四柱关系 -->
|
||||
<view class="card">
|
||||
<view class="section-title">四柱与今日关系</view>
|
||||
<view class="relation-list">
|
||||
<view class="relation-item" wx:for="{{fortune.pillarRelationships}}" wx:key="pillar">
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="font-medium">{{item.pillarLabel}}</text>
|
||||
<text class="text-sm {{item.score >= 0 ? 'text-lucky' : 'text-unlucky'}}">{{item.score > 0 ? '+' : ''}}{{item.score}}</text>
|
||||
</view>
|
||||
<view class="text-sm text-muted mt-1">
|
||||
{{item.userGanzhi}} vs {{item.dayGanzhi}}
|
||||
</view>
|
||||
<view class="flex flex-wrap gap-1 mt-1">
|
||||
<text class="badge badge-outline" wx:if="{{item.stemTenStar}}">{{item.stemTenStar}}</text>
|
||||
<text class="badge badge-lucky" wx:if="{{item.stemCombine}}">天干合</text>
|
||||
<text class="badge badge-unlucky" wx:if="{{item.stemOpposite}}">天干冲</text>
|
||||
<text class="badge badge-lucky" wx:if="{{item.branchCombine}}">地支合</text>
|
||||
<text class="badge badge-unlucky" wx:if="{{item.branchOpposite}}">地支冲</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 建议 -->
|
||||
<view class="card">
|
||||
<view class="section-title">今日建议</view>
|
||||
<view class="suggestion-list">
|
||||
<view class="suggestion-item" wx:for="{{fortune.suggestions}}" wx:key="*this">
|
||||
<text class="text-base">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分项得分 -->
|
||||
<view class="card">
|
||||
<view class="section-title">分项运势</view>
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view class="info-item text-center">
|
||||
<text class="text-xs text-muted">爱情</text>
|
||||
<text class="text-xl font-bold {{fortune.categoryScores.love >= 0 ? 'text-lucky' : 'text-unlucky'}}">{{fortune.categoryScores.love}}</text>
|
||||
</view>
|
||||
<view class="info-item text-center">
|
||||
<text class="text-xs text-muted">事业</text>
|
||||
<text class="text-xl font-bold {{fortune.categoryScores.career >= 0 ? 'text-lucky' : 'text-unlucky'}}">{{fortune.categoryScores.career}}</text>
|
||||
</view>
|
||||
<view class="info-item text-center">
|
||||
<text class="text-xs text-muted">财运</text>
|
||||
<text class="text-xl font-bold {{fortune.categoryScores.wealth >= 0 ? 'text-lucky' : 'text-unlucky'}}">{{fortune.categoryScores.wealth}}</text>
|
||||
</view>
|
||||
<view class="info-item text-center">
|
||||
<text class="text-xs text-muted">健康</text>
|
||||
<text class="text-xl font-bold {{fortune.categoryScores.health >= 0 ? 'text-lucky' : 'text-unlucky'}}">{{fortune.categoryScores.health}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -0,0 +1,47 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.score-card {
|
||||
background: linear-gradient(135deg, #C41E3A 0%, #8B1A2B 100%);
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
padding: 40rpx 24rpx;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 80rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.score-level {
|
||||
font-size: 32rpx;
|
||||
margin-left: 16rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 16rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.relation-item {
|
||||
padding: 16rpx;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.relation-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
const { getTodayInfo, getAlmanacInfo } = require('../../utils/core/index.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
dayInfo: {},
|
||||
almanac: {}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadData();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
loadData() {
|
||||
const dayInfo = getTodayInfo();
|
||||
const almanac = getAlmanacInfo(dayInfo.solarYear, dayInfo.solarMonth, dayInfo.solarDay);
|
||||
this.setData({ dayInfo, almanac });
|
||||
},
|
||||
|
||||
navTo(e) {
|
||||
const url = e.currentTarget.dataset.url;
|
||||
wx.navigateTo({ url });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "万年历",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<view class="container">
|
||||
<!-- 顶部日期卡片 -->
|
||||
<view class="card date-card">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<text class="text-3xl font-bold">{{dayInfo.solarDay}}</text>
|
||||
<text class="text-lg text-muted ml-2">{{dayInfo.solarMonth}}月{{dayInfo.solarYear}}年</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text class="text-base">{{dayInfo.weekDay}}</text>
|
||||
<text class="text-sm text-muted block">{{dayInfo.constellation}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-2 flex items-center gap-2">
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.lunarFestival}}">{{dayInfo.lunarFestival}}</text>
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.solarFestival}}">{{dayInfo.solarFestival}}</text>
|
||||
<text class="badge badge-outline" wx:if="{{dayInfo.solarTerm}}">{{dayInfo.solarTerm}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 农历信息 -->
|
||||
<view class="card">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<text class="text-2xl font-bold font-chinese">{{dayInfo.lunarMonthName}}{{dayInfo.lunarDayName}}</text>
|
||||
<text class="text-sm text-muted block mt-1">{{dayInfo.lunarYearGanzhi}}年 {{dayInfo.lunarMonthGanzhi}}月 {{dayInfo.lunarDayGanzhi}}日</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text class="text-lg font-medium">{{dayInfo.zodiac}}年</text>
|
||||
<text class="text-sm text-muted block">{{dayInfo.moonPhase}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 宜忌 -->
|
||||
<view class="card">
|
||||
<view class="flex gap-2 mb-2">
|
||||
<view class="flex-1">
|
||||
<text class="text-lucky font-bold text-lg">宜</text>
|
||||
<view class="flex flex-wrap gap-1 mt-1">
|
||||
<text class="badge badge-lucky" wx:for="{{almanac.recommends}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="text-unlucky font-bold text-lg">忌</text>
|
||||
<view class="flex flex-wrap gap-1 mt-1">
|
||||
<text class="badge badge-unlucky" wx:for="{{almanac.avoids}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 黄历详情 -->
|
||||
<view class="card">
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view class="p-2 bg-page rounded">
|
||||
<text class="text-xs text-muted">值神</text>
|
||||
<text class="text-base font-medium block">{{almanac.duty}}</text>
|
||||
</view>
|
||||
<view class="p-2 bg-page rounded">
|
||||
<text class="text-xs text-muted">黄道</text>
|
||||
<text class="text-base font-medium block">{{almanac.twelveStar.name}}({{almanac.twelveStar.ecliptic}})</text>
|
||||
</view>
|
||||
<view class="p-2 bg-page rounded">
|
||||
<text class="text-xs text-muted">二十八宿</text>
|
||||
<text class="text-base font-medium block">{{almanac.twentyEightStar.name}}</text>
|
||||
</view>
|
||||
<view class="p-2 bg-page rounded">
|
||||
<text class="text-xs text-muted">六曜</text>
|
||||
<text class="text-base font-medium block">{{almanac.sixStar}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<view class="grid grid-cols-4 gap-2 mt-2">
|
||||
<view class="quick-entry" bindtap="navTo" data-url="/pages/calendar/calendar">
|
||||
<image src="/images/calendar.png" class="quick-icon" mode="aspectFit"/>
|
||||
<text class="text-sm">日历</text>
|
||||
</view>
|
||||
<view class="quick-entry" bindtap="navTo" data-url="/pages/bazi/bazi">
|
||||
<image src="/images/bazi.png" class="quick-icon" mode="aspectFit"/>
|
||||
<text class="text-sm">八字</text>
|
||||
</view>
|
||||
<view class="quick-entry" bindtap="navTo" data-url="/pages/fortune/fortune">
|
||||
<image src="/images/fortune.png" class="quick-icon" mode="aspectFit"/>
|
||||
<text class="text-sm">运势</text>
|
||||
</view>
|
||||
<view class="quick-entry" bindtap="navTo" data-url="/pages/divination/divination">
|
||||
<image src="/images/divination.png" class="quick-icon" mode="aspectFit"/>
|
||||
<text class="text-sm">占卜</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,31 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.date-card {
|
||||
background: linear-gradient(135deg, #C41E3A 0%, #8B1A2B 100%);
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.date-card .text-muted {
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.quick-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
Page({
|
||||
data: {
|
||||
userInfo: null,
|
||||
weekStartDay: 0,
|
||||
ziHourSect: 'lateZiNextDay',
|
||||
solarTime: true,
|
||||
bookmarks: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadSettings();
|
||||
this.loadBookmarks();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadBookmarks();
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
const settings = wx.getStorageSync('lunar-settings') || {};
|
||||
this.setData({
|
||||
weekStartDay: settings.weekStartDay || 0,
|
||||
ziHourSect: settings.ziHourSect || 'lateZiNextDay',
|
||||
solarTime: settings.solarTime !== false
|
||||
});
|
||||
},
|
||||
|
||||
saveSettings() {
|
||||
const { weekStartDay, ziHourSect, solarTime } = this.data;
|
||||
wx.setStorageSync('lunar-settings', { weekStartDay, ziHourSect, solarTime });
|
||||
},
|
||||
|
||||
loadBookmarks() {
|
||||
const bookmarks = wx.getStorageSync('lunar-bookmarks') || [];
|
||||
this.setData({ bookmarks });
|
||||
},
|
||||
|
||||
setWeekStart(e) {
|
||||
this.setData({ weekStartDay: Number(e.currentTarget.dataset.v) });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
setZiSect(e) {
|
||||
this.setData({ ziHourSect: e.currentTarget.dataset.v });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
onSolarTimeChange(e) {
|
||||
this.setData({ solarTime: e.detail.value });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
removeBookmark(e) {
|
||||
const date = e.currentTarget.dataset.date;
|
||||
let bookmarks = this.data.bookmarks.filter(b => b.date !== date);
|
||||
wx.setStorageSync('lunar-bookmarks', bookmarks);
|
||||
this.setData({ bookmarks });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
},
|
||||
|
||||
login() {
|
||||
wx.getUserProfile({
|
||||
desc: '用于完善用户资料',
|
||||
success: (res) => {
|
||||
this.setData({ userInfo: res.userInfo });
|
||||
wx.setStorageSync('lunar-user-info', res.userInfo);
|
||||
wx.showToast({ title: '登录成功', icon: 'success' });
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '登录失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "设置",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<view class="container">
|
||||
<!-- 用户信息 -->
|
||||
<view class="card">
|
||||
<view class="section-title">用户信息</view>
|
||||
<view class="flex items-center gap-2" wx:if="{{userInfo}}">
|
||||
<image src="{{userInfo.avatarUrl}}" class="avatar" mode="aspectFill"/>
|
||||
<view>
|
||||
<text class="text-base font-medium">{{userInfo.nickName}}</text>
|
||||
<text class="text-xs text-muted block">已登录</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-primary" wx:else bindtap="login">微信登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 偏好设置 -->
|
||||
<view class="card">
|
||||
<view class="section-title">偏好设置</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">周起始日</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{weekStartDay === 0 ? 'active' : ''}}" bindtap="setWeekStart" data-v="0">周日</view>
|
||||
<view class="btn-outline {{weekStartDay === 1 ? 'active' : ''}}" bindtap="setWeekStart" data-v="1">周一</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">子时流派</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{ziHourSect === 'lateZiNextDay' ? 'active' : ''}}" bindtap="setZiSect" data-v="lateZiNextDay">晚子时算次日</view>
|
||||
<view class="btn-outline {{ziHourSect === 'lateZiSameDay' ? 'active' : ''}}" bindtap="setZiSect" data-v="lateZiSameDay">晚子时算当日</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">真太阳时校正</text>
|
||||
<switch checked="{{solarTime}}" bindchange="onSolarTimeChange" color="#C41E3A"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏管理 -->
|
||||
<view class="card">
|
||||
<view class="section-title">我的收藏</view>
|
||||
<view class="bookmark-list" wx:if="{{bookmarks.length > 0}}">
|
||||
<view class="bookmark-item" wx:for="{{bookmarks}}" wx:key="date">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<text class="text-base font-medium">{{item.date}}</text>
|
||||
<text class="text-xs text-muted block">{{item.lunarDate}}</text>
|
||||
</view>
|
||||
<view class="btn-ghost text-unlucky" bindtap="removeBookmark" data-date="{{item.date}}">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-center text-muted" wx:else>
|
||||
<text>暂无收藏</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关于 -->
|
||||
<view class="card">
|
||||
<view class="section-title">关于</view>
|
||||
<view class="text-sm text-muted">
|
||||
<text>万年历小程序 v1.0.0</text>
|
||||
<text class="block mt-1">提供农历、黄历、八字、运势、占卜等功能</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,47 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-outline.active {
|
||||
background: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
border-color: #C41E3A;
|
||||
}
|
||||
|
||||
.bookmark-item {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.bookmark-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
const { SOLAR_TERMS, SOLAR_TERM_DATES } = require('../../utils/core/index.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
year: new Date().getFullYear(),
|
||||
terms: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadTerms();
|
||||
},
|
||||
|
||||
loadTerms() {
|
||||
const { year } = this.data;
|
||||
const today = new Date();
|
||||
const terms = [];
|
||||
|
||||
for (let m = 0; m < 12; m++) {
|
||||
for (let t = 0; t < 2; t++) {
|
||||
const termIndex = m * 2 + t;
|
||||
const day = SOLAR_TERM_DATES[m][t];
|
||||
const dateStr = `${year}-${String(m + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const termDate = new Date(year, m, day);
|
||||
terms.push({
|
||||
name: SOLAR_TERMS[termIndex],
|
||||
date: dateStr,
|
||||
isPast: termDate < today
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按日期排序
|
||||
terms.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
this.setData({ terms });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "二十四节气",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="section-title">{{year}}年二十四节气</view>
|
||||
<view class="term-list">
|
||||
<view class="term-item" wx:for="{{terms}}" wx:key="name">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<text class="text-base font-medium">{{item.name}}</text>
|
||||
<text class="text-xs text-muted block">{{item.date}}</text>
|
||||
</view>
|
||||
<text class="badge {{item.isPast ? 'badge-outline' : 'badge-lucky'}}">{{item.isPast ? '已过' : '未来'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.term-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.term-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.term-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"description": "万年历小程序",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"bundle": false,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"urlCheck": true,
|
||||
"scopeDataCheck": false,
|
||||
"coverView": true,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"compileHotReLoad": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": true,
|
||||
"autoAudits": false,
|
||||
"newFeature": false,
|
||||
"uglifyFileName": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"useIsolateContext": true,
|
||||
"nodeModules": false,
|
||||
"enhance": true,
|
||||
"useMultiFrameRuntime": true,
|
||||
"useApiHook": true,
|
||||
"useApiHostProcess": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"packNpmManually": false,
|
||||
"enableEngineNative": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"showES6CompileOption": false,
|
||||
"minifyWXML": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
}
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.4.0",
|
||||
"appid": "touristappid",
|
||||
"projectname": "lunar-mini",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* 农历数据表 1900-2100
|
||||
* 数据来源:香港天文台
|
||||
* 每年一个整数,bit 表示:
|
||||
* 1-12: 1-12月大小月 (1=大月30天, 0=小月29天)
|
||||
* 13-16: 闰月月份 (0=无闰月)
|
||||
* 17: 闰月大小 (1=大月, 0=小月)
|
||||
*/
|
||||
const LUNAR_INFO = [
|
||||
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
|
||||
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
|
||||
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
|
||||
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
|
||||
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
|
||||
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
|
||||
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
|
||||
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
|
||||
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
|
||||
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
|
||||
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
|
||||
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
|
||||
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
|
||||
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
|
||||
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
|
||||
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
|
||||
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
|
||||
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
|
||||
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
|
||||
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
|
||||
0x0d520
|
||||
];
|
||||
|
||||
/** 天干 */
|
||||
const HEAVEN_STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
||||
/** 地支 */
|
||||
const EARTH_BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
||||
/** 生肖 */
|
||||
const ZODIACS = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
|
||||
/** 农历月份名 */
|
||||
const LUNAR_MONTH_NAMES = ['正月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '冬月', '腊月'];
|
||||
/** 农历日期名 */
|
||||
const LUNAR_DAY_NAMES = [
|
||||
'初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十',
|
||||
'十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十',
|
||||
'廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十'
|
||||
];
|
||||
/** 星期名 */
|
||||
const WEEK_NAMES = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
/** 星座 */
|
||||
const CONSTELLATIONS = ['摩羯座', '水瓶座', '双鱼座', '白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座'];
|
||||
/** 星座日期边界 */
|
||||
const CONSTELLATION_DATES = [20, 19, 21, 20, 21, 22, 23, 23, 23, 24, 23, 22];
|
||||
|
||||
/** 二十四节气名 */
|
||||
const SOLAR_TERMS = [
|
||||
'冬至', '小寒', '大寒', '立春', '雨水', '惊蛰',
|
||||
'春分', '清明', '谷雨', '立夏', '小满', '芒种',
|
||||
'夏至', '小暑', '大暑', '立秋', '处暑', '白露',
|
||||
'秋分', '寒露', '霜降', '立冬', '小雪', '大雪'
|
||||
];
|
||||
|
||||
/** 二十四节气大致日期(每月两个) */
|
||||
const SOLAR_TERM_DATES = [
|
||||
[6, 21], // 小寒, 大寒 (1月)
|
||||
[4, 19], // 立春, 雨水 (2月)
|
||||
[6, 21], // 惊蛰, 春分 (3月)
|
||||
[5, 20], // 清明, 谷雨 (4月)
|
||||
[6, 21], // 立夏, 小满 (5月)
|
||||
[6, 21], // 芒种, 夏至 (6月)
|
||||
[7, 23], // 小暑, 大暑 (7月)
|
||||
[8, 23], // 立秋, 处暑 (8月)
|
||||
[8, 23], // 白露, 秋分 (9月)
|
||||
[8, 24], // 寒露, 霜降 (10月)
|
||||
[8, 22], // 立冬, 小雪 (11月)
|
||||
[7, 22] // 大雪, 冬至 (12月)
|
||||
];
|
||||
|
||||
/** 公历节日 */
|
||||
const SOLAR_FESTIVALS = {
|
||||
'1-1': '元旦', '2-14': '情人节', '3-8': '妇女节', '3-12': '植树节',
|
||||
'4-1': '愚人节', '5-1': '劳动节', '5-4': '青年节', '6-1': '儿童节',
|
||||
'7-1': '建党节', '8-1': '建军节', '9-10': '教师节', '10-1': '国庆节',
|
||||
'12-24': '平安夜', '12-25': '圣诞节'
|
||||
};
|
||||
|
||||
/** 农历节日 */
|
||||
const LUNAR_FESTIVALS = {
|
||||
'1-1': '春节', '1-15': '元宵节', '2-2': '龙抬头', '5-5': '端午节',
|
||||
'7-7': '七夕', '7-15': '中元节', '8-15': '中秋节', '9-9': '重阳节',
|
||||
'12-8': '腊八节', '12-23': '小年', '12-30': '除夕'
|
||||
};
|
||||
|
||||
/** 佛教节日 */
|
||||
const BUDDHIST_FESTIVALS = {
|
||||
'1-1': '弥勒菩萨圣诞', '2-8': '释迦牟尼佛出家日', '2-15': '释迦牟尼佛涅槃日',
|
||||
'2-19': '观音菩萨圣诞', '2-21': '普贤菩萨圣诞', '3-16': '准提菩萨圣诞',
|
||||
'4-4': '文殊菩萨圣诞', '4-8': '释迦牟尼佛圣诞(浴佛节)', '5-3': '伽蓝菩萨圣诞',
|
||||
'6-3': '韦驮菩萨圣诞', '6-19': '观音菩萨成道日', '7-13': '大势至菩萨圣诞',
|
||||
'7-15': '盂兰盆节(佛欢喜日)', '7-30': '地藏菩萨圣诞', '8-22': '燃灯佛圣诞',
|
||||
'9-19': '观音菩萨出家日', '9-30': '药师佛圣诞', '10-5': '达摩祖师诞辰',
|
||||
'11-17': '阿弥陀佛圣诞', '12-8': '释迦牟尼佛成道日(腊八)', '12-29': '华严菩萨圣诞'
|
||||
};
|
||||
|
||||
/** 纳音表 */
|
||||
const NAYIN_TABLE = [
|
||||
'海中金', '炉中火', '大林木', '路旁土', '剑锋金', '山头火',
|
||||
'涧下水', '城头土', '白蜡金', '杨柳木', '泉中水', '屋上土',
|
||||
'霹雳火', '松柏木', '长流水', '沙中金', '山下火', '平地木',
|
||||
'壁上土', '金箔金', '覆灯火', '天河水', '大驿土', '钗钏金',
|
||||
'桑柘木', '大溪水', '沙中土', '天上火', '石榴木', '大海水'
|
||||
];
|
||||
|
||||
/** 时辰名 */
|
||||
const HOUR_NAMES = ['子时', '丑时', '寅时', '卯时', '辰时', '巳时', '午时', '未时', '申时', '酉时', '戌时', '亥时'];
|
||||
/** 时辰时间范围 */
|
||||
const HOUR_RANGES = ['23:00-01:00', '01:00-03:00', '03:00-05:00', '05:00-07:00', '07:00-09:00', '09:00-11:00', '11:00-13:00', '13:00-15:00', '15:00-17:00', '17:00-19:00', '19:00-21:00', '21:00-23:00'];
|
||||
|
||||
/** 建除十二神 */
|
||||
const DUTY_NAMES = ['建', '除', '满', '平', '定', '执', '破', '危', '成', '收', '开', '闭'];
|
||||
/** 黄道黑道 */
|
||||
const TWELVE_STARS = ['青龙', '明堂', '天刑', '朱雀', '金匮', '天德', '白虎', '玉堂', '天牢', '玄武', '司命', '勾陈'];
|
||||
/** 二十八宿 */
|
||||
const TWENTY_EIGHT_STARS = [
|
||||
'角木蛟', '亢金龙', '氐土貉', '房日兔', '心月狐', '尾火虎', '箕水豹',
|
||||
'斗木獬', '牛金牛', '女土蝠', '虚日鼠', '危月燕', '室火猪', '壁水貐',
|
||||
'奎木狼', '娄金狗', '胃土雉', '昴日鸡', '毕月乌', '觜火猴', '参水猿',
|
||||
'井木犴', '鬼金羊', '柳土獐', '星日马', '张月鹿', '翼火蛇', '轸水蚓'
|
||||
];
|
||||
/** 九星 */
|
||||
const NINE_STARS = ['一白', '二黑', '三碧', '四绿', '五黄', '六白', '七赤', '八白', '九紫'];
|
||||
/** 六曜 */
|
||||
const SIX_STARS = ['大安', '留连', '速喜', '赤口', '小吉', '空亡'];
|
||||
/** 彭祖百忌 */
|
||||
const PENG_ZU = {
|
||||
stems: ['甲不开仓', '乙不栽植', '丙不修灶', '丁不剃头', '戊不受田', '己不破券', '庚不经络', '辛不合酱', '壬不泱水', '癸不词讼'],
|
||||
branches: ['子不问卜', '丑不冠带', '寅不祭祀', '卯不穿井', '辰不哭泣', '巳不远行', '午不苫盖', '未不服药', '申不安床', '酉不会客', '戌不吃犬', '亥不嫁娶']
|
||||
};
|
||||
|
||||
/** 五行 */
|
||||
const ELEMENTS = ['木', '火', '土', '金', '水'];
|
||||
/** 天干五行 */
|
||||
const STEM_ELEMENTS = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' };
|
||||
/** 地支五行 */
|
||||
const BRANCH_ELEMENTS = { '子': '水', '丑': '土', '寅': '木', '卯': '木', '辰': '土', '巳': '火', '午': '火', '未': '土', '申': '金', '酉': '金', '戌': '土', '亥': '水' };
|
||||
/** 天干阴阳 */
|
||||
const STEM_YINYANG = { '甲': 'yang', '乙': 'yin', '丙': 'yang', '丁': 'yin', '戊': 'yang', '己': 'yin', '庚': 'yang', '辛': 'yin', '壬': 'yang', '癸': 'yin' };
|
||||
/** 地支阴阳 */
|
||||
const BRANCH_YINYANG = { '子': 'yang', '丑': 'yin', '寅': 'yang', '卯': 'yin', '辰': 'yang', '巳': 'yin', '午': 'yang', '未': 'yin', '申': 'yang', '酉': 'yin', '戌': 'yang', '亥': 'yin' };
|
||||
|
||||
/** 地支藏干 */
|
||||
const HIDE_STEMS = {
|
||||
'子': [{ stem: '癸', type: '本气' }],
|
||||
'丑': [{ stem: '己', type: '本气' }, { stem: '癸', type: '中气' }, { stem: '辛', type: '余气' }],
|
||||
'寅': [{ stem: '甲', type: '本气' }, { stem: '丙', type: '中气' }, { stem: '戊', type: '余气' }],
|
||||
'卯': [{ stem: '乙', type: '本气' }],
|
||||
'辰': [{ stem: '戊', type: '本气' }, { stem: '乙', type: '中气' }, { stem: '癸', type: '余气' }],
|
||||
'巳': [{ stem: '丙', type: '本气' }, { stem: '庚', type: '中气' }, { stem: '戊', type: '余气' }],
|
||||
'午': [{ stem: '丁', type: '本气' }, { stem: '己', type: '中气' }],
|
||||
'未': [{ stem: '己', type: '本气' }, { stem: '丁', type: '中气' }, { stem: '乙', type: '余气' }],
|
||||
'申': [{ stem: '庚', type: '本气' }, { stem: '壬', type: '中气' }, { stem: '戊', type: '余气' }],
|
||||
'酉': [{ stem: '辛', type: '本气' }],
|
||||
'戌': [{ stem: '戊', type: '本气' }, { stem: '辛', type: '中气' }, { stem: '丁', type: '余气' }],
|
||||
'亥': [{ stem: '壬', type: '本气' }, { stem: '甲', type: '中气' }]
|
||||
};
|
||||
|
||||
/** 十二长生 */
|
||||
const TERRAINS = ['长生', '沐浴', '冠带', '临官', '帝旺', '衰', '病', '死', '墓', '绝', '胎', '养'];
|
||||
|
||||
/** 十神 */
|
||||
const TEN_STARS = ['比肩', '劫财', '食神', '伤官', '偏财', '正财', '七杀', '正官', '偏印', '正印'];
|
||||
|
||||
/** 地支六合 */
|
||||
const BRANCH_COMBINE = { '子': '丑', '丑': '子', '寅': '亥', '亥': '寅', '卯': '戌', '戌': '卯', '辰': '酉', '酉': '辰', '巳': '申', '申': '巳', '午': '未', '未': '午' };
|
||||
/** 地支六冲 */
|
||||
const BRANCH_OPPOSITE = { '子': '午', '午': '子', '丑': '未', '未': '丑', '寅': '申', '申': '寅', '卯': '酉', '酉': '卯', '辰': '戌', '戌': '辰', '巳': '亥', '亥': '巳' };
|
||||
/** 地支六害 */
|
||||
const BRANCH_HARM = { '子': '未', '未': '子', '丑': '午', '午': '丑', '寅': '巳', '巳': '寅', '卯': '辰', '辰': '卯', '申': '亥', '亥': '申', '酉': '戌', '戌': '酉' };
|
||||
/** 地支相刑 */
|
||||
const BRANCH_PUNISH = [
|
||||
['寅', '巳'], ['巳', '申'], ['申', '寅'],
|
||||
['丑', '戌'], ['戌', '未'], ['未', '丑'],
|
||||
['子', '卯'], ['卯', '子'],
|
||||
['辰', '辰'], ['午', '午'], ['酉', '酉'], ['亥', '亥']
|
||||
];
|
||||
/** 地支三合局 */
|
||||
const THREE_COMBINES = {
|
||||
'水局': ['申', '子', '辰'],
|
||||
'木局': ['亥', '卯', '未'],
|
||||
'火局': ['寅', '午', '戌'],
|
||||
'金局': ['巳', '酉', '丑']
|
||||
};
|
||||
/** 天干合 */
|
||||
const STEM_COMBINE = { '甲': '己', '己': '甲', '乙': '庚', '庚': '乙', '丙': '辛', '辛': '丙', '丁': '壬', '壬': '丁', '戊': '癸', '癸': '戊' };
|
||||
/** 天干冲 */
|
||||
const STEM_OPPOSITE = { '甲': '庚', '庚': '甲', '乙': '辛', '辛': '乙', '丙': '壬', '壬': '丙', '丁': '癸', '癸': '丁' };
|
||||
|
||||
/** 五行相生 */
|
||||
const GENERATES = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
|
||||
/** 五行相克 */
|
||||
const KILLS = { '木': '土', '土': '水', '水': '火', '火': '金', '金': '木' };
|
||||
|
||||
/** 八卦 */
|
||||
const TRIGRAMS = [
|
||||
null,
|
||||
{ index: 1, name: '乾', symbol: '☰', element: '金', direction: '西北', nature: '天', trait: '健', body: '首' },
|
||||
{ index: 2, name: '兑', symbol: '☱', element: '金', direction: '西', nature: '泽', trait: '悦', body: '口' },
|
||||
{ index: 3, name: '离', symbol: '☲', element: '火', direction: '南', nature: '火', trait: '丽', body: '目' },
|
||||
{ index: 4, name: '震', symbol: '☳', element: '木', direction: '东', nature: '雷', trait: '动', body: '足' },
|
||||
{ index: 5, name: '巽', symbol: '☴', element: '木', direction: '东南', nature: '风', trait: '入', body: '股' },
|
||||
{ index: 6, name: '坎', symbol: '☵', element: '水', direction: '北', nature: '水', trait: '陷', body: '耳' },
|
||||
{ index: 7, name: '艮', symbol: '☶', element: '土', direction: '东北', nature: '山', trait: '止', body: '手' },
|
||||
{ index: 8, name: '坤', symbol: '☷', element: '土', direction: '西南', nature: '地', trait: '顺', body: '腹' }
|
||||
];
|
||||
|
||||
/** 六十四卦数据 */
|
||||
const HEXAGRAMS = {
|
||||
'1,1': { name: '乾为天', judgment: '大哉乾元,万物资始,乃统天。云行雨施,品物流形', image: '天行健,君子以自强不息', lines: ['潜龙勿用', '见龙在田,利见大人', '君子终日乾乾,夕惕若厉', '或跃在渊,无咎', '飞龙在天,利见大人', '亢龙有悔'] },
|
||||
'1,5': { name: '天风姤', judgment: '姤,遇也,柔遇刚也。天地相遇,品物咸章', image: '天下有风,姤。后以施命诰四方', lines: ['系于金柅,贞吉', '包有鱼,无咎', '臀无肤,其行次且', '包无鱼,起凶', '以杞包瓜,含章', '姤其角,吝'] },
|
||||
'1,7': { name: '天山遁', judgment: '遁亨,遁而亨也。刚当位而应,与时行也', image: '天下有山,遁。君子以远小人,不恶而严', lines: ['遁尾厉,勿用有攸往', '执之用黄牛之革', '系遁,有疾厉', '好遁,君子吉', '嘉遁,贞吉', '肥遁,无不利'] },
|
||||
'1,8': { name: '天地否', judgment: '否之匪人,不利君子贞。大往小来', image: '天地不交,否。君子以俭德辟难', lines: ['拔茅茹,以其汇,贞吉', '包承,小人吉,大人否', '包羞', '有命无咎,畴离祉', '休否,大人吉', '倾否,先否后喜'] },
|
||||
'5,8': { name: '风地观', judgment: '观,盥而不荐,有孚颙若。观天之神道而四时不忒', image: '风行地上,观。先王以省方观民设教', lines: ['童观,小人无咎', '窥观,利女贞', '观我生进退', '观国之光,利用宾于王', '观我生,君子无咎', '观其生,君子无咎'] },
|
||||
'7,8': { name: '山地剥', judgment: '剥,剥也,柔变刚也。不利有攸往', image: '山附于地,剥。上以厚下安宅', lines: ['剥床以足,蔑贞凶', '剥床以辨,蔑贞凶', '剥之无咎', '剥床以肤,凶', '贯鱼以宫人宠,无不利', '硕果不食,君子得舆'] },
|
||||
'3,8': { name: '火地晋', judgment: '晋,进也。明出地上,顺而丽乎大明', image: '明出地上,晋。君子以自昭明德', lines: ['晋如摧如,贞吉', '晋如愁如,贞吉', '众允,悔亡', '晋如鼫鼠,贞厉', '悔亡,失得勿恤', '晋其角,维用伐邑'] },
|
||||
'3,1': { name: '火天大有', judgment: '大有,柔得尊位,大中而上下应之', image: '火在天上,大有。君子以遏恶扬善,顺天休命', lines: ['无交害,匪咎', '大车以载,有攸往', '公用亨于天子', '匪其彭,无咎', '厥孚交如,威如', '自天佑之,吉无不利'] },
|
||||
'6,6': { name: '坎为水', judgment: '习坎,重险也。水流而不盈,行险而不失其信', image: '水洊至,习坎。君子以常德行习教事', lines: ['习坎,入于坎窞', '坎有险,求小得', '来之坎坎,险且枕', '樽酒簋贰,用缶', '坎不盈,祗既平', '系用徽纆,寘于丛棘'] },
|
||||
'6,2': { name: '水泽节', judgment: '节亨,苦节不可贞', image: '泽上有水,节。君子以制数度议德行', lines: ['不出户庭,无咎', '不出门庭,凶', '不节若,则嗟若', '安节,亨', '甘节,吉', '苦节,贞凶'] },
|
||||
'6,4': { name: '水雷屯', judgment: '屯,刚柔始交而难生。动乎险中,大亨贞', image: '云雷屯,君子以经纶', lines: ['磐桓,利居贞', '屯如邅如,乘马班如', '即鹿无虞,惟入于林中', '乘马班如,求婚媾', '屯其膏,小贞吉', '乘马班如,泣血涟如'] },
|
||||
'6,3': { name: '水火既济', judgment: '既济亨,小者亨也。利贞,初吉终乱', image: '水在火上,既济。君子以思患而豫防之', lines: ['曳其轮,濡其尾', '妇丧其茀,勿逐', '高宗伐鬼方,三年克之', '繻有衣袽,终日戒', '东邻杀牛,不如西邻', '濡其首,厉'] },
|
||||
'2,3': { name: '泽火革', judgment: '革,水火相息。天地革而四时成', image: '泽中有火,革。君子以治历明时', lines: ['巩用黄牛之革', '巳日乃革之,征吉', '征凶,贞厉', '悔亡,有孚改命', '大人虎变,未占有孚', '君子豹变,小人革面'] },
|
||||
'4,3': { name: '雷火丰', judgment: '丰,大也。明以动,故丰', image: '雷电皆至,丰。君子以折狱致刑', lines: ['遇其配主,虽旬无咎', '丰其蔀,日中见斗', '丰其沛,日中见沬', '丰其蔀,日中见斗', '来章,有庆誉', '丰其屋,蔀其家'] },
|
||||
'8,3': { name: '地火明夷', judgment: '明夷,利艰贞。明入地中,明夷', image: '明入地中,明夷。君子以莅众用晦而明', lines: ['明夷于飞,垂其翼', '明夷,夷于左股', '明夷于南狩,得其大首', '入于左腹,获明夷之心', '箕子之明夷,利贞', '不明晦,初登于天'] },
|
||||
'8,6': { name: '地水师', judgment: '师,众也。贞,丈人吉,无咎', image: '地中有水,师。君子以容民畜众', lines: ['师出以律,否臧凶', '在师中,吉无咎', '师或舆尸,凶', '师左次,无咎', '田有禽,利执言', '大君有命,开国承家'] },
|
||||
'7,7': { name: '艮为山', judgment: '艮,止也。时止则止,时行则行', image: '兼山,艮。君子以思不出其位', lines: ['艮其趾,无咎', '艮其腓,不拯其随', '艮其限,列其夤', '艮其身,无咎', '艮其辅,言有序', '敦艮,吉'] },
|
||||
'7,3': { name: '山火贲', judgment: '贲亨,柔来而文刚,故亨', image: '山下有火,贲。君子以明庶政无敢折狱', lines: ['贲其趾,舍车而徒', '贲其须', '贲如濡如,永贞吉', '贲如皤如,白马翰如', '贲于丘园,束帛戋戋', '白贲,无咎'] },
|
||||
'7,1': { name: '山天大畜', judgment: '大畜,刚健笃实辉光,日新其德', image: '天在山中,大畜。君子以多识前言往行', lines: ['有厉,利已', '舆说輹', '良马逐,利艰贞', '童牛之牿,元吉', '豮豕之牙,吉', '何天之衢,亨'] },
|
||||
'7,2': { name: '山泽损', judgment: '损,损下益上,其道上行', image: '山下有泽,损。君子以惩忿窒欲', lines: ['已事遄往,无咎', '利贞,征凶,弗损益之', '三人行则损一人', '损其疾,使遄有喜', '或益之十朋之龟', '弗损益之,无咎'] },
|
||||
'3,2': { name: '火泽睽', judgment: '睽,火动而上,泽动而下', image: '上火下泽,睽。君子以同而异', lines: ['悔亡,丧马勿逐', '遇主于巷,无咎', '见舆曳,其牛掣', '睽孤,遇元夫', '悔亡,厥宗噬肤', '睽孤,见豕负涂'] },
|
||||
'1,2': { name: '天泽履', judgment: '履,柔履刚也。说而应乎乾', image: '上天下泽,履。君子以辩上下定民志', lines: ['素履往,无咎', '履道坦坦,幽人贞吉', '眇能视,跛能履', '履虎尾,愬愬终吉', '夬履,贞厉', '视履考祥,其旋元吉'] },
|
||||
'5,2': { name: '风泽中孚', judgment: '中孚,柔在内而刚得中', image: '泽上有风,中孚。君子以议狱缓死', lines: ['虞吉,有它不燕', '鸣鹤在阴,其子和之', '得敌,或鼓或罢', '月几望,马匹亡', '有孚挛如,无咎', '翰音登于天,贞凶'] },
|
||||
'5,7': { name: '风山渐', judgment: '渐,女归吉也。进得位,往有功也', image: '山上有木,渐。君子以居贤德善俗', lines: ['鸿渐于干,小子厉', '鸿渐于磐,饮食衎衎', '鸿渐于陆,夫征不复', '鸿渐于木,或得其桷', '鸿渐于陵,妇三岁不孕', '鸿渐于逵,其羽可用为仪'] },
|
||||
'4,4': { name: '震为雷', judgment: '震亨。震来虩虩,笑言哑哑,震惊百里', image: '洊雷,震。君子以恐惧修省', lines: ['震来虩虩,后笑言哑哑', '震来厉,亿丧贝', '震苏苏,震行无眚', '震遂泥', '震往来厉,亿无丧', '震索索,视矍矍'] },
|
||||
'4,8': { name: '雷地豫', judgment: '豫,刚应而志行,顺以动', image: '雷出地奋,豫。先王以作乐崇德', lines: ['鸣豫,凶', '介于石,不终日', '盱豫悔,迟有悔', '由豫,大有得', '贞疾,恒不死', '冥豫,成有渝'] },
|
||||
'4,6': { name: '雷水解', judgment: '解,险以动,动而免乎险', image: '雷雨作,解。君子以赦过宥罪', lines: ['无咎', '田获三狐,得黄矢', '负且乘,致寇至', '解而拇,朋至斯孚', '君子维有解,吉', '公用射隼于高墉之上'] },
|
||||
'4,5': { name: '雷风恒', judgment: '恒,久也。刚上而柔下', image: '雷风,恒。君子以立不易方', lines: ['浚恒,贞凶', '悔亡', '不恒其德,或承之羞', '田无禽', '恒其德,贞妇人吉', '振恒,凶'] },
|
||||
'8,5': { name: '地风升', judgment: '柔以时升,巽而顺,刚中而应', image: '地中生木,升。君子以顺德积小以高大', lines: ['允升,大吉', '孚乃利用禴,无咎', '升虚邑', '王用亨于岐山', '贞吉,升阶', '冥升,利于不息之贞'] },
|
||||
'6,5': { name: '水风井', judgment: '井,改邑不改井,无丧无得', image: '木上有水,井。君子以劳民劝相', lines: ['井泥不食,旧井无禽', '井谷射鲋,瓮敝漏', '井渫不食,为我心恻', '井甃,无咎', '井洌,寒泉食', '井收勿幕,有孚元吉'] },
|
||||
'2,5': { name: '泽风大过', judgment: '大过,大者过也。栋桡,本末弱也', image: '泽灭木,大过。君子以独立不惧遁世无闷', lines: ['藉用白茅,无咎', '枯杨生稊,老夫得其女妻', '栋桡,凶', '栋隆,吉', '枯杨生华,老妇得士夫', '过涉灭顶,凶'] },
|
||||
'2,4': { name: '泽雷随', judgment: '随,刚来而下柔,动而说', image: '泽中有雷,随。君子以向晦入宴息', lines: ['官有渝,贞吉', '系小子,失丈夫', '系丈夫,失小子', '随有获,贞凶', '孚于嘉,吉', '拘系之,乃从维之'] },
|
||||
'5,5': { name: '巽为风', judgment: '重巽以申命,刚巽乎中正而志行', image: '随风,巽。君子以申命行事', lines: ['进退,利武人之贞', '巽在床下,用史巫纷若', '频巽,吝', '悔亡,田获三品', '贞吉,悔亡无不利', '巽在床下,丧其资斧'] },
|
||||
'5,1': { name: '风天小畜', judgment: '小畜,柔得位而上下应之', image: '风行天上,小畜。君子以懿文德', lines: ['复自道,何其咎', '牵复,吉', '舆说辐,夫妻反目', '有孚,血去惕出', '有孚挛如,富以其邻', '既雨既处,尚德载'] },
|
||||
'5,3': { name: '风火家人', judgment: '家人,女正位乎内,男正位乎外', image: '风自火出,家人。君子以言有物而行有恒', lines: ['闲有家,悔亡', '无攸遂,在中馈', '家人嗃嗃,悔厉吉', '富家,大吉', '王假有家,勿恤', '有孚威如,终吉'] },
|
||||
'5,4': { name: '风雷益', judgment: '益,损上益下,民说无疆', image: '风雷,益。君子以见善则迁有过则改', lines: ['利用为大作,元吉', '或益之十朋之龟', '益之用凶事,无咎', '中行告公从,利用为依迁国', '有孚惠心,勿问元吉', '莫益之,或击之'] },
|
||||
'1,4': { name: '天雷无妄', judgment: '无妄,刚自外来而为主于内', image: '天下雷行,无妄。先王以茂对时育万物', lines: ['无妄,往吉', '不耕获,不菑畬', '无妄之灾,或系之牛', '可贞,无咎', '无妄之疾,勿药有喜', '无妄,行有眚'] },
|
||||
'3,4': { name: '火雷噬嗑', judgment: '噬嗑亨,利用狱。刚柔分动而明', image: '雷电噬嗑,先王以明罚敕法', lines: ['屦校灭趾,无咎', '噬肤灭鼻,无咎', '噬腊肉,遇毒', '噬干胏,得金矢', '噬干肉,得黄金', '何校灭耳,凶'] },
|
||||
'7,4': { name: '山雷颐', judgment: '颐,贞吉。观颐,自求口实', image: '山下有雷,颐。君子以慎言语节饮食', lines: ['舍尔灵龟,观我朵颐', '颠颐,拂经于丘颐', '拂颐,贞凶', '颠颐,吉', '拂经,居贞吉', '由颐,厉吉,利涉大川'] },
|
||||
'7,5': { name: '山风蛊', judgment: '蛊,元亨。利涉大川,先甲三日后甲三日', image: '山下有风,蛊。君子以振民育德', lines: ['干父之蛊,有子考无咎', '干母之蛊,不可贞', '干父之蛊,小有悔', '裕父之蛊,往见吝', '干父之蛊,用誉', '不事王侯,高尚其事'] },
|
||||
'3,3': { name: '离为火', judgment: '离,丽也。日月丽乎天,百谷草木丽乎土', image: '明两作,离。大人以继明照于四方', lines: ['履错然,敬之无咎', '黄离,元吉', '日昃之离,不鼓缶而歌', '突如其来如,焚如死如弃如', '出涕沱若,戚嗟若', '王用出征,有嘉折首'] },
|
||||
'3,7': { name: '火山旅', judgment: '旅,小亨。旅贞吉', image: '山上有火,旅。君子以明慎用刑而不留狱', lines: ['旅琐琐,斯其所取灾', '旅即次,怀其资', '旅焚其次,丧其童仆', '旅于处,得其资斧', '射雉,一矢亡', '鸟焚其巢,旅人先笑后号咷'] },
|
||||
'3,5': { name: '火风鼎', judgment: '鼎,象也。以木巽火,亨饪也', image: '木上有火,鼎。君子以正位凝命', lines: ['鼎颠趾,利出否', '鼎有实,我仇有疾', '鼎耳革,其行塞', '鼎折足,覆公餗', '鼎黄耳金铉,利贞', '鼎玉铉,大吉'] },
|
||||
'3,6': { name: '火水未济', judgment: '未济亨,小狐汔济,濡其尾', image: '火在水上,未济。君子以慎辨物居方', lines: ['濡其尾,吝', '曳其轮,贞吉', '未济,征凶', '贞吉,悔亡', '贞吉,无悔', '有孚于饮酒,无咎'] },
|
||||
'7,6': { name: '山水蒙', judgment: '蒙亨。匪我求童蒙,童蒙求我', image: '山下出泉,蒙。君子以果行育德', lines: ['发蒙,利用刑人', '包蒙,吉', '勿用取女,见金夫', '困蒙,吝', '童蒙,吉', '击蒙,不利为寇'] },
|
||||
'5,6': { name: '风水涣', judgment: '涣亨。王假有庙,利涉大川', image: '风行水上,涣。先王以享于帝立庙', lines: ['用拯马壮,吉', '涣奔其机,悔亡', '涣其躬,无悔', '涣其群,元吉', '涣汗其大号', '涣其血,去逖出'] },
|
||||
'1,6': { name: '天水讼', judgment: '讼,上刚下险,险而健,讼', image: '天与水违行,讼。君子以作事谋始', lines: ['不永所事,小有言', '不克讼,归而逋', '食旧德,贞厉终吉', '不克讼,复即命渝', '讼,元吉', '或锡之鞶带,终朝三褫'] },
|
||||
'1,3': { name: '天火同人', judgment: '同人,柔得位得中而应乎乾', image: '天与火,同人。君子以类族辨物', lines: ['同人于门,无咎', '同人于宗,吝', '伏戎于莽,升其高陵', '乘其墉,弗克攻', '同人先号咷而后笑', '同人于郊,无悔'] },
|
||||
'8,8': { name: '坤为地', judgment: '至哉坤元,万物资生,乃顺承天', image: '地势坤,君子以厚德载物', lines: ['履霜,坚冰至', '直方大,不习无不利', '含章可贞,或从王事', '括囊,无咎无誉', '黄裳,元吉', '龙战于野,其血玄黄'] },
|
||||
'8,4': { name: '地雷复', judgment: '复亨。出入无疾,朋来无咎', image: '雷在地中,复。先王以至日闭关', lines: ['不远复,无祗悔', '休复,吉', '频复,厉', '中行独复', '敦复,无悔', '迷复,凶有灾眚'] },
|
||||
'8,2': { name: '地泽临', judgment: '临,刚浸而长,说而顺', image: '泽上有地,临。君子以教思无穷容保民无疆', lines: ['咸临,贞吉', '咸临,吉无不利', '甘临,无攸利', '至临,无咎', '知临,大君之宜', '敦临,吉无咎'] },
|
||||
'8,1': { name: '地天泰', judgment: '泰,小往大来,吉亨。天地交而万物通', image: '天地交,泰。后以财成天地之道', lines: ['拔茅茹,以其汇', '包荒,用冯河', '无平不陂,无往不复', '翩翩,不富以其邻', '帝乙归妹,以祉元吉', '城复于隍,勿用师'] },
|
||||
'4,1': { name: '雷天大壮', judgment: '大壮,大者壮也。刚以动,故壮', image: '雷在天上,大壮。君子以非礼勿履', lines: ['壮于趾,征凶', '贞吉', '小人用壮,君子用罔', '贞吉,悔亡', '丧羊于易,无悔', '羝羊触藩,不能退'] },
|
||||
'2,1': { name: '泽天夬', judgment: '夬,决也,刚决柔也。健而说,决而和', image: '泽上于天,夬。君子以施禄及下', lines: ['壮于前趾,往不胜为咎', '惕号,莫夜有戎', '壮于頄,有凶', '臀无肤,其行次且', '苋陆夬夬,中行无咎', '无号,终有凶'] },
|
||||
'6,1': { name: '水天需', judgment: '需,须也。险在前也,刚健而不陷', image: '云上于天,需。君子以饮食宴乐', lines: ['需于郊,利用恒', '需于沙,小有言', '需于泥,致寇至', '需于血,出自穴', '需于酒食,贞吉', '入于穴,有不速之客三人来'] },
|
||||
'6,8': { name: '水地比', judgment: '比,吉也。比,辅也,下顺从也', image: '地上有水,比。先王以建万国亲诸侯', lines: ['有孚比之,无咎', '比之自内,贞吉', '比之匪人', '外比之,贞吉', '显比,王用三驱', '比之无首,凶'] },
|
||||
'2,2': { name: '兑为泽', judgment: '兑,说也。刚中而柔外,说以利贞', image: '丽泽,兑。君子以朋友讲习', lines: ['和兑,吉', '孚兑,吉', '来兑,凶', '商兑未宁,介疾有喜', '孚于剥,有厉', '引兑'] },
|
||||
'2,6': { name: '泽水困', judgment: '困,刚掩也。险以说,困而不失其所', image: '泽无水,困。君子以致命遂志', lines: ['臀困于株木,入于幽谷', '困于酒食,朱绂方来', '困于石,据于蒺藜', '来徐徐,困于金车', '劓刖,困于赤绂', '困于葛藟,于臲兀'] },
|
||||
'2,8': { name: '泽地萃', judgment: '萃,聚也。顺以说,刚中而应', image: '泽上于地,萃。君子以除戎器戒不虞', lines: ['有孚不终,乃乱乃萃', '引吉,无咎', '萃如嗟如,无攸利', '大吉,无咎', '萃有位,无咎', '赍咨涕洟,无咎'] },
|
||||
'2,7': { name: '泽山咸', judgment: '咸,感也。柔上而刚下,二气感应以相与', image: '山上有泽,咸。君子以虚受人', lines: ['咸其拇', '咸其腓,凶', '咸其股,执其随', '贞吉,悔亡', '咸其脢,无悔', '咸其辅颊舌'] },
|
||||
'6,7': { name: '水山蹇', judgment: '蹇,难也,险在前也。见险而能止', image: '山上有水,蹇。君子以反身修德', lines: ['往蹇,来誉', '王臣蹇蹇,匪躬之故', '往蹇,来反', '往蹇,来连', '大蹇,朋来', '往蹇,来硕'] },
|
||||
'8,7': { name: '地山谦', judgment: '谦亨。天道下济而光明,地道卑而上行', image: '地中有山,谦。君子以裒多益寡称物平施', lines: ['谦谦君子,用涉大川', '鸣谦,贞吉', '劳谦君子,有终吉', '无不利,撝谦', '不富以其邻,利用侵伐', '鸣谦,利用行师'] },
|
||||
'4,7': { name: '雷山小过', judgment: '小过,小者过而亨也。过以利贞', image: '山上有雷,小过。君子以行过乎恭', lines: ['飞鸟以凶', '过其祖,遇其妣', '弗过防之,从或戕之', '无咎,弗过遇之', '密云不雨,自我西郊', '弗遇过之,飞鸟离之'] },
|
||||
'4,2': { name: '雷泽归妹', judgment: '归妹,天地之大义也。天地不交而万物不兴', image: '泽上有雷,归妹。君子以永终知敝', lines: ['归妹以娣,跛能履', '眇能视,利幽人之贞', '归妹以须,反归以娣', '归妹愆期,迟归有时', '帝乙归妹,其君之袂', '女承筐无实,士刲羊无血'] }
|
||||
};
|
||||
|
||||
/** 称骨年表 */
|
||||
const YEAR_WEIGHTS = {
|
||||
0: 12, 1: 9, 2: 6, 3: 7, 4: 12, 5: 5, 6: 9, 7: 8, 8: 7, 9: 8,
|
||||
10: 15, 11: 9, 12: 16, 13: 8, 14: 8, 15: 19, 16: 12, 17: 6, 18: 8, 19: 7,
|
||||
20: 5, 21: 15, 22: 6, 23: 16, 24: 15, 25: 7, 26: 9, 27: 12, 28: 10, 29: 7,
|
||||
30: 15, 31: 6, 32: 5, 33: 14, 34: 14, 35: 9, 36: 7, 37: 7, 38: 9, 39: 12,
|
||||
40: 8, 41: 7, 42: 13, 43: 5, 44: 14, 45: 5, 46: 9, 47: 17, 48: 5, 49: 7,
|
||||
50: 12, 51: 8, 52: 8, 53: 6, 54: 19, 55: 6, 56: 8, 57: 16, 58: 10, 59: 6
|
||||
};
|
||||
/** 称骨月表 */
|
||||
const MONTH_WEIGHTS = { 1: 6, 2: 7, 3: 18, 4: 9, 5: 5, 6: 16, 7: 9, 8: 15, 9: 18, 10: 8, 11: 9, 12: 5 };
|
||||
/** 称骨日表 */
|
||||
const DAY_WEIGHTS = {
|
||||
1: 5, 2: 10, 3: 8, 4: 15, 5: 16, 6: 15, 7: 8, 8: 16, 9: 8, 10: 16,
|
||||
11: 9, 12: 17, 13: 8, 14: 17, 15: 10, 16: 8, 17: 9, 18: 18, 19: 5, 20: 15,
|
||||
21: 10, 22: 9, 23: 8, 24: 9, 25: 15, 26: 18, 27: 7, 28: 8, 29: 16, 30: 6
|
||||
};
|
||||
/** 称骨时表 */
|
||||
const HOUR_WEIGHTS = { 0: 16, 1: 6, 2: 7, 3: 10, 4: 9, 5: 16, 6: 10, 7: 8, 8: 8, 9: 9, 10: 6, 11: 6 };
|
||||
/** 称骨歌诀 */
|
||||
const BONE_INTERPRETATIONS = {
|
||||
21: { text: '短命非业谓大凶,平生灾难事重重,凶祸频临陷逆境,终世困苦事不成。', fortune: '下' },
|
||||
22: { text: '身寒骨冷苦伶仃,此命推来行乞人,劳劳碌碌无度日,终年打拱过平生。', fortune: '下' },
|
||||
23: { text: '此命推来骨格轻,求谋作事事难成,妻儿兄弟应难许,别处他乡作散人。', fortune: '下' },
|
||||
24: { text: '此命推来福禄无,门庭困苦总难荣,六亲骨肉皆无靠,流浪他乡作老翁。', fortune: '下' },
|
||||
25: { text: '此命推来祖业微,门庭营度似稀奇,六亲骨肉如冰炭,一世勤劳自把持。', fortune: '中下' },
|
||||
26: { text: '平生衣禄苦中求,独自营谋事不休,离祖出门宜早计,晚来衣禄自无休。', fortune: '中下' },
|
||||
27: { text: '一生作事少商量,难靠祖宗怎主张,独马单枪空做去,早年晚岁总无长。', fortune: '中下' },
|
||||
28: { text: '一生行事似飘蓬,祖宗产业在梦中,若不过房改名姓,也当移徒二三通。', fortune: '中下' },
|
||||
29: { text: '初年运限未曾亨,纵有功名在后成,须过四旬才可立,移居改姓始为良。', fortune: '中' },
|
||||
30: { text: '劳劳碌碌苦中求,东奔西走何日休,若使终身勤与俭,老来稍可免忧愁。', fortune: '中' },
|
||||
31: { text: '忙忙碌碌苦中求,何日云开见日头,难得祖基家可立,中年衣食渐无忧。', fortune: '中' },
|
||||
32: { text: '初年运蹇事难谋,渐有财源如水流,到得中年衣食旺,那时名利一齐收。', fortune: '中上' },
|
||||
33: { text: '早年做事事难成,百年勤劳枉费心,半世自如流水去,后来运到始得金。', fortune: '中上' },
|
||||
34: { text: '此命福气果如何,僧道门中衣禄多,离祖出家方为妙,朝晚拜佛念弥陀。', fortune: '中上' },
|
||||
35: { text: '生平福量不周全,祖业根基觉少传,营事生涯宜守旧,时来衣食胜从前。', fortune: '中' },
|
||||
36: { text: '不须劳碌过平生,独自成家福不轻,早有福星常照命,任君行去百般成。', fortune: '上' },
|
||||
37: { text: '此命般般事不成,弟兄少力自孤行,虽然祖业须微有,来得明时去不明。', fortune: '中下' },
|
||||
38: { text: '一身骨肉最清高,早入簧门姓氏标,待到年将三十六,蓝衫脱去换红袍。', fortune: '上' },
|
||||
39: { text: '此命终身运不通,劳劳作事尽皆空,苦心竭力成家计,到得那时在梦中。', fortune: '中下' },
|
||||
40: { text: '平生衣禄是绵长,件件心中自主张,前面风霜多受过,后来必定享安康。', fortune: '上' },
|
||||
41: { text: '此命推来自不同,为人能干异凡庸,中年还有逍遥福,不比前时运未通。', fortune: '上' },
|
||||
42: { text: '得宽怀处且宽怀,何用双眉皱不开,若使中年命运济,那时名利一齐来。', fortune: '上' },
|
||||
43: { text: '为人心性最聪明,作事轩昂近贵人,衣禄一生天注定,不须劳碌是丰亨。', fortune: '上上' },
|
||||
44: { text: '万事由天莫苦求,须知福碌赖人修,当年财帛难如意,晚景欣然便不忧。', fortune: '中上' },
|
||||
45: { text: '名利推求竟若何,前番辛苦后奔波,命中难养男和女,骨肉扶持也不多。', fortune: '中' },
|
||||
46: { text: '东西南北尽皆通,出姓移居更觉隆,衣禄无穷无数定,中年晚景一般同。', fortune: '上' },
|
||||
47: { text: '此命推求旺末年,妻荣子贵自怡然,平生原有滔滔福,可卜财源若水泉。', fortune: '上' },
|
||||
48: { text: '初年运道未曾通,几许蹉跎命亦穷,兄弟六亲无依靠,一生事业晚来整。', fortune: '中' },
|
||||
49: { text: '此命推来福不轻,自成自立显门庭,从来富贵人钦敬,使婢差奴过一生。', fortune: '上' },
|
||||
50: { text: '为利为名终日劳,中年福禄也多遭,老来自有财星照,不比前番目下高。', fortune: '上' },
|
||||
51: { text: '一世荣华事事通,不须劳碌自亨通,兄弟叔侄皆如意,家业成时福禄宏。', fortune: '上' },
|
||||
52: { text: '一世亨通事事能,不须劳苦自然宁,宗族有光欣喜甚,家产丰盈自称心。', fortune: '上' },
|
||||
53: { text: '此格推来福泽宏,兴家立业在其中,一生衣食安排定,却是人间一福翁。', fortune: '上' },
|
||||
54: { text: '此命推来厚且清,诗书满腹看功成,丰衣足食自然稳,正是人间有福人。', fortune: '上上' },
|
||||
55: { text: '走马扬鞭争利名,少年作事费筹论,一朝福禄源源至,富贵荣华显六亲。', fortune: '上' },
|
||||
56: { text: '此格推来礼义通,一身福禄用无穷,甜酸苦辣皆尝过,滚滚财源稳且丰。', fortune: '上' },
|
||||
57: { text: '福禄丰盈万事全,一身荣耀乐天年,名扬威震人争羡,此世逍遥宛似仙。', fortune: '上上' },
|
||||
58: { text: '平生福禄自然来,名利双全福禄偕,雁塔题名为贵客,紫袍玉带走金阶。', fortune: '上' },
|
||||
59: { text: '细推此格妙且清,必定财高礼义通,甲第之中应有分,扬鞭走马显威荣。', fortune: '上' },
|
||||
60: { text: '一朝金榜快题名,显祖荣宗大器成,衣禄定然无欠缺,田园财帛更丰盈。', fortune: '上上' },
|
||||
61: { text: '不作朝中金榜客,定为世上一财翁,聪明天赋经书熟,名显高科自是荣。', fortune: '上上' },
|
||||
62: { text: '此命生来福不穷,读书必定显亲宗,紫衣玉带为卿相,富贵荣华孰与同。', fortune: '上上' },
|
||||
63: { text: '命主为官福禄长,得来富贵定非常,名题雁塔传金榜,定中高科天下扬。', fortune: '上上' },
|
||||
64: { text: '此命生成福不轻,读书必定有功名,果然富贵前生定,一世荣华事事成。', fortune: '上上' },
|
||||
65: { text: '细推此命福不轻,安国安邦极品人,文纷雕梁徽富贵,威声照耀四方闻。', fortune: '上上' },
|
||||
66: { text: '此命推来福且宏,荣华富贵自然通,命中注定衣禄足,一世亨通稳且丰。', fortune: '上上' },
|
||||
67: { text: '此命生来福自宏,田园家业最高隆,平生衣禄丰盈足,一世荣华万事通。', fortune: '上上' },
|
||||
68: { text: '富贵荣华莫强求,强求不出反成羞,有福之人还自至,无福之人反成忧。', fortune: '中' },
|
||||
69: { text: '君是人间前禄星,一生富贵众人钦,纵然福禄由天定,安享荣华过一生。', fortune: '上上' },
|
||||
70: { text: '此命推来福禄宏,不须劳碌过平生,妻儿和顺皆如意,家道兴隆福自成。', fortune: '上' },
|
||||
71: { text: '此命生来大不同,公侯卿相在其中,一生自有逍遥福,富贵荣华极品隆。', fortune: '上上' },
|
||||
72: { text: '此命生来福泽长,兴家立业有祯祥,一生自有逍遥福,富贵荣华极品良。', fortune: '上上' }
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
LUNAR_INFO,
|
||||
HEAVEN_STEMS,
|
||||
EARTH_BRANCHES,
|
||||
ZODIACS,
|
||||
LUNAR_MONTH_NAMES,
|
||||
LUNAR_DAY_NAMES,
|
||||
WEEK_NAMES,
|
||||
CONSTELLATIONS,
|
||||
CONSTELLATION_DATES,
|
||||
SOLAR_TERMS,
|
||||
SOLAR_TERM_DATES,
|
||||
SOLAR_FESTIVALS,
|
||||
LUNAR_FESTIVALS,
|
||||
BUDDHIST_FESTIVALS,
|
||||
NAYIN_TABLE,
|
||||
HOUR_NAMES,
|
||||
HOUR_RANGES,
|
||||
DUTY_NAMES,
|
||||
TWELVE_STARS,
|
||||
TWENTY_EIGHT_STARS,
|
||||
NINE_STARS,
|
||||
SIX_STARS,
|
||||
PENG_ZU,
|
||||
ELEMENTS,
|
||||
STEM_ELEMENTS,
|
||||
BRANCH_ELEMENTS,
|
||||
STEM_YINYANG,
|
||||
BRANCH_YINYANG,
|
||||
HIDE_STEMS,
|
||||
TERRAINS,
|
||||
TEN_STARS,
|
||||
BRANCH_COMBINE,
|
||||
BRANCH_OPPOSITE,
|
||||
BRANCH_HARM,
|
||||
BRANCH_PUNISH,
|
||||
THREE_COMBINES,
|
||||
STEM_COMBINE,
|
||||
STEM_OPPOSITE,
|
||||
GENERATES,
|
||||
KILLS,
|
||||
TRIGRAMS,
|
||||
HEXAGRAMS,
|
||||
YEAR_WEIGHTS,
|
||||
MONTH_WEIGHTS,
|
||||
DAY_WEIGHTS,
|
||||
HOUR_WEIGHTS,
|
||||
BONE_INTERPRETATIONS
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
*.tsbuildinfo
|
||||
*.local
|
||||
.DS_Store
|
||||
*.log
|
||||
coverage
|
||||
.swc
|
||||
@@ -0,0 +1,72 @@
|
||||
# Lunar Calendar App (万年历)
|
||||
|
||||
Chinese almanac/perpetual calendar app with Bazi calculation, daily fortune, divination tools, and beautiful UI.
|
||||
|
||||
## Documentation
|
||||
|
||||
Detailed docs live in `docs/` — read them before starting any task:
|
||||
|
||||
- **docs/ARCHITECTURE.md** — tech stack, layered design, API surface, routes, stores
|
||||
- **docs/REQUIREMENTS.md** — full feature inventory with completion status
|
||||
- **docs/PROGRESS.md** — milestones, current state, prioritized backlog
|
||||
- **docs/BUGS.md** — known bugs, algorithmic approximations, dead code
|
||||
- **docs/CHANGELOG.md** — version history
|
||||
|
||||
## Tech Stack
|
||||
- **Monorepo**: pnpm workspaces
|
||||
- **Core**: TypeScript + tyme4ts (calendar engine)
|
||||
- **Web**: React 19 + Vite + TailwindCSS v4 + Framer Motion + Zustand + React Router v7
|
||||
- **PWA**: vite-plugin-pwa (configured and built — autoUpdate + Workbox)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
lunar/
|
||||
├── docs/ # Architecture / Requirements / Progress / Bugs / Changelog
|
||||
├── packages/
|
||||
│ ├── core/ # @lunar/core — Pure TS, no UI deps
|
||||
│ │ └── src/
|
||||
│ │ ├── types/ # calendar.ts, bazi.ts, almanac.ts, fortune.ts
|
||||
│ │ ├── transformers/ # tyme4ts → plain objects (day.ts, bazi.ts, almanac.ts)
|
||||
│ │ └── calculators/ # dailyMatch.ts, relationship.ts, elementStrength.ts, plumBlossom.ts, boneWeight.ts
|
||||
│ └── web/ # @lunar/web — React app
|
||||
│ └── src/
|
||||
│ ├── stores/ # Zustand: calendar, user, settings, ui, bookmarks
|
||||
│ ├── hooks/ # useCalendar, useDayDetail, useBazi, useDailyFortune
|
||||
│ ├── components/ # calendar/, layout/, ui/
|
||||
│ └── pages/ # Home, Calendar, DayDetail, Bazi, DailyFortune, Settings, Divination, SolarTerms
|
||||
```
|
||||
|
||||
## Key Architecture Principles
|
||||
1. **tyme4ts objects never enter React** — all transformed to plain JS objects in @lunar/core
|
||||
2. **@lunar/core has zero UI deps** — works in any JS runtime (web, Node, RN, mini-program)
|
||||
3. **Mobile-first responsive** — phone first, desktop sidebar layout
|
||||
4. **Dark mode via CSS custom properties** — theme toggle with system preference detection
|
||||
|
||||
## Commands
|
||||
- `pnpm dev` — Start Vite dev server (port 4258)
|
||||
- `pnpm build` — Build core (ESM+CJS) + web
|
||||
- `pnpm --filter @lunar/core build` — Build core only (tsup)
|
||||
- `pnpm --filter @lunar/web dev` — Start web dev server
|
||||
- `pnpm preview` — Preview production build
|
||||
- `pnpm test` — Run core unit tests (vitest)
|
||||
- `pnpm lint` — Run ESLint on all packages
|
||||
|
||||
## Core API (from @lunar/core)
|
||||
- `getDayInfo(year, month, day)` → DayInfo
|
||||
- `getTodayInfo()` → DayInfo
|
||||
- `getMonthCalendar(year, month, weekStart?)` → DayInfo[][]
|
||||
- `getAlmanacInfo(year, month, day)` → AlmanacInfo
|
||||
- `birthInfoToBazi(params)` → BaziFullResult
|
||||
- `calculateDailyFortune(userBazi, date)` → DailyFortuneResult
|
||||
- `analyzeElementBalance(bazi)` → ElementProfile
|
||||
- `calculatePlumBlossom(y, m, d, hour?)` → PlumBlossomResult
|
||||
- `calculateBoneWeight(...)` → BoneWeightResult
|
||||
- `getBranchRelationship(a, b)` / `getTenStarRelationship(s, o)` / `checkStemCombine` / `checkStemOpposite`
|
||||
- `getBuddhistFestival(lunarMonth, lunarDay)` → string | null
|
||||
- `analyzeShensha(bazi)` → ShenshaInfo[](22 个常见神煞)
|
||||
- `analyzeFortuneGanzhi(ganzhi, dayStem, dayBranch)` → FortuneLuck(大运/流年 vs 日主生克冲合)
|
||||
- `getYearMonths(year)` → YearMonthInfo[](按节气月)
|
||||
|
||||
## Known Issues
|
||||
See **docs/BUGS.md** for the current list. Resolved: core CJS export, week-start setting, PWA manifest metadata, tests (50), lint, 称骨 data tables, 八字流派 switching, shensha, 大运流年 explorer. Remaining: simplified 互卦 algorithm, dead code cleanup.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Lunar Calendar App (万年历)
|
||||
|
||||
Chinese almanac/perpetual calendar app with Bazi calculation, daily fortune, divination tools, and beautiful UI.
|
||||
|
||||
## Documentation
|
||||
|
||||
Detailed docs live in `docs/` — read them before starting any task:
|
||||
|
||||
- **docs/ARCHITECTURE.md** — tech stack, layered design, API surface, routes, stores
|
||||
- **docs/REQUIREMENTS.md** — full feature inventory with completion status
|
||||
- **docs/PROGRESS.md** — milestones, current state, prioritized backlog
|
||||
- **docs/BUGS.md** — known bugs, algorithmic approximations, dead code
|
||||
- **docs/CHANGELOG.md** — version history
|
||||
|
||||
## Tech Stack
|
||||
- **Monorepo**: pnpm workspaces
|
||||
- **Core**: TypeScript + tyme4ts (calendar engine)
|
||||
- **Web**: React 19 + Vite + TailwindCSS v4 + Framer Motion + Zustand + React Router v7
|
||||
- **PWA**: vite-plugin-pwa (configured and built — autoUpdate + Workbox)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
lunar/
|
||||
├── docs/ # Architecture / Requirements / Progress / Bugs / Changelog
|
||||
├── packages/
|
||||
│ ├── core/ # @lunar/core — Pure TS, no UI deps
|
||||
│ │ └── src/
|
||||
│ │ ├── types/ # calendar.ts, bazi.ts, almanac.ts, fortune.ts
|
||||
│ │ ├── transformers/ # tyme4ts → plain objects (day.ts, bazi.ts, almanac.ts)
|
||||
│ │ └── calculators/ # dailyMatch.ts, relationship.ts, elementStrength.ts, plumBlossom.ts, boneWeight.ts
|
||||
│ └── web/ # @lunar/web — React app
|
||||
│ └── src/
|
||||
│ ├── stores/ # Zustand: calendar, user, settings, ui, bookmarks
|
||||
│ ├── hooks/ # useCalendar, useDayDetail, useBazi, useDailyFortune
|
||||
│ ├── components/ # calendar/, layout/, ui/
|
||||
│ └── pages/ # Home, Calendar, DayDetail, Bazi, DailyFortune, Settings, Divination, SolarTerms
|
||||
```
|
||||
|
||||
## Key Architecture Principles
|
||||
1. **tyme4ts objects never enter React** — all transformed to plain JS objects in @lunar/core
|
||||
2. **@lunar/core has zero UI deps** — works in any JS runtime (web, Node, RN, mini-program)
|
||||
3. **Mobile-first responsive** — phone first, desktop sidebar layout
|
||||
4. **Dark mode via CSS custom properties** — theme toggle with system preference detection
|
||||
|
||||
## Commands
|
||||
- `pnpm dev` — Start Vite dev server (port 4258)
|
||||
- `pnpm build` — Build core (ESM+CJS) + web
|
||||
- `pnpm --filter @lunar/core build` — Build core only (tsup)
|
||||
- `pnpm --filter @lunar/web dev` — Start web dev server
|
||||
- `pnpm preview` — Preview production build
|
||||
- `pnpm test` — Run core unit tests (vitest)
|
||||
- `pnpm lint` — Run ESLint on all packages
|
||||
|
||||
## Core API (from @lunar/core)
|
||||
- `getDayInfo(year, month, day)` → DayInfo
|
||||
- `getTodayInfo()` → DayInfo
|
||||
- `getMonthCalendar(year, month, weekStart?)` → DayInfo[][]
|
||||
- `getAlmanacInfo(year, month, day)` → AlmanacInfo
|
||||
- `birthInfoToBazi(params)` → BaziFullResult
|
||||
- `calculateDailyFortune(userBazi, date)` → DailyFortuneResult
|
||||
- `analyzeElementBalance(bazi)` → ElementProfile
|
||||
- `calculatePlumBlossom(y, m, d, hour?)` → PlumBlossomResult
|
||||
- `calculateBoneWeight(...)` → BoneWeightResult
|
||||
- `getBranchRelationship(a, b)` / `getTenStarRelationship(s, o)` / `checkStemCombine` / `checkStemOpposite`
|
||||
- `getBuddhistFestival(lunarMonth, lunarDay)` → string | null
|
||||
- `analyzeShensha(bazi)` → ShenshaInfo[](22 个常见神煞)
|
||||
- `analyzeFortuneGanzhi(ganzhi, dayStem, dayBranch)` → FortuneLuck(大运/流年 vs 日主生克冲合)
|
||||
- `getYearMonths(year)` → YearMonthInfo[](按节气月)
|
||||
|
||||
## Known Issues
|
||||
See **docs/BUGS.md** for the current list. Resolved: core CJS export, week-start setting, PWA manifest metadata, tests (50), lint, 称骨 data tables, 八字流派 switching, shensha, 大运流年 explorer. Remaining: simplified 互卦 algorithm, dead code cleanup.
|
||||
@@ -0,0 +1,56 @@
|
||||
# 万年历(Lunar Calendar App)
|
||||
|
||||
中国农历 / 黄历应用:农历转换、黄历宜忌、八字排盘(大运流年流月流日)、每日运势、神煞、梅花易数、称骨算命、节气、佛教节日。移动端优先,支持 PWA 离线安装。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Monorepo**:pnpm workspaces
|
||||
- **@lunar/core**:TypeScript + [tyme4ts](https://github.com/6tail/tyme4ts)(历法引擎),零 UI 依赖,ESM+CJS 双产物
|
||||
- **@lunar/web**:React 19 + Vite 6 + TailwindCSS v4 + Zustand 5 + React Router v7 + Framer Motion + vite-plugin-pwa
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev # http://localhost:4258
|
||||
pnpm test # core 单元测试(vitest,50 例)
|
||||
pnpm lint # ESLint
|
||||
pnpm build # 构建 core + web(含 PWA)
|
||||
pnpm preview # 预览构建产物
|
||||
```
|
||||
|
||||
> 需要 Node ≥ 18、pnpm ≥ 9。
|
||||
|
||||
## 文档
|
||||
|
||||
详细文档见 `docs/`:
|
||||
|
||||
- [ARCHITECTURE.md](docs/ARCHITECTURE.md) — 架构、API、路由、状态管理
|
||||
- [REQUIREMENTS.md](docs/REQUIREMENTS.md) — 功能点清单与完成状态
|
||||
- [PROGRESS.md](docs/PROGRESS.md) — 里程碑与待办
|
||||
- [BUGS.md](docs/BUGS.md) — 已知 BUG、技术债、死代码
|
||||
- [CHANGELOG.md](docs/CHANGELOG.md) — 变更日志
|
||||
- [RETROSPECT.md](docs/RETROSPECT.md) — 会话回顾(做了什么/没做什么/疑问)
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
lunar/
|
||||
├── docs/ # 项目文档
|
||||
├── packages/
|
||||
│ ├── core/ # @lunar/core —— 纯 TS 计算引擎
|
||||
│ └── web/ # @lunar/web —— React 应用
|
||||
├── AGENTS.md / CLAUDE.md # 开发代理指引
|
||||
└── eslint.config.js # ESLint flat config
|
||||
```
|
||||
|
||||
## 核心能力
|
||||
|
||||
| 模块 | 说明 |
|
||||
|---|---|
|
||||
| 历法 | 公历/农历/干支/节气/季节/儒略日/佛历/伊斯兰历/月相 |
|
||||
| 黄历 | 宜忌、值神(日/时)、二十八宿、彭祖百忌、胎神、冲煞 |
|
||||
| 八字 | 四柱、十神、藏干、十二长生、大运、流年、神煞、流派切换 |
|
||||
| 运势 | 每日运势、流年/流月/流日 vs 日主生克冲合 |
|
||||
| 占卜 | 梅花易数(64 卦)、袁天罡称骨 |
|
||||
| 其他 | 佛教节日、节气列表、收藏、PWA |
|
||||
@@ -0,0 +1,162 @@
|
||||
# 架构文档(Architecture)
|
||||
|
||||
> 最后更新:2026-08-03
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
万年历(Lunar Calendar App):中国农历/黄历应用,提供农历转换、黄历宜忌、八字排盘、每日运势、梅花易数、称骨算命、节气等命理功能,移动端优先,支持 PWA 离线安装。
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|---|---|
|
||||
| Monorepo | pnpm workspaces(`packages/*`) |
|
||||
| 核心引擎 | TypeScript + [tyme4ts](https://github.com/6tail/tyme4ts) ^1.5.1(历法/干支计算) |
|
||||
| 构建 | tsup(core)、Vite 6 + tsc(web) |
|
||||
| UI | React 19、React Router v7、Zustand 5、TailwindCSS v4(CSS-first)、Framer Motion、lucide-react |
|
||||
| PWA | vite-plugin-pwa 0.21(Workbox,已启用) |
|
||||
|
||||
## 3. 目录结构
|
||||
|
||||
```
|
||||
lunar/
|
||||
├── docs/ # 项目文档(本目录,含 RETROSPECT 会话回顾)
|
||||
├── AGENTS.md # 开发代理指引
|
||||
├── package.json # workspace 聚合脚本
|
||||
├── pnpm-workspace.yaml # 工作区定义
|
||||
├── tsconfig.base.json # 共享 TS 配置
|
||||
└── packages/
|
||||
├── core/ # @lunar/core —— 纯 TS 计算引擎,零 UI 依赖
|
||||
│ ├── tsup.config.ts
|
||||
│ └── src/
|
||||
│ ├── index.ts # 公共 API 出口(barrel)
|
||||
│ ├── types/ # calendar.ts / bazi.ts / almanac.ts / fortune.ts
|
||||
│ ├── transformers/ # day.ts / bazi.ts / almanac.ts / yearMonths.ts(tyme4ts → 纯对象)
|
||||
│ └── calculators/ # dailyMatch / relationship / elementStrength / plumBlossom / boneWeight / buddhistDates / shensha / fortuneLuck
|
||||
└── web/ # @lunar/web —— React 应用
|
||||
├── vite.config.ts # Vite + Tailwind v4 + PWA
|
||||
├── index.html
|
||||
└── src/
|
||||
├── App.tsx # 路由 + 懒加载 + 页面过渡 + ErrorBoundary
|
||||
├── main.tsx
|
||||
├── styles/globals.css # 设计令牌(亮/暗主题)
|
||||
├── lib/utils.ts # cn、日期格式化等工具
|
||||
├── stores/ # calendar / user / settings / ui / bookmarks(Zustand)
|
||||
├── hooks/ # useCalendar / useDayDetail / useBazi / useDailyFortune
|
||||
├── components/
|
||||
│ ├── calendar/ # CalendarGrid / CalendarCell / MonthYearPicker / WeekDayBar
|
||||
│ ├── layout/ # AppShell / Header / BottomNav
|
||||
│ ├── ui/ # Badge / Button / Card / AnimatedPanel / ErrorBoundary / Skeleton
|
||||
│ ├── day-detail/ bazi/ daily-fortune/(页面内联实现,目录内暂无独立组件)
|
||||
└── pages/ # 8 个页面(见路由表)
|
||||
```
|
||||
|
||||
## 4. 分层设计
|
||||
|
||||
```
|
||||
┌───────────────────────────────┐
|
||||
│ @lunar/web (React 页面层) │
|
||||
│ pages → hooks → stores │
|
||||
├───────────────────────────────┤
|
||||
│ @lunar/core (纯计算引擎) │
|
||||
│ index.ts │
|
||||
│ ├─ transformers tyme4ts → │
|
||||
│ │ 纯 JS 对象 │
|
||||
│ ├─ calculators 业务算法 │
|
||||
│ └─ types 类型定义 │
|
||||
├───────────────────────────────┤
|
||||
│ tyme4ts (历法计算源) │
|
||||
└───────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.1 核心原则
|
||||
|
||||
1. **tyme4ts 对象永不进入 React**:所有历法对象在 `@lunar/core` 内转换为普通 JS 对象(`DayInfo`、`AlmanacInfo`、`BaziFullResult` 等),React 层只消费纯数据。
|
||||
2. **@lunar/core 零 UI 依赖**:仅依赖 tyme4ts,可在任意 JS 运行时运行(web / Node / RN / 小程序)。
|
||||
3. **移动端优先**:手机优先布局,桌面端自适应。
|
||||
4. **暗色模式**:CSS 自定义属性 + `.dark` class,跟随系统偏好 + 手动切换。
|
||||
|
||||
### 4.2 数据流示例
|
||||
|
||||
```
|
||||
SolarDay (tyme4ts)
|
||||
→ solarDayToDayInfo() # DayInfo
|
||||
→ solarDayToAlmanacInfo() # AlmanacInfo(含 12 时辰)
|
||||
→ birthInfoToBazi() # BaziFullResult(四柱/十神/大运…)
|
||||
→ calculateDailyFortune() # DailyFortuneResult(每日运势)
|
||||
```
|
||||
|
||||
## 5. @lunar/core API 一览
|
||||
|
||||
### 类型
|
||||
`DayInfo`、`AlmanacInfo`、`HourAlmanac`、`PillarInfo`、`HideStemInfo`、`EightCharInfo`、`DecadeFortuneInfo`、`FortuneInfo`、`ChildLimitInfo`、`BaziFullResult`、`PillarRelationship`、`DailyFortuneResult`、`BirthParams`、`BranchRelationship`、`ElementProfile`、`TrigramInfo`、`HexagramInfo`、`PlumBlossomResult`、`BoneWeightResult`
|
||||
|
||||
### 函数
|
||||
|
||||
| 函数 | 说明 |
|
||||
|---|---|
|
||||
| `solarDayToDayInfo(solarDay)` | SolarDay → DayInfo(含季节/节气进度/儒略日/佛历/伊斯兰历/佛教节日) |
|
||||
| `getDayInfo(y, m, d)` / `getTodayInfo()` | 获取某日/今日信息 |
|
||||
| `getMonthCalendar(y, m, weekStart?)` | 月历二维数组(周 × 天),可指定周起始 |
|
||||
| `solarDayToAlmanacInfo(solarDay)` | SolarDay → AlmanacInfo |
|
||||
| `getAlmanacInfo(y, m, d)` | 获取黄历信息(宜忌/值神/冲煞/时辰) |
|
||||
| `birthInfoToBazi(params)` | 出生信息 → 完整八字排盘(`ziSect` 流派参数:晚子时换日) |
|
||||
| `getYearMonths(year)` | 按节气月返回某年 12 个流月 |
|
||||
| `getBranchRelationship(a, b)` | 地支六合/三合/六冲/六害/相刑 |
|
||||
| `getTenStarRelationship(s, o)` / `checkStemCombine` / `checkStemOpposite` | 干支关系判断 |
|
||||
| `calculateDailyFortune(userBazi, date)` | 用户八字 × 日期 → 每日运势评分 |
|
||||
| `analyzeElementBalance(bazi)` | 五行力量分析 |
|
||||
| `calculatePlumBlossom(y, m, d, h?)` | 梅花易数起卦 |
|
||||
| `calculateBoneWeight(...)` | 袁天罡称骨算命 |
|
||||
| `getBuddhistFestival(lunarMonth, lunarDay)` | 农历佛教节日 |
|
||||
| `analyzeShensha(bazi)` | 22 个常见神煞 |
|
||||
| `analyzeFortuneGanzhi(ganzhi, dayStem, dayBranch)` | 大运/流年/流月/流日 vs 日主生克冲合 |
|
||||
|
||||
## 6. 前端状态管理(Zustand)
|
||||
|
||||
| Store | 持久化 Key | 职责 | 备注 |
|
||||
|---|---|---|---|
|
||||
| `calendar` | — | 视图日期 / 选中日期 / 周起始 | `weekStart`、`clearSelection` 当前未被使用 |
|
||||
| `user` | `lunar-user-profiles` | 出生档案(最多 3 个)、active 档案、八字结果 | |
|
||||
| `settings` | `lunar-settings` | 主题、周起始、显示开关、八字来源 | `showLunar` 等 4 个字段未被消费 |
|
||||
| `ui` | — | 侧栏 / 移动端 / 底部面板 | 多数 action 未被消费 |
|
||||
| `bookmarks` | `lunar-bookmarks` | 日期收藏(标记在日历格上) | `getByDate`/`getByLunarDate` 未使用 |
|
||||
|
||||
## 7. 路由(React Router v7,全部懒加载)
|
||||
|
||||
| 路径 | 页面 | 底部导航 |
|
||||
|---|---|---|
|
||||
| `/` | HomePage(今日概览) | ✔ |
|
||||
| `/calendar` | CalendarPage(月/周视图) | ✔ |
|
||||
| `/calendar/:date` | DayDetailPage(日详情) | — |
|
||||
| `/bazi` | BaziPage(八字排盘) | ✔ |
|
||||
| `/daily-fortune` | DailyFortunePage(每日运势) | ✔ |
|
||||
| `/settings` | SettingsPage(设置) | ✔ |
|
||||
| `/divination` | DivinationPage(梅花易数 + 称骨) | 首页快捷入口 |
|
||||
| `/solar-terms` | SolarTermsPage(节气) | 首页快捷入口 |
|
||||
| `*` | 重定向 `/` | — |
|
||||
|
||||
## 8. PWA(已启用)
|
||||
|
||||
- `vite-plugin-pwa`:autoUpdate 模式 + Workbox 预缓存 + google-fonts 运行时缓存
|
||||
- 构建产物:`manifest.webmanifest`、`sw.js`、`registerSW.js`
|
||||
- manifest 已与 index.html 对齐(`lang: zh-CN`、`theme_color: #FFFBF5`)
|
||||
|
||||
## 9. 构建与命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|---|---|
|
||||
| `pnpm dev` | 启动 web dev(端口 4258) |
|
||||
| `pnpm build` | 构建 core → web |
|
||||
| `pnpm --filter @lunar/core build` | 仅构建 core(tsup,ESM+CJS) |
|
||||
| `pnpm --filter @lunar/web build` | 仅构建 web(tsc -b && vite build) |
|
||||
| `pnpm preview` | 预览构建产物 |
|
||||
| `pnpm test` | core 单元测试(vitest,50 例) |
|
||||
| `pnpm lint` | ESLint(flat config + typescript-eslint) |
|
||||
| `pnpm clean` | 清理 dist |
|
||||
|
||||
## 10. 已知架构问题(详见 BUGS.md)
|
||||
|
||||
- 梅花易数互卦为简化实现(上下卦互换,非真·互卦 2-4/3-5 爻法)
|
||||
- 每日运势当日八字固定取午时;称骨极端总重仍取"最近值"
|
||||
- 若干死代码(未使用的 hooks / 组件 / store action,见 BUGS.md 清单)
|
||||
@@ -0,0 +1,64 @@
|
||||
# BUG 记录(Known Issues)
|
||||
|
||||
> 最后更新:2026-08-02
|
||||
> 严重度:🔴 高(影响功能/构建) · 🟠 中 · 🟡 低
|
||||
> 状态:⬜ 待修复 · 🔧 修复中 · ✅ 已修复
|
||||
|
||||
## 0. 已修复汇总(2026-08-02)
|
||||
|
||||
- BUG-01 core CJS 导出 · BUG-02 周起始设置 · BUG-05 lint · BUG-07 版本号 · BUG-08 死分支
|
||||
- BUG-03 PWA manifest 与 index.html 对齐(lang=zh-CN、theme_color 统一)
|
||||
- BUG-09 日历页"回到今天"按钮永不显示(条件恒为 false)
|
||||
- BUG-10 日详情页时辰列表重复 React key(早子/晚子均为"子",改用 ganzhi 作 key)
|
||||
- TECH-03 更正:梅花卦库实测 **64/64 完整**(原 review 报告"缺 8 卦"不属实),已添加测试锁定
|
||||
- TECH-06 以 `solarAdjusted` 类型安全字段替代 `(as any)._solarAdjusted`
|
||||
- TECH-04 称骨数据表整体修正(年表主流版本 + 日表初五 + 歌诀 21~72 补全)
|
||||
- TECH-05 占卜页称骨年索引改用出生年干支
|
||||
- 新增单元测试 50 个(vitest)与 ESLint 配置
|
||||
- 功能深化:八字流派/出生地全国化/海外时区/太阳时开关、神煞、大运流年流月流日交互、历法信息、佛教节日(见 CHANGELOG)
|
||||
|
||||
## 1. 已知 BUG
|
||||
|
||||
| ID | 严重度 | 状态 | 位置 | 描述 |
|
||||
|---|---|---|---|---|
|
||||
| BUG-01 | 🟠 | ✅ | `packages/core` | ~~exports.require 指向不存在的 dist/index.cjs~~ → tsup 改为 `format: ['esm','cjs']`,`dist/index.cjs` 已生成并通过 `require()` 验证 |
|
||||
| BUG-02 | 🟠 | ✅ | `useCalendar` / `CalendarGrid` / `WeekDayBar` / `CalendarPage` | ~~周一开始设置无效~~ → `settings.weekStartDay` 已贯通至 `getMonthCalendar(weekStart)` 与表头渲染 |
|
||||
| BUG-03 | 🟡 | ✅ | `vite.config.ts` / `index.html` | ~~PWA manifest lang=en、theme_color 不一致~~ → manifest 增加 `lang: zh-CN`,`theme_color` 统一为 `#FFFBF5`,与 index.html 一致 |
|
||||
| BUG-04 | 🟡 | ⬜ | `HomePage`(InfoCard) | `highlight` prop 传为布尔值,但渲染时当作 `border-primary/30` 样式类处理,语义不符(需确认意图) |
|
||||
| BUG-05 | 🟡 | ✅ | 根 `package.json` | ~~pnpm lint 失败~~ → 已配置 ESLint 9 flat config + typescript-eslint,两个包均通过 |
|
||||
| BUG-06 | 🟡 | ⬜ | `pnpm-workspace.yaml` | `allowBuilds` 不是 pnpm 标准字段(应为 `onlyBuiltDependencies`),疑似无效配置 |
|
||||
| BUG-07 | 🟡 | ✅ | `SettingsPage` 关于区 | ~~版本号 v0.2~~ → 与 package.json 统一为 v0.1.0 |
|
||||
| BUG-08 | 🟡 | ✅ | `transformers/day.ts` | ~~getMonthCalendar 死分支~~ → 已移除 if/else 相同调用 |
|
||||
| BUG-09 | 🟡 | ✅ | `CalendarPage.tsx` | ~~"回到今天"按钮永不显示~~ → 条件误用 `todaySummary.di.isToday`(恒为 true),改为基于 `viewDate` 判断是否正在查看今天;已在浏览器实测 |
|
||||
| BUG-10 | 🟡 | ✅ | `DayDetailPage.tsx` | ~~时辰列表重复 React key~~ → `getHours()` 返回 13 个时辰(早子 00:00 / 晚子 23:00 均为地支"子"),原用 `h.branch` 作 key 冲突,改为唯一的 `h.ganzhi`;已在浏览器实测无警告 |
|
||||
|
||||
## 2. 算法近似 / 技术债
|
||||
|
||||
> 非紧急,但应在后续迭代中改善。
|
||||
|
||||
| ID | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| TECH-01 | `calculators/dailyMatch.ts` | 当日八字固定取午时(`getHours()[6]`),无法体现时辰差异 |
|
||||
| TECH-02 | `calculators/plumBlossom.ts` | 互卦为简化实现(上下卦互换),非真·互卦(2-4/3-5 爻法) |
|
||||
| TECH-03 | `calculators/plumBlossom.ts` | ✅ 已核实:数据集 64/64 完整,`plumBlossom.test.ts` 含完整性校验用例 |
|
||||
| TECH-04 | `calculators/boneWeight.ts` | ✅ 已修复:歌诀补全 2两1钱~7两2钱(原缺 5两8钱/5两9钱/6两1钱/6两3钱/6两5钱/6两7钱/6两9钱/7两1钱,此前这些总重会错误地取"最近值");年表已按主流版本整体修正(原混用两套网络变体错约 40 处),日表修正初五重量 |
|
||||
| TECH-05 | `DivinationPage` | ✅ 已修复:称骨年索引改用出生年干支(`ec.yearPillar.ganzhi`),不再用日干支近似 |
|
||||
| TECH-06 | `types/bazi.ts` | ✅ 已修复:`BaziFullResult` 新增 `solarAdjusted?: boolean`,替代 `(as any)` 补丁 |
|
||||
| TECH-07 | `calculators/elementStrength.ts` | 五行推断按天干名字映射而非复用 tyme4ts 元素定义(可接受但存在重复逻辑) |
|
||||
| TECH-08 | `lib/utils.ts` | `getChineseDayName` / `getChineseMonthName` 未使用 |
|
||||
|
||||
## 3. 死代码清单
|
||||
|
||||
| 位置 | 内容 | 建议 |
|
||||
|---|---|---|
|
||||
| `hooks/useDayDetail.ts` | 从未被 import | 删除或接入 DayDetailPage |
|
||||
| `hooks/useBazi.ts` | 从未被 import(本轮仅修复了其 lint 错误) | 删除或接入 BaziPage |
|
||||
| `components/ui/AnimatedPanel.tsx` | 从未使用 | 删除或接入日详情弹层 |
|
||||
| `components/ui/Card.tsx` `CardHeader` | 未使用 | 删除导出 |
|
||||
| `components/ui/Skeleton.tsx` `CalendarSkeleton`/`BaziSkeleton` | 未使用 | 删除 |
|
||||
| `stores/ui.ts` | `toggleSideNav`/`closeSideNav`/`setActiveTab`/`openDayDetail`/`closeDayDetail` 及 `activeTab`/`showDayDetail` 未被消费 | 精简或接入 |
|
||||
| `stores/settings.ts` | `showLunar`/`showSolarTerm`/`showHoliday`/`eightCharProvider` 及对应 toggle 未被读取 | 接入设置页或删除 |
|
||||
| `stores/calendar.ts` | `weekStart`/`setWeekStart`/`clearSelection` 未使用(周起始现直接读 settings store) | 删除 |
|
||||
| `stores/bookmarks.ts` | `getByDate`/`getByLunarDate` 未使用 | 配合农历收藏规划 |
|
||||
| `lib/utils.ts` | `getChineseDayName`/`getChineseMonthName` | 删除 |
|
||||
| web 依赖 | `tailwind-variants` 从未 import | 移除依赖 |
|
||||
@@ -0,0 +1,80 @@
|
||||
# 变更日志(Changelog)
|
||||
|
||||
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。版本号遵循 SemVer。
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 文档
|
||||
- 新增 `docs/` 文档体系:架构(ARCHITECTURE)、需求(REQUIREMENTS)、进度(PROGRESS)、BUG(BUGS)、变更日志(CHANGELOG)
|
||||
- 更新 `AGENTS.md` / `CLAUDE.md`:补全新页面/Store/API、修正 PWA 状态、指向文档目录
|
||||
|
||||
### 功能
|
||||
- **八字流派切换**:`birthInfoToBazi` 新增 `ziSect` 参数,设置页新增"八字流派"选项(晚子时算次日 23点换日 / 晚子时算当日 0点换日),BaziPage 排盘实时生效;浏览器实测两种流派排出不同日柱/时柱
|
||||
- **出生时间选择器**:BaziPage 出生时间改为 时/分 下拉选择器(替代不对称的 −/+ 步进),时辰格可点击快速设置时辰
|
||||
- **出生地全国化 + 海外时区**:出生地列出全国 34 个省级城市(含省会经度真太阳时校正),新增"海外"模式按时区(UTC-12~+14,含半小时时区)将当地出生时间换算为北京时间(UTC+8)排盘;换算与真太阳时校正均按日期运算处理,支持跨日回退(浏览器实测:UTC-5 23:30 → 次日北京 12:30;乌鲁木齐 00:30 真太阳 → 前日 22:20)
|
||||
- **真太阳时校正开关**:设置页新增"真太阳时校正"开关(默认开启);关闭后中国出生按北京时间直接排盘,海外时区换算不受影响(浏览器实测:关闭时乌鲁木齐 00:30 排日柱己酉,开启时排戊申)
|
||||
- **宜忌完整显示**:引擎本就返回完整黄历宜忌(如 8/3 共 22 条宜),此前首页 `slice(0,6)` 截断导致显得比别的黄历站少;已放开并标注总数
|
||||
- **历法信息增强**:DayInfo 新增季节、所处节气/第几天/距下节气天数、儒略日、佛历年(公历+543)、伊斯兰历日期(tyme4ts 原生支持 Hijri),日详情页新增"历法信息"卡片
|
||||
- **佛教节日**:tyme4ts 无佛教日期支持,新增 `getBuddhistFestival`(农历 21 个重大佛教节日表),日详情页显示 🪷 徽章
|
||||
- **八字神煞**:新增 `analyzeShensha`(22 个常见神煞:天乙/天厨/文昌贵人、禄神、羊刃、金舆、天德/月德贵人、桃花、驿马、华盖、劫煞、亡神、将星、红鸾、天喜、孤辰、寡宿、天罗、地网、魁罡、阴差阳错),八字页新增"神煞"卡片;查法采用主流排盘工具版本(神煞版本差异已在文档注明)
|
||||
- **大运/流年生克冲合**:新增 `analyzeFortuneGanzhi`(任意干支 vs 日主:十神、五行生克 生我/我生/克我/我克/比和、天干合冲、地支六合三合冲害刑、吉凶分级);八字页"起运·大运"改为全量 10 个大运并附生克冲合,新增"流年(小运)"10 条卡片
|
||||
- **今日流年·流月·流日**:八字页新增卡片,用今日年/月/日干支 vs 日主展示生克冲合
|
||||
- **大运·流年·流月·流日交互下钻**:core 新增 `getYearMonths(year)`(按节气月返回 12 个流月);八字页改为交互浏览器——点选大运 → 流年(按年度切换)→ 流月 12 chips → 该月每日流日列表(带吉凶点与十神),点击日期跳转日详情页;默认定位到当前年份/月份
|
||||
|
||||
### 修复
|
||||
- **称骨算命数据表整体修正**:年份表改为主流通行版本(原混用两套网络变体错约 40 处);日表修正初五(1两6钱);歌诀补全 2两1钱~7两2钱全部 52 条(原缺 5两8钱~7两1钱共 8 条,此前这些总重错误取"最近值")
|
||||
- **占卜页称骨年索引**:改用出生年干支(`ec.yearPillar.ganzhi`),不再用日干支近似
|
||||
- **BUG-01**:core 包改为同时输出 ESM + CJS,`exports.require` 指向的 `dist/index.cjs` 现在真实存在并通过 `require()` 验证
|
||||
- **BUG-02**:周起始设置生效 —— `settings.weekStartDay` 贯通 `useCalendar` → `getMonthCalendar(weekStart)` → `WeekDayBar` 表头渲染
|
||||
- **BUG-03**:PWA manifest 对齐 —— `lang: zh-CN`,`theme_color` 统一为 `#FFFBF5`(与 index.html 一致)
|
||||
- **BUG-05**:配置 ESLint 9 flat config + typescript-eslint,`pnpm lint` 通过(修复 24 处代码问题)
|
||||
- **BUG-07**:设置页版本号统一为 0.1.0
|
||||
- **BUG-08**:移除 `getMonthCalendar` 中的死分支
|
||||
- **BUG-09**:日历页"回到今天"浮动按钮恢复正常 —— 原条件 `todaySummary.di.isToday` 恒为 true 导致按钮永不显示,改为基于 `viewDate` 判断(浏览器实测:非本月可见、点击返回本月)
|
||||
- **BUG-10**:日详情页时辰列表重复 key 修复 —— 早子/晚子地支均为"子",key 改用唯一的干支(`h.ganzhi`)
|
||||
- **TECH-06**:`BaziFullResult` 新增 `solarAdjusted?: boolean` 字段,替代 `(as any)._solarAdjusted` 非类型安全补丁
|
||||
|
||||
### 工程
|
||||
- 为 @lunar/core 引入 vitest 单元测试(7 个测试文件 / 29 个用例),覆盖历法转换、八字排盘、每日运势、梅花易数(含 64 卦完整性校验)、称骨、干支关系、五行分析
|
||||
- 根与子包新增 `test` / `lint` 脚本;根 package.json 标记 `"type": "module"`
|
||||
- 核实梅花卦库为 64/64 完整(此前 review 报告"缺 8 卦"不属实),并以测试固化
|
||||
|
||||
### 已知问题(见 docs/BUGS.md)
|
||||
- 梅花易数互卦仍为简化实现(上下卦互换)
|
||||
- 每日运势当日八字固定取午时;称骨年索引用日干支近似
|
||||
- PWA manifest `lang`/`theme_color` 不一致;死代码待清理
|
||||
|
||||
## [0.1.0] - 2026-06-07
|
||||
|
||||
首个可用版本(monorepo 初始化于 2026-06-06)。
|
||||
|
||||
### 新增 — @lunar/core
|
||||
|
||||
- 历法转换:`getDayInfo` / `getTodayInfo` / `getMonthCalendar` / `solarDayToDayInfo`
|
||||
- 黄历:`getAlmanacInfo` / `solarDayToAlmanacInfo`(含 12 时辰逐时黄历)
|
||||
- 八字:`birthInfoToBazi`(四柱、十神、藏干、十二长生、胎元胎息、命宫身宫、空亡、起运、10 大运、10 流年)
|
||||
- 干支关系:`getBranchRelationship` / `getTenStarRelationship` / `checkStemCombine` / `checkStemOpposite`
|
||||
- 五行分析:`analyzeElementBalance`
|
||||
- 每日运势:`calculateDailyFortune`(四柱打分、宜忌、幸运色/数/方位、分项得分)
|
||||
- 占卜:`calculatePlumBlossom`(梅花易数,56/64 卦)、`calculateBoneWeight`(袁天罡称骨)
|
||||
|
||||
### 新增 — @lunar/web
|
||||
|
||||
- 路由与骨架:8 页面懒加载、页面过渡动画、ErrorBoundary、Suspense 骨架屏
|
||||
- 首页:今日概览、宜忌、运势预览、快捷入口
|
||||
- 日历:月/周视图、年/月切换、收藏星标、运势点、滑动切月、日详情页(四柱/黄历/12 时辰/收藏)
|
||||
- 八字排盘页:档案管理(最多 3 个,localStorage 持久化)、排盘全览、称骨
|
||||
- 每日运势页:日期导航、评分与建议
|
||||
- 占卜页:梅花易数今日卦象、称骨
|
||||
- 节气页:全年 24 节气
|
||||
- 设置页:主题切换(亮/暗/跟随系统)、周起始、阴阳历转换、今日干支
|
||||
- 状态管理:Zustand 5 个 store(calendar / user / settings / ui / bookmarks)
|
||||
- PWA:vite-plugin-pwa 集成(autoUpdate + Workbox 预缓存)
|
||||
- 设计系统:Tailwind v4 设计令牌(亮/暗主题)、Badge / Button / Card / Skeleton
|
||||
|
||||
### 已知问题(见 docs/BUGS.md)
|
||||
|
||||
- core 包 require 导出指向不存在的 `dist/index.cjs`
|
||||
- 周起始设置未生效
|
||||
- 梅花易数互卦简化、卦库缺 8 卦;称骨年索引用日干支近似
|
||||
- 无自动化测试、无 lint
|
||||
@@ -0,0 +1,49 @@
|
||||
# 开发进度(Progress)
|
||||
|
||||
> 最后更新:2026-08-03
|
||||
|
||||
## 1. 里程碑
|
||||
|
||||
| 里程碑 | 时间 | 内容 | 状态 |
|
||||
|---|---|---|---|
|
||||
| M0 项目初始化 | 2026-06-06 | monorepo 脚手架、tsconfig、workspace、核心类型定义 | ✅ |
|
||||
| M1 核心引擎 | 2026-06-06 ~ 06-07 | transformers(day/almanac/bazi)、calculators(relationship/elementStrength) | ✅ |
|
||||
| M2 高级算法 | 2026-06-07 | dailyMatch(每日运势)、plumBlossom(梅花)、boneWeight(称骨) | ✅ |
|
||||
| M3 前端页面 | 2026-06-07 | 8 个页面、5 个 store、路由、布局、组件库 | ✅ |
|
||||
| M4 构建与 PWA | 2026-06-07 | Vite 构建、tsup、PWA 集成(Workbox 缓存) | ✅ |
|
||||
| M5 文档化 | 2026-08-02 | 架构/需求/进度/BUG/CHANGELOG 文档建立 | ✅ |
|
||||
| M6 质量加固 | 2026-08-02 | 单元测试(vitest)、ESLint、CJS 导出修复、周起始修复、版本号 | ✅ |
|
||||
| M7 功能深化 | 2026-08-02 ~ 08-03 | 称骨数据修正、八字流派/出生地/太阳时开关、神煞、大运流年流月流日交互、历法信息、佛教节日 | ✅ |
|
||||
| M8 收尾 | 2026-08-03 | README、文档整理、源码打包(node_modules 清理) | ✅ |
|
||||
|
||||
> 注:主体功能于 2026-06-06 ~ 06-07 完成;2026-08-02 ~ 08-03 进行质量加固与功能深化。
|
||||
|
||||
## 2. 当前状态
|
||||
|
||||
- **可运行**:`pnpm dev` 可启动,`pnpm build` 可产出 core(ESM+CJS)+ web 产物(含 PWA)。
|
||||
- **质量**:50 个单元测试通过(vitest)、`pnpm lint` 通过(ESLint flat config + typescript-eslint)。
|
||||
- 剩余 BUG 与技术债见 BUGS.md。
|
||||
|
||||
## 3. 待办清单(Backlog)
|
||||
|
||||
按优先级排序:
|
||||
|
||||
| 优先级 | 事项 | 类型 | 关联 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| P0 | 修复 core 包 `require` 导出指向不存在的 `dist/index.cjs` | BUG | BUG-01 | ✅ |
|
||||
| P1 | 周起始设置生效 | 功能 | BUG-02 | ✅ |
|
||||
| P1 | 为 core 算法补充单元测试 | 工程 | N5 | ✅ 50 例 |
|
||||
| P2 | 六十四卦数据补全 | 算法 | C10 | ✅ 已核实 64/64 完整,测试锁定 |
|
||||
| P2 | 互卦算法实现真·互卦(2-4/3-5 爻) | 算法 | C10 | ⬜ |
|
||||
| P2 | 清理死代码(useDayDetail/useBazi、AnimatedPanel、CardHeader、未用 store action 等) | 工程 | — | ⬜ |
|
||||
| P3 | PWA manifest 修复 | BUG | BUG-03 | ✅ |
|
||||
| P3 | 农历周期收藏 UI | 功能 | M2 | ⬜ |
|
||||
| P3 | 收藏列表页 | 功能 | M3 | ⬜ |
|
||||
| P4 | 配置 lint 并修复 `pnpm lint` | 工程 | N6 | ✅ |
|
||||
| P4 | 显示开关字段真正消费(showLunar 等) | 功能 | T6 | ⬜ |
|
||||
| P4 | 版本号统一(0.1.0) | BUG | BUG-07 | ✅ |
|
||||
| P4 | 称骨数据表整体修正(年表/日表/歌诀) | 算法 | C11 | ✅ |
|
||||
| P4 | 占卜页称骨年索引用出生年干支 | 算法 | TECH-05 | ✅ |
|
||||
| P5 | 每日运势当日八字取午时 → 改为可选时辰 | 算法 | C9 | ⬜ |
|
||||
| P5 | 称骨极端总重仍取"最近值" → 提示超出范围 | 算法 | TECH-04 | ⬜ |
|
||||
| P5 | README 编写 | 工程 | M8 | ✅ |
|
||||
@@ -0,0 +1,142 @@
|
||||
# 需求文档(Requirements & 功能清单)
|
||||
|
||||
> 最后更新:2026-08-02
|
||||
> 状态说明:✅ 已完成 · 🟡 部分完成 / 有已知问题 · ⬜ 规划中
|
||||
|
||||
## 1. 需求总览
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
|---|---|---|
|
||||
| 历法核心引擎(@lunar/core) | ✅ | 农历/干支/黄历/八字/运势算法 |
|
||||
| 万年历(首页 + 日历 + 日详情) | ✅ | 功能齐全(周起始设置已修复) |
|
||||
| 八字排盘 | 🟡 | 功能齐全,含一个非类型安全补丁 |
|
||||
| 每日运势 | ✅ | 依赖用户档案 |
|
||||
| 占卜工具(梅花易数 / 称骨) | 🟡 | 算法为简化实现(见备注) |
|
||||
| 节气 | ✅ | 24 节气列表 |
|
||||
| 用户档案 + 设置 | 🟡 | 部分设置字段未生效 |
|
||||
| 收藏 | 🟡 | 仅支持公历单日收藏 |
|
||||
| PWA | ✅ | 已构建,manifest 有瑕疵 |
|
||||
|
||||
## 2. 功能点清单
|
||||
|
||||
### 2.1 历法核心(@lunar/core)
|
||||
|
||||
| # | 功能点 | 描述 | 状态 |
|
||||
|---|---|---|---|
|
||||
| C1 | 日期转换 | tyme4ts → DayInfo(公历/农历/干支/星座/生肖/节日/假日/月相) | ✅ |
|
||||
| C2 | 月历生成 | `getMonthCalendar` 周×天网格,含相邻月补位 | ✅ |
|
||||
| C3 | 黄历信息 | 宜忌、值神(日/时)、十二/廿八/九/六曜星、彭祖百忌、胎神、冲煞、纳音、三合/六合 | ✅ |
|
||||
| C4 | 时辰黄历 | 12 时辰逐时黄历(buildHourlyAlmanac) | ✅ |
|
||||
| C5 | 八字排盘 | 四柱、十神、藏干、十二长生、胎元/胎息、命宫/身宫、空亡、起运 | ✅ |
|
||||
| C6 | 大运/流年 | 10 大运 + 10 流年 | ✅ |
|
||||
| C7 | 干支关系 | 六合/三合/六冲/六害/相刑/三合局/天干合冲 | ✅ |
|
||||
| C8 | 五行分析 | 五行力量统计(藏干 0.5 权重)、旺衰、平衡度 | ✅ |
|
||||
| C9 | 每日运势 | 四柱逐柱 vs 当日干支打分(日 40%/月 25%/年 20%/时 15%),输出评分、宜忌、幸运色/数/方位、分项(感情/事业/财运/健康) | 🟡 当日八字固定取午时 |
|
||||
| C10 | 梅花易数 | 时间起卦、变卦、互卦(简化)、体用关系、64 卦辞 | 🟡 互卦简化为上下互换(卦库 64/64 完整,已有测试) |
|
||||
| C11 | 称骨算命 | 年/月/日/时称骨表 + 52 首解诗(2两1钱~7两2钱完整) | ✅ 数据表已按主流版本修正,歌诀补全(极端值仍取最近) |
|
||||
| C12 | 历法信息 | 季节、所处节气/第几天/距下节气、儒略日、佛历年、伊斯兰历(Hijri) | ✅ |
|
||||
| C13 | 佛教节日 | 农历 21 个重大佛教节日(`getBuddhistFestival`) | ✅ 日详情页 🪷 徽章展示 |
|
||||
| C14 | 八字神煞 | 22 个常见神煞(`analyzeShensha`) | ✅ 八字页"神煞"卡片 |
|
||||
| C15 | 大运/流年 vs 日主生克冲合 | 十神、五行生克、天干合冲、地支关系、吉凶(`analyzeFortuneGanzhi`) | ✅ |
|
||||
| C16 | 流年·流月·流日分析 | 任意日期年/月/日干支 vs 日主(`getYearMonths` 按节气月) | ✅ 八字页交互浏览器 |
|
||||
|
||||
### 2.2 首页 HomePage(`/`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| H1 | 今日概览:日期、农历、干支、宜忌摘要 | ✅ |
|
||||
| H2 | 运势预览(需档案) | ✅ |
|
||||
| H3 | 快捷导航(占卜、节气) | ✅ |
|
||||
| H4 | 值神/彭祖/胎神展示 | ✅ |
|
||||
|
||||
### 2.3 日历 CalendarPage(`/calendar`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| K1 | 月视图/周视图切换 | ✅ |
|
||||
| K2 | 年/月切换(MonthYearPicker) | ✅ |
|
||||
| K3 | 格子显示:节日/节气缩写、农历、周末/今日高亮、收藏星标、运势点 | ✅ |
|
||||
| K4 | 点击日期 → 日详情页 | ✅ |
|
||||
| K5 | 手势滑动切换月份 | ✅ |
|
||||
| K6 | 周起始(周一/周日)设置生效 | ✅ 已修复(BUG-02) |
|
||||
| K7 | 非本月时显示"回到今天"浮动按钮,点击返回当月 | ✅ 已修复(BUG-09) |
|
||||
|
||||
### 2.4 日详情 DayDetailPage(`/calendar/:date`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| D1 | 四柱、纳音、值星展示 | ✅ |
|
||||
| D2 | 冲合害煞、宜忌列表 | ✅ |
|
||||
| D3 | 值神、彭祖百忌、胎神 | ✅ |
|
||||
| D4 | 12 时辰逐时黄历 | ✅ |
|
||||
| D5 | 收藏按钮 | ✅ 仅公历收藏 |
|
||||
|
||||
### 2.5 八字 BaziPage(`/bazi`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| B1 | 出生档案管理(最多 3 个,持久化) | ✅ |
|
||||
| B2 | 排盘:四柱 + 十神 + 藏干 | ✅ |
|
||||
| B3 | 五行力量 / 十二长生展示 | ✅ |
|
||||
| B4 | 起运 / 大运 / 流年 | ✅ |
|
||||
| B5 | 称骨集成 | ✅ 使用出生年干支(占卜页已同步修正) |
|
||||
| B6 | 日柱类型安全 | ✅ `solarAdjusted` 字段替代 `(as any)` 补丁 |
|
||||
| B7 | 八字流派切换(晚子时换日 23点 / 0点换日) | ✅ 设置页可切换,排盘实时生效 |
|
||||
| B8 | 出生地:全国 34 省级城市真太阳时校正 | ✅ 含跨日处理,受"真太阳时校正"开关控制 |
|
||||
| B9 | 出生地:海外时区 → 北京时间(UTC+8)换算排盘 | ✅ 含跨日处理 |
|
||||
| B10 | 神煞展示(22 个常见神煞,吉/凶/中性分组) | ✅ |
|
||||
| B11 | 大运全量 10 条 + 流年(小运)10 条,均附生克冲合 | ✅ |
|
||||
| B12 | 大运→流年→流月→流日交互下钻(按年度切换) | ✅ 点选日期跳转日详情 |
|
||||
|
||||
### 2.6 每日运势 DailyFortunePage(`/daily-fortune`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| F1 | 日期导航查看任意日运势 | ✅ |
|
||||
| F2 | 无档案时引导创建 | ✅ |
|
||||
| F3 | 评分等级、宜忌、幸运信息、分项得分 | ✅ |
|
||||
|
||||
### 2.7 占卜 DivinationPage(`/divination`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| P1 | 今日梅花易数卦象 + 体用分析 | 🟡 算法简化 |
|
||||
| P2 | 称骨算命输入与结果 | 🟡 年索引用日干支近似 |
|
||||
|
||||
### 2.8 节气 SolarTermsPage(`/solar-terms`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| S1 | 全年 24 节气列表 | ✅ |
|
||||
|
||||
### 2.9 设置 SettingsPage(`/settings`)
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| T1 | 档案管理入口 | ✅ |
|
||||
| T2 | 农历日期查询(阴阳历转换) | ✅ |
|
||||
| T3 | 今日天干地支/纳音 | ✅ |
|
||||
| T4 | 主题切换(亮/暗/跟随系统) | ✅ |
|
||||
| T5 | 周起始设置 | ✅ 已修复(BUG-02) |
|
||||
| T6 | 显示开关(农历/节气/假日)、八字来源 | 🟡 字段已持久化但未消费 |
|
||||
| T7 | 关于:版本号 | ✅ 已统一为 0.1.0(BUG-07) |
|
||||
| T8 | 真太阳时校正开关(默认开启) | ✅ 关闭后按北京时间排盘 |
|
||||
|
||||
### 2.10 收藏 Bookmarks
|
||||
|
||||
| # | 功能点 | 状态 |
|
||||
|---|---|---|
|
||||
| M1 | 收藏/取消收藏日期,日历格星标 | ✅ |
|
||||
| M2 | 农历周期收藏(每年/月循环) | ⬜ 字段已预留,UI 未提供 |
|
||||
| M3 | 收藏列表页 | ⬜ 规划中 |
|
||||
|
||||
### 2.11 非功能需求
|
||||
|
||||
| # | 需求 | 状态 |
|
||||
|---|---|---|
|
||||
| N1 | PWA 可安装、离线可用 | ✅ 已构建 |
|
||||
| N2 | 移动端优先响应式 | ✅ |
|
||||
| N3 | 暗色模式(跟随系统) | ✅ |
|
||||
| N4 | 页面懒加载 + 骨架屏 + 错误边界 | ✅ |
|
||||
| N5 | 自动化测试 | ✅ 2026-08-02 引入 vitest,29 个用例(历法/八字/运势/梅花/称骨/干支) |
|
||||
| N6 | Lint / 代码规范 | ✅ 2026-08-02 配置 ESLint 9 flat config + typescript-eslint,`pnpm lint` 通过 |
|
||||
@@ -0,0 +1,74 @@
|
||||
# 开发回顾记录(Retrospect)
|
||||
|
||||
> 会话日期:2026-08-02 ~ 2026-08-03
|
||||
> 用途:记录本次会话"做了什么 / 没做什么 / 有疑问的事项",便于事后回顾与决策。
|
||||
|
||||
## 一、本次完成的工作
|
||||
|
||||
### 1. 文档体系(docs/)
|
||||
- 新建 `docs/`:ARCHITECTURE / REQUIREMENTS / PROGRESS / BUGS / CHANGELOG,另加根 README.md
|
||||
- 修正 AGENTS.md / CLAUDE.md 的过时信息(PWA 状态、页面/Store/API 补全),并持续同步
|
||||
|
||||
### 2. BUG 修复(BUGS.md 均已标记 ✅)
|
||||
| ID | 内容 |
|
||||
|---|---|
|
||||
| BUG-01 | core 包 CJS 导出:tsup 增加 cjs 产物,`require()` 实测可用 |
|
||||
| BUG-02 | 周起始设置生效:settings → useCalendar → getMonthCalendar(weekStart) → WeekDayBar |
|
||||
| BUG-03 | PWA manifest 对齐(lang=zh-CN、theme_color=#FFFBF5) |
|
||||
| BUG-05 | 配置 ESLint 9 flat config + typescript-eslint,`pnpm lint` 通过(修复 24 处) |
|
||||
| BUG-07 | 版本号统一 v0.1.0 |
|
||||
| BUG-08 | getMonthCalendar 死分支移除 |
|
||||
| BUG-09 | "回到今天"按钮条件恒 false,改为基于 viewDate 判断 |
|
||||
| BUG-10 | 日详情时辰列表重复 React key(早子/晚子均"子"),改用 ganzhi 作 key |
|
||||
| TECH-06 | `(as any)._solarAdjusted` → `BaziFullResult.solarAdjusted` 类型化 |
|
||||
|
||||
### 3. 功能深化
|
||||
- **八字流派切换**:`birthInfoToBazi` 新增 `ziSect`(晚子时算次日/当日),设置页开关
|
||||
- **出生时间选择器**:时/分下拉 + 时辰格可点
|
||||
- **出生地全国化 + 海外时区**:34 省级城市(真太阳时校正)+ 海外 UTC-12~+14 换算北京时间,均支持跨日
|
||||
- **真太阳时校正开关**(设置页,默认开启)
|
||||
- **宜忌完整显示**:根因是首页 `slice(0,6)`,引擎本有 22 条宜
|
||||
- **历法信息增强**:DayInfo 新增 季节/所处节气/第几天/距下节气/儒略日/佛历年/伊斯兰历(tyme4ts 原生 Hijri)
|
||||
- **佛教节日**:新增 `getBuddhistFestival`(农历 21 个节日),日详情 🪷 徽章
|
||||
- **八字神煞**:新增 `analyzeShensha`(22 个常见神煞),八字页"神煞"卡
|
||||
- **大运/流年生克冲合**:新增 `analyzeFortuneGanzhi`(十神/五行生克/合冲害刑/吉凶)
|
||||
- **大运→流年→流月→流日交互下钻**:新增 `getYearMonths`(节气月);八字页交互浏览器,流日可跳日详情
|
||||
|
||||
### 4. 质量
|
||||
- 单元测试 29 → **50 个**(vitest),覆盖历法/八字/运势/梅花/称骨/干支/五行/神煞/流月/流年
|
||||
- 浏览器 E2E 验证(headless Chrome + CDP 脚本,无新增依赖):周起始、回到今天、流派、出生地、太阳时开关、宜忌、历法信息、神煞、大运流年交互 全部实测通过
|
||||
|
||||
### 5. 工程清理
|
||||
- 整理全部文档;删除 node_modules / dist / tsbuildinfo;源码 241M → 764K;打包 164K(/tmp/lunar-source-20260803.tar.gz)
|
||||
|
||||
## 二、未完成 / 待办(详见 PROGRESS.md Backlog)
|
||||
|
||||
| 事项 | 说明 |
|
||||
|---|---|
|
||||
| 互卦真算法 | 梅花易数互卦仍为上下卦互换简化(2-4/3-5 爻法未实现) |
|
||||
| 死代码清理 | useDayDetail/useBazi hooks、AnimatedPanel、CardHeader、CalendarSkeleton/BaziSkeleton、ui store 未用 action、settings 显示开关字段、calendar store weekStart、bookmarks getByDate、utils 未用函数、tailwind-variants 依赖 |
|
||||
| 显示开关字段 | settings.showLunar/showSolarTerm/showHoliday/eightCharProvider 已持久化但未消费(其中 eightCharProvider 已被 ziHourSect 替换) |
|
||||
| 农历周期收藏 | bookmarks 的 isLunar 字段已预留,UI 未提供;收藏列表页未做 |
|
||||
| 午时假设 | calculateDailyFortune 当日八字固定取午时(TECH-01) |
|
||||
| 称骨极端值 | 超出 2两1钱~7两2钱范围的总重仍取"最近值"(TECH-04 残余) |
|
||||
| 紫微斗数 | 未做(大工程,用户同意先做神煞) |
|
||||
| web 端自动化测试 | 仅 core 有单测;web 无测试框架 |
|
||||
| git | 项目未初始化 git 仓库,无版本管理与回滚能力 |
|
||||
|
||||
## 三、有疑问 / 待确认事项
|
||||
|
||||
1. **称骨年表存在两套网络变体**:本实现采用主流版本(算准网/网易/sunfinelife 三家一致:丙子16钱、戊子15钱等),但另一套变体(丙子19钱、戊子12钱等)也在流通。若用户希望换另一套,改 `boneWeight.ts` 的 `YEAR_WEIGHTS` 即可。
|
||||
2. **天厨贵人查法有版本差异**:本实现采用主流版(甲巳、乙午、丙巳、丁午、戊申、己酉、庚亥、辛子、壬寅、癸卯);传统版(丙寅、丁酉、戊申、己未、庚亥、辛戌、壬卯、癸子)也有出处。代码注释已注明。
|
||||
3. **tyme4ts 十神方向语义**:实测 `A.getTenStar(B)` 返回"B 以 A 为日主"的十神。`dailyMatch.ts` 中的用法方向(userPillar vs dayPillar 同位置比较)语义上存疑,但**未改动**(避免影响每日运势结果);新写的 `fortuneLuck` 已按正确方向实现。需确认 dailyMatch 是否也要调整。
|
||||
4. **佛历年**:按公历 + 543(泰国惯例);bmcx 示例 2570 对应的是其他年份。
|
||||
5. **佛教节日表**:21 个为通行版本,个别日期(如腊月廿九华严菩萨圣诞)在不同资料有出入。
|
||||
6. **默认出生地变化**:默认从"120°E 不校正"改为"北京 116.4°E(-14 分钟真太阳校正)",临界时辰的结果会变。太阳时校正开关默认开启——用户接受度未知,如需要可默认关闭。
|
||||
7. **BUG-04(HomePage InfoCard highlight)**:`highlight` 传布尔但被当作样式类 `border-primary/30` 处理,语义不清,需确认意图(是要"高亮边框"还是别的)。
|
||||
8. **起运前年份 UX**:大运流年浏览器中,出生当年(起运前)不在任何大运区间,自动落到第一柱大运(如 2026 年出生 → 从 2028 起显示)。是否要显示"起运前"年份待定。
|
||||
9. **pnpm-workspace.yaml `allowBuilds`**:非 pnpm 标准字段(应为 onlyBuiltDependencies),疑似无效但无害,未改。
|
||||
10. **打包清理**:已删除 node_modules/dist,开发前需 `pnpm install`。
|
||||
|
||||
## 四、验证方式备忘
|
||||
|
||||
- E2E 通过 headless Chrome(Playwright 缓存目录的 chromium headless shell)+ CDP 协议 Node 脚本驱动,未引入 Playwright npm 依赖;脚本存于 `/tmp/lunar-*.mjs`(会话临时文件,未入库)。
|
||||
- 所有功能均以"单测 + 浏览器实测"双重验证;仅 UI 纯样式类改动(如时间选择器)以 tsc 构建验证为主。
|
||||
@@ -0,0 +1,11 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['**/dist/**', '**/node_modules/**', '**/*.tsbuildinfo'] },
|
||||
tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "lunar-calendar",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "万年历 - Chinese Almanac Calendar App",
|
||||
"scripts": {
|
||||
"dev": "pnpm --filter @lunar/web dev",
|
||||
"build": "pnpm --filter @lunar/core build && pnpm --filter @lunar/web build",
|
||||
"preview": "pnpm --filter @lunar/web preview",
|
||||
"test": "pnpm -r test",
|
||||
"lint": "pnpm -r lint",
|
||||
"clean": "pnpm -r clean"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"pnpm": ">=9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.8.0",
|
||||
"typescript-eslint": "^8.65.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@lunar/core",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Core calculation engine for lunar calendar app",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "tsup --watch",
|
||||
"build": "tsup",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint .",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"tyme4ts": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { birthInfoToBazi } from '../index';
|
||||
|
||||
const GANZHI = /^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/;
|
||||
|
||||
describe('bazi transformer', () => {
|
||||
const bazi = birthInfoToBazi({ year: 1990, month: 5, day: 15, hour: 12, minute: 0, gender: 'male' });
|
||||
|
||||
it('produces four pillars with valid ganzhi', () => {
|
||||
expect(bazi.eightChar.yearPillar.ganzhi).toMatch(GANZHI);
|
||||
expect(bazi.eightChar.monthPillar.ganzhi).toMatch(GANZHI);
|
||||
expect(bazi.eightChar.dayPillar.ganzhi).toMatch(GANZHI);
|
||||
expect(bazi.eightChar.hourPillar.ganzhi).toMatch(GANZHI);
|
||||
});
|
||||
|
||||
it('exposes day master info', () => {
|
||||
expect(bazi.eightChar.dayMasterStem).toMatch(/^[甲乙丙丁戊己庚辛壬癸]$/);
|
||||
expect(['木', '火', '土', '金', '水']).toContain(bazi.eightChar.dayMasterElement);
|
||||
});
|
||||
|
||||
it('returns 10 decade fortunes and 10 annual fortunes', () => {
|
||||
expect(bazi.decadeFortunes).toHaveLength(10);
|
||||
expect(bazi.annualFortunes).toHaveLength(10);
|
||||
expect(bazi.decadeFortunes[0].ganzhi).toMatch(GANZHI);
|
||||
});
|
||||
|
||||
it('returns child limit with non-negative start age', () => {
|
||||
expect(bazi.childLimit.startAge).toBeGreaterThanOrEqual(0);
|
||||
expect(bazi.childLimit.forward).toBeTypeOf('boolean');
|
||||
});
|
||||
|
||||
it('includes hidden stems with ten stars', () => {
|
||||
const yearHs = bazi.eightChar.yearPillar.hideStems;
|
||||
expect(yearHs.length).toBeGreaterThan(0);
|
||||
expect(yearHs[0].stem).toMatch(/^[甲乙丙丁戊己庚辛壬癸]$/);
|
||||
});
|
||||
|
||||
it('default sect: late zi (23:30) rolls to next day', () => {
|
||||
const late = birthInfoToBazi({ year: 2026, month: 8, day: 3, hour: 23, minute: 30, gender: 'male' });
|
||||
expect(late.eightChar.dayPillar.ganzhi).toBe('庚戌');
|
||||
expect(late.eightChar.hourPillar.ganzhi).toBe('丙子');
|
||||
});
|
||||
|
||||
it('earlyZiSameDay sect: late zi (23:30) keeps the current day', () => {
|
||||
const early = birthInfoToBazi({
|
||||
year: 2026, month: 8, day: 3, hour: 23, minute: 30, gender: 'male', ziSect: 'earlyZiSameDay',
|
||||
});
|
||||
expect(early.eightChar.dayPillar.ganzhi).toBe('己酉');
|
||||
expect(early.eightChar.hourPillar.ganzhi).toBe('甲子');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateBoneWeight } from '../index';
|
||||
|
||||
describe('bone weight', () => {
|
||||
it('sums weights correctly for 甲子年正月初一子时', () => {
|
||||
const r = calculateBoneWeight(0, 1, 1, 0);
|
||||
expect(r.yearWeight).toBe(12);
|
||||
expect(r.monthWeight).toBe(6);
|
||||
expect(r.dayWeight).toBe(5);
|
||||
expect(r.hourWeight).toBe(16);
|
||||
expect(r.totalWeight).toBe(39);
|
||||
expect(r.totalLiang).toBe(3);
|
||||
expect(r.totalQian).toBe(9);
|
||||
});
|
||||
|
||||
it('returns a non-empty interpretation with a valid fortune grade', () => {
|
||||
const r = calculateBoneWeight(12, 6, 15, 6);
|
||||
expect(r.interpretation.length).toBeGreaterThan(0);
|
||||
expect(['上上', '上', '中上', '中', '中下', '下']).toContain(r.fortune);
|
||||
});
|
||||
|
||||
it('falls back to the nearest interpretation for unknown totals', () => {
|
||||
// 4 + 9 + 9 + 9 = 31 → exact match exists; use an out-of-range input
|
||||
const r = calculateBoneWeight(0, 1, 1, 11); // 12 + 6 + 5 + 6 = 29
|
||||
expect(r.totalWeight).toBe(29);
|
||||
expect(r.interpretation.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('year table matches the mainstream 称骨年表', () => {
|
||||
const cases: [number, number][] = [
|
||||
[0, 12], // 甲子
|
||||
[7, 8], // 辛未
|
||||
[16, 12], // 庚辰
|
||||
[17, 6], // 辛巳
|
||||
[18, 8], // 壬午
|
||||
[21, 15], // 乙酉
|
||||
[51, 8], // 乙卯
|
||||
[54, 19], // 戊午
|
||||
];
|
||||
for (const [idx, wt] of cases) {
|
||||
const r = calculateBoneWeight(idx, 1, 1, 0);
|
||||
expect(r.yearWeight, `year index ${idx}`).toBe(wt);
|
||||
}
|
||||
});
|
||||
|
||||
it('day table: 初五 is 1两6钱', () => {
|
||||
const r = calculateBoneWeight(0, 1, 5, 0);
|
||||
expect(r.dayWeight).toBe(16);
|
||||
});
|
||||
|
||||
it('previously missing poems (5两8钱~7两1钱) resolve exactly, not by nearest match', () => {
|
||||
const cases: [number, number, number, number, number, string][] = [
|
||||
[18, 6, 26, 5, 58, '雁塔题名'],
|
||||
[15, 6, 8, 7, 59, '甲第之中'],
|
||||
[24, 3, 18, 6, 61, '金榜客'],
|
||||
[42, 6, 26, 5, 63, '定中高科'],
|
||||
[54, 9, 18, 6, 65, '安邦'],
|
||||
[54, 6, 8, 5, 67, '田园家业'],
|
||||
[54, 6, 18, 5, 69, '前禄星'],
|
||||
[54, 3, 26, 5, 71, '公侯卿相'],
|
||||
];
|
||||
for (const [y, m, d, h, total, phrase] of cases) {
|
||||
const r = calculateBoneWeight(y, m, d, h);
|
||||
expect(r.totalWeight, `expected total ${total}`).toBe(total);
|
||||
expect(r.interpretation, `poem for ${total}`).toContain(phrase);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getDayInfo, getMonthCalendar, getTodayInfo } from '../index';
|
||||
|
||||
describe('day transformers', () => {
|
||||
it('returns correct lunar info for 2024-02-10 (春节正月初一)', () => {
|
||||
const di = getDayInfo(2024, 2, 10);
|
||||
expect(di.solarDate).toBe('2024-02-10');
|
||||
expect(di.lunarMonth).toBe(1);
|
||||
expect(di.lunarDay).toBe(1);
|
||||
expect(di.lunarDayName).toBe('初一');
|
||||
expect(di.lunarYearGanzhi).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
||||
expect(di.weekDayIndex).toBe(6);
|
||||
expect(di.isWeekend).toBe(true);
|
||||
});
|
||||
|
||||
it('getTodayInfo returns today', () => {
|
||||
const now = new Date();
|
||||
const di = getTodayInfo();
|
||||
expect(di.solarYear).toBe(now.getFullYear());
|
||||
expect(di.solarMonth).toBe(now.getMonth() + 1);
|
||||
expect(di.solarDay).toBe(now.getDate());
|
||||
expect(di.isToday).toBe(true);
|
||||
});
|
||||
|
||||
it('getMonthCalendar respects weekStart', () => {
|
||||
// 2024-02-01 is a Thursday
|
||||
const sunFirst = getMonthCalendar(2024, 2, 0);
|
||||
const monFirst = getMonthCalendar(2024, 2, 1);
|
||||
|
||||
for (const row of sunFirst) expect(row).toHaveLength(7);
|
||||
for (const row of monFirst) expect(row).toHaveLength(7);
|
||||
|
||||
expect(sunFirst[0][0].weekDayIndex).toBe(0); // Sunday first
|
||||
expect(monFirst[0][0].weekDayIndex).toBe(1); // Monday first
|
||||
|
||||
// Both grids must contain the 1st of the month
|
||||
const flatSun = sunFirst.flat();
|
||||
const flatMon = monFirst.flat();
|
||||
expect(flatSun.some(d => d.solarDate === '2024-02-01')).toBe(true);
|
||||
expect(flatMon.some(d => d.solarDate === '2024-02-01')).toBe(true);
|
||||
});
|
||||
|
||||
it('provides season, term progress, julian, buddhist era and hijri date', () => {
|
||||
const di = getDayInfo(2026, 8, 3); // 大暑期间,农历六月廿一
|
||||
expect(di.season).toBe('夏季');
|
||||
expect(di.termDayIndex).toBeGreaterThan(0);
|
||||
expect(di.nextSolarTerm).toBe('立秋');
|
||||
expect(di.daysToNextTerm).toBe(4);
|
||||
expect(di.julianDay).toBeGreaterThan(2450000);
|
||||
expect(di.buddhistYear).toBe(2026 + 543);
|
||||
expect(di.hijriDate).toMatch(/^\d{4}年\d{2}月\d{2}日$/);
|
||||
});
|
||||
|
||||
it('marks Buddhist festivals by lunar date', () => {
|
||||
// 2024-05-15 is 佛诞(浴佛节)农历四月初八
|
||||
const di = getDayInfo(2024, 5, 15);
|
||||
expect(di.lunarMonth).toBe(4);
|
||||
expect(di.lunarDay).toBe(8);
|
||||
expect(di.buddhistFestival).toBe('释迦牟尼佛圣诞(浴佛节)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { analyzeElementBalance, birthInfoToBazi } from '../index';
|
||||
|
||||
describe('element balance', () => {
|
||||
const eightChar = birthInfoToBazi({
|
||||
year: 1990, month: 5, day: 15, hour: 12, minute: 0, gender: 'male',
|
||||
}).eightChar;
|
||||
|
||||
const profile = analyzeElementBalance(eightChar);
|
||||
|
||||
it('has positive total that matches sum of parts', () => {
|
||||
expect(profile.total).toBeGreaterThan(0);
|
||||
expect(profile.wood + profile.fire + profile.earth + profile.metal + profile.water)
|
||||
.toBeCloseTo(profile.total);
|
||||
});
|
||||
|
||||
it('identifies dominant and weakest elements', () => {
|
||||
expect(['木', '火', '土', '金', '水']).toContain(profile.dominant);
|
||||
expect(['木', '火', '土', '金', '水']).toContain(profile.weakest);
|
||||
expect(profile.isBalanced).toBeTypeOf('boolean');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateDailyFortune, birthInfoToBazi } from '../index';
|
||||
|
||||
const SCORE_LEVELS = ['great', 'good', 'fair', 'poor', 'bad'] as const;
|
||||
|
||||
describe('daily fortune', () => {
|
||||
const eightChar = birthInfoToBazi({
|
||||
year: 1990, month: 5, day: 15, hour: 12, minute: 0, gender: 'female',
|
||||
}).eightChar;
|
||||
|
||||
const fortune = calculateDailyFortune(eightChar, new Date(2026, 7, 2));
|
||||
|
||||
it('returns bounded score and valid level', () => {
|
||||
expect(fortune.overallScore).toBeGreaterThanOrEqual(-100);
|
||||
expect(fortune.overallScore).toBeLessThanOrEqual(100);
|
||||
expect(SCORE_LEVELS).toContain(fortune.scoreLevel);
|
||||
});
|
||||
|
||||
it('evaluates all four pillars', () => {
|
||||
expect(fortune.pillarRelationships).toHaveLength(4);
|
||||
expect(fortune.pillarRelationships.map(r => r.pillar)).toEqual(['year', 'month', 'day', 'hour']);
|
||||
});
|
||||
|
||||
it('returns the requested date', () => {
|
||||
expect(fortune.date).toBe('2026-08-02');
|
||||
});
|
||||
|
||||
it('produces suggestions and category scores', () => {
|
||||
expect(fortune.suggestions.length).toBeGreaterThan(0);
|
||||
expect(fortune.luckyAspects.length).toBeGreaterThan(0);
|
||||
const { love, career, wealth, health } = fortune.categoryScores;
|
||||
expect([love, career, wealth, health].every(v => v >= -100 && v <= 100)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { analyzeFortuneGanzhi, birthInfoToBazi } from '../index';
|
||||
|
||||
describe('fortune luck (大运/流年 vs 日主)', () => {
|
||||
// 2026-08-03 12:00: 日干己、日支酉
|
||||
const bazi = birthInfoToBazi({ year: 2026, month: 8, day: 3, hour: 12, minute: 0, gender: 'male' });
|
||||
const dayStem = bazi.eightChar.dayPillar.heavenStem;
|
||||
const dayBranch = bazi.eightChar.dayPillar.earthBranch;
|
||||
|
||||
it('甲子 vs 己酉: 正官 + 天干合 + 克我 → 吉', () => {
|
||||
const r = analyzeFortuneGanzhi('甲子', dayStem, dayBranch);
|
||||
expect(r.tenStar).toBe('正官');
|
||||
expect(r.stemCombine).toBe(true);
|
||||
expect(r.elementRelation).toBe('克我');
|
||||
expect(r.level).toBe('吉');
|
||||
});
|
||||
|
||||
it('乙酉 vs 己酉: 七杀 + 自刑 → 凶', () => {
|
||||
const r = analyzeFortuneGanzhi('乙酉', dayStem, dayBranch);
|
||||
expect(r.tenStar).toBe('七杀');
|
||||
expect(r.branchPunish).toBe(true); // 酉酉自刑
|
||||
expect(r.level).toBe('凶');
|
||||
});
|
||||
|
||||
it('丙子 vs 己未: 正印 + 子未六害', () => {
|
||||
const r = analyzeFortuneGanzhi('丙子', '己', '未');
|
||||
expect(r.tenStar).toBe('正印');
|
||||
expect(r.branchHarm).toBe(true);
|
||||
});
|
||||
|
||||
it('all decade fortunes produce valid luck info', () => {
|
||||
for (const df of bazi.decadeFortunes) {
|
||||
const r = analyzeFortuneGanzhi(df.ganzhi, dayStem, dayBranch);
|
||||
expect(['比和', '生我', '我生', '克我', '我克']).toContain(r.elementRelation);
|
||||
expect(['吉', '凶', '平']).toContain(r.level);
|
||||
expect(r.score).toBeGreaterThanOrEqual(-10);
|
||||
expect(r.score).toBeLessThanOrEqual(10);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { calculatePlumBlossom } from '../index';
|
||||
|
||||
describe('plum blossom', () => {
|
||||
it('produces expected hexagram for known input (2026-08-02 12:00)', () => {
|
||||
const r = calculatePlumBlossom(2026, 8, 2, 12);
|
||||
expect(r.upperTrigram.name).toBe('巽');
|
||||
expect(r.lowerTrigram.name).toBe('兑');
|
||||
expect(r.originalHexagram.name).toBe('风泽中孚');
|
||||
expect(r.originalHexagram.changingLine).toBe(4);
|
||||
expect(r.relationship).toBeTruthy();
|
||||
});
|
||||
|
||||
it('is deterministic', () => {
|
||||
const a = calculatePlumBlossom(2024, 3, 5, 8);
|
||||
const b = calculatePlumBlossom(2024, 3, 5, 8);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
it('dataset covers all 64 hexagram combinations', () => {
|
||||
const srcFile = fileURLToPath(new URL('../calculators/plumBlossom.ts', import.meta.url));
|
||||
const src = fs.readFileSync(srcFile, 'utf8');
|
||||
const keys = [...src.matchAll(/'(\d,\d)':/g)].map(m => m[1]);
|
||||
|
||||
expect(keys).toHaveLength(64);
|
||||
expect(new Set(keys).size).toBe(64);
|
||||
for (let upper = 1; upper <= 8; upper++) {
|
||||
for (let lower = 1; lower <= 8; lower++) {
|
||||
expect(keys).toContain(`${upper},${lower}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('never falls back to a placeholder across a broad date grid', () => {
|
||||
for (let y = 2000; y <= 2030; y++) {
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
for (let d = 1; d <= 28; d += 7) {
|
||||
const r = calculatePlumBlossom(y, m, d, 12);
|
||||
// Real hexagram names are 3+ chars; fallback names are 2-char concatenations
|
||||
expect(r.originalHexagram.name.length).toBeGreaterThan(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getBranchRelationship,
|
||||
getTenStarRelationship,
|
||||
checkStemCombine,
|
||||
checkStemOpposite,
|
||||
} from '../index';
|
||||
|
||||
describe('relationship calculator', () => {
|
||||
it('detects 六合 (子丑合)', () => {
|
||||
expect(getBranchRelationship('子', '丑').combine).toBe(true);
|
||||
expect(getBranchRelationship('子', '午').combine).toBe(false);
|
||||
});
|
||||
|
||||
it('detects 六冲 (子午冲)', () => {
|
||||
expect(getBranchRelationship('子', '午').opposite).toBe(true);
|
||||
});
|
||||
|
||||
it('detects 三合 (申子辰水局)', () => {
|
||||
const rel = getBranchRelationship('申', '辰');
|
||||
expect(rel.threeCombine).toBe(true);
|
||||
expect(rel.formation).toBe('水局');
|
||||
});
|
||||
|
||||
it('detects 六害 (子未害)', () => {
|
||||
expect(getBranchRelationship('子', '未').harm).toBe(true);
|
||||
});
|
||||
|
||||
it('detects 相刑 (寅巳无恩之刑)', () => {
|
||||
expect(getBranchRelationship('寅', '巳').punish).toBe(true);
|
||||
});
|
||||
|
||||
it('detects 天干合 (甲己合)', () => {
|
||||
expect(checkStemCombine('甲', '己')).toBe(true);
|
||||
expect(checkStemCombine('甲', '乙')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects 天干冲 (甲庚冲)', () => {
|
||||
expect(checkStemOpposite('甲', '庚')).toBe(true);
|
||||
});
|
||||
|
||||
it('computes ten star for a day master (甲日主见戊土 → 偏财)', () => {
|
||||
expect(getTenStarRelationship('甲', '戊')).toContain('财');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { analyzeShensha, birthInfoToBazi } from '../index';
|
||||
|
||||
describe('shensha', () => {
|
||||
// 2026-08-03 12:00: 年柱丙午、月柱乙未、日柱己酉、时柱庚午
|
||||
const bazi = birthInfoToBazi({ year: 2026, month: 8, day: 3, hour: 12, minute: 0, gender: 'male' });
|
||||
|
||||
it('finds 天乙贵人 by year stem 丙 → 亥酉, hit 日支酉', () => {
|
||||
const s = analyzeShensha(bazi.eightChar);
|
||||
const t = s.find(x => x.name === '天乙贵人');
|
||||
expect(t).toBeDefined();
|
||||
expect(t!.foundIn).toContain('日支');
|
||||
expect(t!.anchors.some(a => a.includes('年干'))).toBe(true);
|
||||
});
|
||||
|
||||
it('finds 文昌贵人 by day stem 己 → 酉, hit 日支酉', () => {
|
||||
const s = analyzeShensha(bazi.eightChar);
|
||||
const t = s.find(x => x.name === '文昌贵人');
|
||||
expect(t).toBeDefined();
|
||||
expect(t!.anchors.some(a => a.includes('日干'))).toBe(true);
|
||||
});
|
||||
|
||||
it('finds 禄神 and 羊刃 by day/year stem 己/丙 → 午', () => {
|
||||
const s = analyzeShensha(bazi.eightChar);
|
||||
const lu = s.find(x => x.name === '禄神');
|
||||
const yang = s.find(x => x.name === '羊刃');
|
||||
expect(lu).toBeDefined();
|
||||
expect(lu!.foundIn).toEqual(expect.arrayContaining(['年支', '时支']));
|
||||
expect(yang).toBeDefined();
|
||||
});
|
||||
|
||||
it('finds 桃花 by day branch 酉 (巳酉丑) → 午', () => {
|
||||
const s = analyzeShensha(bazi.eightChar);
|
||||
const t = s.find(x => x.name.startsWith('桃花'));
|
||||
expect(t).toBeDefined();
|
||||
expect(t!.foundIn).toContain('年支');
|
||||
});
|
||||
|
||||
it('finds 将星 and 红鸾', () => {
|
||||
const s = analyzeShensha(bazi.eightChar);
|
||||
expect(s.find(x => x.name === '将星')).toBeDefined(); // 年支午(寅午戌) → 午
|
||||
expect(s.find(x => x.name === '红鸾')).toBeDefined(); // 年支午 → 酉,日支酉
|
||||
});
|
||||
|
||||
it('does not flag 魁罡/阴差阳错 for day 己酉', () => {
|
||||
const s = analyzeShensha(bazi.eightChar);
|
||||
expect(s.find(x => x.name === '魁罡')).toBeUndefined();
|
||||
expect(s.find(x => x.name === '阴差阳错')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('all stem rules cover all 10 stems and branch rules cover 12 branches', () => {
|
||||
const s = analyzeShensha(bazi.eightChar);
|
||||
expect(s.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getYearMonths } from '../index';
|
||||
|
||||
describe('getYearMonths (流月)', () => {
|
||||
it('returns 12 months starting from 立春', () => {
|
||||
const months = getYearMonths(2026);
|
||||
expect(months).toHaveLength(12);
|
||||
expect(months[0].name).toBe('正月');
|
||||
expect(months[0].startDate).toBe('2026-02-04'); // 立春 2026
|
||||
expect(months[0].ganzhi).toBe('庚寅'); // 丙午年 五虎遁 → 寅月庚寅
|
||||
expect(months[11].name).toBe('腊月');
|
||||
expect(months[11].startDate).toMatch(/^2027-01-/); // 小寒 2027
|
||||
});
|
||||
|
||||
it('endDate is the day before the next 节', () => {
|
||||
const months = getYearMonths(2026);
|
||||
expect(months[0].endDate).toBe('2026-03-04'); // 惊蛰 2026-03-05 前一天
|
||||
expect(months[1].ganzhi).toBe('辛卯'); // 惊蛰起卯月
|
||||
});
|
||||
|
||||
it('all month ganzhi match the 五虎遁 sequence', () => {
|
||||
const months = getYearMonths(2026);
|
||||
const seq = ['庚寅', '辛卯', '壬辰', '癸巳', '甲午', '乙未', '丙申', '丁酉', '戊戌', '己亥', '庚子', '辛丑'];
|
||||
expect(months.map(m => m.ganzhi)).toEqual(seq);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 袁天罡称骨算命法 (Bone Weight Fortune Telling)
|
||||
* Based on year/month/day/hour pillars to calculate total "bone weight"
|
||||
* Each unit = 钱 (1两 = 10钱)
|
||||
*/
|
||||
|
||||
export interface BoneWeightResult {
|
||||
yearWeight: number; // in 钱
|
||||
monthWeight: number;
|
||||
dayWeight: number;
|
||||
hourWeight: number;
|
||||
totalWeight: number; // in 钱
|
||||
totalLiang: number; // 两
|
||||
totalQian: number; // 钱
|
||||
interpretation: string; // The fate poem/interpretation
|
||||
fortune: '上上' | '上' | '中上' | '中' | '中下' | '下';
|
||||
}
|
||||
|
||||
// Year bone weight table (by 干支 year stem-branch)
|
||||
// 主流称骨年表(百度百科/网易/算准网等通行版本):year % 60 → weight in 钱
|
||||
const YEAR_WEIGHTS: Record<number, number> = {
|
||||
0: 12, 1: 9, 2: 6, 3: 7, 4: 12, 5: 5, 6: 9, 7: 8, 8: 7, 9: 8,
|
||||
10: 15, 11: 9, 12: 16, 13: 8, 14: 8, 15: 19, 16: 12, 17: 6, 18: 8, 19: 7,
|
||||
20: 5, 21: 15, 22: 6, 23: 16, 24: 15, 25: 7, 26: 9, 27: 12, 28: 10, 29: 7,
|
||||
30: 15, 31: 6, 32: 5, 33: 14, 34: 14, 35: 9, 36: 7, 37: 7, 38: 9, 39: 12,
|
||||
40: 8, 41: 7, 42: 13, 43: 5, 44: 14, 45: 5, 46: 9, 47: 17, 48: 5, 49: 7,
|
||||
50: 12, 51: 8, 52: 8, 53: 6, 54: 19, 55: 6, 56: 8, 57: 16, 58: 10, 59: 6,
|
||||
};
|
||||
|
||||
// Month bone weight (lunar month 1-12)
|
||||
const MONTH_WEIGHTS: Record<number, number> = {
|
||||
1: 6, 2: 7, 3: 18, 4: 9, 5: 5, 6: 16,
|
||||
7: 9, 8: 15, 9: 18, 10: 8, 11: 9, 12: 5,
|
||||
};
|
||||
|
||||
// Day bone weight (lunar day 1-30)
|
||||
const DAY_WEIGHTS: Record<number, number> = {
|
||||
1: 5, 2: 10, 3: 8, 4: 15, 5: 16, 6: 15, 7: 8, 8: 16, 9: 8, 10: 16,
|
||||
11: 9, 12: 17, 13: 8, 14: 17, 15: 10, 16: 8, 17: 9, 18: 18, 19: 5, 20: 15,
|
||||
21: 10, 22: 9, 23: 8, 24: 9, 25: 15, 26: 18, 27: 7, 28: 8, 29: 16, 30: 6,
|
||||
};
|
||||
|
||||
// Hour bone weight (时辰, 地支 index 0-11)
|
||||
const HOUR_WEIGHTS: Record<number, number> = {
|
||||
0: 16, // 子时 23-01
|
||||
1: 6, // 丑时 01-03
|
||||
2: 7, // 寅时 03-05
|
||||
3: 10, // 卯时 05-07
|
||||
4: 9, // 辰时 07-09
|
||||
5: 16, // 巳时 09-11
|
||||
6: 10, // 午时 11-13
|
||||
7: 8, // 未时 13-15
|
||||
8: 8, // 申时 15-17
|
||||
9: 9, // 酉时 17-19
|
||||
10: 6, // 戌时 19-21
|
||||
11: 6, // 亥时 21-23
|
||||
};
|
||||
|
||||
// Interpretation for each total weight (in 钱)
|
||||
const INTERPRETATIONS: Record<number, { text: string; fortune: string }> = {
|
||||
21: { text: '短命非业谓大凶,平生灾难事重重,凶祸频临陷逆境,终世困苦事不成。', fortune: '下' },
|
||||
22: { text: '身寒骨冷苦伶仃,此命推来行乞人,劳劳碌碌无度日,终年打拱过平生。', fortune: '下' },
|
||||
23: { text: '此命推来骨格轻,求谋作事事难成,妻儿兄弟应难许,别处他乡作散人。', fortune: '下' },
|
||||
24: { text: '此命推来福禄无,门庭困苦总难荣,六亲骨肉皆无靠,流浪他乡作老翁。', fortune: '下' },
|
||||
25: { text: '此命推来祖业微,门庭营度似稀奇,六亲骨肉如冰炭,一世勤劳自把持。', fortune: '中下' },
|
||||
26: { text: '平生衣禄苦中求,独自营谋事不休,离祖出门宜早计,晚来衣禄自无休。', fortune: '中下' },
|
||||
27: { text: '一生作事少商量,难靠祖宗怎主张,独马单枪空做去,早年晚岁总无长。', fortune: '中下' },
|
||||
28: { text: '一生行事似飘蓬,祖宗产业在梦中,若不过房改名姓,也当移徒二三通。', fortune: '中下' },
|
||||
29: { text: '初年运限未曾亨,纵有功名在后成,须过四旬才可立,移居改姓始为良。', fortune: '中' },
|
||||
30: { text: '劳劳碌碌苦中求,东奔西走何日休,若使终身勤与俭,老来稍可免忧愁。', fortune: '中' },
|
||||
31: { text: '忙忙碌碌苦中求,何日云开见日头,难得祖基家可立,中年衣食渐无忧。', fortune: '中' },
|
||||
32: { text: '初年运蹇事难谋,渐有财源如水流,到得中年衣食旺,那时名利一齐收。', fortune: '中上' },
|
||||
33: { text: '早年做事事难成,百年勤劳枉费心,半世自如流水去,后来运到始得金。', fortune: '中上' },
|
||||
34: { text: '此命福气果如何,僧道门中衣禄多,离祖出家方为妙,朝晚拜佛念弥陀。', fortune: '中上' },
|
||||
35: { text: '生平福量不周全,祖业根基觉少传,营事生涯宜守旧,时来衣食胜从前。', fortune: '中' },
|
||||
36: { text: '不须劳碌过平生,独自成家福不轻,早有福星常照命,任君行去百般成。', fortune: '上' },
|
||||
37: { text: '此命般般事不成,弟兄少力自孤行,虽然祖业须微有,来得明时去不明。', fortune: '中下' },
|
||||
38: { text: '一身骨肉最清高,早入簧门姓氏标,待到年将三十六,蓝衫脱去换红袍。', fortune: '上' },
|
||||
39: { text: '此命终身运不通,劳劳作事尽皆空,苦心竭力成家计,到得那时在梦中。', fortune: '中下' },
|
||||
40: { text: '平生衣禄是绵长,件件心中自主张,前面风霜多受过,后来必定享安康。', fortune: '上' },
|
||||
41: { text: '此命推来自不同,为人能干异凡庸,中年还有逍遥福,不比前时运未通。', fortune: '上' },
|
||||
42: { text: '得宽怀处且宽怀,何用双眉皱不开,若使中年命运济,那时名利一齐来。', fortune: '上' },
|
||||
43: { text: '为人心性最聪明,作事轩昂近贵人,衣禄一生天注定,不须劳碌是丰亨。', fortune: '上上' },
|
||||
44: { text: '万事由天莫苦求,须知福碌赖人修,当年财帛难如意,晚景欣然便不忧。', fortune: '中上' },
|
||||
45: { text: '名利推求竟若何,前番辛苦后奔波,命中难养男和女,骨肉扶持也不多。', fortune: '中' },
|
||||
46: { text: '东西南北尽皆通,出姓移居更觉隆,衣禄无穷无数定,中年晚景一般同。', fortune: '上' },
|
||||
47: { text: '此命推求旺末年,妻荣子贵自怡然,平生原有滔滔福,可卜财源若水泉。', fortune: '上' },
|
||||
48: { text: '初年运道未曾通,几许蹉跎命亦穷,兄弟六亲无依靠,一生事业晚来整。', fortune: '中' },
|
||||
49: { text: '此命推来福不轻,自成自立显门庭,从来富贵人钦敬,使婢差奴过一生。', fortune: '上' },
|
||||
50: { text: '为利为名终日劳,中年福禄也多遭,老来自有财星照,不比前番目下高。', fortune: '上' },
|
||||
51: { text: '一世荣华事事通,不须劳碌自亨通,兄弟叔侄皆如意,家业成时福禄宏。', fortune: '上' },
|
||||
52: { text: '一世亨通事事能,不须劳苦自然宁,宗族有光欣喜甚,家产丰盈自称心。', fortune: '上' },
|
||||
53: { text: '此格推来福泽宏,兴家立业在其中,一生衣食安排定,却是人间一福翁。', fortune: '上' },
|
||||
54: { text: '此命推来厚且清,诗书满腹看功成,丰衣足食自然稳,正是人间有福人。', fortune: '上上' },
|
||||
55: { text: '走马扬鞭争利名,少年作事费筹论,一朝福禄源源至,富贵荣华显六亲。', fortune: '上' },
|
||||
56: { text: '此格推来礼义通,一身福禄用无穷,甜酸苦辣皆尝过,滚滚财源稳且丰。', fortune: '上' },
|
||||
57: { text: '福禄丰盈万事全,一身荣耀乐天年,名扬威震人争羡,此世逍遥宛似仙。', fortune: '上上' },
|
||||
58: { text: '平生福禄自然来,名利双全福禄偕,雁塔题名为贵客,紫袍玉带走金阶。', fortune: '上' },
|
||||
59: { text: '细推此格妙且清,必定财高礼义通,甲第之中应有分,扬鞭走马显威荣。', fortune: '上' },
|
||||
60: { text: '一朝金榜快题名,显祖荣宗大器成,衣禄定然无欠缺,田园财帛更丰盈。', fortune: '上上' },
|
||||
61: { text: '不作朝中金榜客,定为世上一财翁,聪明天赋经书熟,名显高科自是荣。', fortune: '上上' },
|
||||
62: { text: '此命生来福不穷,读书必定显亲宗,紫衣玉带为卿相,富贵荣华孰与同。', fortune: '上上' },
|
||||
63: { text: '命主为官福禄长,得来富贵定非常,名题雁塔传金榜,定中高科天下扬。', fortune: '上上' },
|
||||
64: { text: '此命生成福不轻,读书必定有功名,果然富贵前生定,一世荣华事事成。', fortune: '上上' },
|
||||
65: { text: '细推此命福不轻,安国安邦极品人,文纷雕梁徽富贵,威声照耀四方闻。', fortune: '上上' },
|
||||
66: { text: '此命推来福且宏,荣华富贵自然通,命中注定衣禄足,一世亨通稳且丰。', fortune: '上上' },
|
||||
67: { text: '此命生来福自宏,田园家业最高隆,平生衣禄丰盈足,一世荣华万事通。', fortune: '上上' },
|
||||
68: { text: '富贵荣华莫强求,强求不出反成羞,有福之人还自至,无福之人反成忧。', fortune: '中' },
|
||||
69: { text: '君是人间前禄星,一生富贵众人钦,纵然福禄由天定,安享荣华过一生。', fortune: '上上' },
|
||||
70: { text: '此命推来福禄宏,不须劳碌过平生,妻儿和顺皆如意,家道兴隆福自成。', fortune: '上' },
|
||||
71: { text: '此命生来大不同,公侯卿相在其中,一生自有逍遥福,富贵荣华极品隆。', fortune: '上上' },
|
||||
72: { text: '此命生来福泽长,兴家立业有祯祥,一生自有逍遥福,富贵荣华极品良。', fortune: '上上' },
|
||||
};
|
||||
|
||||
/** Calculate Bone Weight Fortune from lunar calendar data */
|
||||
export function calculateBoneWeight(
|
||||
lunarYearGanzhiIndex: number, // 0-59 六十甲子 index
|
||||
lunarMonth: number, // 1-12
|
||||
lunarDay: number, // 1-30
|
||||
earthBranchHourIndex: number, // 0-11 地支时辰 index
|
||||
): BoneWeightResult {
|
||||
const yearWt = YEAR_WEIGHTS[lunarYearGanzhiIndex] || 9;
|
||||
const monthWt = MONTH_WEIGHTS[lunarMonth] || 9;
|
||||
const dayWt = DAY_WEIGHTS[lunarDay] || 9;
|
||||
const hourWt = HOUR_WEIGHTS[earthBranchHourIndex] || 9;
|
||||
|
||||
const totalWeight = yearWt + monthWt + dayWt + hourWt;
|
||||
const totalLiang = Math.floor(totalWeight / 10);
|
||||
const totalQian = totalWeight % 10;
|
||||
|
||||
// Find closest interpretation
|
||||
const interpretation = findInterpretation(totalWeight);
|
||||
|
||||
return {
|
||||
yearWeight: yearWt,
|
||||
monthWeight: monthWt,
|
||||
dayWeight: dayWt,
|
||||
hourWeight: hourWt,
|
||||
totalWeight,
|
||||
totalLiang,
|
||||
totalQian,
|
||||
interpretation: interpretation.text,
|
||||
fortune: interpretation.fortune as BoneWeightResult['fortune'],
|
||||
};
|
||||
}
|
||||
|
||||
function findInterpretation(totalQian: number): { text: string; fortune: string } {
|
||||
// Direct match
|
||||
if (INTERPRETATIONS[totalQian]) return INTERPRETATIONS[totalQian];
|
||||
|
||||
// Find closest
|
||||
const keys = Object.keys(INTERPRETATIONS).map(Number).sort((a, b) => a - b);
|
||||
let closest = keys[0];
|
||||
let minDiff = Math.abs(totalQian - closest);
|
||||
for (const k of keys) {
|
||||
const diff = Math.abs(totalQian - k);
|
||||
if (diff < minDiff) { minDiff = diff; closest = k; }
|
||||
}
|
||||
|
||||
return INTERPRETATIONS[closest] || { text: '命格推来,自有天定', fortune: '中' };
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 佛教节日(农历)— tyme4ts 不提供,这里维护常用重大佛教日期表
|
||||
*/
|
||||
|
||||
const BUDDHIST_FESTIVALS: Record<string, string> = {
|
||||
'1-1': '弥勒菩萨圣诞',
|
||||
'2-8': '释迦牟尼佛出家日',
|
||||
'2-15': '释迦牟尼佛涅槃日',
|
||||
'2-19': '观音菩萨圣诞',
|
||||
'2-21': '普贤菩萨圣诞',
|
||||
'3-16': '准提菩萨圣诞',
|
||||
'4-4': '文殊菩萨圣诞',
|
||||
'4-8': '释迦牟尼佛圣诞(浴佛节)',
|
||||
'5-3': '伽蓝菩萨圣诞',
|
||||
'6-3': '韦驮菩萨圣诞',
|
||||
'6-19': '观音菩萨成道日',
|
||||
'7-13': '大势至菩萨圣诞',
|
||||
'7-15': '盂兰盆节(佛欢喜日)',
|
||||
'7-30': '地藏菩萨圣诞',
|
||||
'8-22': '燃灯佛圣诞',
|
||||
'9-19': '观音菩萨出家日',
|
||||
'9-30': '药师佛圣诞',
|
||||
'10-5': '达摩祖师诞辰',
|
||||
'11-17': '阿弥陀佛圣诞',
|
||||
'12-8': '释迦牟尼佛成道日(腊八)',
|
||||
'12-29': '华严菩萨圣诞',
|
||||
};
|
||||
|
||||
/** Get Buddhist festival name by lunar month/day (闰月不重复过节) */
|
||||
export function getBuddhistFestival(lunarMonth: number, lunarDay: number): string | null {
|
||||
return BUDDHIST_FESTIVALS[`${lunarMonth}-${lunarDay}`] || null;
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { SolarDay } from 'tyme4ts';
|
||||
import type { EightCharInfo, PillarInfo } from '../types/bazi';
|
||||
import type { DailyFortuneResult, PillarRelationship } from '../types/fortune';
|
||||
import {
|
||||
getBranchRelationship,
|
||||
getTenStarRelationship,
|
||||
checkStemCombine,
|
||||
checkStemOpposite,
|
||||
} from './relationship';
|
||||
|
||||
const PILLAR_LABELS: Record<string, string> = {
|
||||
year: '年柱', month: '月柱', day: '日柱', hour: '时柱',
|
||||
};
|
||||
|
||||
const PILLAR_KEYS = ['year', 'month', 'day', 'hour'] as const;
|
||||
|
||||
export function calculateDailyFortune(userBazi: EightCharInfo, date: Date): DailyFortuneResult {
|
||||
const solarDay = SolarDay.fromYmd(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
const lunarDay = solarDay.getLunarDay();
|
||||
const lunarHour = lunarDay.getHours()[6];
|
||||
const dayEightChar = lunarHour.getEightChar();
|
||||
|
||||
const dayPillars: Record<string, { ganzhi: string; stem: string; branch: string }> = {
|
||||
year: { ganzhi: dayEightChar.getYear().getName(), stem: dayEightChar.getYear().getHeavenStem().getName(), branch: dayEightChar.getYear().getEarthBranch().getName() },
|
||||
month: { ganzhi: dayEightChar.getMonth().getName(), stem: dayEightChar.getMonth().getHeavenStem().getName(), branch: dayEightChar.getMonth().getEarthBranch().getName() },
|
||||
day: { ganzhi: dayEightChar.getDay().getName(), stem: dayEightChar.getDay().getHeavenStem().getName(), branch: dayEightChar.getDay().getEarthBranch().getName() },
|
||||
hour: { ganzhi: dayEightChar.getHour().getName(), stem: dayEightChar.getHour().getHeavenStem().getName(), branch: dayEightChar.getHour().getEarthBranch().getName() },
|
||||
};
|
||||
|
||||
const userPillars: Record<string, PillarInfo> = {
|
||||
year: userBazi.yearPillar, month: userBazi.monthPillar, day: userBazi.dayPillar, hour: userBazi.hourPillar,
|
||||
};
|
||||
|
||||
const relationships: PillarRelationship[] = [];
|
||||
|
||||
for (const key of PILLAR_KEYS) {
|
||||
const userPillar = userPillars[key]; const dayP = dayPillars[key];
|
||||
const stemTenStar = getTenStarRelationship(userPillar.heavenStem, dayP.stem);
|
||||
const stemCombine = checkStemCombine(userPillar.heavenStem, dayP.stem);
|
||||
const stemOpposite = checkStemOpposite(userPillar.heavenStem, dayP.stem);
|
||||
const branchRel = getBranchRelationship(userPillar.earthBranch, dayP.branch);
|
||||
|
||||
let score = 0;
|
||||
if (stemTenStar) {
|
||||
const good = ['正印','偏印','食神','正财','偏财','正官'];
|
||||
const bad = ['七杀','劫财','伤官'];
|
||||
if (good.includes(stemTenStar)) score += 4;
|
||||
else if (bad.includes(stemTenStar)) score -= 3;
|
||||
else score += 1;
|
||||
}
|
||||
if (stemCombine) score += 5;
|
||||
if (stemOpposite) score -= 6;
|
||||
if (branchRel.combine) score += 4;
|
||||
if (branchRel.threeCombine) score += 3;
|
||||
if (branchRel.opposite) score -= 5;
|
||||
if (branchRel.harm) score -= 4;
|
||||
if (branchRel.punish) score -= 3;
|
||||
score = Math.max(-10, Math.min(10, score));
|
||||
|
||||
relationships.push({
|
||||
pillar: key, pillarLabel: PILLAR_LABELS[key],
|
||||
userGanzhi: userPillar.ganzhi, dayGanzhi: dayP.ganzhi,
|
||||
stemTenStar, stemCombine, stemOpposite,
|
||||
branchCombine: branchRel.combine, branchThreeCombine: branchRel.threeCombine,
|
||||
branchOpposite: branchRel.opposite, branchHarm: branchRel.harm,
|
||||
branchPunish: branchRel.punish, branchFormation: branchRel.formation,
|
||||
score,
|
||||
});
|
||||
}
|
||||
|
||||
const weightedScore =
|
||||
relationships[0].score * 0.20 + relationships[1].score * 0.25 +
|
||||
relationships[2].score * 0.40 + relationships[3].score * 0.15;
|
||||
const overallScore = Math.max(-100, Math.min(100, Math.round(weightedScore * 10)));
|
||||
const scoreLevel: DailyFortuneResult['scoreLevel'] =
|
||||
overallScore >= 50 ? 'great' : overallScore >= 20 ? 'good' :
|
||||
overallScore >= -20 ? 'fair' : overallScore >= -50 ? 'poor' : 'bad';
|
||||
|
||||
const luckyAspects = generateLuckyAspects(relationships);
|
||||
const unluckyAspects = generateUnluckyAspects(relationships);
|
||||
const suggestions = generateSuggestions(relationships, userBazi, overallScore);
|
||||
const affectedAreas = determineAffectedAreas(relationships);
|
||||
const luckyMeta = computeLuckyMeta(userBazi, dayPillars.day.ganzhi, overallScore);
|
||||
const categoryScores = computeCategoryScores(relationships);
|
||||
|
||||
const lunarDateStr = `${lunarDay.getLunarMonth().getLunarYear().getYear()}年${lunarDay.getLunarMonth().getName()}${lunarDay.getName()}`;
|
||||
|
||||
return {
|
||||
date: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`,
|
||||
lunarDate: lunarDateStr, dayGanzhi: dayPillars.day.ganzhi,
|
||||
overallScore, scoreLevel, pillarRelationships: relationships,
|
||||
luckyAspects, unluckyAspects, suggestions, affectedAreas, luckyMeta, categoryScores,
|
||||
};
|
||||
}
|
||||
|
||||
function generateLuckyAspects(relationships: PillarRelationship[]): string[] {
|
||||
const seen = new Set<string>(); const aspects: string[] = [];
|
||||
const tenStarPlain: Record<string, string> = {
|
||||
'正官':'事业运不错,工作上容易得到认可和赏识',
|
||||
'七杀':'进取心强,适合挑战难题和竞争性事务',
|
||||
'正印':'学习运佳,适合读书充电或向长辈请教',
|
||||
'偏印':'灵感丰富,适合研究和创意工作',
|
||||
'食神':'心情舒畅,适合休闲放松和享受生活',
|
||||
'伤官':'表达欲强,适合沟通交流和展示自我',
|
||||
'正财':'财运平稳,适合处理日常财务和长期投资',
|
||||
'偏财':'偏财运佳,可能有意外之财或投资收益',
|
||||
'比肩':'人际关系和睦,容易得到朋友和同事的帮助',
|
||||
'劫财':'社交活跃,适合参加聚会和团队活动',
|
||||
};
|
||||
const hasStemCombine = relationships.some(r => r.stemCombine);
|
||||
const hasBranchCombine = relationships.some(r => r.branchCombine || r.branchThreeCombine);
|
||||
|
||||
for (const rel of relationships) {
|
||||
if (rel.stemTenStar && tenStarPlain[rel.stemTenStar] && !seen.has(rel.stemTenStar)) {
|
||||
aspects.push(tenStarPlain[rel.stemTenStar]); seen.add(rel.stemTenStar);
|
||||
}
|
||||
}
|
||||
if (hasStemCombine && !seen.has('combine')) { aspects.push('天时地利人和,容易遇到贵人相助和好机会'); seen.add('combine'); }
|
||||
if (hasBranchCombine && !seen.has('branch')) { aspects.push('身边关系和谐,适合与人合作或开展团队项目'); seen.add('branch'); }
|
||||
if (aspects.length === 0) aspects.push('今天整体运势平稳,适合按部就班处理日常事务');
|
||||
return aspects.slice(0, 4);
|
||||
}
|
||||
|
||||
function generateUnluckyAspects(relationships: PillarRelationship[]): string[] {
|
||||
const aspects: string[] = [];
|
||||
const hasOpposite = relationships.some(r => r.branchOpposite);
|
||||
const hasHarm = relationships.some(r => r.branchHarm);
|
||||
const hasPunish = relationships.some(r => r.branchPunish);
|
||||
const hasStemOpposite = relationships.some(r => r.stemOpposite);
|
||||
const hasQiSha = relationships.some(r => r.stemTenStar === '七杀');
|
||||
const hasJieCai = relationships.some(r => r.stemTenStar === '劫财');
|
||||
const hasShangGuan = relationships.some(r => r.stemTenStar === '伤官');
|
||||
|
||||
if (hasStemOpposite) aspects.push('今天可能遇到突发变化或计划被打乱,保持冷静和弹性很重要');
|
||||
if (hasOpposite) aspects.push('与他人意见不合的可能性较高,尽量避免争执和正面冲突');
|
||||
if (hasHarm) aspects.push('留意身边的小人和是非,不要轻易透露自己的计划和秘密');
|
||||
if (hasPunish) aspects.push('容易说错话或做出不恰当的举动,多听少说更安全');
|
||||
if (hasQiSha) aspects.push('压力较大的一天,注意调节情绪,重要决策可暂缓');
|
||||
if (hasJieCai) aspects.push('财务方面要谨慎,不宜大额消费或借钱给别人');
|
||||
if (hasShangGuan) aspects.push('表达时注意方式方法,容易词不达意引起误会');
|
||||
if (aspects.length === 0) aspects.push('今天没有什么特别需要注意的,保持平常心即可');
|
||||
return aspects.slice(0, 3);
|
||||
}
|
||||
|
||||
function generateSuggestions(
|
||||
relationships: PillarRelationship[],
|
||||
userBazi: EightCharInfo,
|
||||
overallScore: number,
|
||||
): string[] {
|
||||
const s: string[] = [];
|
||||
const de = userBazi.dayMasterElement;
|
||||
const colors: Record<string,string> = { '木':'绿色/青色','火':'红色/紫色','土':'黄色/棕色','金':'白色/浅色','水':'蓝色/黑色' };
|
||||
const dirs: Record<string,string> = { '木':'东方','火':'南方','土':'中央','金':'西方','水':'北方' };
|
||||
const hasConflict = relationships.some(r => r.branchOpposite || r.stemOpposite);
|
||||
const hasCombine = relationships.some(r => r.branchCombine || r.stemCombine);
|
||||
const hasHarm = relationships.some(r => r.branchHarm);
|
||||
const hasPunish = relationships.some(r => r.branchPunish);
|
||||
|
||||
const goodPool = [
|
||||
de ? `👔 幸运色${colors[de]||'浅色'},穿对颜色运气更好` : `👔 今天穿浅色衣服心情更好`,
|
||||
`✅ 适合推进重要事项,果断决策会有好结果`,
|
||||
`📝 把计划写下来按部就班执行,效率翻倍`,
|
||||
`🎯 专注一件事比多线作战更有效果`,
|
||||
`🤝 适合约重要的人见面,贵人运在线`,
|
||||
`🌅 早起一点早晨的效率最高`,
|
||||
`📊 适合总结和复盘能发现新机会`,
|
||||
`🏃 运动或户外活动对运势有加成`,
|
||||
];
|
||||
const fairPool = [
|
||||
`💡 运势平稳该做什么就做什么`,
|
||||
`☕ 适合处理日常事务不宜做重大改变`,
|
||||
`📖 适合学习充电多读多听少说`,
|
||||
`🧹 整理收纳会让心情变好`,
|
||||
`🍵 节奏放慢一点稳扎稳打`,
|
||||
`📞 适合和老朋友聊聊天`,
|
||||
];
|
||||
const poorPool = [
|
||||
`🧘 适合保守行事多观察多思考`,
|
||||
`☕ 适合独处和复盘少社交`,
|
||||
`🛡️ 以守成为主避免冲动`,
|
||||
`📿 冥想或听音乐能化解负面情绪`,
|
||||
`🙏 遇到不顺心的事深呼吸三秒`,
|
||||
`📝 把想法写下来明天再行动`,
|
||||
];
|
||||
const badPool = [
|
||||
`🛡️ 宜静不宜动重要决策能缓则缓`,
|
||||
`🚫 避免签合同或做出重大承诺`,
|
||||
`🧘 适合冥想休息养精蓄锐`,
|
||||
`📿 去清净处走走转换气场`,
|
||||
`💤 早点休息好睡眠是最好的转运`,
|
||||
`🙏 好事多磨心态会好很多`,
|
||||
];
|
||||
|
||||
let pool = fairPool;
|
||||
if (overallScore >= 30) pool = goodPool;
|
||||
else if (overallScore >= 0) pool = fairPool;
|
||||
else if (overallScore >= -30) pool = poorPool;
|
||||
else pool = badPool;
|
||||
|
||||
const seed = new Date().getDate() + new Date().getMonth() * 31;
|
||||
s.push(pool[seed % pool.length]);
|
||||
s.push(pool[(seed + 5) % pool.length]);
|
||||
|
||||
if (overallScore >= 10 && de && dirs[de]) s.push(`🧭 向${dirs[de]}行事有意外好运`);
|
||||
if (hasCombine && !hasConflict) s.push('🤝 人际关系活跃适合合作洽谈');
|
||||
else if (hasConflict && hasHarm) s.push('⚠️ 人际关系有暗礁保持微笑少说话');
|
||||
else if (hasConflict) s.push('🙏 意见不合时先认同再引导');
|
||||
else if (hasPunish) s.push('💬 说话前过遍脑子今天容易说错话');
|
||||
|
||||
const seen = new Set<string>();
|
||||
const unique = s.filter(t => { if (seen.has(t)) return false; seen.add(t); return true; });
|
||||
return unique.slice(0, 4);
|
||||
}
|
||||
|
||||
function computeLuckyMeta(userBazi: EightCharInfo, dayGanzhi: string, score: number) {
|
||||
const de = userBazi.dayMasterElement;
|
||||
const colorMap: Record<string, string[]> = { '木': ['绿色','青色','翠绿'], '火': ['红色','紫色','粉色'], '土': ['黄色','棕色','米色'], '金': ['白色','银色','浅灰'], '水': ['蓝色','黑色','深灰'] };
|
||||
const dirMap: Record<string, string> = { '木': '东方', '火': '南方', '土': '中央', '金': '西方', '水': '北方' };
|
||||
const actMap: Record<string, string[]> = {
|
||||
'木': ['户外散步','园艺','阅读','写作'], '火': ['社交','演讲','创意','运动'], '土': ['整理收纳','理财','烹饪','冥想'],
|
||||
'金': ['商务洽谈','签约','购物','美容'], '水': ['学习进修','旅行','游泳','听音乐'],
|
||||
};
|
||||
const colors = colorMap[de] || ['红色','白色'];
|
||||
const direction = dirMap[de] || '东方';
|
||||
const seed = parseInt(dayGanzhi.charCodeAt(0).toString()) + new Date().getDate();
|
||||
const n1 = (seed % 9) + 1;
|
||||
const n2 = ((seed * 3 + 7) % 9) + 1;
|
||||
const activities = actMap[de] || ['运动','阅读'];
|
||||
const activity = activities[seed % activities.length];
|
||||
return {
|
||||
colors: [colors[0], colors[1]],
|
||||
numbers: [n1, n2],
|
||||
direction,
|
||||
element: de,
|
||||
activity: score >= 0 ? activity : activities[(seed + 3) % activities.length],
|
||||
};
|
||||
}
|
||||
|
||||
function computeCategoryScores(relationships: PillarRelationship[]) {
|
||||
const tenStarLove: Record<string,number> = { '正官':8,'七杀':-2,'正印':3,'偏印':2,'食神':6,'伤官':5,'正财':4,'偏财':3 };
|
||||
const tenStarCareer: Record<string,number> = { '正官':10,'七杀':8,'正印':5,'偏印':4,'食神':3,'伤官':2,'正财':2 };
|
||||
const tenStarWealth: Record<string,number> = { '正财':10,'偏财':8,'食神':6,'伤官':4,'正官':2,'七杀':-2 };
|
||||
const tenStarHealth: Record<string,number> = { '正印':8,'偏印':6,'比肩':5,'食神':4,'七杀':-4,'伤官':-2 };
|
||||
|
||||
let love=0, career=0, wealth=0, health=0;
|
||||
for (const rel of relationships) {
|
||||
const ts = rel.stemTenStar;
|
||||
if (!ts) continue;
|
||||
const w = rel.pillar === 'day' ? 3 : rel.pillar === 'month' ? 2 : 1;
|
||||
love += (tenStarLove[ts]||0) * w + (rel.branchHarm?-5:0) * w + (rel.branchCombine?4:0) * w;
|
||||
career += (tenStarCareer[ts]||0) * w + (rel.branchOpposite?-4:0) * w + (rel.branchThreeCombine?5:0) * w;
|
||||
wealth += (tenStarWealth[ts]||0) * w + (rel.stemCombine?5:0) * w;
|
||||
health += (tenStarHealth[ts]||0) * w + (rel.branchOpposite?-3:0) * w;
|
||||
}
|
||||
// Normalize to -100..100
|
||||
const clamp = (v:number) => Math.max(-100, Math.min(100, Math.round(v * 2.5)));
|
||||
return { love: clamp(love), career: clamp(career), wealth: clamp(wealth), health: clamp(health) };
|
||||
}
|
||||
|
||||
function determineAffectedAreas(relationships: PillarRelationship[]): string[] {
|
||||
const areas = new Set<string>();
|
||||
const areaMapping: Record<string, string> = {
|
||||
'正财':'财运','偏财':'偏财/投资','正官':'事业','七杀':'事业/压力',
|
||||
'正印':'学业','偏印':'学业/智慧','食神':'创作/享乐','伤官':'口才/表达',
|
||||
'比肩':'人际关系','劫财':'竞争/人际',
|
||||
};
|
||||
for (const rel of relationships) {
|
||||
if (rel.stemTenStar && areaMapping[rel.stemTenStar]) areas.add(areaMapping[rel.stemTenStar]);
|
||||
if (rel.branchOpposite && rel.pillar === 'year') areas.add('长辈/根基');
|
||||
if (rel.branchOpposite && rel.pillar === 'month') areas.add('事业/家庭');
|
||||
if (rel.branchOpposite && rel.pillar === 'day') areas.add('婚姻/健康');
|
||||
if (rel.branchOpposite && rel.pillar === 'hour') areas.add('子女/晚年');
|
||||
}
|
||||
return areas.size > 0 ? [...areas] : ['综合运势'];
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { EightCharInfo } from '../types/bazi';
|
||||
|
||||
/** Five Element profile */
|
||||
export interface ElementProfile {
|
||||
wood: number;
|
||||
fire: number;
|
||||
earth: number;
|
||||
metal: number;
|
||||
water: number;
|
||||
total: number;
|
||||
dominant: string;
|
||||
weakest: string;
|
||||
isBalanced: boolean;
|
||||
}
|
||||
|
||||
/** Analyze the five element balance in a Bazi chart */
|
||||
export function analyzeElementBalance(bazi: EightCharInfo): ElementProfile {
|
||||
const pillars = [bazi.yearPillar, bazi.monthPillar, bazi.dayPillar, bazi.hourPillar];
|
||||
|
||||
const counts: Record<string, number> = {
|
||||
'木': 0,
|
||||
'火': 0,
|
||||
'土': 0,
|
||||
'金': 0,
|
||||
'水': 0,
|
||||
};
|
||||
|
||||
for (const pillar of pillars) {
|
||||
// Count stem element
|
||||
if (pillar.elementStem && counts[pillar.elementStem] !== undefined) {
|
||||
counts[pillar.elementStem] += 1;
|
||||
}
|
||||
// Count branch element
|
||||
if (pillar.elementBranch && counts[pillar.elementBranch] !== undefined) {
|
||||
counts[pillar.elementBranch] += 1;
|
||||
}
|
||||
// Count hidden stem elements (half weight)
|
||||
for (const hs of pillar.hideStems) {
|
||||
// Infer element from stem name
|
||||
const element = inferElementFromStem(hs.stem);
|
||||
if (element && counts[element] !== undefined) {
|
||||
counts[element] += 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = counts['木'] + counts['火'] + counts['土'] + counts['金'] + counts['水'];
|
||||
|
||||
// Find dominant and weakest
|
||||
let dominant = '木';
|
||||
let weakest = '木';
|
||||
let maxCount = 0;
|
||||
let minCount = Infinity;
|
||||
|
||||
for (const [element, count] of Object.entries(counts)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
dominant = element;
|
||||
}
|
||||
if (count < minCount) {
|
||||
minCount = count;
|
||||
weakest = element;
|
||||
}
|
||||
}
|
||||
|
||||
// Balance check: each element should be within 30% of ideal (20% each)
|
||||
const ideal = total / 5;
|
||||
let isBalanced = true;
|
||||
for (const count of Object.values(counts)) {
|
||||
if (Math.abs(count - ideal) > ideal * 0.4) {
|
||||
isBalanced = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
wood: counts['木'] || 0,
|
||||
fire: counts['火'] || 0,
|
||||
earth: counts['土'] || 0,
|
||||
metal: counts['金'] || 0,
|
||||
water: counts['水'] || 0,
|
||||
total,
|
||||
dominant,
|
||||
weakest,
|
||||
isBalanced,
|
||||
};
|
||||
}
|
||||
|
||||
/** Infer five element from heavenly stem name */
|
||||
function inferElementFromStem(stem: string): string | null {
|
||||
const stemElements: Record<string, string> = {
|
||||
'甲': '木', '乙': '木',
|
||||
'丙': '火', '丁': '火',
|
||||
'戊': '土', '己': '土',
|
||||
'庚': '金', '辛': '金',
|
||||
'壬': '水', '癸': '水',
|
||||
};
|
||||
return stemElements[stem] || null;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 大运/流年/流月/流日 与日主的生克冲合分析
|
||||
* 将任意干支与日主(日干/日支)比较,输出十神、五行生克、天干合冲、地支关系与简化吉凶。
|
||||
*/
|
||||
import { getTenStarRelationship, checkStemCombine, checkStemOpposite, getBranchRelationship } from './relationship';
|
||||
|
||||
export interface FortuneLuck {
|
||||
ganzhi: string;
|
||||
tenStar: string | null;
|
||||
/** 该柱五行 vs 日主五行:比和/生我/我生/克我/我克 */
|
||||
elementRelation: string;
|
||||
stemCombine: boolean;
|
||||
stemOpposite: boolean;
|
||||
branchCombine: boolean;
|
||||
branchThreeCombine: boolean;
|
||||
branchOpposite: boolean;
|
||||
branchHarm: boolean;
|
||||
branchPunish: boolean;
|
||||
score: number;
|
||||
level: '吉' | '凶' | '平';
|
||||
}
|
||||
|
||||
const STEM_ELEMENT: Record<string, string> = {
|
||||
'甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土',
|
||||
'己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水',
|
||||
};
|
||||
const GENERATES: Record<string, string> = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
|
||||
const KILLS: Record<string, string> = { '木': '土', '土': '水', '水': '火', '火': '金', '金': '木' };
|
||||
|
||||
const GOOD_TEN_STARS = ['正印', '偏印', '食神', '正财', '偏财', '正官'];
|
||||
const BAD_TEN_STARS = ['七杀', '劫财', '伤官'];
|
||||
|
||||
/** Analyze an external ganzhi (大运/流年/流月/流日) against the day master pillar */
|
||||
export function analyzeFortuneGanzhi(
|
||||
ganzhi: string,
|
||||
dayStem: string,
|
||||
dayBranch: string,
|
||||
): FortuneLuck {
|
||||
const stem = ganzhi[0];
|
||||
const branch = ganzhi[1];
|
||||
|
||||
const tenStar = getTenStarRelationship(dayStem, stem);
|
||||
|
||||
const el = STEM_ELEMENT[stem];
|
||||
const de = STEM_ELEMENT[dayStem];
|
||||
let elementRelation: string;
|
||||
if (!el || !de || el === de) elementRelation = '比和';
|
||||
else if (GENERATES[el] === de) elementRelation = '生我';
|
||||
else if (GENERATES[de] === el) elementRelation = '我生';
|
||||
else if (KILLS[el] === de) elementRelation = '克我';
|
||||
else elementRelation = '我克';
|
||||
|
||||
const stemCombine = checkStemCombine(stem, dayStem);
|
||||
const stemOpposite = checkStemOpposite(stem, dayStem);
|
||||
const br = getBranchRelationship(branch, dayBranch);
|
||||
|
||||
let score = 0;
|
||||
if (tenStar) {
|
||||
if (GOOD_TEN_STARS.includes(tenStar)) score += 4;
|
||||
else if (BAD_TEN_STARS.includes(tenStar)) score -= 3;
|
||||
else score += 1;
|
||||
}
|
||||
if (stemCombine) score += 5;
|
||||
if (stemOpposite) score -= 6;
|
||||
if (br.combine) score += 4;
|
||||
if (br.threeCombine) score += 3;
|
||||
if (br.opposite) score -= 5;
|
||||
if (br.harm) score -= 4;
|
||||
if (br.punish) score -= 3;
|
||||
score = Math.max(-10, Math.min(10, score));
|
||||
|
||||
const level: FortuneLuck['level'] = score >= 3 ? '吉' : score <= -3 ? '凶' : '平';
|
||||
|
||||
return {
|
||||
ganzhi,
|
||||
tenStar,
|
||||
elementRelation,
|
||||
stemCombine,
|
||||
stemOpposite,
|
||||
branchCombine: br.combine,
|
||||
branchThreeCombine: br.threeCombine,
|
||||
branchOpposite: br.opposite,
|
||||
branchHarm: br.harm,
|
||||
branchPunish: br.punish,
|
||||
score,
|
||||
level,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 梅花易数 (Plum Blossom I-Ching Divination)
|
||||
* Based on time (year, month, day, hour) to derive hexagrams
|
||||
*/
|
||||
export interface TrigramInfo {
|
||||
index: number; // 1-8 (乾兑离震巽坎艮坤)
|
||||
name: string; // Chinese name
|
||||
symbol: string; // Unicode trigram symbol
|
||||
element: string; // Five element
|
||||
direction: string; // Direction
|
||||
nature: string; // Natural phenomenon
|
||||
trait: string; // Personality trait
|
||||
body: string; // Body part
|
||||
}
|
||||
|
||||
export interface HexagramInfo {
|
||||
number: number; // 1-64
|
||||
name: string; // Chinese name e.g. "乾为天"
|
||||
upperTrigram: TrigramInfo;
|
||||
lowerTrigram: TrigramInfo;
|
||||
changingLine: number; // 1-6, which line changes
|
||||
interpretation: string; // Overall interpretation
|
||||
judgment: string; // 彖辞
|
||||
image: string; // 象辞
|
||||
lines: string[]; // 6 line interpretations
|
||||
}
|
||||
|
||||
export interface PlumBlossomResult {
|
||||
originalHexagram: HexagramInfo;
|
||||
transformedHexagram: HexagramInfo | null;
|
||||
mutualHexagram: HexagramInfo | null;
|
||||
upperTrigram: TrigramInfo;
|
||||
lowerTrigram: TrigramInfo;
|
||||
changingLine: number;
|
||||
constitution: string; // 体卦
|
||||
function: string; // 用卦
|
||||
relationship: string; // 体用关系
|
||||
}
|
||||
|
||||
// 8 Trigrams (八卦)
|
||||
const TRIGRAMS: Record<number, TrigramInfo> = {
|
||||
1: { index: 1, name: '乾', symbol: '☰', element: '金', direction: '西北', nature: '天', trait: '健', body: '首' },
|
||||
2: { index: 2, name: '兑', symbol: '☱', element: '金', direction: '西', nature: '泽', trait: '悦', body: '口' },
|
||||
3: { index: 3, name: '离', symbol: '☲', element: '火', direction: '南', nature: '火', trait: '丽', body: '目' },
|
||||
4: { index: 4, name: '震', symbol: '☳', element: '木', direction: '东', nature: '雷', trait: '动', body: '足' },
|
||||
5: { index: 5, name: '巽', symbol: '☴', element: '木', direction: '东南', nature: '风', trait: '入', body: '股' },
|
||||
6: { index: 6, name: '坎', symbol: '☵', element: '水', direction: '北', nature: '水', trait: '陷', body: '耳' },
|
||||
7: { index: 7, name: '艮', symbol: '☶', element: '土', direction: '东北', nature: '山', trait: '止', body: '手' },
|
||||
8: { index: 8, name: '坤', symbol: '☷', element: '土', direction: '西南', nature: '地', trait: '顺', body: '腹' },
|
||||
};
|
||||
|
||||
// All 64 Hexagrams (complete I-Ching)
|
||||
const H: Record<string, { name: string; judgment: string; image: string; lines: string[] }> = {
|
||||
// 乾宫八卦 (1-8)
|
||||
'1,1':{name:'乾为天',judgment:'大哉乾元,万物资始,乃统天。云行雨施,品物流形',image:'天行健,君子以自强不息',lines:['潜龙勿用','见龙在田,利见大人','君子终日乾乾,夕惕若厉','或跃在渊,无咎','飞龙在天,利见大人','亢龙有悔']},
|
||||
'1,5':{name:'天风姤',judgment:'姤,遇也,柔遇刚也。天地相遇,品物咸章',image:'天下有风,姤。后以施命诰四方',lines:['系于金柅,贞吉','包有鱼,无咎','臀无肤,其行次且','包无鱼,起凶','以杞包瓜,含章','姤其角,吝']},
|
||||
'1,7':{name:'天山遁',judgment:'遁亨,遁而亨也。刚当位而应,与时行也',image:'天下有山,遁。君子以远小人,不恶而严',lines:['遁尾厉,勿用有攸往','执之用黄牛之革','系遁,有疾厉','好遁,君子吉','嘉遁,贞吉','肥遁,无不利']},
|
||||
'1,8':{name:'天地否',judgment:'否之匪人,不利君子贞。大往小来',image:'天地不交,否。君子以俭德辟难',lines:['拔茅茹,以其汇,贞吉','包承,小人吉,大人否','包羞','有命无咎,畴离祉','休否,大人吉','倾否,先否后喜']},
|
||||
'5,8':{name:'风地观',judgment:'观,盥而不荐,有孚颙若。观天之神道而四时不忒',image:'风行地上,观。先王以省方观民设教',lines:['童观,小人无咎','窥观,利女贞','观我生进退','观国之光,利用宾于王','观我生,君子无咎','观其生,君子无咎']},
|
||||
'7,8':{name:'山地剥',judgment:'剥,剥也,柔变刚也。不利有攸往',image:'山附于地,剥。上以厚下安宅',lines:['剥床以足,蔑贞凶','剥床以辨,蔑贞凶','剥之无咎','剥床以肤,凶','贯鱼以宫人宠,无不利','硕果不食,君子得舆']},
|
||||
'3,8':{name:'火地晋',judgment:'晋,进也。明出地上,顺而丽乎大明',image:'明出地上,晋。君子以自昭明德',lines:['晋如摧如,贞吉','晋如愁如,贞吉','众允,悔亡','晋如鼫鼠,贞厉','悔亡,失得勿恤','晋其角,维用伐邑']},
|
||||
'3,1':{name:'火天大有',judgment:'大有,柔得尊位,大中而上下应之',image:'火在天上,大有。君子以遏恶扬善,顺天休命',lines:['无交害,匪咎','大车以载,有攸往','公用亨于天子','匪其彭,无咎','厥孚交如,威如','自天佑之,吉无不利']},
|
||||
// 坎宫八卦 (9-16)
|
||||
'6,6':{name:'坎为水',judgment:'习坎,重险也。水流而不盈,行险而不失其信',image:'水洊至,习坎。君子以常德行习教事',lines:['习坎,入于坎窞','坎有险,求小得','来之坎坎,险且枕','樽酒簋贰,用缶','坎不盈,祗既平','系用徽纆,寘于丛棘']},
|
||||
'6,2':{name:'水泽节',judgment:'节亨,苦节不可贞',image:'泽上有水,节。君子以制数度议德行',lines:['不出户庭,无咎','不出门庭,凶','不节若,则嗟若','安节,亨','甘节,吉','苦节,贞凶']},
|
||||
'6,4':{name:'水雷屯',judgment:'屯,刚柔始交而难生。动乎险中,大亨贞',image:'云雷屯,君子以经纶',lines:['磐桓,利居贞','屯如邅如,乘马班如','即鹿无虞,惟入于林中','乘马班如,求婚媾','屯其膏,小贞吉','乘马班如,泣血涟如']},
|
||||
'6,3':{name:'水火既济',judgment:'既济亨,小者亨也。利贞,初吉终乱',image:'水在火上,既济。君子以思患而豫防之',lines:['曳其轮,濡其尾','妇丧其茀,勿逐','高宗伐鬼方,三年克之','繻有衣袽,终日戒','东邻杀牛,不如西邻','濡其首,厉']},
|
||||
'2,3':{name:'泽火革',judgment:'革,水火相息。天地革而四时成',image:'泽中有火,革。君子以治历明时',lines:['巩用黄牛之革','巳日乃革之,征吉','征凶,贞厉','悔亡,有孚改命','大人虎变,未占有孚','君子豹变,小人革面']},
|
||||
'4,3':{name:'雷火丰',judgment:'丰,大也。明以动,故丰',image:'雷电皆至,丰。君子以折狱致刑',lines:['遇其配主,虽旬无咎','丰其蔀,日中见斗','丰其沛,日中见沬','丰其蔀,日中见斗','来章,有庆誉','丰其屋,蔀其家']},
|
||||
'8,3':{name:'地火明夷',judgment:'明夷,利艰贞。明入地中,明夷',image:'明入地中,明夷。君子以莅众用晦而明',lines:['明夷于飞,垂其翼','明夷,夷于左股','明夷于南狩,得其大首','入于左腹,获明夷之心','箕子之明夷,利贞','不明晦,初登于天']},
|
||||
'8,6':{name:'地水师',judgment:'师,众也。贞,丈人吉,无咎',image:'地中有水,师。君子以容民畜众',lines:['师出以律,否臧凶','在师中,吉无咎','师或舆尸,凶','师左次,无咎','田有禽,利执言','大君有命,开国承家']},
|
||||
// 艮宫八卦 (17-24)
|
||||
'7,7':{name:'艮为山',judgment:'艮,止也。时止则止,时行则行',image:'兼山,艮。君子以思不出其位',lines:['艮其趾,无咎','艮其腓,不拯其随','艮其限,列其夤','艮其身,无咎','艮其辅,言有序','敦艮,吉']},
|
||||
'7,3':{name:'山火贲',judgment:'贲亨,柔来而文刚,故亨',image:'山下有火,贲。君子以明庶政无敢折狱',lines:['贲其趾,舍车而徒','贲其须','贲如濡如,永贞吉','贲如皤如,白马翰如','贲于丘园,束帛戋戋','白贲,无咎']},
|
||||
'7,1':{name:'山天大畜',judgment:'大畜,刚健笃实辉光,日新其德',image:'天在山中,大畜。君子以多识前言往行',lines:['有厉,利已','舆说輹','良马逐,利艰贞','童牛之牿,元吉','豮豕之牙,吉','何天之衢,亨']},
|
||||
'7,2':{name:'山泽损',judgment:'损,损下益上,其道上行',image:'山下有泽,损。君子以惩忿窒欲',lines:['已事遄往,无咎','利贞,征凶,弗损益之','三人行则损一人','损其疾,使遄有喜','或益之十朋之龟','弗损益之,无咎']},
|
||||
'3,2':{name:'火泽睽',judgment:'睽,火动而上,泽动而下',image:'上火下泽,睽。君子以同而异',lines:['悔亡,丧马勿逐','遇主于巷,无咎','见舆曳,其牛掣','睽孤,遇元夫','悔亡,厥宗噬肤','睽孤,见豕负涂']},
|
||||
'1,2':{name:'天泽履',judgment:'履,柔履刚也。说而应乎乾',image:'上天下泽,履。君子以辩上下定民志',lines:['素履往,无咎','履道坦坦,幽人贞吉','眇能视,跛能履','履虎尾,愬愬终吉','夬履,贞厉','视履考祥,其旋元吉']},
|
||||
'5,2':{name:'风泽中孚',judgment:'中孚,柔在内而刚得中',image:'泽上有风,中孚。君子以议狱缓死',lines:['虞吉,有它不燕','鸣鹤在阴,其子和之','得敌,或鼓或罢','月几望,马匹亡','有孚挛如,无咎','翰音登于天,贞凶']},
|
||||
'5,7':{name:'风山渐',judgment:'渐,女归吉也。进得位,往有功也',image:'山上有木,渐。君子以居贤德善俗',lines:['鸿渐于干,小子厉','鸿渐于磐,饮食衎衎','鸿渐于陆,夫征不复','鸿渐于木,或得其桷','鸿渐于陵,妇三岁不孕','鸿渐于逵,其羽可用为仪']},
|
||||
// 震宫八卦 (25-32)
|
||||
'4,4':{name:'震为雷',judgment:'震亨。震来虩虩,笑言哑哑,震惊百里',image:'洊雷,震。君子以恐惧修省',lines:['震来虩虩,后笑言哑哑','震来厉,亿丧贝','震苏苏,震行无眚','震遂泥','震往来厉,亿无丧','震索索,视矍矍']},
|
||||
'4,8':{name:'雷地豫',judgment:'豫,刚应而志行,顺以动',image:'雷出地奋,豫。先王以作乐崇德',lines:['鸣豫,凶','介于石,不终日','盱豫悔,迟有悔','由豫,大有得','贞疾,恒不死','冥豫,成有渝']},
|
||||
'4,6':{name:'雷水解',judgment:'解,险以动,动而免乎险',image:'雷雨作,解。君子以赦过宥罪',lines:['无咎','田获三狐,得黄矢','负且乘,致寇至','解而拇,朋至斯孚','君子维有解,吉','公用射隼于高墉之上']},
|
||||
'4,5':{name:'雷风恒',judgment:'恒,久也。刚上而柔下',image:'雷风,恒。君子以立不易方',lines:['浚恒,贞凶','悔亡','不恒其德,或承之羞','田无禽','恒其德,贞妇人吉','振恒,凶']},
|
||||
'8,5':{name:'地风升',judgment:'柔以时升,巽而顺,刚中而应',image:'地中生木,升。君子以顺德积小以高大',lines:['允升,大吉','孚乃利用禴,无咎','升虚邑','王用亨于岐山','贞吉,升阶','冥升,利于不息之贞']},
|
||||
'6,5':{name:'水风井',judgment:'井,改邑不改井,无丧无得',image:'木上有水,井。君子以劳民劝相',lines:['井泥不食,旧井无禽','井谷射鲋,瓮敝漏','井渫不食,为我心恻','井甃,无咎','井洌,寒泉食','井收勿幕,有孚元吉']},
|
||||
'2,5':{name:'泽风大过',judgment:'大过,大者过也。栋桡,本末弱也',image:'泽灭木,大过。君子以独立不惧遁世无闷',lines:['藉用白茅,无咎','枯杨生稊,老夫得其女妻','栋桡,凶','栋隆,吉','枯杨生华,老妇得士夫','过涉灭顶,凶']},
|
||||
'2,4':{name:'泽雷随',judgment:'随,刚来而下柔,动而说',image:'泽中有雷,随。君子以向晦入宴息',lines:['官有渝,贞吉','系小子,失丈夫','系丈夫,失小子','随有获,贞凶','孚于嘉,吉','拘系之,乃从维之']},
|
||||
// 巽宫八卦 (33-40)
|
||||
'5,5':{name:'巽为风',judgment:'重巽以申命,刚巽乎中正而志行',image:'随风,巽。君子以申命行事',lines:['进退,利武人之贞','巽在床下,用史巫纷若','频巽,吝','悔亡,田获三品','贞吉,悔亡无不利','巽在床下,丧其资斧']},
|
||||
'5,1':{name:'风天小畜',judgment:'小畜,柔得位而上下应之',image:'风行天上,小畜。君子以懿文德',lines:['复自道,何其咎','牵复,吉','舆说辐,夫妻反目','有孚,血去惕出','有孚挛如,富以其邻','既雨既处,尚德载']},
|
||||
'5,3':{name:'风火家人',judgment:'家人,女正位乎内,男正位乎外',image:'风自火出,家人。君子以言有物而行有恒',lines:['闲有家,悔亡','无攸遂,在中馈','家人嗃嗃,悔厉吉','富家,大吉','王假有家,勿恤','有孚威如,终吉']},
|
||||
'5,4':{name:'风雷益',judgment:'益,损上益下,民说无疆',image:'风雷,益。君子以见善则迁有过则改',lines:['利用为大作,元吉','或益之十朋之龟','益之用凶事,无咎','中行告公从,利用为依迁国','有孚惠心,勿问元吉','莫益之,或击之']},
|
||||
'1,4':{name:'天雷无妄',judgment:'无妄,刚自外来而为主于内',image:'天下雷行,物与无妄。先王以茂对时育万物',lines:['无妄,往吉','不耕获,不菑畬','无妄之灾,或系之牛','可贞,无咎','无妄之疾,勿药有喜','无妄,行有眚']},
|
||||
'3,4':{name:'火雷噬嗑',judgment:'噬嗑亨,利用狱。刚柔分动而明',image:'雷电噬嗑,先王以明罚敕法',lines:['屦校灭趾,无咎','噬肤灭鼻,无咎','噬腊肉,遇毒','噬干胏,得金矢','噬干肉,得黄金','何校灭耳,凶']},
|
||||
'7,4':{name:'山雷颐',judgment:'颐,贞吉。观颐,自求口实',image:'山下有雷,颐。君子以慎言语节饮食',lines:['舍尔灵龟,观我朵颐','颠颐,拂经于丘颐','拂颐,贞凶','颠颐,吉','拂经,居贞吉','由颐,厉吉,利涉大川']},
|
||||
'7,5':{name:'山风蛊',judgment:'蛊,元亨。利涉大川,先甲三日后甲三日',image:'山下有风,蛊。君子以振民育德',lines:['干父之蛊,有子考无咎','干母之蛊,不可贞','干父之蛊,小有悔','裕父之蛊,往见吝','干父之蛊,用誉','不事王侯,高尚其事']},
|
||||
// 离宫八卦 (41-48)
|
||||
'3,3':{name:'离为火',judgment:'离,丽也。日月丽乎天,百谷草木丽乎土',image:'明两作,离。大人以继明照于四方',lines:['履错然,敬之无咎','黄离,元吉','日昃之离,不鼓缶而歌','突如其来如,焚如死如弃如','出涕沱若,戚嗟若','王用出征,有嘉折首']},
|
||||
'3,7':{name:'火山旅',judgment:'旅,小亨。旅贞吉',image:'山上有火,旅。君子以明慎用刑而不留狱',lines:['旅琐琐,斯其所取灾','旅即次,怀其资','旅焚其次,丧其童仆','旅于处,得其资斧','射雉,一矢亡','鸟焚其巢,旅人先笑后号咷']},
|
||||
'3,5':{name:'火风鼎',judgment:'鼎,象也。以木巽火,亨饪也',image:'木上有火,鼎。君子以正位凝命',lines:['鼎颠趾,利出否','鼎有实,我仇有疾','鼎耳革,其行塞','鼎折足,覆公餗','鼎黄耳金铉,利贞','鼎玉铉,大吉']},
|
||||
'3,6':{name:'火水未济',judgment:'未济亨,小狐汔济,濡其尾',image:'火在水上,未济。君子以慎辨物居方',lines:['濡其尾,吝','曳其轮,贞吉','未济,征凶','贞吉,悔亡','贞吉,无悔','有孚于饮酒,无咎']},
|
||||
'7,6':{name:'山水蒙',judgment:'蒙亨。匪我求童蒙,童蒙求我',image:'山下出泉,蒙。君子以果行育德',lines:['发蒙,利用刑人','包蒙,吉','勿用取女,见金夫','困蒙,吝','童蒙,吉','击蒙,不利为寇']},
|
||||
'5,6':{name:'风水涣',judgment:'涣亨。王假有庙,利涉大川',image:'风行水上,涣。先王以享于帝立庙',lines:['用拯马壮,吉','涣奔其机,悔亡','涣其躬,无悔','涣其群,元吉','涣汗其大号','涣其血,去逖出']},
|
||||
'1,6':{name:'天水讼',judgment:'讼,上刚下险,险而健,讼',image:'天与水违行,讼。君子以作事谋始',lines:['不永所事,小有言','不克讼,归而逋','食旧德,贞厉终吉','不克讼,复即命渝','讼,元吉','或锡之鞶带,终朝三褫']},
|
||||
'1,3':{name:'天火同人',judgment:'同人,柔得位得中而应乎乾',image:'天与火,同人。君子以类族辨物',lines:['同人于门,无咎','同人于宗,吝','伏戎于莽,升其高陵','乘其墉,弗克攻','同人先号咷而后笑','同人于郊,无悔']},
|
||||
// 坤宫八卦 (49-56)
|
||||
'8,8':{name:'坤为地',judgment:'至哉坤元,万物资生,乃顺承天',image:'地势坤,君子以厚德载物',lines:['履霜,坚冰至','直方大,不习无不利','含章可贞,或从王事','括囊,无咎无誉','黄裳,元吉','龙战于野,其血玄黄']},
|
||||
'8,4':{name:'地雷复',judgment:'复亨。出入无疾,朋来无咎',image:'雷在地中,复。先王以至日闭关',lines:['不远复,无祗悔','休复,吉','频复,厉','中行独复','敦复,无悔','迷复,凶有灾眚']},
|
||||
'8,2':{name:'地泽临',judgment:'临,刚浸而长,说而顺',image:'泽上有地,临。君子以教思无穷容保民无疆',lines:['咸临,贞吉','咸临,吉无不利','甘临,无攸利','至临,无咎','知临,大君之宜','敦临,吉无咎']},
|
||||
'8,1':{name:'地天泰',judgment:'泰,小往大来,吉亨。天地交而万物通',image:'天地交,泰。后以财成天地之道',lines:['拔茅茹,以其汇','包荒,用冯河','无平不陂,无往不复','翩翩,不富以其邻','帝乙归妹,以祉元吉','城复于隍,勿用师']},
|
||||
'4,1':{name:'雷天大壮',judgment:'大壮,大者壮也。刚以动,故壮',image:'雷在天上,大壮。君子以非礼勿履',lines:['壮于趾,征凶','贞吉','小人用壮,君子用罔','贞吉,悔亡','丧羊于易,无悔','羝羊触藩,不能退']},
|
||||
'2,1':{name:'泽天夬',judgment:'夬,决也,刚决柔也。健而说,决而和',image:'泽上于天,夬。君子以施禄及下',lines:['壮于前趾,往不胜为咎','惕号,莫夜有戎','壮于頄,有凶','臀无肤,其行次且','苋陆夬夬,中行无咎','无号,终有凶']},
|
||||
'6,1':{name:'水天需',judgment:'需,须也。险在前也,刚健而不陷',image:'云上于天,需。君子以饮食宴乐',lines:['需于郊,利用恒','需于沙,小有言','需于泥,致寇至','需于血,出自穴','需于酒食,贞吉','入于穴,有不速之客三人来']},
|
||||
'6,8':{name:'水地比',judgment:'比,吉也。比,辅也,下顺从也',image:'地上有水,比。先王以建万国亲诸侯',lines:['有孚比之,无咎','比之自内,贞吉','比之匪人','外比之,贞吉','显比,王用三驱','比之无首,凶']},
|
||||
// 兑宫八卦 (57-64)
|
||||
'2,2':{name:'兑为泽',judgment:'兑,说也。刚中而柔外,说以利贞',image:'丽泽,兑。君子以朋友讲习',lines:['和兑,吉','孚兑,吉','来兑,凶','商兑未宁,介疾有喜','孚于剥,有厉','引兑']},
|
||||
'2,6':{name:'泽水困',judgment:'困,刚掩也。险以说,困而不失其所',image:'泽无水,困。君子以致命遂志',lines:['臀困于株木,入于幽谷','困于酒食,朱绂方来','困于石,据于蒺藜','来徐徐,困于金车','劓刖,困于赤绂','困于葛藟,于臲兀']},
|
||||
'2,8':{name:'泽地萃',judgment:'萃,聚也。顺以说,刚中而应',image:'泽上于地,萃。君子以除戎器戒不虞',lines:['有孚不终,乃乱乃萃','引吉,无咎','萃如嗟如,无攸利','大吉,无咎','萃有位,无咎','赍咨涕洟,无咎']},
|
||||
'2,7':{name:'泽山咸',judgment:'咸,感也。柔上而刚下,二气感应以相与',image:'山上有泽,咸。君子以虚受人',lines:['咸其拇','咸其腓,凶','咸其股,执其随','贞吉,悔亡','咸其脢,无悔','咸其辅颊舌']},
|
||||
'6,7':{name:'水山蹇',judgment:'蹇,难也,险在前也。见险而能止',image:'山上有水,蹇。君子以反身修德',lines:['往蹇,来誉','王臣蹇蹇,匪躬之故','往蹇,来反','往蹇,来连','大蹇,朋来','往蹇,来硕']},
|
||||
'8,7':{name:'地山谦',judgment:'谦亨。天道下济而光明,地道卑而上行',image:'地中有山,谦。君子以裒多益寡称物平施',lines:['谦谦君子,用涉大川','鸣谦,贞吉','劳谦君子,有终吉','无不利,撝谦','不富以其邻,利用侵伐','鸣谦,利用行师']},
|
||||
'4,7':{name:'雷山小过',judgment:'小过,小者过而亨也。过以利贞',image:'山上有雷,小过。君子以行过乎恭',lines:['飞鸟以凶','过其祖,遇其妣','弗过防之,从或戕之','无咎,弗过遇之','密云不雨,自我西郊','弗遇过之,飞鸟离之']},
|
||||
'4,2':{name:'雷泽归妹',judgment:'归妹,天地之大义也。天地不交而万物不兴',image:'泽上有雷,归妹。君子以永终知敝',lines:['归妹以娣,跛能履','眇能视,利幽人之贞','归妹以须,反归以娣','归妹愆期,迟归有时','帝乙归妹,其君之袂','女承筐无实,士刲羊无血']},
|
||||
};
|
||||
|
||||
// Generate all 64 hexagrams
|
||||
function buildHexagramMap() {
|
||||
const map: Record<string, HexagramInfo> = {};
|
||||
for (let upper = 1; upper <= 8; upper++) {
|
||||
for (let lower = 1; lower <= 8; lower++) {
|
||||
const key = `${upper},${lower}`;
|
||||
const hName = H[key];
|
||||
if (hName) {
|
||||
map[key] = {
|
||||
number: (upper - 1) * 8 + lower,
|
||||
name: hName.name,
|
||||
upperTrigram: TRIGRAMS[upper],
|
||||
lowerTrigram: TRIGRAMS[lower],
|
||||
changingLine: 0,
|
||||
interpretation: hName.judgment,
|
||||
judgment: hName.judgment,
|
||||
image: hName.image,
|
||||
lines: hName.lines,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const HEXAGRAM_MAP = buildHexagramMap();
|
||||
|
||||
/** Calculate Plum Blossom I-Ching from date and time */
|
||||
export function calculatePlumBlossom(
|
||||
year: number,
|
||||
month: number,
|
||||
day: number,
|
||||
hour: number = 12,
|
||||
): PlumBlossomResult {
|
||||
// Use full numbers for more entropy
|
||||
const yearNum = year;
|
||||
const monthNum = month;
|
||||
const dayNum = day;
|
||||
const hourIndex = Math.floor(((hour + 1) % 24) / 2); // 0-11 地支时辰
|
||||
|
||||
// Standard Plum Blossom formula:
|
||||
// Upper trigram: (year + month + day) % 8 → 0-7 → +1 → 1-8
|
||||
const upperIdx = ((yearNum + monthNum + dayNum) % 8) + 1;
|
||||
// Lower trigram: (month + day + hourIndex + 1) % 8 + 1
|
||||
const lowerIdx = ((monthNum + dayNum + hourIndex + 1) % 8) + 1;
|
||||
// Changing line: (year + month + day + hourIndex + 1) % 6 + 1
|
||||
const changingLine = ((yearNum + monthNum + dayNum + hourIndex + 1) % 6) + 1;
|
||||
|
||||
const upperTrigram = TRIGRAMS[upperIdx];
|
||||
const lowerTrigram = TRIGRAMS[lowerIdx];
|
||||
|
||||
// Original hexagram
|
||||
const originalKey = `${upperIdx},${lowerIdx}`;
|
||||
const originalHexagram = HEXAGRAM_MAP[originalKey] || createFallbackHexagram(upperIdx, lowerIdx, changingLine);
|
||||
|
||||
// Transformed hexagram (after changing line)
|
||||
let transUpperIdx = upperIdx;
|
||||
let transLowerIdx = lowerIdx;
|
||||
// Changing line 1-3 affects lower trigram, 4-6 affects upper
|
||||
if (changingLine >= 4) {
|
||||
transUpperIdx = flipTrigramLine(upperIdx, changingLine - 3);
|
||||
} else {
|
||||
transLowerIdx = flipTrigramLine(lowerIdx, changingLine);
|
||||
}
|
||||
const transKey = `${transUpperIdx},${transLowerIdx}`;
|
||||
const transformedHexagram = HEXAGRAM_MAP[transKey] || createFallbackHexagram(transUpperIdx, transLowerIdx, 0);
|
||||
|
||||
// Mutual hexagram (互卦): 2-4 lines → lower, 3-5 lines → upper
|
||||
// Simplified: use middle two trigrams
|
||||
const mutualUpperIdx = lowerIdx; // simplified
|
||||
const mutualLowerIdx = upperIdx; // simplified
|
||||
const mutualKey = `${mutualUpperIdx},${mutualLowerIdx}`;
|
||||
const mutualHexagram = HEXAGRAM_MAP[mutualKey] || null;
|
||||
|
||||
// Constitution (体卦) and Function (用卦)
|
||||
// 体卦 = lower trigram, 用卦 = upper trigram
|
||||
const constitution = lowerTrigram.element;
|
||||
const function_ = upperTrigram.element;
|
||||
const relationship = getElementRelationship(constitution, function_);
|
||||
|
||||
return {
|
||||
originalHexagram: { ...originalHexagram, changingLine },
|
||||
transformedHexagram,
|
||||
mutualHexagram,
|
||||
upperTrigram,
|
||||
lowerTrigram,
|
||||
changingLine,
|
||||
constitution,
|
||||
function: function_,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
function flipTrigramLine(trigramIdx: number, line: number): number {
|
||||
// Flip a single line of the trigram
|
||||
// Trigrams encoded as bits: 乾111=7, 兑110=6, 离101=5, 震100=4, 巽011=3, 坎010=2, 艮001=1, 坤000=0
|
||||
const encoding = [0, 7, 6, 5, 4, 3, 2, 1, 0]; // index → bit pattern
|
||||
let bits = encoding[trigramIdx] || 0;
|
||||
bits ^= (1 << (line - 1)); // flip the line
|
||||
// Map back
|
||||
const decoding = [8, 7, 6, 2, 5, 3, 4, 1]; // bit pattern → index
|
||||
return decoding[bits] || trigramIdx;
|
||||
}
|
||||
|
||||
function getElementRelationship(body: string, func: string): string {
|
||||
const cycle: Record<string, string> = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
|
||||
const reverse: Record<string, string> = { '木': '水', '水': '金', '金': '土', '土': '火', '火': '木' };
|
||||
|
||||
if (body === func) return '比和(体用相同,诸事顺利)';
|
||||
if (cycle[body] === func) return '体生用(泄气,宜守不宜攻)';
|
||||
if (reverse[body] === func) return '用生体(得力,有贵人相助)';
|
||||
if (cycle[func] === body) return '用克体(受制,诸事不顺)';
|
||||
if (reverse[func] === body) return '体克用(主动,需付出努力)';
|
||||
return '体用相生';
|
||||
}
|
||||
|
||||
function createFallbackHexagram(upper: number, lower: number, changingLine: number): HexagramInfo {
|
||||
const ut = TRIGRAMS[upper];
|
||||
const lt = TRIGRAMS[lower];
|
||||
return {
|
||||
number: (upper - 1) * 8 + lower,
|
||||
name: `${ut.name}${lt.name}`,
|
||||
upperTrigram: ut,
|
||||
lowerTrigram: lt,
|
||||
changingLine,
|
||||
interpretation: '此卦象需结合具体事理参详',
|
||||
judgment: '',
|
||||
image: '',
|
||||
lines: ['','','','','',''],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
HeavenStem,
|
||||
EarthBranch,
|
||||
} from 'tyme4ts';
|
||||
|
||||
/** Relationship classification for branch interactions */
|
||||
export interface BranchRelationship {
|
||||
combine: boolean; // 六合
|
||||
threeCombine: boolean; // 三合
|
||||
opposite: boolean; // 六冲
|
||||
harm: boolean; // 六害
|
||||
punish: boolean; // 相刑
|
||||
formation: string | null; // 三合局名称
|
||||
}
|
||||
|
||||
/** Three-combine formations (三合局) */
|
||||
const THREE_COMBINES: Record<string, { branches: string[]; name: string; element: string }> = {
|
||||
'水局': { branches: ['申', '子', '辰'], name: '水局', element: '水' },
|
||||
'木局': { branches: ['亥', '卯', '未'], name: '木局', element: '木' },
|
||||
'火局': { branches: ['寅', '午', '戌'], name: '火局', element: '火' },
|
||||
'金局': { branches: ['巳', '酉', '丑'], name: '金局', element: '金' },
|
||||
};
|
||||
|
||||
/** Check branch relationships between two earth branches */
|
||||
export function getBranchRelationship(branchA: string, branchB: string): BranchRelationship {
|
||||
const a = EarthBranch.fromName(branchA);
|
||||
const b = EarthBranch.fromName(branchB);
|
||||
|
||||
// Six combine (六合): each branch's combine partner
|
||||
const combinePartner = a.getCombine();
|
||||
const hasCombine = combinePartner ? combinePartner.getName() === b.getName() : false;
|
||||
|
||||
// Opposite (六冲): each branch's opposite
|
||||
const oppositePartner = a.getOpposite();
|
||||
const hasOpposite = oppositePartner ? oppositePartner.getName() === b.getName() : false;
|
||||
|
||||
// Harm (六害): each branch's harm partner
|
||||
let hasHarm = false;
|
||||
try {
|
||||
const harmPartner = a.getHarm();
|
||||
hasHarm = harmPartner ? harmPartner.getName() === b.getName() : false;
|
||||
} catch { /* harm may not be available */ }
|
||||
|
||||
// Three combine (三合): check if both branches are in the same formation
|
||||
let hasThreeCombine = false;
|
||||
let formation: string | null = null;
|
||||
for (const [name, info] of Object.entries(THREE_COMBINES)) {
|
||||
if (info.branches.includes(branchA) && info.branches.includes(branchB)) {
|
||||
hasThreeCombine = true;
|
||||
formation = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Punish (相刑)
|
||||
const hasPunish = checkPunish(branchA, branchB);
|
||||
|
||||
return {
|
||||
combine: hasCombine,
|
||||
threeCombine: hasThreeCombine,
|
||||
opposite: hasOpposite,
|
||||
harm: hasHarm,
|
||||
punish: hasPunish,
|
||||
formation,
|
||||
};
|
||||
}
|
||||
|
||||
/** Check for punishment relationship between two branches */
|
||||
function checkPunish(a: string, b: string): boolean {
|
||||
// Self punishment (自刑)
|
||||
const selfPunish = ['辰', '午', '酉', '亥'];
|
||||
if (a === b && selfPunish.includes(a)) return true;
|
||||
|
||||
// Classic punishment pairs
|
||||
const punishPairs: [string, string][] = [
|
||||
['寅', '巳'], ['巳', '申'], ['申', '寅'], // 无恩之刑
|
||||
['丑', '戌'], ['戌', '未'], ['未', '丑'], // 恃势之刑
|
||||
['子', '卯'], ['卯', '子'], // 无礼之刑
|
||||
];
|
||||
|
||||
return punishPairs.some(([x, y]) => x === a && y === b);
|
||||
}
|
||||
|
||||
/** Get Ten Star relationship between two heavenly stems */
|
||||
export function getTenStarRelationship(subjectStem: string, objectStem: string): string {
|
||||
try {
|
||||
const s = HeavenStem.fromName(subjectStem);
|
||||
const o = HeavenStem.fromName(objectStem);
|
||||
return s.getTenStar(o).getName();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if two heavenly stems combine (天干合) */
|
||||
export function checkStemCombine(stemA: string, stemB: string): boolean {
|
||||
const combinePairs: Record<string, string> = {
|
||||
'甲': '己', '己': '甲',
|
||||
'乙': '庚', '庚': '乙',
|
||||
'丙': '辛', '辛': '丙',
|
||||
'丁': '壬', '壬': '丁',
|
||||
'戊': '癸', '癸': '戊',
|
||||
};
|
||||
return combinePairs[stemA] === stemB;
|
||||
}
|
||||
|
||||
/** Check if two heavenly stems oppose (天干冲) */
|
||||
export function checkStemOpposite(stemA: string, stemB: string): boolean {
|
||||
const oppositePairs: Record<string, string> = {
|
||||
'甲': '庚', '庚': '甲',
|
||||
'乙': '辛', '辛': '乙',
|
||||
'丙': '壬', '壬': '丙',
|
||||
'丁': '癸', '癸': '丁',
|
||||
};
|
||||
return oppositePairs[stemA] === stemB;
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 八字神煞 (Shen Sha)
|
||||
* 神煞是固定规则查表 + 四柱匹配的标记,规则来源《三命通会》《渊海子平》。
|
||||
* 注意:部分神煞网上存在版本差异,本模块采用主流排盘工具通行版本。
|
||||
*/
|
||||
import type { EightCharInfo, PillarInfo } from '../types/bazi';
|
||||
|
||||
export interface ShenshaInfo {
|
||||
name: string;
|
||||
/** 吉/凶/中性 */
|
||||
type: '吉' | '凶' | '中性';
|
||||
/** 分类:贵人/文星/财禄/感情/变动/威权/灾煞/孤独/格局 */
|
||||
category: string;
|
||||
/** 命中查法说明,如 "日干己查四支" */
|
||||
anchors: string[];
|
||||
/** 命中位置,如 ["年支","时支"] */
|
||||
foundIn: string[];
|
||||
/** 吉凶含义简述 */
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ShenshaRule {
|
||||
name: string;
|
||||
type: ShenshaInfo['type'];
|
||||
category: string;
|
||||
description: string;
|
||||
/** 按天干(年干/日干)查四支 */
|
||||
byStem?: Record<string, string[]>;
|
||||
/** 按地支(年支/日支)查四支 */
|
||||
byBranch?: Record<string, string[]>;
|
||||
/** 按月支查四干 */
|
||||
byMonthStem?: Record<string, string[]>;
|
||||
/** 日柱特殊格局 */
|
||||
byDayPillar?: string[];
|
||||
}
|
||||
|
||||
const PILLAR_KEYS = ['year', 'month', 'day', 'hour'] as const;
|
||||
|
||||
const RULES: ShenshaRule[] = [
|
||||
{
|
||||
name: '天乙贵人',
|
||||
type: '吉',
|
||||
category: '贵人',
|
||||
description: '最吉之神煞,主逢凶化吉、贵人相助',
|
||||
byStem: {
|
||||
'甲': ['丑', '未'], '戊': ['丑', '未'],
|
||||
'乙': ['子', '申'], '己': ['子', '申'],
|
||||
'丙': ['亥', '酉'], '丁': ['亥', '酉'],
|
||||
'壬': ['卯', '巳'], '癸': ['卯', '巳'],
|
||||
'辛': ['寅', '午'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '天厨贵人',
|
||||
type: '吉',
|
||||
category: '贵人',
|
||||
description: '主口福与衣食之禄,衣食无忧',
|
||||
byStem: {
|
||||
'甲': ['巳'], '乙': ['午'], '丙': ['巳'], '丁': ['午'],
|
||||
'戊': ['申'], '己': ['酉'], '庚': ['亥'], '辛': ['子'],
|
||||
'壬': ['寅'], '癸': ['卯'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '文昌贵人',
|
||||
type: '吉',
|
||||
category: '文星',
|
||||
description: '主聪明好学、利科名学业',
|
||||
byStem: {
|
||||
'甲': ['巳'], '乙': ['午'], '丙': ['申'], '丁': ['酉'],
|
||||
'戊': ['申'], '己': ['酉'], '庚': ['亥'], '辛': ['子'],
|
||||
'壬': ['寅'], '癸': ['卯'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '禄神',
|
||||
type: '吉',
|
||||
category: '财禄',
|
||||
description: '日干之禄,主衣禄与财运根基',
|
||||
byStem: {
|
||||
'甲': ['寅'], '乙': ['卯'], '丙': ['巳'], '丁': ['午'],
|
||||
'戊': ['巳'], '己': ['午'], '庚': ['申'], '辛': ['酉'],
|
||||
'壬': ['亥'], '癸': ['子'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '羊刃',
|
||||
type: '凶',
|
||||
category: '灾煞',
|
||||
description: '刚烈冲动之星,主脾气刚猛,喜用则有权柄',
|
||||
byStem: {
|
||||
'甲': ['卯'], '乙': ['寅'], '丙': ['午'], '丁': ['巳'],
|
||||
'戊': ['午'], '己': ['巳'], '庚': ['酉'], '辛': ['申'],
|
||||
'壬': ['子'], '癸': ['亥'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '金舆',
|
||||
type: '吉',
|
||||
category: '财禄',
|
||||
description: '禄前二位,主婚恋顺遂、富贵安逸',
|
||||
byStem: {
|
||||
'甲': ['辰'], '乙': ['巳'], '丙': ['未'], '丁': ['申'],
|
||||
'戊': ['未'], '己': ['申'], '庚': ['戌'], '辛': ['亥'],
|
||||
'壬': ['丑'], '癸': ['寅'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '天德贵人',
|
||||
type: '吉',
|
||||
category: '贵人',
|
||||
description: '主心地仁慈、逢凶化吉,利化解灾厄',
|
||||
byMonthStem: {
|
||||
'寅': ['丁'], '卯': ['申'], '辰': ['壬'], '巳': ['辛'],
|
||||
'午': ['亥'], '未': ['甲'], '申': ['癸'], '酉': ['寅'],
|
||||
'戌': ['丙'], '亥': ['乙'], '子': ['巳'], '丑': ['庚'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '月德贵人',
|
||||
type: '吉',
|
||||
category: '贵人',
|
||||
description: '主福荫深厚、遇难呈祥',
|
||||
byMonthStem: {
|
||||
'寅': ['丙'], '午': ['丙'], '戌': ['丙'],
|
||||
'申': ['壬'], '子': ['壬'], '辰': ['壬'],
|
||||
'亥': ['甲'], '卯': ['甲'], '未': ['甲'],
|
||||
'巳': ['庚'], '酉': ['庚'], '丑': ['庚'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '桃花(咸池)',
|
||||
type: '中性',
|
||||
category: '感情',
|
||||
description: '主异性缘、魅力与风流,利艺术才华',
|
||||
byBranch: {
|
||||
'申': ['酉'], '子': ['酉'], '辰': ['酉'],
|
||||
'寅': ['卯'], '午': ['卯'], '戌': ['卯'],
|
||||
'巳': ['午'], '酉': ['午'], '丑': ['午'],
|
||||
'亥': ['子'], '卯': ['子'], '未': ['子'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '驿马',
|
||||
type: '中性',
|
||||
category: '变动',
|
||||
description: '主奔波变动、外出发展,动中得财',
|
||||
byBranch: {
|
||||
'申': ['寅'], '子': ['寅'], '辰': ['寅'],
|
||||
'寅': ['申'], '午': ['申'], '戌': ['申'],
|
||||
'巳': ['亥'], '酉': ['亥'], '丑': ['亥'],
|
||||
'亥': ['巳'], '卯': ['巳'], '未': ['巳'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '华盖',
|
||||
type: '中性',
|
||||
category: '孤独',
|
||||
description: '主艺术天赋、悟性高,也主清高孤傲',
|
||||
byBranch: {
|
||||
'申': ['辰'], '子': ['辰'], '辰': ['辰'],
|
||||
'寅': ['戌'], '午': ['戌'], '戌': ['戌'],
|
||||
'巳': ['丑'], '酉': ['丑'], '丑': ['丑'],
|
||||
'亥': ['未'], '卯': ['未'], '未': ['未'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '劫煞',
|
||||
type: '凶',
|
||||
category: '灾煞',
|
||||
description: '主突发变故、是非破财,宜谨慎',
|
||||
byBranch: {
|
||||
'申': ['巳'], '子': ['巳'], '辰': ['巳'],
|
||||
'寅': ['亥'], '午': ['亥'], '戌': ['亥'],
|
||||
'巳': ['寅'], '酉': ['寅'], '丑': ['寅'],
|
||||
'亥': ['申'], '卯': ['申'], '未': ['申'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '亡神',
|
||||
type: '凶',
|
||||
category: '灾煞',
|
||||
description: '主心机深、谋略强,亦主口舌是非',
|
||||
byBranch: {
|
||||
'申': ['亥'], '子': ['亥'], '辰': ['亥'],
|
||||
'寅': ['巳'], '午': ['巳'], '戌': ['巳'],
|
||||
'巳': ['申'], '酉': ['申'], '丑': ['申'],
|
||||
'亥': ['寅'], '卯': ['寅'], '未': ['寅'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '将星',
|
||||
type: '吉',
|
||||
category: '威权',
|
||||
description: '主领导才能与威严,掌权柄之星',
|
||||
byBranch: {
|
||||
'申': ['子'], '子': ['子'], '辰': ['子'],
|
||||
'寅': ['午'], '午': ['午'], '戌': ['午'],
|
||||
'巳': ['酉'], '酉': ['酉'], '丑': ['酉'],
|
||||
'亥': ['卯'], '卯': ['卯'], '未': ['卯'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '红鸾',
|
||||
type: '吉',
|
||||
category: '感情',
|
||||
description: '主婚恋喜事、姻缘早成',
|
||||
byBranch: {
|
||||
'子': ['卯'], '丑': ['寅'], '寅': ['丑'], '卯': ['子'],
|
||||
'辰': ['亥'], '巳': ['戌'], '午': ['酉'], '未': ['申'],
|
||||
'申': ['未'], '酉': ['午'], '戌': ['巳'], '亥': ['辰'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '天喜',
|
||||
type: '吉',
|
||||
category: '感情',
|
||||
description: '红鸾对宫,主喜事临门、人缘佳',
|
||||
byBranch: {
|
||||
'子': ['酉'], '丑': ['申'], '寅': ['未'], '卯': ['午'],
|
||||
'辰': ['巳'], '巳': ['辰'], '午': ['卯'], '未': ['寅'],
|
||||
'申': ['丑'], '酉': ['子'], '戌': ['亥'], '亥': ['戌'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '孤辰',
|
||||
type: '凶',
|
||||
category: '孤独',
|
||||
description: '主孤独离群、六亲缘薄',
|
||||
byBranch: {
|
||||
'亥': ['寅'], '子': ['寅'], '丑': ['寅'],
|
||||
'寅': ['巳'], '卯': ['巳'], '辰': ['巳'],
|
||||
'巳': ['申'], '午': ['申'], '未': ['申'],
|
||||
'申': ['亥'], '酉': ['亥'], '戌': ['亥'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '寡宿',
|
||||
type: '凶',
|
||||
category: '孤独',
|
||||
description: '主孤寡之象,婚恋宜晚',
|
||||
byBranch: {
|
||||
'亥': ['戌'], '子': ['戌'], '丑': ['戌'],
|
||||
'寅': ['丑'], '卯': ['丑'], '辰': ['丑'],
|
||||
'巳': ['辰'], '午': ['辰'], '未': ['辰'],
|
||||
'申': ['未'], '酉': ['未'], '戌': ['未'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '天罗',
|
||||
type: '凶',
|
||||
category: '灾煞',
|
||||
description: '戌亥为天罗,主困顿束缚,男命尤忌',
|
||||
byBranch: { '子': ['戌', '亥'], '丑': ['戌', '亥'], '寅': ['戌', '亥'], '卯': ['戌', '亥'], '辰': ['戌', '亥'], '巳': ['戌', '亥'], '午': ['戌', '亥'], '未': ['戌', '亥'], '申': ['戌', '亥'], '酉': ['戌', '亥'], '戌': ['戌', '亥'], '亥': ['戌', '亥'] },
|
||||
},
|
||||
{
|
||||
name: '地网',
|
||||
type: '凶',
|
||||
category: '灾煞',
|
||||
description: '辰巳为地网,主束缚波折,女命尤忌',
|
||||
byBranch: { '子': ['辰', '巳'], '丑': ['辰', '巳'], '寅': ['辰', '巳'], '卯': ['辰', '巳'], '辰': ['辰', '巳'], '巳': ['辰', '巳'], '午': ['辰', '巳'], '未': ['辰', '巳'], '申': ['辰', '巳'], '酉': ['辰', '巳'], '戌': ['辰', '巳'], '亥': ['辰', '巳'] },
|
||||
},
|
||||
{
|
||||
name: '魁罡',
|
||||
type: '中性',
|
||||
category: '格局',
|
||||
description: '日柱魁罡,主刚毅果断、聪明果敢,忌刑冲',
|
||||
byDayPillar: ['庚辰', '庚戌', '壬辰', '戊戌'],
|
||||
},
|
||||
{
|
||||
name: '阴差阳错',
|
||||
type: '凶',
|
||||
category: '格局',
|
||||
description: '日柱阴差阳错,主婚姻不顺、易生波折',
|
||||
byDayPillar: ['丙子', '丁丑', '戊寅', '辛卯', '壬辰', '癸巳', '丙午', '丁未', '戊申', '辛酉', '壬戌', '癸亥'],
|
||||
},
|
||||
];
|
||||
|
||||
const PILLAR_LABELS: Record<string, string> = {
|
||||
yearStem: '年干', monthStem: '月干', dayStem: '日干', hourStem: '时干',
|
||||
yearBranch: '年支', monthBranch: '月支', dayBranch: '日支', hourBranch: '时支',
|
||||
};
|
||||
|
||||
/** Analyze Shen Sha presence in a Bazi chart */
|
||||
export function analyzeShensha(bazi: EightCharInfo): ShenshaInfo[] {
|
||||
const pillars: Record<string, PillarInfo> = {
|
||||
year: bazi.yearPillar, month: bazi.monthPillar, day: bazi.dayPillar, hour: bazi.hourPillar,
|
||||
};
|
||||
const stems: Record<string, string> = {};
|
||||
const branches: Record<string, string> = {};
|
||||
for (const k of PILLAR_KEYS) {
|
||||
stems[`${k}Stem`] = pillars[k].heavenStem;
|
||||
branches[`${k}Branch`] = pillars[k].earthBranch;
|
||||
}
|
||||
const dayGanzhi = pillars.day.ganzhi;
|
||||
|
||||
const result: ShenshaInfo[] = [];
|
||||
|
||||
for (const rule of RULES) {
|
||||
const anchors: string[] = [];
|
||||
const foundIn = new Set<string>();
|
||||
|
||||
if (rule.byStem) {
|
||||
// 年干、日干查四支
|
||||
for (const anchorKey of ['yearStem', 'dayStem'] as const) {
|
||||
const targets = rule.byStem[stems[anchorKey]];
|
||||
if (!targets) continue;
|
||||
const hits = PILLAR_KEYS.filter(k => targets.includes(branches[`${k}Branch`]));
|
||||
if (hits.length > 0) {
|
||||
anchors.push(PILLAR_LABELS[anchorKey]);
|
||||
hits.forEach(k => foundIn.add(PILLAR_LABELS[`${k}Branch`]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.byBranch) {
|
||||
// 年支、日支查四支
|
||||
for (const anchorKey of ['yearBranch', 'dayBranch'] as const) {
|
||||
const targets = rule.byBranch[branches[anchorKey]];
|
||||
if (!targets) continue;
|
||||
const hits = PILLAR_KEYS.filter(k => targets.includes(branches[`${k}Branch`]));
|
||||
if (hits.length > 0) {
|
||||
anchors.push(PILLAR_LABELS[anchorKey]);
|
||||
hits.forEach(k => foundIn.add(PILLAR_LABELS[`${k}Branch`]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.byMonthStem) {
|
||||
// 月支查四干
|
||||
const targets = rule.byMonthStem[branches.monthBranch];
|
||||
if (targets) {
|
||||
const hits = PILLAR_KEYS.filter(k => targets.includes(stems[`${k}Stem`]));
|
||||
if (hits.length > 0) {
|
||||
anchors.push('月支');
|
||||
hits.forEach(k => foundIn.add(PILLAR_LABELS[`${k}Stem`]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.byDayPillar && rule.byDayPillar.includes(dayGanzhi)) {
|
||||
anchors.push('日柱');
|
||||
foundIn.add('日柱');
|
||||
}
|
||||
|
||||
if (foundIn.size > 0) {
|
||||
result.push({
|
||||
name: rule.name,
|
||||
type: rule.type,
|
||||
category: rule.category,
|
||||
anchors,
|
||||
foundIn: [...foundIn],
|
||||
description: rule.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Types
|
||||
export type {
|
||||
DayInfo,
|
||||
AlmanacInfo,
|
||||
HourAlmanac,
|
||||
PillarInfo,
|
||||
HideStemInfo,
|
||||
EightCharInfo,
|
||||
DecadeFortuneInfo,
|
||||
FortuneInfo,
|
||||
ChildLimitInfo,
|
||||
BaziFullResult,
|
||||
PillarRelationship,
|
||||
DailyFortuneResult,
|
||||
} from './types';
|
||||
|
||||
// Transformers
|
||||
export {
|
||||
solarDayToDayInfo,
|
||||
getMonthCalendar,
|
||||
getDayInfo,
|
||||
getTodayInfo,
|
||||
} from './transformers/day';
|
||||
|
||||
export {
|
||||
solarDayToAlmanacInfo,
|
||||
getAlmanacInfo,
|
||||
} from './transformers/almanac';
|
||||
|
||||
export {
|
||||
birthInfoToBazi,
|
||||
} from './transformers/bazi';
|
||||
export type { BirthParams } from './transformers/bazi';
|
||||
|
||||
export {
|
||||
getYearMonths,
|
||||
} from './transformers/yearMonths';
|
||||
export type { YearMonthInfo } from './transformers/yearMonths';
|
||||
|
||||
// Calculators
|
||||
export {
|
||||
getBranchRelationship,
|
||||
getTenStarRelationship,
|
||||
checkStemCombine,
|
||||
checkStemOpposite,
|
||||
} from './calculators/relationship';
|
||||
export type { BranchRelationship } from './calculators/relationship';
|
||||
|
||||
export {
|
||||
calculateDailyFortune,
|
||||
} from './calculators/dailyMatch';
|
||||
|
||||
export {
|
||||
analyzeElementBalance,
|
||||
} from './calculators/elementStrength';
|
||||
export type { ElementProfile } from './calculators/elementStrength';
|
||||
|
||||
export {
|
||||
calculatePlumBlossom,
|
||||
} from './calculators/plumBlossom';
|
||||
export type {
|
||||
TrigramInfo,
|
||||
HexagramInfo,
|
||||
PlumBlossomResult,
|
||||
} from './calculators/plumBlossom';
|
||||
|
||||
export {
|
||||
calculateBoneWeight,
|
||||
} from './calculators/boneWeight';
|
||||
export type { BoneWeightResult } from './calculators/boneWeight';
|
||||
|
||||
export {
|
||||
getBuddhistFestival,
|
||||
} from './calculators/buddhistDates';
|
||||
|
||||
export {
|
||||
analyzeShensha,
|
||||
} from './calculators/shensha';
|
||||
export type { ShenshaInfo } from './calculators/shensha';
|
||||
|
||||
export {
|
||||
analyzeFortuneGanzhi,
|
||||
} from './calculators/fortuneLuck';
|
||||
export type { FortuneLuck } from './calculators/fortuneLuck';
|
||||
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
SolarDay,
|
||||
type LunarHour,
|
||||
} from 'tyme4ts';
|
||||
import type { AlmanacInfo, HourAlmanac } from '../types/almanac';
|
||||
|
||||
/** Transform a SolarDay into a full AlmanacInfo object */
|
||||
export function solarDayToAlmanacInfo(solarDay: SolarDay): AlmanacInfo {
|
||||
const lunarDay = solarDay.getLunarDay();
|
||||
const sixtyCycle = lunarDay.getSixtyCycle();
|
||||
const stem = sixtyCycle.getHeavenStem();
|
||||
const branch = sixtyCycle.getEarthBranch();
|
||||
|
||||
// Duty officer
|
||||
const duty = lunarDay.getDuty();
|
||||
|
||||
// Twelve star
|
||||
const twelveStar = lunarDay.getTwelveStar();
|
||||
const ecliptic = twelveStar.getEcliptic();
|
||||
|
||||
// Twenty-eight star
|
||||
const twentyEightStar = lunarDay.getTwentyEightStar();
|
||||
|
||||
// Nine star
|
||||
const nineStar = lunarDay.getNineStar();
|
||||
|
||||
// Six star
|
||||
const sixStar = lunarDay.getSixStar ? lunarDay.getSixStar().getName() : '';
|
||||
|
||||
// Minor Ren
|
||||
const minorRen = lunarDay.getMinorRen ? lunarDay.getMinorRen() : null;
|
||||
|
||||
// Fetus
|
||||
const fetusDay = lunarDay.getFetusDay();
|
||||
|
||||
// Recommendations and avoidances
|
||||
let recommends: string[] = [];
|
||||
let avoids: string[] = [];
|
||||
try {
|
||||
recommends = lunarDay.getRecommends().map(r => r.getName());
|
||||
avoids = lunarDay.getAvoids().map(a => a.getName());
|
||||
} catch { /* may throw for some dates */ }
|
||||
|
||||
// Gods
|
||||
const goodGods: string[] = [];
|
||||
const badGods: string[] = [];
|
||||
try {
|
||||
const gods = lunarDay.getGods();
|
||||
for (const god of gods) {
|
||||
const luck = god.getLuck();
|
||||
if (luck) {
|
||||
if (luck.getName() === '吉') {
|
||||
goodGods.push(god.getName());
|
||||
} else {
|
||||
badGods.push(god.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* may throw */ }
|
||||
|
||||
// Peng Zu taboos
|
||||
const pengZu = sixtyCycle.getPengZu();
|
||||
|
||||
// Branch relationships
|
||||
const opposite = branch.getOpposite();
|
||||
const harm = branch.getHarm();
|
||||
const combine = branch.getCombine();
|
||||
|
||||
// Evil direction
|
||||
const ominous = branch.getOminous ? branch.getOminous() : null;
|
||||
|
||||
// Na Yin
|
||||
const nayin = sixtyCycle.getSound().getName();
|
||||
|
||||
// Moon phase
|
||||
let phase = '';
|
||||
try {
|
||||
const p = lunarDay.getPhase();
|
||||
if (p) phase = p.getName();
|
||||
} catch { /* no phase info */ }
|
||||
|
||||
// Hourly almanac
|
||||
const hourDetails = buildHourlyAlmanac(lunarDay.getHours(), recommends, avoids);
|
||||
|
||||
// Determine duty luck
|
||||
const dutyName = duty.getName();
|
||||
const luckyDuties = ['除', '执', '危', '成', '开'];
|
||||
const unluckyDuties = ['建', '满', '平', '破', '收', '闭'];
|
||||
let dutyLuck: 'good' | 'bad' | 'neutral' = 'neutral';
|
||||
if (luckyDuties.includes(dutyName)) dutyLuck = 'good';
|
||||
else if (unluckyDuties.includes(dutyName)) dutyLuck = 'bad';
|
||||
// Note: duty luck also depends on day branch; simplified here
|
||||
|
||||
return {
|
||||
duty: dutyName,
|
||||
dutyLuck,
|
||||
twelveStar: {
|
||||
name: twelveStar.getName(),
|
||||
ecliptic: ecliptic ? ecliptic.getName() : '',
|
||||
luck: ecliptic && ecliptic.getName() === '黄道' ? 'good' : 'bad',
|
||||
},
|
||||
twentyEightStar: {
|
||||
name: twentyEightStar.getName(),
|
||||
luck: twentyEightStar.getLuck()?.getName() === '吉' ? 'good' : 'bad',
|
||||
animal: twentyEightStar.getAnimal()?.getName() || '',
|
||||
},
|
||||
nineStar: {
|
||||
name: nineStar.getName(),
|
||||
color: nineStar.getColor ? nineStar.getColor() : '',
|
||||
element: nineStar.getElement ? nineStar.getElement().getName() : '',
|
||||
},
|
||||
sixStar,
|
||||
minorRen: minorRen ? {
|
||||
name: minorRen.getName(),
|
||||
luck: minorRen.getLuck()?.getName() === '吉' ? 'good' : 'bad',
|
||||
element: minorRen.getElement()?.getName() || '',
|
||||
} : { name: '', luck: 'bad' as const, element: '' },
|
||||
phase,
|
||||
fetus: {
|
||||
direction: fetusDay.getDirection()?.getName() || '',
|
||||
side: fetusDay.getSide() !== undefined ? (fetusDay.getSide() as unknown as number === 0 ? '房内' : '房外') : '',
|
||||
position: fetusDay.getName(),
|
||||
},
|
||||
recommends,
|
||||
avoids,
|
||||
goodGods,
|
||||
badGods,
|
||||
dayStem: stem.getName(),
|
||||
dayBranch: branch.getName(),
|
||||
dayGanzhi: sixtyCycle.getName(),
|
||||
pengZu: pengZu.getName(),
|
||||
pengZuStem: pengZu.getPengZuHeavenStem()?.getName() || '',
|
||||
pengZuBranch: pengZu.getPengZuEarthBranch()?.getName() || '',
|
||||
clash: `${opposite.getZodiac().getName()}(${opposite.getName()})`,
|
||||
harm: harm ? `${harm.getZodiac().getName()}(${harm.getName()})` : '',
|
||||
combine: combine ? `${combine.getZodiac().getName()}(${combine.getName()})` : '',
|
||||
evilDirection: ominous ? ominous.getName() : '',
|
||||
nayin,
|
||||
hourDetails,
|
||||
};
|
||||
}
|
||||
|
||||
function buildHourlyAlmanac(hours: LunarHour[], _dayRecommends: string[], _dayAvoids: string[]): HourAlmanac[] {
|
||||
return hours.map(hour => {
|
||||
const hourSixtyCycle = hour.getSixtyCycle();
|
||||
const twelveStar = hour.getTwelveStar ? hour.getTwelveStar() : null;
|
||||
const nineStar = hour.getNineStar ? hour.getNineStar() : null;
|
||||
|
||||
let hourRecommends: string[] = [];
|
||||
let hourAvoids: string[] = [];
|
||||
try {
|
||||
hourRecommends = hour.getRecommends().map(r => r.getName());
|
||||
hourAvoids = hour.getAvoids().map(a => a.getName());
|
||||
} catch { /* may throw */ }
|
||||
|
||||
const branchName = hourSixtyCycle.getEarthBranch().getName();
|
||||
const hourNames: Record<string, string> = {
|
||||
'子': '子时', '丑': '丑时', '寅': '寅时', '卯': '卯时',
|
||||
'辰': '辰时', '巳': '巳时', '午': '午时', '未': '未时',
|
||||
'申': '申时', '酉': '酉时', '戌': '戌时', '亥': '亥时',
|
||||
};
|
||||
const hourRanges: Record<string, string> = {
|
||||
'子': '23:00-01:00', '丑': '01:00-03:00', '寅': '03:00-05:00',
|
||||
'卯': '05:00-07:00', '辰': '07:00-09:00', '巳': '09:00-11:00',
|
||||
'午': '11:00-13:00', '未': '13:00-15:00', '申': '15:00-17:00',
|
||||
'酉': '17:00-19:00', '戌': '19:00-21:00', '亥': '21:00-23:00',
|
||||
};
|
||||
|
||||
return {
|
||||
branch: branchName,
|
||||
name: hourNames[branchName] || branchName,
|
||||
range: hourRanges[branchName] || '',
|
||||
ganzhi: hourSixtyCycle.getName(),
|
||||
recommends: hourRecommends,
|
||||
avoids: hourAvoids,
|
||||
twelveStar: twelveStar?.getName() || '',
|
||||
nineStar: nineStar?.getName() || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Get AlmanacInfo for a specific date */
|
||||
export function getAlmanacInfo(year: number, month: number, day: number): AlmanacInfo {
|
||||
const solarDay = SolarDay.fromYmd(year, month, day);
|
||||
return solarDayToAlmanacInfo(solarDay);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import {
|
||||
SolarTime,
|
||||
Gender,
|
||||
ChildLimit,
|
||||
HideHeavenStemType,
|
||||
YinYang,
|
||||
type SixtyCycle,
|
||||
type HeavenStem,
|
||||
} from 'tyme4ts';
|
||||
import type {
|
||||
PillarInfo,
|
||||
HideStemInfo,
|
||||
DecadeFortuneInfo,
|
||||
FortuneInfo,
|
||||
BaziFullResult,
|
||||
} from '../types/bazi';
|
||||
|
||||
export interface BirthParams {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
hour: number;
|
||||
minute: number;
|
||||
gender: 'male' | 'female';
|
||||
/** 八字流派:lateZiNextDay=晚子时算次日(23点换日,默认);earlyZiSameDay=晚子时算当日(0点换日) */
|
||||
ziSect?: 'lateZiNextDay' | 'earlyZiSameDay';
|
||||
}
|
||||
|
||||
/** Transform birth parameters into full Bazi result */
|
||||
export function birthInfoToBazi(params: BirthParams): BaziFullResult {
|
||||
const { year, month, day, hour, minute, gender, ziSect = 'lateZiNextDay' } = params;
|
||||
|
||||
const solarTime = SolarTime.fromYmdHms(year, month, day, hour, minute, 0);
|
||||
// 早子时流派:23:00 后出生按当日早子时排四柱(日柱不换日),起运仍按真实出生时间
|
||||
const ziHour =
|
||||
ziSect === 'earlyZiSameDay' && hour >= 23
|
||||
? SolarTime.fromYmdHms(year, month, day, 0, minute, 0)
|
||||
: solarTime;
|
||||
const lunarHour = ziHour.getLunarHour();
|
||||
const eightChar = lunarHour.getEightChar();
|
||||
|
||||
const yearPillar = eightChar.getYear();
|
||||
const monthPillar = eightChar.getMonth();
|
||||
const dayPillar = eightChar.getDay();
|
||||
const hourPillar = eightChar.getHour();
|
||||
|
||||
// Day master
|
||||
const dayStem = dayPillar.getHeavenStem();
|
||||
const dayMasterStem = dayStem.getName();
|
||||
const dayMasterElement = dayStem.getElement().getName();
|
||||
|
||||
// Extract pillars with Ten Star relative to day master
|
||||
const yearPillarInfo = extractPillarInfo(yearPillar, dayStem);
|
||||
const monthPillarInfo = extractPillarInfo(monthPillar, dayStem);
|
||||
const dayPillarInfo = extractPillarInfo(dayPillar, dayStem);
|
||||
const hourPillarInfo = extractPillarInfo(hourPillar, dayStem);
|
||||
|
||||
// Fetal origin, fetal breath, own sign, body sign
|
||||
const fetalOrigin = eightChar.getFetalOrigin();
|
||||
const fetalBreath = eightChar.getFetalBreath();
|
||||
const ownSign = eightChar.getOwnSign();
|
||||
const bodySign = eightChar.getBodySign();
|
||||
|
||||
// Empty branches
|
||||
const emptyTen = dayPillar.getTen();
|
||||
const emptyBranches = emptyTen ? dayPillar.getExtraEarthBranches().map(b => b.getName()) : [];
|
||||
|
||||
// Child limit
|
||||
const genderEnum = gender === 'male' ? Gender.MAN : Gender.WOMAN;
|
||||
const childLimit = ChildLimit.fromSolarTime(solarTime, genderEnum);
|
||||
const startDecade = childLimit.getStartDecadeFortune();
|
||||
const startFortune = childLimit.getStartFortune();
|
||||
|
||||
// Decade fortunes (大运) — 10 decades
|
||||
const decadeFortunes: DecadeFortuneInfo[] = [];
|
||||
let currentDecade = startDecade;
|
||||
for (let i = 0; i < 10 && currentDecade; i++) {
|
||||
const sc = currentDecade.getSixtyCycle();
|
||||
decadeFortunes.push({
|
||||
index: i,
|
||||
ganzhi: sc.getName(),
|
||||
startAge: currentDecade.getStartAge(),
|
||||
endAge: currentDecade.getEndAge(),
|
||||
startYear: currentDecade.getStartLunarYear().getYear(),
|
||||
endYear: currentDecade.getEndLunarYear().getYear(),
|
||||
heavenStem: sc.getHeavenStem().getName(),
|
||||
earthBranch: sc.getEarthBranch().getName(),
|
||||
nayin: sc.getSound().getName(),
|
||||
});
|
||||
currentDecade = currentDecade.next(1) as typeof currentDecade;
|
||||
}
|
||||
|
||||
// Annual fortunes (流年) — 10 years
|
||||
const annualFortunes: FortuneInfo[] = [];
|
||||
let currentFortune = startFortune;
|
||||
for (let i = 0; i < 10 && currentFortune; i++) {
|
||||
const sc = currentFortune.getSixtyCycle();
|
||||
annualFortunes.push({
|
||||
age: currentFortune.getAge(),
|
||||
year: currentFortune.getLunarYear().getYear(),
|
||||
ganzhi: sc.getName(),
|
||||
nayin: sc.getSound().getName(),
|
||||
});
|
||||
currentFortune = currentFortune.next(1) as typeof currentFortune;
|
||||
}
|
||||
|
||||
return {
|
||||
eightChar: {
|
||||
birthDate: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
|
||||
birthTime: `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,
|
||||
gender,
|
||||
yearPillar: yearPillarInfo,
|
||||
monthPillar: monthPillarInfo,
|
||||
dayPillar: dayPillarInfo,
|
||||
hourPillar: hourPillarInfo,
|
||||
dayMaster: `${dayMasterStem}${dayMasterElement}`,
|
||||
dayMasterStem,
|
||||
dayMasterElement,
|
||||
fetalOrigin: fetalOrigin.getName(),
|
||||
fetalBreath: fetalBreath.getName(),
|
||||
ownSign: ownSign.getName(),
|
||||
bodySign: bodySign.getName(),
|
||||
emptyBranches,
|
||||
emptyTen: emptyTen ? emptyTen.getName() : '',
|
||||
},
|
||||
childLimit: {
|
||||
startTime: childLimit.getStartTime().toString(),
|
||||
endTime: childLimit.getEndTime().toString(),
|
||||
yearCount: childLimit.getYearCount(),
|
||||
monthCount: childLimit.getMonthCount(),
|
||||
dayCount: childLimit.getDayCount(),
|
||||
hourCount: childLimit.getHourCount(),
|
||||
minuteCount: childLimit.getMinuteCount(),
|
||||
forward: childLimit.isForward(),
|
||||
startAge: childLimit.getStartAge(),
|
||||
endAge: childLimit.getEndAge(),
|
||||
},
|
||||
decadeFortunes,
|
||||
annualFortunes,
|
||||
};
|
||||
}
|
||||
|
||||
/** Extract pillar info from a SixtyCycle, with Ten Star relative to day master */
|
||||
function extractPillarInfo(sixtyCycle: SixtyCycle, dayMasterStem: HeavenStem): PillarInfo {
|
||||
const stem = sixtyCycle.getHeavenStem();
|
||||
const branch = sixtyCycle.getEarthBranch();
|
||||
const sound = sixtyCycle.getSound();
|
||||
|
||||
// Hidden stems
|
||||
const hideStems: HideStemInfo[] = [];
|
||||
try {
|
||||
const allHideStems = branch.getHideHeavenStems();
|
||||
if (allHideStems) {
|
||||
for (const hs of allHideStems) {
|
||||
let typeName = '';
|
||||
const type = hs.getType();
|
||||
if (type === HideHeavenStemType.MAIN) typeName = '本气';
|
||||
else if (type === HideHeavenStemType.MIDDLE) typeName = '中气';
|
||||
else if (type === HideHeavenStemType.RESIDUAL) typeName = '余气';
|
||||
|
||||
let tenStarName: string | null = null;
|
||||
try {
|
||||
tenStarName = hs.getHeavenStem().getTenStar(dayMasterStem).getName();
|
||||
} catch { /* ten star may not be available */ }
|
||||
|
||||
hideStems.push({
|
||||
stem: hs.getHeavenStem().getName(),
|
||||
type: typeName,
|
||||
tenStar: tenStarName,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch { /* hide stems may not be available */ }
|
||||
|
||||
// Terrain (十二长生)
|
||||
let terrainName = '';
|
||||
let terrainFortune: 'good' | 'bad' | 'neutral' = 'neutral';
|
||||
try {
|
||||
const terrain = stem.getTerrain(branch);
|
||||
terrainName = terrain.getName();
|
||||
const goodTerrain = ['长生', '冠带', '临官', '帝旺', '胎', '养'];
|
||||
const badTerrain = ['死', '墓', '绝'];
|
||||
if (goodTerrain.includes(terrainName)) terrainFortune = 'good';
|
||||
else if (badTerrain.includes(terrainName)) terrainFortune = 'bad';
|
||||
} catch { /* terrain might fail */ }
|
||||
|
||||
// Ten Star
|
||||
let tenStarName: string | null = null;
|
||||
try {
|
||||
tenStarName = stem.getTenStar(dayMasterStem).getName();
|
||||
} catch { /* ten star may not be available */ }
|
||||
|
||||
const yinYang = stem.getYinYang();
|
||||
const branchYinYang = branch.getYinYang();
|
||||
|
||||
return {
|
||||
ganzhi: sixtyCycle.getName(),
|
||||
heavenStem: stem.getName(),
|
||||
earthBranch: branch.getName(),
|
||||
elementStem: stem.getElement().getName(),
|
||||
elementBranch: branch.getElement().getName(),
|
||||
yinYangStem: yinYang === YinYang.YANG ? 'yang' : 'yin',
|
||||
yinYangBranch: branchYinYang === YinYang.YANG ? 'yang' : 'yin',
|
||||
hideStems,
|
||||
nayin: sound.getName(),
|
||||
terrain: {
|
||||
name: terrainName,
|
||||
fortune: terrainFortune,
|
||||
},
|
||||
tenStar: tenStarName,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
SolarDay,
|
||||
SolarMonth,
|
||||
} from 'tyme4ts';
|
||||
import type { DayInfo } from '../types/calendar';
|
||||
import { getBuddhistFestival } from '../calculators/buddhistDates';
|
||||
|
||||
const SEASON_BY_MONTH: Record<number, string> = {
|
||||
3: '春季', 4: '春季', 5: '春季',
|
||||
6: '夏季', 7: '夏季', 8: '夏季',
|
||||
9: '秋季', 10: '秋季', 11: '秋季',
|
||||
12: '冬季', 1: '冬季', 2: '冬季',
|
||||
};
|
||||
|
||||
/** Transform a SolarDay into a plain DayInfo object */
|
||||
export function solarDayToDayInfo(solarDay: SolarDay): DayInfo {
|
||||
const lunarDay = solarDay.getLunarDay();
|
||||
const lunarMonth = lunarDay.getLunarMonth();
|
||||
const week = solarDay.getWeek();
|
||||
const constellation = solarDay.getConstellation();
|
||||
const term = solarDay.getTerm();
|
||||
const termDay = solarDay.getTermDay();
|
||||
const month = solarDay.getSolarMonth().getMonth();
|
||||
const solarYear = solarDay.getSolarMonth().getSolarYear().getYear();
|
||||
|
||||
// Solar term
|
||||
const isTermDay = termDay !== null;
|
||||
const solarTerm = isTermDay ? term.getName() : null;
|
||||
let solarTermTime: string | null = null;
|
||||
if (isTermDay) {
|
||||
try {
|
||||
const jd = term.getJulianDay();
|
||||
if (jd) {
|
||||
const st = jd.getSolarTime();
|
||||
solarTermTime = `${String(st.getHour()).padStart(2,'0')}:${String(st.getMinute()).padStart(2,'0')}`;
|
||||
}
|
||||
} catch { /* time not available */ }
|
||||
}
|
||||
|
||||
// Season + term progress (every day belongs to a solar term)
|
||||
const season = SEASON_BY_MONTH[month] || '';
|
||||
let currentSolarTerm: string | null = null;
|
||||
let termDayIndex: number | null = null;
|
||||
let nextSolarTerm: string | null = null;
|
||||
let daysToNextTerm: number | null = null;
|
||||
try {
|
||||
const currentTerm = solarDay.getTerm();
|
||||
const nextTerm = currentTerm.next(1);
|
||||
currentSolarTerm = currentTerm.getName();
|
||||
termDayIndex = solarDay.subtract(currentTerm.getSolarDay()) + 1;
|
||||
nextSolarTerm = nextTerm.getName();
|
||||
daysToNextTerm = nextTerm.getSolarDay().subtract(solarDay);
|
||||
} catch { /* term may fail */ }
|
||||
|
||||
// Julian day, Buddhist era, Islamic/Hijri date
|
||||
let julianDay: number | null = null;
|
||||
try { julianDay = solarDay.getJulianDay().getDay(); } catch { /* */ }
|
||||
const buddhistYear = solarYear + 543;
|
||||
let hijriDate: string | null = null;
|
||||
try {
|
||||
const hd = solarDay.getHijriDay();
|
||||
const hm = hd.getHijriMonth();
|
||||
hijriDate = `${hm.getHijriYear().getYear()}年${String(hm.getIndexInYear()).padStart(2, '0')}月${String(hd.getDay()).padStart(2, '0')}日`;
|
||||
} catch { /* */ }
|
||||
const buddhistFestival = getBuddhistFestival(lunarMonth.getMonth(), lunarDay.getDay());
|
||||
|
||||
// Phenology
|
||||
let phenology: string | null = null;
|
||||
try {
|
||||
const pd = solarDay.getPhenologyDay();
|
||||
if (pd) phenology = pd.getPhenology().getName();
|
||||
} catch { /* not available for all dates */ }
|
||||
|
||||
// Dog days
|
||||
let dogDay: string | null = null;
|
||||
try {
|
||||
const dd = solarDay.getDogDay();
|
||||
if (dd) dogDay = dd.getDog().getName();
|
||||
} catch { /* not in dog days */ }
|
||||
|
||||
// Nine-day cold
|
||||
let nineDay: string | null = null;
|
||||
try {
|
||||
const nd = solarDay.getNineDay();
|
||||
if (nd) nineDay = nd.getNine().getName();
|
||||
} catch { /* not in nine days */ }
|
||||
|
||||
// Moon phase
|
||||
let moonPhase: string | null = null;
|
||||
try {
|
||||
const phase = solarDay.getPhase();
|
||||
if (phase) moonPhase = phase.getName();
|
||||
} catch { /* not available */ }
|
||||
|
||||
// Festivals
|
||||
let lunarFestival: string | null = null;
|
||||
try {
|
||||
const lf = lunarDay.getFestival();
|
||||
if (lf) lunarFestival = lf.getName();
|
||||
} catch { /* no festival */ }
|
||||
|
||||
let solarFestival: string | null = null;
|
||||
try {
|
||||
const sf = solarDay.getFestival();
|
||||
if (sf) solarFestival = sf.getName();
|
||||
} catch { /* no festival */ }
|
||||
|
||||
let legalHoliday: { name: string; isWork: boolean } | null = null;
|
||||
try {
|
||||
const lh = solarDay.getLegalHoliday();
|
||||
if (lh) legalHoliday = { name: lh.getName(), isWork: lh.isWork() };
|
||||
} catch { /* not a legal holiday */ }
|
||||
|
||||
// Gan-Zhi
|
||||
const daySixtyCycle = lunarDay.getSixtyCycle();
|
||||
const yearSixtyCycle = lunarDay.getYearSixtyCycle();
|
||||
const monthSixtyCycle = lunarDay.getMonthSixtyCycle();
|
||||
|
||||
// Today check
|
||||
const now = new Date();
|
||||
const isToday =
|
||||
solarDay.getSolarMonth().getSolarYear().getYear() === now.getFullYear() &&
|
||||
solarDay.getSolarMonth().getMonth() === now.getMonth() + 1 &&
|
||||
solarDay.getDay() === now.getDate();
|
||||
|
||||
const weekDayIndex = week.getIndex();
|
||||
const isWeekend = weekDayIndex === 0 || weekDayIndex === 6;
|
||||
|
||||
return {
|
||||
solarDate: `${solarDay.getSolarMonth().getSolarYear().getYear()}-${String(solarDay.getSolarMonth().getMonth()).padStart(2, '0')}-${String(solarDay.getDay()).padStart(2, '0')}`,
|
||||
solarDay: solarDay.getDay(),
|
||||
solarMonth: solarDay.getSolarMonth().getMonth(),
|
||||
solarYear: solarDay.getSolarMonth().getSolarYear().getYear(),
|
||||
weekDay: week.getName(),
|
||||
weekDayIndex,
|
||||
constellation: constellation.getName(),
|
||||
solarTerm,
|
||||
solarTermTime,
|
||||
isTermDay,
|
||||
currentSolarTerm,
|
||||
season,
|
||||
termDayIndex,
|
||||
nextSolarTerm,
|
||||
daysToNextTerm,
|
||||
julianDay,
|
||||
buddhistYear,
|
||||
hijriDate,
|
||||
buddhistFestival,
|
||||
phenology,
|
||||
dogDay,
|
||||
nineDay,
|
||||
lunarYear: lunarMonth.getLunarYear().getYear(),
|
||||
lunarMonth: lunarMonth.getMonth(),
|
||||
lunarMonthName: lunarMonth.getName(),
|
||||
lunarDay: lunarDay.getDay(),
|
||||
lunarDayName: lunarDay.getName(),
|
||||
isLeapMonth: lunarMonth.isLeap(),
|
||||
lunarYearGanzhi: yearSixtyCycle.getName(),
|
||||
lunarMonthGanzhi: monthSixtyCycle.getName(),
|
||||
lunarDayGanzhi: daySixtyCycle.getName(),
|
||||
zodiac: daySixtyCycle.getEarthBranch().getZodiac().getName(),
|
||||
lunarFestival,
|
||||
solarFestival,
|
||||
legalHoliday,
|
||||
moonPhase,
|
||||
isToday,
|
||||
isWeekend,
|
||||
dayOfWeek: weekDayIndex,
|
||||
};
|
||||
}
|
||||
|
||||
/** Get calendar days for a month as a 2D array (weeks × days) */
|
||||
export function getMonthCalendar(year: number, month: number, weekStart: 0 | 1 = 0): DayInfo[][] {
|
||||
const solarMonth = SolarMonth.fromYm(year, month);
|
||||
const weekCount = solarMonth.getWeekCount(weekStart);
|
||||
const weeks = solarMonth.getWeeks(weekStart);
|
||||
|
||||
const result: DayInfo[][] = [];
|
||||
for (let w = 0; w < weekCount; w++) {
|
||||
const week = weeks[w];
|
||||
const days = week.getDays();
|
||||
const row: DayInfo[] = [];
|
||||
for (const day of days) {
|
||||
row.push(solarDayToDayInfo(day));
|
||||
}
|
||||
result.push(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Get DayInfo for a specific date */
|
||||
export function getDayInfo(year: number, month: number, day: number): DayInfo {
|
||||
const solarDay = SolarDay.fromYmd(year, month, day);
|
||||
return solarDayToDayInfo(solarDay);
|
||||
}
|
||||
|
||||
/** Get today's DayInfo */
|
||||
export function getTodayInfo(): DayInfo {
|
||||
const now = new Date();
|
||||
return getDayInfo(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 流月计算:按节气月(立春起 12 节)划分某公历年的 12 个流月
|
||||
*/
|
||||
import { SolarTerm } from 'tyme4ts';
|
||||
import { getDayInfo } from './day';
|
||||
|
||||
export interface YearMonthInfo {
|
||||
/** 0-11,从寅月(正月/立春)起 */
|
||||
index: number;
|
||||
/** 正月..腊月 */
|
||||
name: string;
|
||||
/** 起始公历月 */
|
||||
solarMonth: number;
|
||||
/** 节日期 YYYY-MM-DD */
|
||||
startDate: string;
|
||||
/** 月末 YYYY-MM-DD(下一节前一天) */
|
||||
endDate: string;
|
||||
/** 月柱干支 */
|
||||
ganzhi: string;
|
||||
}
|
||||
|
||||
// SolarTerm 索引:冬至0 小寒1 大寒2 立春3 雨水4 惊蛰5 春分6 清明7 谷雨8 立夏9 小满10 芒种11 夏至12 小暑13 大暑14 立秋15 处暑16 白露17 秋分18 寒露19 霜降20 立冬21 小雪22 大雪23
|
||||
const JIE_INDICES = [3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 1]; // 立春..小寒(次年)
|
||||
|
||||
const MONTH_NAME_BY_BRANCH: Record<string, string> = {
|
||||
'寅': '正月', '卯': '二月', '辰': '三月', '巳': '四月', '午': '五月', '未': '六月',
|
||||
'申': '七月', '酉': '八月', '戌': '九月', '亥': '十月', '子': '冬月', '丑': '腊月',
|
||||
};
|
||||
|
||||
function pad(n: number): string {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
function toDateStr(day: { getYear(): number; getMonth(): number; getDay(): number }): string {
|
||||
return `${day.getYear()}-${pad(day.getMonth())}-${pad(day.getDay())}`;
|
||||
}
|
||||
|
||||
/** Get the 12 节气月(流月)of a solar year, starting from 立春 */
|
||||
export function getYearMonths(year: number): YearMonthInfo[] {
|
||||
const months: YearMonthInfo[] = [];
|
||||
for (let k = 0; k < 12; k++) {
|
||||
const termYear = k === 11 ? year + 1 : year;
|
||||
const start = SolarTerm.fromIndex(termYear, JIE_INDICES[k]).getSolarDay();
|
||||
const end = k === 11
|
||||
? start
|
||||
: SolarTerm.fromIndex(year, JIE_INDICES[k + 1]).getSolarDay().next(-1);
|
||||
const di = getDayInfo(start.getYear(), start.getMonth(), start.getDay());
|
||||
const branch = di.lunarMonthGanzhi[1];
|
||||
months.push({
|
||||
index: k,
|
||||
name: MONTH_NAME_BY_BRANCH[branch] || di.lunarMonthName || `${k + 1}月`,
|
||||
solarMonth: start.getMonth(),
|
||||
startDate: toDateStr(start),
|
||||
endDate: toDateStr(end),
|
||||
ganzhi: di.lunarMonthGanzhi,
|
||||
});
|
||||
}
|
||||
return months;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/** Full almanac (黄历) information for a single day */
|
||||
export interface AlmanacInfo {
|
||||
// Duty officer (建除十二值神)
|
||||
duty: string;
|
||||
dutyLuck: 'good' | 'bad' | 'neutral';
|
||||
|
||||
// Twelve star (黄道黑道十二神)
|
||||
twelveStar: {
|
||||
name: string;
|
||||
ecliptic: string; // 黄道 or 黑道
|
||||
luck: 'good' | 'bad';
|
||||
};
|
||||
|
||||
// Twenty-eight lunar mansion (二十八星宿)
|
||||
twentyEightStar: {
|
||||
name: string;
|
||||
luck: 'good' | 'bad';
|
||||
animal: string;
|
||||
};
|
||||
|
||||
// Nine star (九星)
|
||||
nineStar: {
|
||||
name: string;
|
||||
color: string;
|
||||
element: string;
|
||||
};
|
||||
|
||||
// Six star (六曜)
|
||||
sixStar: string;
|
||||
|
||||
// Minor Ren (小六壬)
|
||||
minorRen: {
|
||||
name: string;
|
||||
luck: 'good' | 'bad';
|
||||
element: string;
|
||||
};
|
||||
|
||||
// Moon phase
|
||||
phase: string;
|
||||
|
||||
// Fetus god (胎神)
|
||||
fetus: {
|
||||
direction: string;
|
||||
side: string; // 房内/房外
|
||||
position: string; // Full position description
|
||||
};
|
||||
|
||||
// Recommendations and avoidances (宜忌)
|
||||
recommends: string[];
|
||||
avoids: string[];
|
||||
|
||||
// Gods (神煞)
|
||||
goodGods: string[];
|
||||
badGods: string[];
|
||||
|
||||
// Day stem-branch info
|
||||
dayStem: string; // 天干 e.g. "甲"
|
||||
dayBranch: string; // 地支 e.g. "子"
|
||||
dayGanzhi: string; // 干支 e.g. "甲子"
|
||||
|
||||
// Peng Zu taboo (彭祖百忌)
|
||||
pengZu: string;
|
||||
pengZuStem: string; // 天干禁忌
|
||||
pengZuBranch: string; // 地支禁忌
|
||||
|
||||
// Chong/Sha/Harm/Combine (冲煞害合)
|
||||
clash: string; // 冲 e.g. "马(午)"
|
||||
harm: string; // 害 e.g. "羊(未)"
|
||||
combine: string; // 合 e.g. "牛(丑)"
|
||||
evilDirection: string;// 煞 e.g. "北"
|
||||
|
||||
// Na Yin sound (纳音)
|
||||
nayin: string;
|
||||
|
||||
// Hourly almanac
|
||||
hourDetails: HourAlmanac[];
|
||||
}
|
||||
|
||||
/** Hourly almanac for each of the 12 two-hour periods (时辰) */
|
||||
export interface HourAlmanac {
|
||||
branch: string; // 地支 e.g. "子"
|
||||
name: string; // 时辰名 e.g. "子时"
|
||||
range: string; // Time range e.g. "23:00-01:00"
|
||||
ganzhi: string; // Hour pillar e.g. "甲子"
|
||||
recommends: string[];
|
||||
avoids: string[];
|
||||
twelveStar: string;
|
||||
nineStar: string;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/** A single pillar (柱) in the Bazi — year, month, day, or hour */
|
||||
export interface PillarInfo {
|
||||
ganzhi: string; // "甲子"
|
||||
heavenStem: string; // "甲"
|
||||
earthBranch: string; // "子"
|
||||
elementStem: string; // Stem's five element "木"
|
||||
elementBranch: string; // Branch's five element "水"
|
||||
yinYangStem: 'yin' | 'yang';
|
||||
yinYangBranch: 'yin' | 'yang';
|
||||
hideStems: HideStemInfo[];
|
||||
nayin: string; // Na Yin sound "海中金"
|
||||
terrain: {
|
||||
name: string; // 十二长生 stage
|
||||
fortune: 'good' | 'bad' | 'neutral';
|
||||
};
|
||||
tenStar: string | null; // Ten Star relative to day master
|
||||
}
|
||||
|
||||
/** Hidden stem within an earth branch (藏干) */
|
||||
export interface HideStemInfo {
|
||||
stem: string; // "甲"
|
||||
type: string; // "本气" | "中气" | "余气"
|
||||
tenStar: string | null; // Ten Star relative to day master
|
||||
}
|
||||
|
||||
/** Complete Bazi (八字) result for a birth date/time */
|
||||
export interface EightCharInfo {
|
||||
birthDate: string; // ISO date
|
||||
birthTime: string; // HH:mm
|
||||
gender: 'male' | 'female';
|
||||
|
||||
yearPillar: PillarInfo;
|
||||
monthPillar: PillarInfo;
|
||||
dayPillar: PillarInfo; // Day master pillar
|
||||
hourPillar: PillarInfo;
|
||||
|
||||
// Derived
|
||||
dayMaster: string; // "甲木" — day stem + element
|
||||
dayMasterStem: string; // "甲"
|
||||
dayMasterElement: string; // "木"
|
||||
|
||||
fetalOrigin: string; // 胎元
|
||||
fetalBreath: string; // 胎息
|
||||
ownSign: string; // 命宫
|
||||
bodySign: string; // 身宫
|
||||
emptyBranches: string[]; // 空亡 branches
|
||||
emptyTen: string; // 旬
|
||||
}
|
||||
|
||||
/** Decade fortune (大运) */
|
||||
export interface DecadeFortuneInfo {
|
||||
index: number;
|
||||
ganzhi: string;
|
||||
startAge: number;
|
||||
endAge: number;
|
||||
startYear: number;
|
||||
endYear: number;
|
||||
heavenStem: string;
|
||||
earthBranch: string;
|
||||
nayin: string;
|
||||
}
|
||||
|
||||
/** Annual fortune (流年/小运) */
|
||||
export interface FortuneInfo {
|
||||
age: number;
|
||||
year: number;
|
||||
ganzhi: string;
|
||||
nayin: string;
|
||||
}
|
||||
|
||||
/** Child limit (起运) information */
|
||||
export interface ChildLimitInfo {
|
||||
startTime: string; // ISO datetime
|
||||
endTime: string;
|
||||
yearCount: number;
|
||||
monthCount: number;
|
||||
dayCount: number;
|
||||
hourCount: number;
|
||||
minuteCount: number;
|
||||
forward: boolean; // 顺排/逆排
|
||||
startAge: number;
|
||||
endAge: number;
|
||||
}
|
||||
|
||||
/** Full Bazi analysis result */
|
||||
export interface BaziFullResult {
|
||||
eightChar: EightCharInfo;
|
||||
childLimit: ChildLimitInfo;
|
||||
decadeFortunes: DecadeFortuneInfo[];
|
||||
annualFortunes: FortuneInfo[];
|
||||
/** True when the caller applied solar-time longitude correction to the birth time */
|
||||
solarAdjusted?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/** Core calendar day information — framework-agnostic plain object */
|
||||
export interface DayInfo {
|
||||
/** ISO date string YYYY-MM-DD */
|
||||
solarDate: string;
|
||||
solarDay: number;
|
||||
solarMonth: number;
|
||||
solarYear: number;
|
||||
/** Chinese weekday name: 日,一,二,三,四,五,六 */
|
||||
weekDay: string;
|
||||
/** 0=Sunday ... 6=Saturday */
|
||||
weekDayIndex: number;
|
||||
/** Western zodiac constellation name */
|
||||
constellation: string;
|
||||
/** Solar term name if this day is a term day */
|
||||
solarTerm: string | null;
|
||||
/** Exact solar term time (HH:mm) */
|
||||
solarTermTime: string | null;
|
||||
/** Whether this day is the exact solar term transition day */
|
||||
isTermDay: boolean;
|
||||
/** The solar term this day belongs to (e.g. 大暑) */
|
||||
currentSolarTerm: string | null;
|
||||
/** Season name: 春季/夏季/秋季/冬季 */
|
||||
season: string;
|
||||
/** 1-based day index within the current solar term (节气第几天) */
|
||||
termDayIndex: number | null;
|
||||
/** Next solar term name */
|
||||
nextSolarTerm: string | null;
|
||||
/** Days until the next solar term */
|
||||
daysToNextTerm: number | null;
|
||||
/** Julian Day number (儒略日) */
|
||||
julianDay: number | null;
|
||||
/** Buddhist Era year (佛历年 = 公历年 + 543) */
|
||||
buddhistYear: number | null;
|
||||
/** Islamic/Hijri date as "1448年02月18日" */
|
||||
hijriDate: string | null;
|
||||
/** Buddhist festival name (农历) if applicable */
|
||||
buddhistFestival: string | null;
|
||||
/** 72 phenology name if applicable */
|
||||
phenology: string | null;
|
||||
/** Three periods (三伏) if applicable */
|
||||
dogDay: string | null;
|
||||
/** Nine-day cold period (数九) if applicable */
|
||||
nineDay: string | null;
|
||||
|
||||
// Lunar calendar
|
||||
lunarYear: number;
|
||||
lunarMonth: number;
|
||||
/** Chinese lunar month name e.g. "五月" */
|
||||
lunarMonthName: string;
|
||||
lunarDay: number;
|
||||
/** Chinese lunar day name e.g. "初十", "廿一" */
|
||||
lunarDayName: string;
|
||||
isLeapMonth: boolean;
|
||||
/** Gan-Zhi of the lunar year e.g. "丙午" */
|
||||
lunarYearGanzhi: string;
|
||||
/** Gan-Zhi of the lunar month */
|
||||
lunarMonthGanzhi: string;
|
||||
/** Gan-Zhi of the lunar day */
|
||||
lunarDayGanzhi: string;
|
||||
/** Chinese zodiac animal */
|
||||
zodiac: string;
|
||||
|
||||
// Festivals & holidays
|
||||
lunarFestival: string | null;
|
||||
solarFestival: string | null;
|
||||
legalHoliday: { name: string; isWork: boolean } | null;
|
||||
|
||||
// Moon phase
|
||||
moonPhase: string | null;
|
||||
|
||||
// Metadata
|
||||
isToday: boolean;
|
||||
isWeekend: boolean;
|
||||
/** Day-of-week position within the month grid (0-based column) */
|
||||
dayOfWeek: number;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/** Relationship between a single pillar in user's Bazi and day's Bazi */
|
||||
export interface PillarRelationship {
|
||||
pillar: 'year' | 'month' | 'day' | 'hour';
|
||||
pillarLabel: string; // "年柱", "月柱", "日柱", "时柱"
|
||||
|
||||
userGanzhi: string;
|
||||
dayGanzhi: string;
|
||||
|
||||
// Heaven stem relationships
|
||||
stemTenStar: string; // Ten Star: day stem vs user stem
|
||||
stemCombine: boolean; // 天干合
|
||||
stemOpposite: boolean; // 天干冲
|
||||
|
||||
// Earth branch relationships
|
||||
branchCombine: boolean; // 六合
|
||||
branchThreeCombine: boolean; // 三合
|
||||
branchOpposite: boolean; // 六冲
|
||||
branchHarm: boolean; // 六害
|
||||
branchPunish: boolean; // 相刑
|
||||
branchFormation: string | null;// 三合局名
|
||||
|
||||
// Score contribution
|
||||
score: number; // -10 to +10
|
||||
}
|
||||
|
||||
/** Complete daily fortune result for a user on a specific day */
|
||||
export interface DailyFortuneResult {
|
||||
date: string; // ISO date
|
||||
lunarDate: string; // Lunar date description
|
||||
dayGanzhi: string; // Day's Gan-Zhi
|
||||
|
||||
/** Overall score from -100 to +100 */
|
||||
overallScore: number;
|
||||
/** Score level classification */
|
||||
scoreLevel: 'great' | 'good' | 'fair' | 'poor' | 'bad';
|
||||
|
||||
/** Pillar-by-pillar relationship analysis */
|
||||
pillarRelationships: PillarRelationship[];
|
||||
|
||||
/** Positive aspects of the day */
|
||||
luckyAspects: string[];
|
||||
/** Negative aspects / warnings */
|
||||
unluckyAspects: string[];
|
||||
/** Actionable suggestions */
|
||||
suggestions: string[];
|
||||
|
||||
/** Primary affected life area */
|
||||
affectedAreas: string[];
|
||||
|
||||
/** Lucky meta info for the day (personalized) */
|
||||
luckyMeta: {
|
||||
colors: string[];
|
||||
numbers: number[];
|
||||
direction: string;
|
||||
element: string;
|
||||
activity: string;
|
||||
};
|
||||
|
||||
/** Category scores (-100 to +100) */
|
||||
categoryScores: {
|
||||
love: number;
|
||||
career: number;
|
||||
wealth: number;
|
||||
health: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export type { DayInfo } from './calendar';
|
||||
export type { AlmanacInfo, HourAlmanac } from './almanac';
|
||||
export type {
|
||||
PillarInfo,
|
||||
HideStemInfo,
|
||||
EightCharInfo,
|
||||
DecadeFortuneInfo,
|
||||
FortuneInfo,
|
||||
ChildLimitInfo,
|
||||
BaziFullResult,
|
||||
} from './bazi';
|
||||
export type {
|
||||
PillarRelationship,
|
||||
DailyFortuneResult,
|
||||
} from './fortune';
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"lib": ["ES2022"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#FFFBF5" />
|
||||
<meta name="description" content="万年历 - 农历黄历八字运势" />
|
||||
<title>万年历</title>
|
||||
<script>
|
||||
// Prevent dark mode flicker
|
||||
(function() {
|
||||
const theme = localStorage.getItem('lunar-theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-foreground antialiased">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@lunar/web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lunar/core": "workspace:*",
|
||||
"framer-motion": "^12.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"zustand": "^5.0.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-variants": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"vite-plugin-pwa": "^0.21.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="12" fill="#C41E3A"/>
|
||||
<text x="32" y="44" text-anchor="middle" font-size="36" font-family="serif" fill="white">历</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -0,0 +1,68 @@
|
||||
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { AppShell } from './components/layout/AppShell';
|
||||
import { ErrorBoundary } from './components/ui/ErrorBoundary';
|
||||
import { PageSkeleton } from './components/ui/Skeleton';
|
||||
|
||||
// Lazy load pages
|
||||
const HomePage = lazy(() => import('./pages/HomePage'));
|
||||
const CalendarPage = lazy(() => import('./pages/CalendarPage'));
|
||||
const DayDetailPage = lazy(() => import('./pages/DayDetailPage'));
|
||||
const BaziPage = lazy(() => import('./pages/BaziPage'));
|
||||
const DailyFortunePage = lazy(() => import('./pages/DailyFortunePage'));
|
||||
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
|
||||
const DivinationPage = lazy(() => import('./pages/DivinationPage'));
|
||||
const SolarTermsPage = lazy(() => import('./pages/SolarTermsPage'));
|
||||
|
||||
const pageVariants = {
|
||||
initial: { opacity: 0, y: 12 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: -12 },
|
||||
};
|
||||
|
||||
function PageTransition({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function SafePage({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<PageTransition>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<AnimatePresence mode="wait">
|
||||
<Suspense fallback={<PageSkeleton />}>
|
||||
<Routes location={location} key={location.pathname}>
|
||||
<Route path="/" element={<SafePage><HomePage /></SafePage>} />
|
||||
<Route path="/calendar" element={<SafePage><CalendarPage /></SafePage>} />
|
||||
<Route path="/calendar/:date" element={<SafePage><DayDetailPage /></SafePage>} />
|
||||
<Route path="/bazi" element={<SafePage><BaziPage /></SafePage>} />
|
||||
<Route path="/daily-fortune" element={<SafePage><DailyFortunePage /></SafePage>} />
|
||||
<Route path="/settings" element={<SafePage><SettingsPage /></SafePage>} />
|
||||
<Route path="/divination" element={<SafePage><DivinationPage /></SafePage>} />
|
||||
<Route path="/solar-terms" element={<SafePage><SolarTermsPage /></SafePage>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AnimatePresence>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { cn } from '../../lib/utils';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Star } from 'lucide-react';
|
||||
import type { DayInfo } from '@lunar/core';
|
||||
|
||||
interface CalendarCellProps {
|
||||
dayInfo: DayInfo;
|
||||
isSelected: boolean;
|
||||
isCurrentMonth: boolean;
|
||||
fortuneScore?: number;
|
||||
hasBookmark?: boolean;
|
||||
expanded?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
// Short festival abbreviations
|
||||
function shortFestival(di: DayInfo): string | null {
|
||||
if (di.lunarFestival) {
|
||||
if (di.lunarFestival.includes('春节')) return '春节';
|
||||
if (di.lunarFestival.includes('元宵')) return '元宵';
|
||||
if (di.lunarFestival.includes('清明')) return '清明';
|
||||
if (di.lunarFestival.includes('端午')) return '端午';
|
||||
if (di.lunarFestival.includes('七夕')) return '七夕';
|
||||
if (di.lunarFestival.includes('中秋')) return '中秋';
|
||||
if (di.lunarFestival.includes('重阳')) return '重阳';
|
||||
if (di.lunarFestival.includes('除夕')) return '除夕';
|
||||
if (di.lunarFestival.includes('腊八')) return '腊八';
|
||||
if (di.lunarFestival.includes('冬至')) return '冬至';
|
||||
if (di.lunarFestival.includes('小年') || di.lunarFestival.includes('灶')) return '小年';
|
||||
return '节';
|
||||
}
|
||||
if (di.solarFestival) {
|
||||
if (di.solarFestival.includes('元旦')) return '元旦';
|
||||
if (di.solarFestival.includes('国庆')) return '国庆';
|
||||
if (di.solarFestival.includes('劳动')) return '劳动';
|
||||
if (di.solarFestival.includes('妇女')) return '妇女';
|
||||
if (di.solarFestival.includes('儿童')) return '儿童';
|
||||
if (di.solarFestival.includes('情人')) return '情人';
|
||||
return '节';
|
||||
}
|
||||
if (di.legalHoliday && !di.legalHoliday.isWork) return '休';
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CalendarCell({ dayInfo, isSelected, isCurrentMonth, fortuneScore, hasBookmark, expanded, onClick }: CalendarCellProps) {
|
||||
const fest = shortFestival(dayInfo);
|
||||
const hasTerm = !!dayInfo.solarTerm;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.92 }}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center justify-center aspect-square p-0.5',
|
||||
'border-b border-r border-border/50',
|
||||
!isCurrentMonth && 'opacity-30',
|
||||
isSelected && !dayInfo.isToday && 'bg-primary/10 rounded-lg',
|
||||
dayInfo.isToday && 'bg-primary rounded-md',
|
||||
)}
|
||||
>
|
||||
{/* Festival / Solar term label at top */}
|
||||
{(fest || hasTerm) && (
|
||||
<span className={cn(
|
||||
'text-[8px] leading-none font-medium relative z-10 truncate max-w-full px-0.5',
|
||||
fest ? 'text-red-500' : 'text-green-600',
|
||||
)}>
|
||||
{fest || (hasTerm ? dayInfo.solarTerm!.slice(0, 2) : '')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Fortune dot (if user has bazi) */}
|
||||
{fortuneScore !== undefined && (
|
||||
<div className={`absolute top-0.5 left-0.5 w-1.5 h-1.5 rounded-full z-10 ${
|
||||
fortuneScore >= 20 ? 'bg-red-500' : fortuneScore <= -20 ? 'bg-slate-500' : 'bg-amber-400'
|
||||
}`} />
|
||||
)}
|
||||
|
||||
{/* Bookmark star */}
|
||||
{hasBookmark && (
|
||||
<Star size={8} className="absolute top-0.5 right-0.5 text-amber-500 fill-amber-500 z-10" />
|
||||
)}
|
||||
|
||||
{/* Solar day number */}
|
||||
<span className={cn(
|
||||
'text-sm font-semibold leading-tight relative z-10',
|
||||
dayInfo.isToday && 'text-white',
|
||||
dayInfo.isWeekend && !dayInfo.isToday && 'text-red-500',
|
||||
!isCurrentMonth && 'text-muted',
|
||||
)}>
|
||||
{dayInfo.solarDay}
|
||||
</span>
|
||||
|
||||
{/* Lunar day name or festival */}
|
||||
<span className={cn(
|
||||
'text-[10px] leading-tight relative z-10 truncate max-w-full px-0.5',
|
||||
dayInfo.isToday && 'text-white/80',
|
||||
!dayInfo.isToday && 'text-secondary',
|
||||
dayInfo.lunarDay === 1 && !dayInfo.isToday && 'text-primary font-medium',
|
||||
dayInfo.lunarDay === 15 && !dayInfo.isToday && 'text-primary font-medium',
|
||||
)}>
|
||||
{dayInfo.lunarDayName}
|
||||
</span>
|
||||
|
||||
{/* Extra info in week view */}
|
||||
{expanded && (
|
||||
<div className="relative z-10 mt-0.5 space-y-0.5">
|
||||
{dayInfo.lunarFestival && (
|
||||
<span className="text-[8px] text-red-500 block truncate">{dayInfo.lunarFestival}</span>
|
||||
)}
|
||||
{dayInfo.solarTerm && (
|
||||
<span className="text-[8px] text-green-600 block truncate">{dayInfo.solarTerm}</span>
|
||||
)}
|
||||
{fortuneScore !== undefined && (
|
||||
<span className={`text-[8px] font-medium ${fortuneScore>=20?'text-red-500':fortuneScore<=-20?'text-slate-500':'text-amber-500'}`}>
|
||||
{fortuneScore>=20?'吉':fortuneScore<=-20?'凶':'平'} {fortuneScore}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { DayInfo } from '@lunar/core';
|
||||
import { CalendarCell } from './CalendarCell';
|
||||
import { WeekDayBar } from './WeekDayBar';
|
||||
import { MonthYearPicker } from './MonthYearPicker';
|
||||
|
||||
interface CalendarGridProps {
|
||||
days: DayInfo[][];
|
||||
viewDate: Date;
|
||||
selectedDate: Date | null;
|
||||
weekStart?: 0 | 1;
|
||||
fortuneScores?: Record<string, number> | null;
|
||||
bookmarkedDates?: Set<string>;
|
||||
onPrevMonth: () => void;
|
||||
onNextMonth: () => void;
|
||||
onPrevYear: () => void;
|
||||
onNextYear: () => void;
|
||||
onSelectDate: (d: Date) => void;
|
||||
}
|
||||
|
||||
export function CalendarGrid({
|
||||
days, viewDate, selectedDate, weekStart = 0, fortuneScores, bookmarkedDates,
|
||||
onPrevMonth, onNextMonth, onPrevYear, onNextYear, onSelectDate,
|
||||
}: CalendarGridProps) {
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<'month'|'week'>('month');
|
||||
const monthKey = `${viewDate.getFullYear()}-${viewDate.getMonth()}`;
|
||||
|
||||
const handleDayClick = (dayInfo: DayInfo) => {
|
||||
const [y, m, d] = dayInfo.solarDate.split('-').map(Number);
|
||||
onSelectDate(new Date(y, m - 1, d));
|
||||
navigate(`/calendar/${dayInfo.solarDate}`);
|
||||
};
|
||||
|
||||
const isSelected = (di: DayInfo): boolean => {
|
||||
if (!selectedDate) return false;
|
||||
const [y, m, d] = di.solarDate.split('-').map(Number);
|
||||
return selectedDate.getFullYear()===y && selectedDate.getMonth()===m-1 && selectedDate.getDate()===d;
|
||||
};
|
||||
|
||||
// Week view: show the week containing the selected date, or current week
|
||||
const selectedWeek = selectedDate
|
||||
? days.findIndex(w => w.some(d => {
|
||||
const [y,m,day] = d.solarDate.split('-').map(Number);
|
||||
return selectedDate.getFullYear()===y && selectedDate.getMonth()===m-1 && selectedDate.getDate()===day;
|
||||
}))
|
||||
: -1;
|
||||
const displayWeek = selectedWeek >= 0 ? selectedWeek : 0;
|
||||
const displayDays = viewMode === 'week' ? (days[displayWeek] || days[0] || []) : days.flat();
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<MonthYearPicker viewDate={viewDate}
|
||||
onPrevMonth={onPrevMonth} onNextMonth={onNextMonth}
|
||||
onPrevYear={onPrevYear} onNextYear={onNextYear} />
|
||||
|
||||
{/* View mode toggle */}
|
||||
<div className="flex rounded-lg bg-card border border-border p-0.5">
|
||||
<button onClick={() => setViewMode('month')}
|
||||
className={`flex-1 py-1 text-xs rounded-md font-medium transition-colors ${viewMode==='month'?'bg-primary text-white':'text-secondary'}`}>月</button>
|
||||
<button onClick={() => setViewMode('week')}
|
||||
className={`flex-1 py-1 text-xs rounded-md font-medium transition-colors ${viewMode==='week'?'bg-primary text-white':'text-secondary'}`}>周</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
|
||||
<WeekDayBar weekStart={weekStart} />
|
||||
<motion.div key={`${monthKey}-${viewMode}`} initial={{opacity:0}} animate={{opacity:1}} transition={{duration:0.12}}
|
||||
className={`grid grid-cols-7 ${viewMode==='week'?'auto-rows-fr':''}`}>
|
||||
{displayDays.map((dayInfo, idx) => (
|
||||
<CalendarCell key={`${dayInfo.solarDate}-${idx}`} dayInfo={dayInfo}
|
||||
isSelected={isSelected(dayInfo)}
|
||||
isCurrentMonth={dayInfo.solarMonth===viewDate.getMonth()+1 && dayInfo.solarYear===viewDate.getFullYear()}
|
||||
fortuneScore={fortuneScores?.[dayInfo.solarDate]}
|
||||
hasBookmark={bookmarkedDates?.has(dayInfo.solarDate)}
|
||||
expanded={viewMode === 'week'}
|
||||
onClick={() => handleDayClick(dayInfo)} />
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Month summary (fortune) */}
|
||||
{fortuneScores && (
|
||||
<MonthSummary scores={fortuneScores} days={days} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MonthSummary({ scores, days }: { scores: Record<string, number>; days: DayInfo[][] }) {
|
||||
const allDays = days.flat().filter(d => scores[d.solarDate] !== undefined);
|
||||
if (allDays.length === 0) return null;
|
||||
const good = allDays.filter(d => scores[d.solarDate] >= 20).length;
|
||||
const bad = allDays.filter(d => scores[d.solarDate] <= -20).length;
|
||||
const fair = allDays.length - good - bad;
|
||||
const best = allDays.reduce((a, b) => (scores[a.solarDate] > scores[b.solarDate]) ? a : b);
|
||||
const worst = allDays.reduce((a, b) => (scores[a.solarDate] < scores[b.solarDate]) ? a : b);
|
||||
return (
|
||||
<div className="bg-card rounded-xl border border-border p-2.5">
|
||||
<p className="text-xs font-medium mb-1.5">本月运势概览</p>
|
||||
<div className="flex items-center gap-3 text-xs mb-1.5">
|
||||
<span className="text-red-500">吉 {good}天</span>
|
||||
<span className="text-amber-500">平 {fair}天</span>
|
||||
<span className="text-slate-500">凶 {bad}天</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px] text-muted">
|
||||
<span>最佳: {best.lunarMonthName}{best.lunarDayName} ({scores[best.solarDate]})</span>
|
||||
<span>谨慎: {worst.lunarMonthName}{worst.lunarDayName} ({scores[worst.solarDate]})</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||