├── .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 | [![npm](https://img.shields.io/npm/v/react-iztro?logo=npm&logoColor=%23CB3837)](https://www.npmjs.com/package/react-iztro) 14 | [![npm](https://img.shields.io/npm/dt/react-iztro?logo=npm&logoColor=%23CB3837)](https://www.npmjs.com/package/react-iztro) 15 | [![GitHub](https://img.shields.io/github/license/sylarlong/react-iztro)](https://www.npmjs.com/package/react-iztro) 16 | [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/SylarLong/react-iztro)](https://www.npmjs.com/package/react-iztro) 17 | [![Package Quality](https://packagequality.com/shield/react-iztro.svg)](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 | react-iztro 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 |
      225 | 226 |
      231 | 232 | setHoroscopeDate?.(new Date())} 235 | > 236 | 今 237 | 238 |
      239 |
    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 | --------------------------------------------------------------------------------