├── .babelrc.json
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .nojekyll
├── .storybook
├── main.ts
└── preview.ts
├── LICENSE
├── README.md
├── package.json
├── src
├── Izpalace
│ ├── Izpalace.css
│ ├── Izpalace.tsx
│ ├── Izpalace.type.ts
│ └── index.ts
├── IzpalaceCenter
│ ├── Item.tsx
│ ├── IzpalaceCenter.css
│ ├── IzpalaceCenter.tsx
│ ├── Line.tsx
│ └── index.ts
├── Izstar
│ ├── Izstar.tsx
│ ├── Izstar.type.ts
│ └── index.ts
├── Iztrolabe
│ ├── Iztrolabe.css
│ ├── Iztrolabe.stories.tsx
│ ├── Iztrolabe.tsx
│ ├── Iztrolabe.type.ts
│ └── index.ts
├── config
│ └── types.ts
├── index.ts
└── theme
│ └── default.css
├── tsconfig.json
└── yarn.lock
/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "sourceType": "unambiguous",
3 | "presets": [
4 | [
5 | "@babel/preset-env",
6 | {
7 | "targets": {
8 | "chrome": 100,
9 | "safari": 15,
10 | "firefox": 91
11 | }
12 | }
13 | ],
14 | "@babel/preset-typescript",
15 | "@babel/preset-react"
16 | ],
17 | "plugins": []
18 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | coverage
4 | .vscode
5 | .github
6 | .storybook
7 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/eslint-recommended",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "rules": {}
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | .vscode
14 | lib
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 |
48 | # Snowpack dependency directory (https://snowpack.dev/)
49 | web_modules/
50 |
51 | # TypeScript cache
52 | *.tsbuildinfo
53 |
54 | # Optional npm cache directory
55 | .npm
56 |
57 | # Optional eslint cache
58 | .eslintcache
59 |
60 | # Optional stylelint cache
61 | .stylelintcache
62 |
63 | # Microbundle cache
64 | .rpt2_cache/
65 | .rts2_cache_cjs/
66 | .rts2_cache_es/
67 | .rts2_cache_umd/
68 |
69 | # Optional REPL history
70 | .node_repl_history
71 |
72 | # Output of 'npm pack'
73 | *.tgz
74 |
75 | # Yarn Integrity file
76 | .yarn-integrity
77 |
78 | # dotenv environment variable files
79 | .env
80 | .env.development.local
81 | .env.test.local
82 | .env.production.local
83 | .env.local
84 |
85 | # parcel-bundler cache (https://parceljs.org/)
86 | .cache
87 | .parcel-cache
88 |
89 | # Next.js build output
90 | .next
91 | out
92 |
93 | # Nuxt.js build / generate output
94 | .nuxt
95 | dist
96 |
97 | # Gatsby files
98 | .cache/
99 | # Comment in the public line in if your project uses Gatsby and not Next.js
100 | # https://nextjs.org/blog/next-9-1#public-directory-support
101 | # public
102 |
103 | # vuepress build output
104 | .vuepress/dist
105 |
106 | # vuepress v2.x temp and cache directory
107 | .temp
108 | .cache
109 |
110 | # Docusaurus cache and generated files
111 | .docusaurus
112 |
113 | # Serverless directories
114 | .serverless/
115 |
116 | # FuseBox cache
117 | .fusebox/
118 |
119 | # DynamoDB Local files
120 | .dynamodb/
121 |
122 | # TernJS port file
123 | .tern-port
124 |
125 | # Stores VSCode versions used for testing VSCode extensions
126 | .vscode-test
127 |
128 | # yarn v2
129 | .yarn/cache
130 | .yarn/unplugged
131 | .yarn/build-state.yml
132 | .yarn/install-state.gz
133 | .pnp.*
134 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from "@storybook/react-webpack5";
2 |
3 | const config: StorybookConfig = {
4 | stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
5 | addons: [
6 | "@storybook/addon-links",
7 | "@storybook/addon-essentials",
8 | "@storybook/addon-onboarding",
9 | "@storybook/addon-interactions",
10 | ],
11 | framework: {
12 | name: "@storybook/react-webpack5",
13 | options: {},
14 | },
15 | docs: {
16 | autodocs: "tag",
17 | },
18 | };
19 | export default config;
20 |
--------------------------------------------------------------------------------
/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import type { Preview } from "@storybook/react";
2 |
3 | const preview: Preview = {
4 | parameters: {
5 | actions: { argTypesRegex: "^on[A-Z].*" },
6 | controls: {
7 | matchers: {
8 | color: /(background|color)$/i,
9 | date: /Date$/,
10 | },
11 | },
12 | },
13 | };
14 |
15 | export default preview;
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Sylar Long
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 📦 react-iztro
4 |
5 | 基于 [iztro](https://github.com/SylarLong/iztro) 实现的react组件,用于生成一张紫微斗数星盘。
6 |
7 | react component of [iztro](https://github.com/SylarLong/iztro) used to generate an astrolabe of Zi Wei Dou Shu.
8 |
9 |
10 |
11 |
12 |
13 | [](https://www.npmjs.com/package/react-iztro)
14 | [](https://www.npmjs.com/package/react-iztro)
15 | [](https://www.npmjs.com/package/react-iztro)
16 | [](https://www.npmjs.com/package/react-iztro)
17 | [](https://packagequality.com/#?package=react-iztro)
18 |
19 |
20 |
21 | ---
22 |
23 | ## 功能
24 |
25 | - 展示完整紫微斗数星盘
26 |
27 | 包含所有 `主星`,`辅星`,`杂耀`,`四化`,`神煞`,`流耀` 以及星耀的 `亮度`。高亮显示重要的星耀,比如 `桃花星`,`解神`,`禄存` 和 `天马`。
28 |
29 | - 合理的星耀分布
30 |
31 | 用不同的颜色和字号来将 `星耀`,`宫名`,`宫干` 等分区域显示,解盘一目了然,直击重点。
32 |
33 | - 清晰的运限指示
34 |
35 | 在宫位中明显的标示出 `大限`、`小限`、`流年`、`流月`、`流日`、`流时` 所在宫位,点击运限指示按钮以后会显示重排后的运限宫名以及运限四化,更加方便的使用叠宫技巧解盘。
36 |
37 | - 流耀显示
38 |
39 | 展示出各个流派都需要的 `流耀`,可自行选择自己熟悉的流耀进行解盘。
40 |
41 | - 三方四正指示线
42 |
43 | 在中宫会显示 `三方四正` 指示线,点击运限时指示线的指向会动态跟随选中的最小那个运限流动,比如同时选择 `流年` 和 `流月`,指示线会跟随 `流月`。
44 |
45 | - 强大的动态运限
46 |
47 | 在 `中宫` 里,除了显示基本信息和三方四正线以外,还加入了可以调整运限的按钮组,可以非常方便的移动各个维度的运限。
48 |
49 | - 实用的飞星展示
50 |
51 | 点击宫干,可以看到宫干飞化出去的四化(以星耀背景色表示,红色:`禄`,蓝色:`权`,绿色:`科`,黑色:`忌`)。宫干有自化的时候会在星耀前面显示一条代表四化的色条。
52 |
53 | - 简单易用的组件
54 |
55 | 零配置快速集成到你的页面中,对于集成几乎没有学习成本。你可以根据自己的页面风格自行调整样式,或控制各个元素的显示与隐藏(通过覆盖默认样式)。
56 |
57 | 集成到页面中的界面如下图所示。你也可以直接访问官方的 [紫微派 - 紫微斗数在线排盘](https://ziwei.pub/astrolabe) 查看效果。
58 |
59 |
60 |
61 | 如果你觉得该组件对你有用,希望给个⭐️⭐️鼓励一下。
62 |
63 | ## 安装
64 |
65 | ```sh
66 | npm install react-iztro -S
67 | ```
68 |
69 | 当然你也可以使用 yarn
70 |
71 | ```sh
72 | yarn add react-iztro
73 | ```
74 |
75 | ## 使用
76 |
77 | ```ts
78 | import {Iztrolabe} from "react-iztro"
79 |
80 | function App() {
81 | return (
82 |
83 |
91 |
92 | );
93 | }
94 |
95 | export default App;
96 |
97 | ```
98 |
99 | ## 克隆到本地
100 |
101 | 如果你想将代码克隆到本地查看或者修改代码,可以fork本仓库到你自己的仓库里,然后用以下步骤进行
102 |
103 | 1. 克隆代码
104 |
105 | ```
106 | git clone https://github.com/SylarLong/react-iztro.git
107 | ```
108 |
109 | 2. 安装依赖
110 |
111 | ```
112 | npm install
113 | ```
114 |
115 | 或者
116 |
117 | ```
118 | yarn
119 | ```
120 |
121 | 3. 启动
122 |
123 | ```
124 | npm run storybook
125 | ```
126 |
127 | 或者
128 |
129 | ```
130 | yarn storybook
131 | ```
132 |
133 | 4. 预览
134 |
135 | 打开浏览器,输入 http://localhost:6006 即可预览。
136 |
137 | ## 贡献
138 |
139 | 如果你想对本程序进行贡献,可以 `fork` 本仓库到你的仓库里进行改进,完成开发或者修复以后提交 `pull request` 到本仓库。
140 |
141 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-iztro",
3 | "version": "1.4.1",
4 | "description": "基于iztro实现的react紫微斗数星盘组件。A react component used to generate an astrolabe of ziweidoushu based on iztro.",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "scripts": {
8 | "lint": "eslint . --ext .ts",
9 | "format": "prettier --write \"src/**/*.ts\"",
10 | "storybook": "storybook dev -p 6006",
11 | "clean": "rimraf lib/",
12 | "copy-files": "copyfiles -u 1 src/**/*.css lib/",
13 | "build": "npm run clean && tsc && npm run copy-files",
14 | "build:sb": "storybook build",
15 | "build-storybook": "storybook build",
16 | "prepare": "npm run build",
17 | "prepublishOnly": "npm run lint",
18 | "preversion": "npm run lint",
19 | "version": "npm run format && git add -A src",
20 | "postversion": "git push && git push --tags"
21 | },
22 | "files": [
23 | "lib/**/*"
24 | ],
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/SylarLong/react-iztro.git"
28 | },
29 | "keywords": [
30 | "iztro",
31 | "ziweidoushu",
32 | "紫微斗数",
33 | "astrolabe",
34 | "astrology",
35 | "horoscope"
36 | ],
37 | "author": "SylarLong",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/SylarLong/react-iztro/issues",
41 | "email": "sylarlong@gmail.com"
42 | },
43 | "homepage": "https://docs.iztro.com",
44 | "devDependencies": {
45 | "@babel/preset-env": "^7.22.15",
46 | "@babel/preset-react": "^7.22.15",
47 | "@babel/preset-typescript": "^7.22.15",
48 | "@storybook/addon-essentials": "^7.4.0",
49 | "@storybook/addon-interactions": "^7.4.0",
50 | "@storybook/addon-links": "^7.4.0",
51 | "@storybook/addon-onboarding": "^1.0.8",
52 | "@storybook/blocks": "^7.4.0",
53 | "@storybook/react": "^7.4.0",
54 | "@storybook/react-vite": "^7.4.0",
55 | "@storybook/react-webpack5": "^7.4.0",
56 | "@storybook/testing-library": "^0.2.0",
57 | "@types/react": "^18.2.21",
58 | "@typescript-eslint/eslint-plugin": "^6.7.0",
59 | "@typescript-eslint/parser": "^6.7.0",
60 | "copyfiles": "^2.4.1",
61 | "eslint": "^8.49.0",
62 | "react": "^18.2.0",
63 | "react-dom": "^18.2.0",
64 | "rimraf": "^5.0.1",
65 | "storybook": "^7.4.0",
66 | "typescript": "^5.0.1"
67 | },
68 | "dependencies": {
69 | "classnames": "^2.3.2",
70 | "iztro": "2.5.1",
71 | "iztro-hook": "1.3.1",
72 | "lunar-lite": "^0.2.3"
73 | },
74 | "resolutions": {
75 | "jackspeak": "2.1.1"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Izpalace/Izpalace.css:
--------------------------------------------------------------------------------
1 | .iztro-palace {
2 | padding: 3px;
3 | display: grid;
4 | text-transform: capitalize;
5 | grid-template-rows: auto auto auto 50px;
6 | grid-template-columns: repeat(3, 1fr);
7 | grid-template-areas:
8 | "major minor adj"
9 | "horo horo adj"
10 | "fate fate fate"
11 | "ft ft ft";
12 | transition: all 0.25s ease-in-out;
13 | grid-auto-flow: column;
14 | }
15 | .iztro-palace.focused-palace {
16 | background-color: #aab8d32f;
17 | }
18 | .iztro-palace.opposite-palace {
19 | background-color: #93f73d4f;
20 | }
21 | .iztro-palace.surrounded-palace {
22 | background-color: #aff46f24;
23 | }
24 | .iztro-palace-major {
25 | grid-area: major;
26 | }
27 | .iztro-palace-minor {
28 | grid-area: minor;
29 | justify-self: center;
30 | }
31 | .iztro-palace-adj {
32 | grid-area: adj;
33 | display: inline-flex;
34 | justify-self: flex-end;
35 | gap: 3px;
36 | white-space: nowrap;
37 | text-align: right;
38 | }
39 | .iztro-palace-horo-star {
40 | grid-area: horo;
41 | align-self: center;
42 | }
43 | .iztro-palace-horo-star .stars {
44 | display: flex;
45 | gap: 3px;
46 | }
47 | .iztro-palace-scope {
48 | white-space: nowrap;
49 | text-align: center;
50 | }
51 | .iztro-palace-scope-decadal {
52 | font-weight: 700;
53 | }
54 | .iztro-palace-fate {
55 | grid-area: fate;
56 | align-self: flex-end;
57 | white-space: nowrap;
58 | justify-content: center;
59 | display: flex;
60 | gap: 3px;
61 | height: 17px;
62 | }
63 |
64 | .iztro-palace-fate .iztro-palace-decadal-active {
65 | background-color: var(--iztro-color-decadal);
66 | }
67 |
68 | .iztro-palace-fate .iztro-palace-yearly-active {
69 | background-color: var(--iztro-color-yearly);
70 | }
71 |
72 | .iztro-palace-fate .iztro-palace-monthly-active {
73 | background-color: var(--iztro-color-monthly);
74 | }
75 |
76 | .iztro-palace-fate .iztro-palace-daily-active {
77 | background-color: var(--iztro-color-daily);
78 | }
79 |
80 | .iztro-palace-fate .iztro-palace-hourly-active {
81 | background-color: var(--iztro-color-hourly);
82 | }
83 |
84 | .iztro-palace-footer {
85 | grid-area: ft;
86 | display: grid;
87 | grid-template-columns: auto auto auto;
88 | align-self: flex-start;
89 | }
90 |
91 | .iztro-palace-lft24 {
92 | text-align: left;
93 | }
94 |
95 | .iztro-palace-rgt24 {
96 | text-align: right;
97 | }
98 | .iztro-palace-name {
99 | cursor: pointer;
100 | text-wrap: nowrap;
101 | }
102 | .iztro-palace-name .iztro-palace-name-wrapper {
103 | position: relative;
104 | }
105 | .iztro-palace-name .iztro-palace-name-taichi {
106 | position: absolute;
107 | font-size: 12px;
108 | line-height: 14px;
109 | background-color: var(--iztro-color-major);
110 | padding: 0 2px;
111 | color: #fff;
112 | z-index: 2;
113 | border-radius: 0 4px 4px 0;
114 | font-weight: normal !important;
115 | bottom: 0;
116 | }
117 | .iztro-palace-gz {
118 | text-align: right;
119 | cursor: pointer;
120 | }
121 | .iztro-palace-gz span {
122 | display: inline-block;
123 | padding: 0 1px;
124 | text-wrap: nowrap;
125 | }
126 | .iztro-palace-dynamic-name {
127 | text-align: center;
128 | display: flex;
129 | white-space: nowrap;
130 | gap: 3px;
131 | justify-content: center;
132 | }
133 | .iztro-palace-dynamic-name .iztro-palace-dynamic-name-decadal {
134 | color: var(--iztro-color-decadal);
135 | }
136 | .iztro-palace-dynamic-name .iztro-palace-dynamic-name-yearly {
137 | color: var(--iztro-color-yearly);
138 | }
139 | .iztro-palace-dynamic-name .iztro-palace-dynamic-name-monthly {
140 | color: var(--iztro-color-monthly);
141 | }
142 | .iztro-palace-dynamic-name .iztro-palace-dynamic-name-daily {
143 | color: var(--iztro-color-daily);
144 | }
145 | .iztro-palace-dynamic-name .iztro-palace-dynamic-name-hourly {
146 | color: var(--iztro-color-hourly);
147 | }
148 |
--------------------------------------------------------------------------------
/src/Izpalace/Izpalace.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { HoroscopeForPalace, IzpalaceProps } from "./Izpalace.type";
3 | import classNames from "classnames";
4 | import "./Izpalace.css";
5 | import { Izstar } from "../Izstar";
6 | import { HeavenlyStemKey, PalaceKey, kot, t } from "iztro/lib/i18n";
7 | import { fixIndex } from "iztro/lib/utils";
8 | import { Scope } from "iztro/lib/data/types";
9 |
10 | export const Izpalace = ({
11 | index,
12 | taichiPalace,
13 | focusedIndex,
14 | onFocused,
15 | horoscope,
16 | activeHeavenlyStem,
17 | toggleActiveHeavenlyStem,
18 | hoverHeavenlyStem,
19 | setHoverHeavenlyStem,
20 | showDecadalScope = false,
21 | showYearlyScope = false,
22 | showMonthlyScope = false,
23 | showDailyScope = false,
24 | showHourlyScope = false,
25 | toggleScope,
26 | toggleTaichiPoint,
27 | ...palace
28 | }: IzpalaceProps) => {
29 | const horoscopeNames = useMemo(() => {
30 | const horoscopeNames = [];
31 |
32 | if (horoscope?.decadal.index === index) {
33 | horoscopeNames.push({
34 | ...horoscope.decadal,
35 | scope: "decadal" as Scope,
36 | show: showDecadalScope,
37 | });
38 | }
39 |
40 | if (horoscope?.yearly.index === index) {
41 | horoscopeNames.push({
42 | ...horoscope.yearly,
43 | scope: "yearly" as Scope,
44 | show: showYearlyScope,
45 | });
46 | }
47 |
48 | if (horoscope?.monthly.index === index) {
49 | horoscopeNames.push({
50 | ...horoscope.monthly,
51 | scope: "monthly" as Scope,
52 | show: showMonthlyScope,
53 | });
54 | }
55 |
56 | if (horoscope?.daily.index === index) {
57 | horoscopeNames.push({
58 | ...horoscope.daily,
59 | scope: "daily" as Scope,
60 | show: showDailyScope,
61 | });
62 | }
63 |
64 | if (horoscope?.hourly.index === index) {
65 | horoscopeNames.push({
66 | ...horoscope.hourly,
67 | scope: "hourly" as Scope,
68 | show: showHourlyScope,
69 | });
70 | }
71 |
72 | if (horoscope?.age.index === index) {
73 | horoscopeNames.push({
74 | name: horoscope.age.name,
75 | heavenlyStem: undefined,
76 | scope: "age" as Scope,
77 | show: false,
78 | });
79 | }
80 |
81 | return horoscopeNames;
82 | }, [
83 | horoscope,
84 | showDecadalScope,
85 | showYearlyScope,
86 | showMonthlyScope,
87 | showDailyScope,
88 | showHourlyScope,
89 | ]);
90 |
91 | const horoscopeMutagens = useMemo(() => {
92 | if (!horoscope) {
93 | return [];
94 | }
95 |
96 | return [
97 | {
98 | mutagen: horoscope.decadal.mutagen,
99 | scope: "decadal" as Scope,
100 | show: showDecadalScope,
101 | },
102 | {
103 | mutagen: horoscope.yearly.mutagen,
104 | scope: "yearly" as Scope,
105 | show: showYearlyScope,
106 | },
107 | {
108 | mutagen: horoscope.monthly.mutagen,
109 | scope: "monthly" as Scope,
110 | show: showMonthlyScope,
111 | },
112 | {
113 | mutagen: horoscope.daily.mutagen,
114 | scope: "daily" as Scope,
115 | show: showDailyScope,
116 | },
117 | {
118 | mutagen: horoscope.hourly.mutagen,
119 | scope: "hourly" as Scope,
120 | show: showHourlyScope,
121 | },
122 | ];
123 | }, [
124 | horoscope,
125 | showDecadalScope,
126 | showYearlyScope,
127 | showMonthlyScope,
128 | showDailyScope,
129 | showHourlyScope,
130 | ]);
131 |
132 | return (
133 | onFocused?.(index)}
145 | onMouseLeave={() => onFocused?.(undefined)}
146 | >
147 |
148 | {palace.majorStars.map((star) => (
149 | (
154 | palace.heavenlyStem,
155 | "Heavenly"
156 | )}
157 | horoscopeMutagens={horoscopeMutagens}
158 | {...star}
159 | />
160 | ))}
161 |
162 |
163 | {palace.minorStars.map((star) => (
164 | (
169 | palace.heavenlyStem,
170 | "Heavenly"
171 | )}
172 | horoscopeMutagens={horoscopeMutagens}
173 | {...star}
174 | />
175 | ))}
176 |
177 |
178 |
179 | {palace.adjectiveStars.slice(5).map((star) => (
180 |
181 | ))}
182 |
183 |
184 | {palace.adjectiveStars.slice(0, 5).map((star) => (
185 |
186 | ))}
187 |
188 |
189 |
190 |
191 | {horoscope?.decadal?.stars &&
192 | horoscope?.decadal?.stars[index].map((star) => (
193 |
194 | ))}
195 |
196 |
197 | {horoscope?.yearly?.stars &&
198 | horoscope?.yearly?.stars[index].map((star) => (
199 |
200 | ))}
201 |
202 |
203 |
204 | {horoscopeNames?.map((item) => (
205 | toggleScope?.(item.scope as Scope) : undefined
212 | }
213 | >
214 | {item.name}
215 | {item.heavenlyStem && `·${item.heavenlyStem}`}
216 |
217 | ))}
218 |
219 |
220 |
221 |
222 |
{palace.changsheng12}
223 |
{palace.boshi12}
224 |
225 |
toggleTaichiPoint?.(index)}
228 | >
229 |
230 | {palace.name}
231 |
232 | {taichiPalace &&
233 | (kot(taichiPalace) === kot("命宫")
234 | ? "☯"
235 | : taichiPalace)}
236 |
237 |
238 | {palace.isBodyPalace && (
239 |
240 | ·{t("bodyPalace")}
241 |
242 | )}
243 |
244 |
245 |
246 |
247 |
248 | {palace.ages.slice(0, 7).join(" ")}
249 |
250 |
251 | {palace.decadal.range.join(" - ")}
252 |
253 |
254 |
255 | {showDecadalScope && (
256 |
257 | {horoscope?.decadal.palaceNames[index]}
258 |
259 | )}
260 | {showYearlyScope && (
261 |
262 | {horoscope?.yearly.palaceNames[index]}
263 |
264 | )}
265 | {showMonthlyScope && (
266 |
267 | {horoscope?.monthly.palaceNames[index]}
268 |
269 | )}
270 | {showDailyScope && (
271 |
272 | {horoscope?.daily.palaceNames[index]}
273 |
274 | )}
275 | {showHourlyScope && (
276 |
277 | {horoscope?.hourly.palaceNames[index]}
278 |
279 | )}
280 |
281 |
282 |
283 |
284 |
285 | {showYearlyScope
286 | ? horoscope?.yearly.yearlyDecStar.suiqian12[index]
287 | : palace.suiqian12}
288 |
289 |
290 | {showYearlyScope
291 | ? horoscope?.yearly.yearlyDecStar.jiangqian12[index]
292 | : palace.jiangqian12}
293 |
294 |
295 |
296 |
(palace.heavenlyStem, "Heavenly"),
301 | })}
302 | onClick={() =>
303 | toggleActiveHeavenlyStem?.(
304 | kot(palace.heavenlyStem, "Heavenly")
305 | )
306 | }
307 | onMouseEnter={() =>
308 | setHoverHeavenlyStem?.(
309 | kot(palace.heavenlyStem, "Heavenly")
310 | )
311 | }
312 | onMouseLeave={() => setHoverHeavenlyStem?.(undefined)}
313 | >
314 | (palace.heavenlyStem, "Heavenly"),
319 | })}
320 | >
321 | {palace.heavenlyStem}
322 | {palace.earthlyBranch}
323 |
324 |
325 |
326 |
327 |
328 | );
329 | };
330 |
--------------------------------------------------------------------------------
/src/Izpalace/Izpalace.type.ts:
--------------------------------------------------------------------------------
1 | import { IFunctionalHoroscope } from "iztro/lib/astro/FunctionalHoroscope";
2 | import { IFunctionalPalace } from "iztro/lib/astro/FunctionalPalace";
3 | import { HoroscopeItem, Scope } from "iztro/lib/data/types";
4 | import { HeavenlyStemKey } from "iztro/lib/i18n";
5 |
6 | export type IzpalaceProps = {
7 | index: number;
8 | taichiPalace?: string;
9 | focusedIndex?: number;
10 | horoscope?: IFunctionalHoroscope;
11 | showDecadalScope?: boolean;
12 | showYearlyScope?: boolean;
13 | showMonthlyScope?: boolean;
14 | showDailyScope?: boolean;
15 | showHourlyScope?: boolean;
16 | activeHeavenlyStem?: HeavenlyStemKey;
17 | hoverHeavenlyStem?: HeavenlyStemKey;
18 | setHoverHeavenlyStem?: (heavenlyStem?: HeavenlyStemKey) => void;
19 | toggleActiveHeavenlyStem?: (heavenlyStem: HeavenlyStemKey) => void;
20 | toggleScope?: (scope: Scope) => void;
21 | onFocused?: (index?: number) => void;
22 | toggleTaichiPoint?: (index: number) => void;
23 | } & IFunctionalPalace;
24 |
25 | export type HoroscopeForPalace = {
26 | scope: Scope;
27 | show: boolean;
28 | } & Partial;
29 |
--------------------------------------------------------------------------------
/src/Izpalace/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Izpalace";
2 |
--------------------------------------------------------------------------------
/src/IzpalaceCenter/Item.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ReactNode } from "react";
3 | import classNames from "classnames";
4 | import "./IzpalaceCenter.css";
5 |
6 | export type ItemProps = {
7 | title: ReactNode;
8 | content: ReactNode;
9 | };
10 |
11 | export const Item = ({ title, content }: ItemProps) => {
12 | return (
13 |
14 |
15 | {content}
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/IzpalaceCenter/IzpalaceCenter.css:
--------------------------------------------------------------------------------
1 | .iztro-center-palace {
2 | grid-area: ct;
3 | position: relative;
4 | width: 100%;
5 | height: 100%;
6 | }
7 | .iztro-center-palace-centralize {
8 | text-align: center;
9 | }
10 | .iztro-center-palace ul.basic-info {
11 | margin: 10px;
12 | padding: 0;
13 | display: grid;
14 | grid-template-columns: repeat(2, 1fr);
15 | column-gap: 20px;
16 | }
17 | .iztro-center-palace ul.basic-info li {
18 | list-style: none;
19 | }
20 | .iztro-center-palace .center-title {
21 | padding: 5px 0;
22 | margin: 0;
23 | font-size: var(--iztro-star-font-size-big);
24 | font-weight: bold;
25 | text-align: center;
26 | border-bottom: 1px dashed var(--iztro-color-border);
27 | }
28 |
29 | .horo-buttons {
30 | margin: 10px;
31 | font-size: var(--iztro-star-font-size-small);
32 | display: flex;
33 | justify-content: space-around;
34 | }
35 | .horo-buttons .center-button {
36 | display: block;
37 | text-align: center;
38 | padding: 5px;
39 | border: 1px solid var(--iztro-color-border);
40 | cursor: pointer;
41 | transition: all 0.25s ease-in-out;
42 | color: var(--iztro-color-text);
43 | user-select: none;
44 | }
45 | .horo-buttons .center-button:not(.disabled):hover {
46 | color: var(--iztro-color-major);
47 | background-color: var(--iztro-color-border);
48 | }
49 | .horo-buttons .center-button.disabled {
50 | opacity: 0.5;
51 | cursor: not-allowed;
52 | }
53 | .horo-buttons .center-horo-hour {
54 | display: flex;
55 | align-items: center;
56 | }
57 |
58 | .iztro-copyright {
59 | position: absolute;
60 | bottom: 3px;
61 | right: 3px;
62 | font-size: 12px;
63 | color: rgba(0, 0, 0, 0.2);
64 | text-decoration: none;
65 | text-shadow: 1px 1px rgba(255, 255, 255, 0.3);
66 | }
67 |
68 | #palace-line {
69 | stroke: var(--iztro-color-awesome);
70 | opacity: 0.6;
71 | transition: all 0.25s ease-in-out;
72 | }
73 | #palace-line.decadal {
74 | stroke: var(--iztro-color-decadal);
75 | }
76 |
77 | .solar-horoscope {
78 | display: flex;
79 | align-items: center;
80 | gap: 10px;
81 | }
82 |
83 | .solar-horoscope-centralize {
84 | justify-content: center;
85 | }
86 |
87 | .solar-horoscope .today {
88 | display: inline-block;
89 | font-size: var(--iztro-star-font-size-small);
90 | cursor: pointer;
91 | border: 1px solid var(--iztro-color-border);
92 | padding: 0 5px;
93 | transition: all 0.25s ease-in-out;
94 | }
95 |
96 | .solar-horoscope .today:hover {
97 | color: var(--iztro-color-major);
98 | background-color: var(--iztro-color-border);
99 | }
--------------------------------------------------------------------------------
/src/IzpalaceCenter/IzpalaceCenter.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React, { useCallback, useMemo } from "react";
3 | import FunctionalAstrolabe from "iztro/lib/astro/FunctionalAstrolabe";
4 | import { Item, ItemProps } from "./Item";
5 | import "./IzpalaceCenter.css";
6 | import { Line } from "./Line";
7 | import { fixEarthlyBranchIndex } from "iztro/lib/utils";
8 | import { Scope } from "iztro/lib/data/types";
9 | import { IFunctionalHoroscope } from "iztro/lib/astro/FunctionalHoroscope";
10 | import { normalizeDateStr, solar2lunar } from "lunar-lite";
11 | import { GenderName, kot, t } from "iztro/lib/i18n";
12 | import { CHINESE_TIME } from "iztro/lib/data";
13 |
14 | type IzpalaceCenterProps = {
15 | astrolabe?: FunctionalAstrolabe;
16 | horoscope?: IFunctionalHoroscope;
17 | horoscopeDate?: string | Date;
18 | horoscopeHour?: number;
19 | arrowIndex?: number;
20 | arrowScope?: Scope;
21 | setHoroscopeDate?: React.Dispatch<
22 | React.SetStateAction
23 | >;
24 | setHoroscopeHour?: React.Dispatch>;
25 | centerPalaceAlign?: boolean;
26 | };
27 |
28 | export const IzpalaceCenter = ({
29 | astrolabe,
30 | horoscope,
31 | arrowIndex,
32 | arrowScope,
33 | horoscopeDate = new Date(),
34 | horoscopeHour = 0,
35 | setHoroscopeDate,
36 | setHoroscopeHour,
37 | centerPalaceAlign,
38 | }: IzpalaceCenterProps) => {
39 | const records: ItemProps[] = useMemo(
40 | () => [
41 | {
42 | title: "五行局:",
43 | content: astrolabe?.fiveElementsClass,
44 | },
45 | {
46 | title: "年龄(虚岁):",
47 | content: `${horoscope?.age.nominalAge} 岁`,
48 | },
49 | {
50 | title: "四柱:",
51 | content: astrolabe?.chineseDate,
52 | },
53 | {
54 | title: "阳历:",
55 | content: astrolabe?.solarDate,
56 | },
57 | {
58 | title: "农历:",
59 | content: astrolabe?.lunarDate,
60 | },
61 | {
62 | title: "时辰:",
63 | content: `${astrolabe?.time}(${astrolabe?.timeRange})`,
64 | },
65 | {
66 | title: "生肖:",
67 | content: astrolabe?.zodiac,
68 | },
69 | {
70 | title: "星座:",
71 | content: astrolabe?.sign,
72 | },
73 | {
74 | title: "命主:",
75 | content: astrolabe?.soul,
76 | },
77 | {
78 | title: "身主:",
79 | content: astrolabe?.body,
80 | },
81 | {
82 | title: "命宫:",
83 | content: astrolabe?.earthlyBranchOfSoulPalace,
84 | },
85 | {
86 | title: "身宫:",
87 | content: astrolabe?.earthlyBranchOfBodyPalace,
88 | },
89 | ],
90 | [astrolabe, horoscope]
91 | );
92 |
93 | const horoDate = useMemo(() => {
94 | const dateStr = horoscopeDate ?? new Date();
95 | const [year, month, date] = normalizeDateStr(dateStr);
96 | const dt = new Date(year, month - 1, date);
97 |
98 | return {
99 | solar: `${year}-${month}-${date}`,
100 | lunar: solar2lunar(dateStr).toString(true),
101 | prevDecadalDisabled: dt.setFullYear(dt.getFullYear() - 1),
102 | };
103 | }, [horoscopeDate]);
104 |
105 | const onHoroscopeButtonClicked = (scope: Scope, value: number) => {
106 | if (!astrolabe?.solarDate) {
107 | return true;
108 | }
109 |
110 | const [year, month, date] = normalizeDateStr(horoscopeDate);
111 | const dt = new Date(year, month - 1, date);
112 | const [birthYear, birthMonth, birthDate] = normalizeDateStr(
113 | astrolabe.solarDate
114 | );
115 | const birthday = new Date(birthYear, birthMonth - 1, birthDate);
116 | let hour = horoscopeHour;
117 |
118 | switch (scope) {
119 | case "hourly":
120 | hour = horoscopeHour + value;
121 |
122 | if (horoscopeHour + value > 11) {
123 | // 如果大于亥时,则加一天,时辰变为早子时
124 | dt.setDate(dt.getDate() + 1);
125 | hour = 0;
126 | } else if (horoscopeHour + value < 0) {
127 | // 如果小于早子时,则减一天,时辰变为亥时
128 | dt.setDate(dt.getDate() - 1);
129 | hour = 11;
130 | }
131 | break;
132 | case "daily":
133 | dt.setDate(dt.getDate() + value);
134 | break;
135 | case "monthly":
136 | dt.setMonth(dt.getMonth() + value);
137 | break;
138 | case "yearly":
139 | case "decadal":
140 | dt.setFullYear(dt.getFullYear() + value);
141 | break;
142 | }
143 |
144 | if (dt.getTime() >= birthday.getTime()) {
145 | setHoroscopeDate?.(dt);
146 | setHoroscopeHour?.(hour);
147 | }
148 | };
149 |
150 | const shouldBeDisabled = useCallback(
151 | (dateStr: string | Date, scope: Scope, value: number) => {
152 | if (!astrolabe?.solarDate) {
153 | return true;
154 | }
155 |
156 | const [year, month, date] = normalizeDateStr(dateStr);
157 | const dt = new Date(year, month - 1, date);
158 | const [birthYear, birthMonth, birthDate] = normalizeDateStr(
159 | astrolabe.solarDate
160 | );
161 | const birthday = new Date(birthYear, birthMonth - 1, birthDate);
162 |
163 | switch (scope) {
164 | case "hourly":
165 | if (horoscopeHour + value > 11) {
166 | dt.setDate(dt.getDate() + 1);
167 | } else if (horoscopeHour + value < 0) {
168 | dt.setDate(dt.getDate() - 1);
169 | }
170 |
171 | break;
172 | case "daily":
173 | dt.setDate(dt.getDate() + value);
174 | break;
175 | case "monthly":
176 | dt.setMonth(dt.getMonth() + value);
177 | break;
178 | case "yearly":
179 | case "decadal":
180 | dt.setFullYear(dt.getFullYear() + value);
181 | break;
182 | }
183 |
184 | if (dt.getTime() < birthday.getTime()) {
185 | return true;
186 | }
187 |
188 | return false;
189 | },
190 | [horoscopeHour, astrolabe]
191 | );
192 |
193 | return (
194 |
199 | {astrolabe?.earthlyBranchOfSoulPalace && (
200 |
207 | )}
208 |
209 | (
211 | astrolabe?.gender ?? ""
212 | )}`}
213 | >
214 | {kot(astrolabe?.gender ?? "") === "male" ? "♂" : "♀"}
215 |
216 | 基本信息
217 |
218 |
219 | {records.map((rec, idx) => (
220 |
221 | ))}
222 |
223 |
运限信息
224 |
240 |
241 | onHoroscopeButtonClicked("yearly", -10)}
246 | >
247 | ◀限
248 |
249 | onHoroscopeButtonClicked("yearly", -1)}
254 | >
255 | ◀年
256 |
257 | onHoroscopeButtonClicked("monthly", -1)}
262 | >
263 | ◀月
264 |
265 | onHoroscopeButtonClicked("daily", -1)}
270 | >
271 | ◀日
272 |
273 | onHoroscopeButtonClicked("hourly", -1)}
278 | >
279 | ◀时
280 |
281 |
282 | {t(CHINESE_TIME[horoscopeHour])}
283 |
284 | onHoroscopeButtonClicked("hourly", 1)}
287 | >
288 | 时▶
289 |
290 | onHoroscopeButtonClicked("daily", 1)}
293 | >
294 | 日▶
295 |
296 | onHoroscopeButtonClicked("monthly", 1)}
299 | >
300 | 月▶
301 |
302 | onHoroscopeButtonClicked("yearly", 1)}
305 | >
306 | 年▶
307 |
308 | onHoroscopeButtonClicked("yearly", 10)}
311 | >
312 | 限▶
313 |
314 |
315 |
320 |
321 | Powered by iztro
322 |
323 |
324 |
325 | );
326 | };
327 |
--------------------------------------------------------------------------------
/src/IzpalaceCenter/Line.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef } from "react";
2 | import { fixIndex } from "iztro/lib/utils";
3 | import { Scope } from "iztro/lib/data/types";
4 |
5 | type LineProps = {
6 | index: number;
7 | scope?: Scope;
8 | };
9 |
10 | export const Line = ({ index, scope }: LineProps) => {
11 | const line = useRef(null);
12 |
13 | const strokeColor = useMemo(() => {
14 | if (scope) {
15 | const element = document.getElementsByClassName(
16 | "iztro-astrolabe-theme-default"
17 | )[0];
18 | const computedStyle = getComputedStyle(element);
19 |
20 | // 获取CSS中定义的变量的值
21 | return computedStyle.getPropertyValue(`--iztro-color-${scope}`);
22 | }
23 |
24 | return "rgba(245,0,0)";
25 | }, [scope]);
26 |
27 | useEffect(() => {
28 | const idx = index;
29 | const canvasDom = line.current;
30 |
31 | if (!canvasDom || idx < 0) {
32 | return;
33 | }
34 |
35 | const { height, width } = (
36 | canvasDom as HTMLElement
37 | ).getBoundingClientRect();
38 |
39 | canvasDom.width = width;
40 | canvasDom.height = height;
41 |
42 | const w = width / 2;
43 | const h = height / 2;
44 | const points = [
45 | [0, h * 2],
46 | [0, h * 1.5],
47 | [0, h * 0.5],
48 | [0, 0],
49 | [w * 0.5, 0],
50 | [w * 1.5, 0],
51 | [w * 2, 0],
52 | [w * 2, h * 0.5],
53 | [w * 2, h * 1.5],
54 | [w * 2, h * 2],
55 | [w * 1.5, h * 2],
56 | [w * 0.5, h * 2],
57 | ];
58 |
59 | //第二步:获取上下文
60 | const canvasCtx = canvasDom.getContext("2d");
61 |
62 | if (!canvasCtx) {
63 | return;
64 | }
65 |
66 | canvasCtx.clearRect(0, 0, canvasDom.width, canvasDom.height);
67 |
68 | canvasCtx.strokeStyle = strokeColor;
69 | canvasCtx.lineWidth = 1;
70 | canvasCtx.globalAlpha = 0.5;
71 |
72 | const dgIdx = fixIndex(idx + 6);
73 | const q4Idx = fixIndex(idx + 4);
74 | const h4Idx = fixIndex(idx - 4);
75 |
76 | canvasCtx.beginPath();
77 | canvasCtx.moveTo(points[dgIdx][0], points[dgIdx][1]);
78 | canvasCtx.lineTo(points[idx][0], points[idx][1]);
79 | canvasCtx.lineTo(points[q4Idx][0], points[q4Idx][1]);
80 | canvasCtx.lineTo(points[h4Idx][0], points[h4Idx][1]);
81 | canvasCtx.lineTo(points[idx][0], points[idx][1]);
82 |
83 | canvasCtx.stroke();
84 | }, [index, strokeColor]);
85 |
86 | return (
87 |
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/src/IzpalaceCenter/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./IzpalaceCenter";
2 |
--------------------------------------------------------------------------------
/src/Izstar/Izstar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { IzstarProps } from "./Izstar.type";
3 | import classNames from "classnames";
4 | import { MUTAGEN } from "iztro/lib/data";
5 | import { MutagenKey, kot, t } from "iztro/lib/i18n";
6 | import { getMutagensByHeavenlyStem } from "iztro/lib/utils";
7 |
8 | export const Izstar = ({
9 | horoscopeMutagens,
10 | activeHeavenlyStem,
11 | hoverHeavenlyStem,
12 | palaceHeavenlyStem,
13 | ...star
14 | }: IzstarProps) => {
15 | const mutagenStyle = useMemo(() => {
16 | if (!activeHeavenlyStem) {
17 | return "";
18 | }
19 |
20 | const mutagens = getMutagensByHeavenlyStem(t(activeHeavenlyStem));
21 | const index = mutagens.indexOf(star.name);
22 |
23 | if (index < 0) {
24 | return "";
25 | }
26 |
27 | return `iztro-star-mutagen-${index}`;
28 | }, [activeHeavenlyStem, star.name]);
29 |
30 | const hoverMutagenStyle = useMemo(() => {
31 | if (!hoverHeavenlyStem) {
32 | return "";
33 | }
34 |
35 | const mutagens = getMutagensByHeavenlyStem(t(hoverHeavenlyStem));
36 | const index = mutagens.indexOf(star.name);
37 |
38 | if (index < 0) {
39 | return "";
40 | }
41 |
42 | return `iztro-star-hover-mutagen-${index}`;
43 | }, [hoverHeavenlyStem, star.name]);
44 |
45 | const selfMutagenStyle = useMemo(() => {
46 | if (!palaceHeavenlyStem || activeHeavenlyStem || hoverHeavenlyStem) {
47 | return undefined;
48 | }
49 |
50 | const mutagens = getMutagensByHeavenlyStem(t(palaceHeavenlyStem));
51 | const index = mutagens.indexOf(star.name);
52 |
53 | if (index < 0) {
54 | return "";
55 | }
56 |
57 | return `iztro-star-self-mutagen-${index}`;
58 | }, [palaceHeavenlyStem, activeHeavenlyStem, hoverHeavenlyStem]);
59 |
60 | return (
61 |
62 |
73 | {star.name}
74 |
75 | {star.brightness}
76 | {star.mutagen && (
77 | (star.mutagen))}`
81 | )}
82 | >
83 | {star.mutagen}
84 |
85 | )}
86 | {horoscopeMutagens?.map((item) => {
87 | if (item.mutagen.includes(star.name) && item.show) {
88 | return (
89 |
96 | {t(MUTAGEN[item.mutagen.indexOf(star.name)])}
97 |
98 | );
99 | }
100 | })}
101 |
102 | );
103 | };
104 |
--------------------------------------------------------------------------------
/src/Izstar/Izstar.type.ts:
--------------------------------------------------------------------------------
1 | import FunctionalStar from "iztro/lib/star/FunctionalStar";
2 | import { HeavenlyStemKey, StarName } from "iztro/lib/i18n";
3 | import { Scope } from "iztro/lib/data/types";
4 |
5 | export type HoroscopeMutagen = {
6 | mutagen: StarName[];
7 | scope: Scope;
8 | show: boolean;
9 | };
10 |
11 | export type IzstarProps = {
12 | palaceHeavenlyStem?: HeavenlyStemKey;
13 | activeHeavenlyStem?: HeavenlyStemKey;
14 | hoverHeavenlyStem?: HeavenlyStemKey;
15 | horoscopeMutagens?: HoroscopeMutagen[];
16 | } & FunctionalStar;
17 |
--------------------------------------------------------------------------------
/src/Izstar/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Izstar";
2 |
--------------------------------------------------------------------------------
/src/Iztrolabe/Iztrolabe.css:
--------------------------------------------------------------------------------
1 | .iztro-astrolabe {
2 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
3 | display: grid;
4 | position: relative;
5 | width: 100%;
6 | grid-gap: 3px;
7 | grid-template-columns: repeat(4, 1fr);
8 | grid-auto-rows: 1fr;
9 | grid-template-areas:
10 | "g3 g4 g5 g6"
11 | "g2 ct ct g7"
12 | "g1 ct ct g8"
13 | "g0 g11 g10 g9";
14 | }
15 |
16 | .iztro-star-mutagen {
17 | font-weight: normal;
18 | font-size: var(--iztro-star-font-size-small);
19 | border-radius: 4px;
20 | color: #fff;
21 | display: inline-block;
22 | margin-left: 1px;
23 | padding: 0 2px;
24 | }
25 |
26 | .star-with-mutagen {
27 | position: relative;
28 | }
29 |
30 | .star-with-mutagen::before {
31 | bottom: 0;
32 | content: " ";
33 | left: -4px;
34 | position: absolute;
35 | top: 0;
36 | width: 4px;
37 | transition: all 0.25s ease-in-out;
38 | }
39 |
40 | .star-with-mutagen::after {
41 | content: " ";
42 | position: absolute;
43 | left: 0;
44 | bottom: -4px;
45 | right: 0;
46 | height: 4px;
47 | transition: all 0.25s ease-in-out;
48 | }
--------------------------------------------------------------------------------
/src/Iztrolabe/Iztrolabe.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 | import { Iztrolabe as IztroAstrolabe } from "./Iztrolabe";
4 | import { IztrolabeProps } from "./Iztrolabe.type";
5 |
6 | const meta: Meta = {
7 | component: IztroAstrolabe,
8 | argTypes: {
9 | birthday: { type: "string", required: true },
10 | birthTime: {
11 | type: "number",
12 | control: {
13 | type: "select",
14 | labels: {
15 | 0: "早子时(00:00~01:00)",
16 | 1: "丑时(01:00~03:00)",
17 | 2: "寅时(03:00~05:00)",
18 | 3: "卯时(05:00~07:00)",
19 | 4: "辰时(07:00~09:00)",
20 | 5: "巳时(09:00~11:00)",
21 | 6: "午时(11:00~13:00)",
22 | 7: "未时(13:00~15:00)",
23 | 8: "申时(15:00~17:00)",
24 | 9: "酉时(17:00~19:00)",
25 | 10: "戌时(19:00~21:00)",
26 | 11: "亥时(21:00~23:00)",
27 | 12: "晚子时(23:00~00:00)",
28 | },
29 | },
30 | min: 0,
31 | max: 12,
32 | reqired: true,
33 | options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
34 | },
35 | gender: {
36 | type: "string",
37 | control: "inline-radio",
38 | options: ["male", "female"],
39 | required: true,
40 | },
41 | birthdayType: {
42 | type: "string",
43 | control: "inline-radio",
44 | options: ["lunar", "solar"],
45 | },
46 | isLeapMonth: { type: "boolean", if: { arg: "birthdayType", eq: "lunar" } },
47 | fixLeap: { type: "boolean" },
48 | lang: {
49 | type: "string",
50 | control: {
51 | type: "select",
52 | labels: {
53 | 0: "简体中文",
54 | 1: "繁体中文",
55 | 2: "日语",
56 | 3: "韩语",
57 | 4: "英语",
58 | 5: "越南语",
59 | },
60 | },
61 | options: ["zh-CN", "zh-TW", "ja-JP", "ko-KR", "en-US", "vi-VN"],
62 | },
63 | centerPalaceAlign: {
64 | type: "boolean",
65 | description: "中宫居中对齐",
66 | defaultValue: false,
67 | control: {
68 | type: "boolean",
69 | labels: {
70 | true: "居中",
71 | false: "默认",
72 | },
73 | },
74 | },
75 | },
76 | };
77 | export default meta;
78 |
79 | type Story = StoryObj;
80 |
81 | export const Iztrolabe: Story = (args: IztrolabeProps) => (
82 |
83 |
89 |
90 | );
91 |
92 | Iztrolabe.args = {
93 | birthday: "2023-9-4",
94 | birthTime: 0,
95 | gender: "female",
96 | birthdayType: "solar",
97 | isLeapMonth: false,
98 | fixLeap: true,
99 | lang: "zh-CN",
100 | options: {
101 | yearDivide: "exact",
102 | },
103 | };
104 |
--------------------------------------------------------------------------------
/src/Iztrolabe/Iztrolabe.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from "react";
2 | import { Izpalace } from "../Izpalace/Izpalace";
3 | import { IztrolabeProps } from "./Iztrolabe.type";
4 | import { IzpalaceCenter } from "../IzpalaceCenter";
5 | import classNames from "classnames";
6 | import { useIztro } from "iztro-hook";
7 | import "./Iztrolabe.css";
8 | import "../theme/default.css";
9 | import { Scope } from "iztro/lib/data/types";
10 | import { HeavenlyStemKey } from "iztro/lib/i18n";
11 | import { getPalaceNames } from "iztro/lib/astro";
12 |
13 | export const Iztrolabe: React.FC = (props) => {
14 | const [taichiPoint, setTaichiPoint] = useState(-1);
15 | const [taichiPalaces, setTaichiPalaces] = useState();
16 | const [activeHeavenlyStem, setActiveHeavenlyStem] =
17 | useState();
18 | const [hoverHeavenlyStem, setHoverHeavenlyStem] = useState();
19 | const [focusedIndex, setFocusedIndex] = useState();
20 | const [showDecadal, setShowDecadal] = useState(false);
21 | const [showYearly, setShowYearly] = useState(false);
22 | const [showMonthly, setShowMonthly] = useState(false);
23 | const [showDaily, setShowDaily] = useState(false);
24 | const [showHourly, setShowShowHourly] = useState(false);
25 | const [horoscopeDate, setHoroscopeDate] = useState();
26 | const [horoscopeHour, setHoroscopeHour] = useState();
27 | const { astrolabe, horoscope, setHoroscope } = useIztro({
28 | birthday: props.birthday,
29 | birthTime: props.birthTime,
30 | gender: props.gender,
31 | birthdayType: props.birthdayType,
32 | fixLeap: props.fixLeap,
33 | isLeapMonth: props.isLeapMonth,
34 | lang: props.lang,
35 | options: props.options,
36 | });
37 |
38 | const toggleShowScope = (scope: Scope) => {
39 | switch (scope) {
40 | case "decadal":
41 | setShowDecadal(!showDecadal);
42 | break;
43 | case "yearly":
44 | setShowYearly(!showYearly);
45 | break;
46 | case "monthly":
47 | setShowMonthly(!showMonthly);
48 | break;
49 | case "daily":
50 | setShowDaily(!showDaily);
51 | break;
52 | case "hourly":
53 | setShowShowHourly(!showHourly);
54 | break;
55 | }
56 | };
57 |
58 | const toggleActiveHeavenlyStem = (heavenlyStem: HeavenlyStemKey) => {
59 | if (heavenlyStem === activeHeavenlyStem) {
60 | setActiveHeavenlyStem(undefined);
61 | } else {
62 | setActiveHeavenlyStem(heavenlyStem);
63 | }
64 | };
65 |
66 | const dynamic = useMemo(() => {
67 | if (showHourly) {
68 | return {
69 | arrowIndex: horoscope?.hourly.index,
70 | arrowScope: "hourly" as Scope,
71 | };
72 | }
73 |
74 | if (showDaily) {
75 | return {
76 | arrowIndex: horoscope?.daily.index,
77 | arrowScope: "daily" as Scope,
78 | };
79 | }
80 |
81 | if (showMonthly) {
82 | return {
83 | arrowIndex: horoscope?.monthly.index,
84 | arrowScope: "monthly" as Scope,
85 | };
86 | }
87 |
88 | if (showYearly) {
89 | return {
90 | arrowIndex: horoscope?.yearly.index,
91 | arrowScope: "yearly" as Scope,
92 | };
93 | }
94 |
95 | if (showDecadal) {
96 | return {
97 | arrowIndex: horoscope?.decadal.index,
98 | arrowScope: "decadal" as Scope,
99 | };
100 | }
101 | }, [showDecadal, showYearly, showMonthly, showDaily, showHourly, horoscope]);
102 |
103 | useEffect(() => {
104 | setHoroscopeDate(props.horoscopeDate ?? new Date());
105 | setHoroscopeHour(props.horoscopeHour ?? 0);
106 | }, [props.horoscopeDate, props.horoscopeHour]);
107 |
108 | useEffect(() => {
109 | setHoroscope(horoscopeDate ?? new Date(), horoscopeHour);
110 | }, [horoscopeDate, horoscopeHour]);
111 |
112 | useEffect(() => {
113 | if (taichiPoint < 0) {
114 | setTaichiPalaces(undefined);
115 | } else {
116 | const palaces = getPalaceNames(taichiPoint);
117 |
118 | setTaichiPalaces(palaces);
119 | }
120 | }, [taichiPoint]);
121 |
122 | const toggleTaichiPoint = (index: number) => {
123 | if (taichiPoint === index) {
124 | setTaichiPoint(-1);
125 | } else {
126 | setTaichiPoint(index);
127 | }
128 | };
129 |
130 | return (
131 |
134 | {astrolabe?.palaces.map((palace) => {
135 | return (
136 |
155 | );
156 | })}
157 |
167 |
168 | );
169 | };
170 |
--------------------------------------------------------------------------------
/src/Iztrolabe/Iztrolabe.type.ts:
--------------------------------------------------------------------------------
1 | import { IztroInput } from "iztro-hook/lib/index.type";
2 | import { NestedProps } from "../config/types";
3 |
4 | export type IztrolabeProps = {
5 | width?: number | string;
6 | horoscopeDate?: string | Date;
7 | horoscopeHour?: number;
8 | centerPalaceAlign?: boolean;
9 | } & IztroInput &
10 | NestedProps;
11 |
--------------------------------------------------------------------------------
/src/Iztrolabe/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Iztrolabe";
2 | export * from "./Iztrolabe.type";
3 |
--------------------------------------------------------------------------------
/src/config/types.ts:
--------------------------------------------------------------------------------
1 | type BuildPowersOf2LengthArrays<
2 | N extends number,
3 | R extends never[][]
4 | > = R[0][N] extends never
5 | ? R
6 | : BuildPowersOf2LengthArrays;
7 |
8 | type ConcatLargestUntilDone<
9 | N extends number,
10 | R extends never[][],
11 | B extends never[]
12 | > = B["length"] extends N
13 | ? B
14 | : [...R[0], ...B][N] extends never
15 | ? ConcatLargestUntilDone<
16 | N,
17 | R extends [R[0], ...infer U] ? (U extends never[][] ? U : never) : never,
18 | B
19 | >
20 | : ConcatLargestUntilDone<
21 | N,
22 | R extends [R[0], ...infer U] ? (U extends never[][] ? U : never) : never,
23 | [...R[0], ...B]
24 | >;
25 |
26 | type Replace = { [K in keyof R]: T };
27 |
28 | type TupleOf = number extends N
29 | ? T[]
30 | : {
31 | [K in N]: BuildPowersOf2LengthArrays extends infer U
32 | ? U extends never[][]
33 | ? Replace, T>
34 | : never
35 | : never;
36 | }[N];
37 |
38 | type RangeOf = Partial>["length"];
39 |
40 | export type RangeOfNumber =
41 | | Exclude, RangeOf>
42 | | From;
43 |
44 | export type NestedProps = {
45 | children?: React.ReactNode;
46 | className?: string;
47 | };
48 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Iztrolabe";
2 |
--------------------------------------------------------------------------------
/src/theme/default.css:
--------------------------------------------------------------------------------
1 | .iztro-astrolabe-theme-default {
2 | --iztro-star-font-size-big: 13px;
3 | --iztro-star-font-size-small: 12px;
4 | --iztro-color-major: #531dab;
5 | --iztro-color-focus: #000;
6 | --iztro-color-quan: #2f54eb;
7 | --iztro-color-tough: #612500;
8 | --iztro-color-awesome: #d4380d;
9 | --iztro-color-active: #1890ff;
10 | --iztro-color-happy: #c41d7f;
11 | --iztro-color-nice: #237804;
12 | --iztro-color-decorator-1: #90983c;
13 | --iztro-color-decorator-2: #813359;
14 | --iztro-color-text: #8c8c8c;
15 | --iztro-color-border: #00152912;
16 | --iztro-color-decadal: var(--iztro-color-active);
17 | --iztro-color-yearly: var(--iztro-color-decorator-2);
18 | --iztro-color-monthly: var(--iztro-color-nice);
19 | --iztro-color-daily: var(--iztro-color-decorator-1);
20 | --iztro-color-hourly: var(--iztro-color-text);
21 | }
22 |
23 | .iztro-astrolabe {
24 | text-align: left;
25 | }
26 |
27 | .iztro-palace {
28 | border: 1px solid var(--iztro-color-border);
29 | }
30 |
31 | .iztro-star-soft,
32 | .iztro-star-tough,
33 | .iztro-star-adjective,
34 | .iztro-star-flower,
35 | .iztro-star-helper,
36 | .iztro-palace-fate,
37 | .iztro-palace-horo-star,
38 | .iztro-palace-scope,
39 | .iztro-palace-dynamic-name,
40 | .iztro-palace-lft24,
41 | .iztro-palace-rgt24 {
42 | font-size: var(--iztro-star-font-size-small);
43 | font-weight: normal;
44 | text-wrap: nowrap;
45 | }
46 | .iztro-palace-scope-age {
47 | text-wrap: balance;
48 | }
49 | .iztro-palace-scope-age,
50 | .iztro-palace-scope-decadal {
51 | color: var(--iztro-color-text);
52 | }
53 |
54 | .iztro-palace-lft24 {
55 | color: var(--iztro-color-decorator-1);
56 | }
57 | .iztro-palace-rgt24 {
58 | color: var(--iztro-color-decorator-2);
59 | text-wrap: nowrap;
60 | }
61 |
62 | .iztro-star-major,
63 | .iztro-star-tianma,
64 | .iztro-star-lucun,
65 | .iztro-palace-name,
66 | .iztro-palace-gz {
67 | font-size: var(--iztro-star-font-size-big);
68 | font-weight: bold;
69 | }
70 |
71 | .iztro-star-tianma {
72 | color: var(--iztro-color-active);
73 | }
74 | .iztro-star-lucun {
75 | color: var(--iztro-color-awesome);
76 | }
77 |
78 | .iztro-palace-horo-star .iztro-star {
79 | opacity: 0.75;
80 | }
81 | .iztro-palace-horo-star .iztro-star-tianma,
82 | .iztro-palace-horo-star .iztro-star-lucun {
83 | font-weight: normal;
84 | font-size: var(--iztro-star-font-size-small);
85 | }
86 |
87 | .iztro-star-brightness,
88 | .iztro-star-adjective {
89 | font-style: normal;
90 | font-weight: normal;
91 | color: var(--iztro-color-text);
92 | }
93 |
94 | .iztro-star-brightness {
95 | opacity: 0.5;
96 | }
97 |
98 | .iztro-star-major,
99 | .iztro-star-soft,
100 | .iztro-palace-name {
101 | color: var(--iztro-color-major);
102 | }
103 | .iztro-star-tough {
104 | color: var(--iztro-color-tough);
105 | }
106 | .iztro-star-flower {
107 | color: var(--iztro-color-happy);
108 | }
109 | .iztro-star-helper,
110 | .iztro-palace-gz {
111 | color: var(--iztro-color-nice);
112 | }
113 |
114 | .iztro-star-mutagen.mutagen-0 {
115 | background-color: var(--iztro-color-awesome);
116 | }
117 | .iztro-star-mutagen.mutagen-1 {
118 | background-color: var(--iztro-color-quan);
119 | }
120 | .iztro-star-mutagen.mutagen-2 {
121 | background-color: var(--iztro-color-nice);
122 | }
123 | .iztro-star-mutagen.mutagen-3 {
124 | background-color: var(--iztro-color-focus);
125 | }
126 |
127 | .iztro-star-mutagen.mutagen-decadal {
128 | background-color: var(--iztro-color-decadal);
129 | opacity: 0.6;
130 | }
131 | .iztro-star-mutagen.mutagen-yearly {
132 | background-color: var(--iztro-color-yearly);
133 | opacity: 0.6;
134 | }
135 | .iztro-star-mutagen.mutagen-monthly {
136 | background-color: var(--iztro-color-monthly);
137 | opacity: 0.6;
138 | }
139 | .iztro-star-mutagen.mutagen-daily {
140 | background-color: var(--iztro-color-daily);
141 | opacity: 0.6;
142 | }
143 | .iztro-star-mutagen.mutagen-hourly {
144 | background-color: var(--iztro-color-hourly);
145 | opacity: 0.6;
146 | }
147 |
148 | .iztro-palace-gz .iztro-palace-gz-active {
149 | background-color: var(--iztro-color-nice);
150 | color: #fff;
151 | font-weight: normal;
152 | }
153 |
154 | .iztro-star-mutagen-0 {
155 | background-color: var(--iztro-color-awesome);
156 | color: #fff;
157 | font-weight: normal;
158 | }
159 |
160 | .iztro-star-mutagen-1 {
161 | background-color: var(--iztro-color-quan);
162 | color: #fff;
163 | font-weight: normal;
164 | }
165 |
166 | .iztro-star-mutagen-2 {
167 | background-color: var(--iztro-color-nice);
168 | color: #fff;
169 | font-weight: normal;
170 | }
171 |
172 | .iztro-star-mutagen-3 {
173 | background-color: var(--iztro-color-focus);
174 | color: #fff;
175 | font-weight: normal;
176 | }
177 |
178 | .iztro-star-self-mutagen-0::before {
179 | background-color: var(--iztro-color-awesome);
180 | }
181 | .iztro-star-self-mutagen-1::before {
182 | background-color: var(--iztro-color-quan);
183 | }
184 | .iztro-star-self-mutagen-2::before {
185 | background-color: var(--iztro-color-nice);
186 | }
187 | .iztro-star-self-mutagen-3::before {
188 | background-color: var(--iztro-color-focus);
189 | }
190 |
191 | .iztro-star-hover-mutagen-0::after {
192 | background-color: var(--iztro-color-awesome);
193 | }
194 | .iztro-star-hover-mutagen-1::after {
195 | background-color: var(--iztro-color-quan);
196 | }
197 | .iztro-star-hover-mutagen-2::after {
198 | background-color: var(--iztro-color-nice);
199 | }
200 | .iztro-star-hover-mutagen-3::after {
201 | background-color: var(--iztro-color-focus);
202 | }
203 |
204 | .iztro-palace-name-body {
205 | font-size: var(--iztro-star-font-size-small);
206 | font-weight: normal;
207 | position: absolute;
208 | margin-top: 2px;
209 | }
210 |
211 | .iztro-palace-fate span {
212 | display: block;
213 | padding: 0 3px;
214 | border-radius: 4px;
215 | color: #fff;
216 | background-color: var(--iztro-color-major);
217 | cursor: pointer;
218 | }
219 |
220 | .iztro-palace-center-item {
221 | font-size: var(--iztro-star-font-size-small);
222 | line-height: 22px;
223 | }
224 |
225 | .iztro-palace-center-item label {
226 | color: var(--iztro-color-text);
227 | }
228 |
229 | .iztro-palace-center-item span {
230 | color: var(--iztro-color-decorator-1);
231 | }
232 |
233 | .gender {
234 | display: inline-block;
235 | margin-right: 5px;
236 | }
237 | .gender.gender-male {
238 | color: var(--iztro-color-quan);
239 | }
240 | .gender.gender-female {
241 | color: var(--iztro-color-happy);
242 | }
243 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "resolveJsonModule": true,
6 | "esModuleInterop": true,
7 | "jsx": "react",
8 | "declaration": true,
9 | "outDir": "./lib",
10 | "strict": true
11 | },
12 | "include": ["src"],
13 | "exclude": [
14 | "lib",
15 | "node_modules",
16 | "src/**/*.test.tsx",
17 | "src/**/*.stories.tsx"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------