feat: build mini app frontend pages

This commit is contained in:
zhouzhibo committed 2026-10-13 22:50:37 +08:00
1 parent ca36409e41
commit 869376979c
84 files changed
+16922 -86

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
.swc/
.DS_Store
+13 -86
View File
@@ -1,92 +1,19 @@
# hera-web
# Hera 小程序前端
Taro + React 微信小程序前端,页面按参考图片生成:
- `pages/my/index`:我的
- `pages/profile-edit/index`:我的页面上点击小铅笔修改的页面
- `pages/follow/index`:关注我们
- `pages/contact/index`:联系我们
- `pages/agreement/index`:用户协议 / 隐私协议
- 吉分兑换在“我的”页面中以弹窗形式呈现
## Getting started
## 本地开发
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://101.43.139.56/git/hz/hera-web.git
git branch -M main
git push -uf origin main
```bash
npm install
npm run dev:weapp
```
## Integrate with your tools
- [ ] [Set up project integrations](http://101.43.139.56/git/hz/hera-web/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
后端默认请求 `http://localhost:8080`,可在 `src/services/request.ts` 修改。
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
presets: [
['taro', {
framework: 'react',
ts: true
}]
]
}
+33
View File
@@ -0,0 +1,33 @@
import { defineConfig } from '@tarojs/cli'
export default defineConfig(async () => ({
projectName: 'hera-web',
date: '2026-08-12',
designWidth: 750,
deviceRatio: {
640: 2.34 / 2,
750: 1,
828: 1.81 / 2
},
sourceRoot: 'src',
outputRoot: 'dist',
plugins: ['@tarojs/plugin-framework-react'],
defineConstants: {},
copy: {
patterns: [],
options: {}
},
framework: 'react',
compiler: 'webpack5',
mini: {
postcss: {
pxtransform: {
enable: true,
config: {}
},
cssModules: {
enable: false
}
}
}
}))
+36
View File
@@ -0,0 +1,36 @@
{
"name": "hera-web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev:weapp": "taro build --type weapp --watch",
"build:weapp": "taro build --type weapp"
},
"dependencies": {
"@babel/runtime": "^7.25.6",
"@tarojs/components": "3.6.35",
"@tarojs/helper": "3.6.35",
"@tarojs/plugin-framework-react": "3.6.35",
"@tarojs/plugin-platform-weapp": "3.6.35",
"@tarojs/react": "3.6.35",
"@tarojs/runtime": "3.6.35",
"@tarojs/shared": "3.6.35",
"@tarojs/taro": "3.6.35",
"lunar-typescript": "^1.8.6",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@tarojs/cli": "3.6.35",
"@tarojs/plugin-platform-h5": "3.6.35",
"@tarojs/webpack5-runner": "3.6.35",
"@types/react": "^18.3.5",
"@types/webpack-env": "^1.18.5",
"babel-preset-taro": "3.6.35",
"react-refresh": "0.14.0",
"typescript": "5.5.4",
"webpack": "5.78.0"
}
}
+13096
View File
File diff suppressed because it is too large. Load diff
+18
View File
@@ -0,0 +1,18 @@
packages:
- .
allowBuilds:
'@swc/core': true
'@tarojs/binding': true
core-js: true
core-js-pure: true
esbuild: true
swiper: true
onlyBuiltDependencies:
- "@swc/core"
- "@tarojs/binding"
- core-js
- core-js-pure
- esbuild
- swiper
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+359
View File
@@ -0,0 +1,359 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Hera 首页预览</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
background: #eef6ff;
color: #29243a;
}
.phone {
max-width: 470px;
min-height: 100vh;
margin: 0 auto;
padding: 118px 18px 112px;
background: linear-gradient(180deg, rgba(154,124,255,.92) 0%, rgba(220,201,255,.76) 27%, rgba(253,241,255,.9) 52%, #eef6ff 100%);
position: relative;
overflow: hidden;
}
.capsule {
position: absolute;
top: 58px;
right: 18px;
width: 126px;
height: 40px;
border-radius: 22px;
background: rgba(255,255,255,.72);
display: flex;
align-items: center;
justify-content: space-around;
font-size: 26px;
font-weight: 900;
}
.day-switch { display: flex; align-items: center; gap: 28px; }
.avatar-orb {
width: 72px; height: 72px; border-radius: 50%;
background: #fffef0; display: flex; align-items: center; justify-content: center;
box-shadow: 0 8px 18px rgba(119,102,154,.12);
}
.water-face {
width: 45px; height: 52px; border-radius: 50% 50% 52% 52%;
background: linear-gradient(180deg, #9ff5ff, #39c6ea);
color: #3274c8; text-align: center; line-height: 42px; font-size: 22px;
}
.day-tabs { flex: 1; display: grid; grid-template-columns: repeat(3, 1fr); text-align: center; }
.day-tab { color: rgba(42,34,62,.58); position: relative; }
.day-tab + .day-tab:before {
content: ""; position: absolute; left: -1px; top: 12px; width: 1px; height: 8px; background: rgba(42,34,62,.4);
}
.day-label { display: block; font-size: 19px; }
.day-status { display: inline-block; margin-top: 6px; min-width: 58px; height: 27px; border-radius: 15px; line-height: 27px; font-size: 16px; }
.day-tab.active { color: #29243a; font-weight: 900; }
.day-tab.active .day-label { font-size: 26px; }
.day-tab.active .day-status { background: #7956f4; color: #fff; }
.date-row { margin-top: 34px; display: flex; justify-content: space-between; color: #4f485f; font-size: 15px; }
.month-day { font-size: 22px; font-weight: 900; }
.summary-card {
margin-top: 24px; min-height: 126px; border-radius: 12px;
background: linear-gradient(105deg, #7139f3, #8344ff 54%, #d35af3);
color: #fff; padding: 26px 18px; display: flex; align-items: center; justify-content: space-between;
overflow: hidden; position: relative;
}
.summary-card:after {
content: ""; position: absolute; right: -18px; top: -24px; width: 140px; height: 140px; border-radius: 50%;
background: radial-gradient(circle, rgba(131,226,255,.95), rgba(61,121,250,.72) 52%, rgba(255,255,255,0) 70%);
}
.summary-copy { width: 245px; font-size: 20px; line-height: 1.75; font-weight: 800; z-index: 1; }
.index-orb {
width: 92px; height: 92px; border-radius: 50%;
background: radial-gradient(circle at 45% 36%, #e8ffff, #86dfff 42%, #5a71ff);
box-shadow: 0 0 0 7px rgba(255,255,255,.22);
display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1;
}
.index-number { font-size: 42px; line-height: .95; font-weight: 900; }
.index-label { font-size: 13px; margin-top: 4px; }
.lucky-grid { margin-top: 14px; display: grid; grid-template-columns: 1.35fr .75fr; gap: 12px; }
.lucky-box {
min-height: 58px; border-radius: 12px; padding: 0 14px; background: rgba(255,255,255,.55);
display: flex; align-items: center; gap: 10px; font-size: 16px;
}
.colors { background: rgba(246,233,255,.72); }
.numbers { background: rgba(234,225,255,.72); }
.direction { background: rgba(255,241,198,.82); }
.hours { background: rgba(196,235,255,.82); }
.lucky-label { font-size: 18px; font-weight: 900; white-space: nowrap; }
.swatch { width: 28px; height: 28px; border-radius: 8px; }
.color-chip, .hour-chip { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.slot { width: 28px; height: 36px; border-radius: 7px; background: linear-gradient(180deg,#8f75ff,#7454ef); color: #fff; font-size: 28px; text-align: center; line-height: 36px; font-weight: 900; }
.branch { width: 25px; height: 25px; border-radius: 50%; background: #5bb8df; color: #fff; text-align: center; line-height: 25px; font-weight: 900; }
.dimension-head { margin-top: 28px; display: flex; justify-content: space-between; align-items: center; }
.dimension-head strong { font-size: 27px; }
.dimension-head span { color: #6d6578; font-size: 16px; }
.dimension-list { margin-top: 16px; display: flex; flex-direction: column; gap: 13px; }
.card {
min-height: 126px; border-radius: 12px; background: rgba(255,255,255,.86);
padding: 18px 42px 18px 18px; position: relative; display: flex; gap: 13px;
box-shadow: 0 8px 18px rgba(90,79,116,.06);
}
.icon { width: 36px; height: 36px; flex: none; border-radius: 12px; }
.heart { background: radial-gradient(circle at 32% 32%, #ffd2df, #ff79a3 68%, #fff 70%); transform: rotate(-10deg); }
.gourd { background: radial-gradient(circle at 50% 28%, #ffc65c 0 28%, transparent 29%), radial-gradient(circle at 50% 65%, #f0a222 0 48%, transparent 49%); }
.vase { background: radial-gradient(circle at 50% 18%, #d5fff0 0 24%, transparent 25%), linear-gradient(180deg, #7df0c9, #2ccda0); border-radius: 18px 18px 10px 10px; }
.coin { border-radius: 50%; background: radial-gradient(circle, #fff8bb 0 33%, #f1b82d 34% 100%); }
.fan { border-radius: 50% 50% 8px 8px; background: conic-gradient(from 235deg, #c29cff, #fff, #8e63ef, #c29cff); }
.scroll { background: repeating-linear-gradient(90deg, #ffe8aa 0 7px, #f5c868 8px 12px); }
.bamboo { background: linear-gradient(135deg, transparent 20%, #30d3b9 21% 38%, transparent 39% 50%, #25bfa8 51% 68%, transparent 69%); }
.horse { border-radius: 50% 18px 50% 18px; background: linear-gradient(135deg, #dbe9ff, #6b94f8); }
.title-row { display: flex; align-items: center; gap: 12px; }
.title { font-size: 22px; font-weight: 900; white-space: nowrap; }
.headline { color: #5d5667; font-size: 17px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.detail { margin-top: 18px; color: #666071; font-size: 17px; line-height: 1.7; }
.tag {
position: absolute; right: 18px; top: 20px; width: 28px; height: 28px; border-radius: 50%;
color: #fff; font-weight: 900; line-height: 28px; text-align: center; font-size: 16px;
}
.tag.yi { background: #4bc660; }
.tag.ji { background: #f45b72; }
.arrow { position: absolute; right: 18px; bottom: 16px; font-size: 32px; }
.tabbar {
position: fixed; left: 50%; bottom: 0; transform: translateX(-50%);
width: min(470px, 100vw); height: 94px; border-radius: 18px 18px 0 0; background: rgba(255,255,255,.96);
display: flex; align-items: center; justify-content: space-around; box-shadow: 0 -8px 24px rgba(85,79,110,.1);
font-size: 17px; font-weight: 900;
}
.tab { width: 86px; display: flex; flex-direction: column; align-items: center; gap: 7px; }
.tab.active { color: #ff9b2a; }
.tab-icon { width: 24px; height: 20px; border-radius: 0 0 12px 12px; background: currentColor; position: relative; }
.tab-icon:before { content: ""; position: absolute; left: 3px; top: -12px; width: 18px; height: 18px; border-radius: 12px 12px 7px 7px; background: currentColor; }
.mine-icon { width: 28px; height: 19px; border-radius: 14px 14px 8px 8px; background: currentColor; position: relative; }
.mine-icon:before { content: ""; position: absolute; left: 8px; top: -15px; width: 12px; height: 12px; border-radius: 50%; background: currentColor; }
.realm { margin-top: -48px; }
.realm-face { width: 66px; height: 66px; border-radius: 50%; border: 7px solid #f0f7ff; background: #fff; display: grid; place-items: center; color: #ff79a3; font-size: 34px; box-shadow: 0 8px 18px rgba(121,129,164,.16); }
.page-view { display: none; }
.page-view.active { display: block; }
.tab { border: 0; background: transparent; color: #29243a; cursor: pointer; font: inherit; }
.tab.active { color: #ff9b2a; }
.my-profile { display: flex; align-items: center; min-height: 78px; margin-top: 2px; }
.my-info { flex: 1; margin-left: 20px; min-width: 0; }
.my-name-row { display: flex; align-items: center; gap: 7px; }
.my-name { font-size: 25px; line-height: 1.1; font-weight: 900; }
.gender { width: 23px; height: 23px; border-radius: 50%; background: #5cc8ff; color: #fff; display: grid; place-items: center; font-size: 17px; font-weight: 900; }
.my-birthday { margin-top: 12px; display: flex; align-items: center; gap: 7px; color: #6e6378; font-size: 16px; border: 0; background: transparent; padding: 0; cursor: pointer; }
.my-arrow { color: rgba(38,32,56,.45); font-size: 36px; }
.quick-card { margin-top: 34px; min-height: 112px; border-radius: 16px; background: rgba(255,255,255,.76); display: flex; align-items: center; gap: 34px; padding: 15px 28px; box-shadow: 0 12px 28px rgba(94,76,134,.08); }
.quick-entry { width: 78px; text-align: center; font-size: 17px; color: #2b2637; }
.quick-icon { width: 50px; height: 50px; margin: 0 auto 11px; border-radius: 50%; background: #ece4ff; display: grid; place-items: center; box-shadow: 0 8px 18px rgba(138,106,194,.12); }
.quick-rabbit { color: #5c4438; font-size: 38px; }
.quick-drop { width: 36px; height: 44px; border-radius: 50% 50% 52% 52%; background: linear-gradient(180deg,#9ff5ff,#39c6ea); color: #3274c8; line-height: 34px; font-size: 20px; }
.palace { margin-top: 28px; }
.section-line { display: flex; align-items: center; gap: 9px; }
.section-line strong { font-size: 27px; }
.section-line span { font-size: 18px; }
.section-line em { color: #e74f83; font-style: normal; font-weight: 900; }
.palace-empty { width: 118px; height: 150px; margin-top: 17px; border-radius: 12px; background: linear-gradient(180deg,#ffe3a3,#fff7a8); box-shadow: 0 14px 24px rgba(216,170,67,.18); }
.settings { margin-top: 26px; border-radius: 17px; background: rgba(255,255,255,.92); padding: 0 20px; box-shadow: 0 14px 34px rgba(93,83,114,.08); }
.settings-row { min-height: 67px; display: flex; align-items: center; gap: 16px; border-bottom: 1px solid rgba(220,224,236,.76); font-size: 20px; }
.settings-row:last-child { border-bottom: 0; }
.settings-row span:nth-child(2) { flex: 1; }
.menu-mark { width: 29px; height: 29px; border: 3px solid #252238; border-radius: 9px; display: grid; place-items: center; font-size: 16px; }
.menu-headset { border-radius: 50% 50% 42% 42%; border-bottom-color: transparent; }
.menu-doc { position: relative; }
.menu-doc:after { content: ""; position: absolute; right: -6px; bottom: -6px; width: 13px; height: 13px; border-radius: 50%; border: 3px solid #252238; background: #fff; }
.tiangong-preview { min-height: 100vh; margin: -118px -18px -112px; padding: 92px 42px 140px; background: #fff; color: #202126; }
.tiangong-title { display: block; margin-bottom: 10px; text-align: center; color: #26272b; font-size: 29px; font-weight: 700; }
.tiangong-subtitle { display: block; margin-bottom: 42px; text-align: center; color: #9a9da5; font-size: 15px; }
.deity-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px 18px; }
.deity-card { background: transparent; border: 0; padding: 0; text-align: center; }
.deity-card.locked .deity-figure { opacity: .28; filter: grayscale(1); }
.deity-figure { height: 122px; background: #fff; display: grid; place-items: center; overflow: hidden; }
.deity-figure img { width: 100%; height: 100%; object-fit: cover; display: block; }
.deity-name { height: 32px; margin-top: 3px; background: #dceddf; color: #283b32; line-height: 32px; font-size: 14px; font-weight: 600; }
.phone.realm-active { padding-top: 118px; background: #fff; }
.phone.realm-active .capsule { display: none; }
.phone.realm-active .tabbar { background: rgba(255,255,255,.96); }
.page-view.tiangong-detail-preview { height: 100vh; margin: -118px -18px -112px; padding: 0; display: none; flex-direction: column; overflow: hidden; background: #fff9eb; color: #25262a; }
.page-view.tiangong-detail-preview.active { display: flex; }
.detail-nav-preview { height: 70px; flex: none; display: flex; align-items: center; justify-content: center; background: #fff; }
.detail-nav-preview strong { font-size: 21px; }
.cash-scene { width: 100%; min-height: 0; flex: 1; display: block; object-fit: cover; }
.cash-actions { height: 116px; padding: 13px 25px; flex: none; display: flex; justify-content: space-between; background: rgba(255,247,226,.86); backdrop-filter: blur(8px); }
.cash-action { width: 43px; height: 76px; padding: 10px 0; border: 0; border-radius: 23px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; background: #e3f0e9; color: #25282b; font-size: 17px; line-height: 1; box-shadow: 0 4px 10px rgba(129,154,137,.18); cursor: pointer; }
.phone.detail-active + .tabbar { display: none; }
.page-view.subpage-preview { min-height: 100vh; margin: -118px -18px -112px; padding: 28px 22px 118px; display: none; background: linear-gradient(180deg,#a38af5 0%,#d6cafa 31%,#edf4ff 66%,#eaf3ff 100%); color: #28243a; }
.page-view.subpage-preview.active { display: block; }
.preview-back { border: 0; background: transparent; color: #40365e; font-size: 40px; cursor: pointer; }
.element-preview { padding-top: 22px !important; }
.element-plot { height: 330px; margin-top: 8px; position: relative; display: grid; place-items: center; }
.radar-shape { width: 220px; height: 220px; clip-path: polygon(50% 0,98% 35%,79% 97%,21% 97%,2% 35%); background: repeating-radial-gradient(circle at center, transparent 0 24px, rgba(180,130,101,.2) 25px 27px); border: 1px solid rgba(180,130,101,.25); }
.radar-value { position: absolute; padding: 4px 10px; border-radius: 13px; color:#fff; font-weight:900; font-size:13px; }
.radar-value.earth { top:0; background:#c8836d; }.radar-value.metal { right:7px; top:116px; background:#d5aa52; }.radar-value.water { right:63px; bottom:4px; background:#5bb8df; }.radar-value.wood { left:63px; bottom:4px; background:#69bb7e; }.radar-value.fire { left:4px; top:116px; background:#ed6d79; }
.element-info-card { margin-top: 20px; padding: 22px 18px; border-radius: 14px; background:rgba(255,255,255,.75); }
.element-info-card + .element-info-card { margin-top: 16px; }.element-info-card b { display:block; font-size:19px; }.element-info-card p { margin:14px 0 0; color:#686273; font-size:15px; line-height:1.75; }.element-info-card .tag { position:static; display:inline-block; width:auto; height:auto; padding:7px 18px; border-radius:20px; margin-right:12px; font-size:16px; line-height:1; }.tag.day { background:#ff9b2a; }.tag.good { background:#49bb68; }.tag.avoid { background:#ef5b80; }
.follow-preview { text-align:center; }.follow-title-preview { margin-top:140px; color:#8f640d; font-size:34px; line-height:1.65; font-weight:900; }.follow-tip-preview { display:block; margin-top:28px; color:#9e8b65; font-size:16px; }.follow-qr-row { margin-top:52px; display:flex; justify-content:center; gap:18px; }.follow-qr-card { width:142px; padding:12px 10px 18px; border-radius:18px; background:#fff; box-shadow:0 8px 18px rgba(95,75,38,.16); color:#7a4a20; font-size:16px; font-weight:900; }.follow-qr-card img { width:118px; height:118px; object-fit:cover; border-radius:9px; display:block; margin:0 auto 12px; }.follow-search { margin-top:45px; display:flex; justify-content:center; gap:14px; }.follow-search span { padding:10px 14px; border-radius:20px; background:rgba(255,255,255,.8); color:#7a4a20; font-size:13px; box-shadow:0 4px 12px rgba(98,80,49,.1); }.follow-other { display:block; margin-top:30px; color:#9e8b65; font-size:15px; }.follow-brand { display:block; margin-top:26px; color:#8f640d; font-size:34px; font-weight:400; }
.agreement-preview { background:#fffdf7 !important; color:#292929; }.agreement-heading { text-align:center; font-size:24px; color:#8f640d; font-weight:700; }.agreement-tabs-preview { margin-top:40px; display:flex; justify-content:space-around; color:#8f640d; font-size:18px; font-weight:700; }.agreement-tabs-preview .selected { border-bottom:3px solid #8f640d; padding-bottom:12px; }.agreement-copy { margin-top:38px; font-size:17px; line-height:1.95; color:#353535; }.agreement-copy h3 { font-size:22px; }.phone.subpage-active .capsule { display:none; }.phone.full-subpage + .tabbar { display:none; }
</style>
</head>
<body>
<main class="phone">
<div class="capsule">••• ◎</div>
<section class="page-view active" data-page="home">
<section class="day-switch">
<div class="avatar-orb"><div class="water-face">⌣</div></div>
<div class="day-tabs">
<div class="day-tab"><span class="day-label">昨日</span><span class="day-status">小逆</span></div>
<div class="day-tab active"><span class="day-label">今日</span><span class="day-status">小逆</span></div>
<div class="day-tab"><span class="day-label">明日</span><span class="day-status">平日</span></div>
</div>
</section>
<section class="date-row">
<span><span class="month-day">10月12日</span> 星期一</span>
<span>农历丙午年 九月初三</span>
</section>
<section class="summary-card">
<div class="summary-copy">小逆之日,少言多思,养精蓄锐为上。</div>
<div class="index-orb"><div class="index-number">42</div><div class="index-label">综合指数</div></div>
</section>
<section class="lucky-grid">
<div class="lucky-box colors"><strong class="lucky-label">吉色</strong><span class="swatch" style="background:#a7ad2a"></span><span>绿茶色</span><span class="swatch" style="background:#50c3cf"></span><span>瀑布蓝</span></div>
<div class="lucky-box numbers"><strong class="lucky-label">吉数</strong><span class="slot">6</span><span class="slot">8</span></div>
<div class="lucky-box direction"><strong class="lucky-label">吉向</strong><span>⌖</span><span>东北</span></div>
<div class="lucky-box hours"><strong class="lucky-label">吉时</strong><span class="hour-chip"><span class="branch">子</span><span>23-01点</span></span><span class="hour-chip"><span class="branch">申</span><span>15-17点</span></span></div>
</section>
<section class="dimension-head"><strong>维度解析</strong><span>排序 ⇅</span></section>
<section class="dimension-list">
<article class="card"><div class="icon heart"></div><div><div class="title-row"><span class="title">情感</span><span class="headline">默契收集,心流时刻</span></div><div class="detail">适合捕捉和TA的默契瞬间,让这些小细节成为关系里的锚点。</div></div><span class="tag yi">宜</span><span class="arrow">›</span></article>
<article class="card"><div class="icon gourd"></div><div><div class="title-row"><span class="title">健康</span><span class="headline">润肺养身,舒缓秋燥</span></div><div class="detail">秋日干燥适合润肺养身,舒缓身体的秋燥不适感。</div></div><span class="tag yi">宜</span><span class="arrow">›</span></article>
<article class="card"><div class="icon vase"></div><div><div class="title-row"><span class="title">生活</span><span class="headline">收纳旧物,沉淀回忆</span></div><div class="detail">秋日里适合收纳旧物,在整理中沉淀那些温暖的小回忆。</div></div><span class="tag yi">宜</span><span class="arrow">›</span></article>
<article class="card"><div class="icon coin"></div><div><div class="title-row"><span class="title">财富</span><span class="headline">勿乱囤货,按需消费</span></div><div class="detail">财富能量忌盲目囤积,适合避开非必要的批量采购,保持理性消费。</div></div><span class="tag ji">忌</span><span class="arrow">›</span></article>
<article class="card"><div class="icon fan"></div><div><div class="title-row"><span class="title">社交</span><span class="headline">勿涉纷争,保持中立</span></div><div class="detail">易卷入人际是非,适合远离矛盾,保持中立姿态。</div></div><span class="tag ji">忌</span><span class="arrow">›</span></article>
<article class="card"><div class="icon scroll"></div><div><div class="title-row"><span class="title">学业</span><span class="headline">基础松散,学业忌跳级</span></div><div class="detail">底层知识不牢,别急于学习进阶内容。</div></div><span class="tag ji">忌</span><span class="arrow">›</span></article>
<article class="card"><div class="icon bamboo"></div><div><div class="title-row"><span class="title">事业</span><span class="headline">敷衍了事,别凑活</span></div><div class="detail">今日易因松散出错,适合认真对待每一项小任务,别敷衍。</div></div><span class="tag ji">忌</span><span class="arrow">›</span></article>
<article class="card"><div class="icon horse"></div><div><div class="title-row"><span class="title">出行</span><span class="headline">勿负重行,轻装简从</span></div><div class="detail">今日出行负重易生疲惫,建议轻装出门,避免带过多不必要的物品。</div></div><span class="tag ji">忌</span><span class="arrow">›</span></article>
</section>
</section>
<section class="page-view tiangong-preview" data-page="realm">
<span class="tiangong-title">天宫</span>
<span class="tiangong-subtitle">供奉神祇,收集属于你的福运</span>
<div class="deity-grid">
<div class="deity-card"><div class="deity-figure"><img src="./assets/tiangong/wealth.png" alt="财神"></div><div class="deity-name">财神</div></div>
<div class="deity-card locked"><div class="deity-figure"><img src="./assets/tiangong/fortune.png" alt="禄星"></div><div class="deity-name">禄星</div></div>
<div class="deity-card locked"><div class="deity-figure"><img src="./assets/tiangong/longevity.png" alt="寿星"></div><div class="deity-name">寿星</div></div>
<div class="deity-card locked"><div class="deity-figure"><img src="./assets/tiangong/wenchang.png" alt="文昌星"></div><div class="deity-name">文昌星</div></div>
<div class="deity-card locked"><div class="deity-figure"><img src="./assets/tiangong/matchmaker.png" alt="月老"></div><div class="deity-name">月老</div></div>
</div>
</section>
<section class="page-view tiangong-detail-preview" data-page="cash-detail">
<div class="detail-nav-preview"><strong>敬神</strong></div>
<img class="cash-scene" src="./assets/tiangong/wealth-detail-scene.jpg" alt="财神供奉场景">
<div class="cash-actions">
<button class="cash-action"><span>上</span><span>香</span></button><button class="cash-action"><span>祈</span><span>愿</span></button><button class="cash-action"><span>回</span><span>向</span></button><button class="cash-action"><span>送</span><span>神</span></button><button class="cash-action" id="detailBack"><span>返</span><span>回</span></button>
</div>
</section>
<section class="page-view" data-page="my">
<section class="my-profile">
<div class="avatar-orb"><div class="water-face">⌣</div></div>
<div class="my-info">
<div class="my-name-row"><span class="my-name">You</span><span class="gender">♂</span></div>
<button class="my-birthday" id="birthdayToggle"><span>▦</span><span id="birthdayText">19**_06_18</span><span id="birthdayEye">⌣</span></button>
</div>
<span class="my-arrow">›</span>
</section>
<section class="quick-card">
<div class="quick-entry"><div class="quick-icon quick-rabbit">⌇</div><span>我的生肖</span></div>
<div class="quick-entry" id="elementEntry"><div class="quick-icon"><div class="quick-drop">⌣</div></div><span>我的元素</span></div>
</section>
<section class="palace">
<div class="section-line"><strong>我的天宫</strong><span>(已收集 <em>1</em> 款)</span></div>
<div class="palace-empty"></div>
</section>
<section class="settings">
<div class="settings-row"><span class="menu-mark menu-headset"></span><span>联系我们</span><span>›</span></div>
<div class="settings-row" id="followEntry"><span class="menu-mark">✦</span><span>关注我们</span><span>›</span></div>
<div class="settings-row" id="agreementEntry"><span class="menu-mark menu-doc"></span><span>用户协议</span><span>›</span></div>
</section>
</section>
<section class="page-view subpage-preview element-preview" data-page="element">
<button class="preview-back" data-back="my">‹</button>
<div class="element-plot"><div class="radar-shape"></div><span class="radar-value earth">● 土 42%</span><span class="radar-value metal">✦ 金 22%</span><span class="radar-value water">● 水 8%</span><span class="radar-value wood">● 木 22%</span><span class="radar-value fire">● 火 7%</span></div>
<div class="element-info-card"><b><span class="tag day">日主</span>壬</b><p>日主,即出生日期中的天干,其五行属性直接代表自身能量基础。</p><p>你适应力强,擅长在变化中寻找机会与路径。</p></div>
<div class="element-info-card"><b><span class="tag good">喜用</span>金水</b><p>喜用,是更容易让事情进展顺、做得稳、发挥得好的行为方式。</p><p>你擅长运用水的变通与金的锐利,遇事能灵活调整方向。</p></div>
<div class="element-info-card"><b><span class="tag avoid">忌用</span>木火土</b><p>忌用,是容易让你更累、更乱或效率不佳的行为方式。</p></div>
</section>
<section class="page-view subpage-preview follow-preview" data-page="follow">
<button class="preview-back" data-back="my">‹</button><div class="follow-title-preview">关注我们<br>一触即吉</div><span class="follow-tip-preview">点击图片保存,扫码关注</span><div class="follow-qr-row"><div class="follow-qr-card"><img src="./assets/follow-redbook.jpg" alt="小红书二维码">小红书</div><div class="follow-qr-card"><img src="./assets/follow-video.jpg" alt="视频号二维码">视频号</div></div><div class="follow-search"><span>那小吉 ⌕</span><span>那吉 nagiflow ⌕</span></div><span class="follow-other">其他平台搜索我们</span><span class="follow-brand">那吉<br>NAGI FLOW</span>
</section>
<section class="page-view subpage-preview agreement-preview" data-page="agreement">
<button class="preview-back" data-back="my">‹</button><div class="agreement-heading">协议</div><div class="agreement-tabs-preview"><span class="selected">用户协议</span><span>隐私政策</span></div><div class="agreement-copy"><h3>用户协议</h3><p>为使用小程序软件服务,您应当阅读并遵守相关注册协议以及专项规则。请您审慎阅读、充分理解各条款内容,特别是免除或限制责任的相应条款。</p><h3>一、协议的范围</h3><p>本协议是您与那吉之间关于使用那吉软件服务所订立的协议。所有规则均为本协议不可分割的一部分,与协议正文具有同等法律效力。</p><h3>二、适用范围</h3><p>本政策适用于您通过本应用进行的所有操作及我们因此收集的个人信息。</p></div>
</section>
</main>
<nav class="tabbar">
<button class="tab active" data-target="home"><span class="tab-icon"></span><span>首页</span></button>
<button class="tab realm" data-target="realm"><span class="realm-face">⌁</span><span>天宫</span></button>
<button class="tab" data-target="my"><span class="mine-icon"></span><span>我的</span></button>
</nav>
<script>
const tabs = Array.from(document.querySelectorAll('.tabbar .tab'));
const pages = Array.from(document.querySelectorAll('.page-view'));
const phone = document.querySelector('.phone');
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
const target = tab.dataset.target;
tabs.forEach((item) => item.classList.toggle('active', item === tab));
pages.forEach((page) => page.classList.toggle('active', page.dataset.page === target));
phone.classList.toggle('realm-active', target === 'realm');
phone.classList.remove('detail-active', 'subpage-active', 'full-subpage');
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
const wealthCard = document.querySelector('[data-page="realm"] .deity-card');
const detailBack = document.getElementById('detailBack');
function showCashDetail() {
pages.forEach((page) => page.classList.toggle('active', page.dataset.page === 'cash-detail'));
phone.classList.add('realm-active');
phone.classList.add('detail-active');
phone.classList.remove('subpage-active', 'full-subpage');
document.querySelector('.tab.realm').classList.add('active');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function showRealm() {
pages.forEach((page) => page.classList.toggle('active', page.dataset.page === 'realm'));
phone.classList.add('realm-active');
phone.classList.remove('detail-active');
phone.classList.remove('subpage-active', 'full-subpage');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function showSubpage(name, fullScreen = false) {
pages.forEach((page) => page.classList.toggle('active', page.dataset.page === name));
phone.classList.add('subpage-active');
phone.classList.toggle('full-subpage', fullScreen);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function showMy() {
pages.forEach((page) => page.classList.toggle('active', page.dataset.page === 'my'));
phone.classList.remove('subpage-active', 'full-subpage');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
wealthCard?.addEventListener('click', showCashDetail);
detailBack?.addEventListener('click', showRealm);
document.getElementById('elementEntry')?.addEventListener('click', () => showSubpage('element'));
document.getElementById('followEntry')?.addEventListener('click', () => showSubpage('follow', true));
document.getElementById('agreementEntry')?.addEventListener('click', () => showSubpage('agreement', true));
document.querySelectorAll('[data-back="my"]').forEach((button) => button.addEventListener('click', showMy));
let birthdayVisible = false;
const birthdayToggle = document.getElementById('birthdayToggle');
const birthdayText = document.getElementById('birthdayText');
const birthdayEye = document.getElementById('birthdayEye');
birthdayToggle?.addEventListener('click', () => {
birthdayVisible = !birthdayVisible;
birthdayText.textContent = birthdayVisible ? '1998_06_18' : '19**_06_18';
birthdayEye.textContent = birthdayVisible ? '◉' : '⌣';
});
</script>
</body>
</html>
+14
View File
@@ -0,0 +1,14 @@
{
"miniprogramRoot": "dist/",
"projectname": "hera-web",
"description": "Hera WeChat mini program",
"appid": "wxa76a2ef9e8ba612e",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"minified": true
},
"compileType": "miniprogram"
}
+34
View File
@@ -0,0 +1,34 @@
export default defineAppConfig({
pages: [
'pages/home/index',
'pages/login/index',
'pages/my/index',
'pages/fortune/index',
'pages/tiangong-detail/index',
'pages/profile-edit/index',
'pages/contact/index',
'pages/follow/index',
'pages/agreement/index'
],
window: {
backgroundTextStyle: 'light',
navigationStyle: 'custom',
backgroundColor: '#fffdf7'
},
tabBar: {
custom: true,
color: '#cfcfcf',
selectedColor: '#9a6d0f',
backgroundColor: '#fffdf7',
list: [
{
pagePath: 'pages/home/index',
text: '首页'
},
{
pagePath: 'pages/my/index',
text: '我的'
}
]
}
})
+92
View File
@@ -0,0 +1,92 @@
page {
min-height: 100%;
background: #fffdf7;
color: #8f640d;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}
button {
margin: 0;
padding: 0;
background: transparent;
border: 0;
line-height: 1;
}
button::after {
border: 0;
}
.page {
min-height: 100vh;
box-sizing: border-box;
padding: 96px 56px 170px;
background: #fffdf7;
}
.soft-bg {
background:
radial-gradient(circle at 22% 14%, rgba(230, 255, 206, 0.72), transparent 240px),
radial-gradient(circle at 82% 80%, rgba(255, 232, 241, 0.75), transparent 280px),
linear-gradient(180deg, #fbfff2 0%, #fffdf7 52%, #fff7fb 100%);
}
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10;
height: 92px;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 0 32px 18px;
box-sizing: border-box;
background: transparent;
}
.nav-title {
color: #7d5b12;
font-size: 34px;
font-weight: 700;
}
.back {
position: absolute;
left: 34px;
bottom: 14px;
width: 48px;
height: 48px;
color: #8f640d;
font-size: 52px;
line-height: 42px;
}
.pill {
min-height: 98px;
border-radius: 54px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 18px 34px rgba(118, 94, 49, 0.14);
}
.gold-button {
height: 96px;
border-radius: 54px;
background: linear-gradient(90deg, #ffd52b, #ffb800);
color: #fff;
font-size: 34px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.input-pill {
height: 78px;
border-radius: 40px;
background: #fff7de;
color: #8f640d;
padding: 0 28px;
box-sizing: border-box;
}
+8
View File
@@ -0,0 +1,8 @@
import { PropsWithChildren } from 'react'
import './app.scss'
function App({ children }: PropsWithChildren) {
return children
}
export default App
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+20
View File
@@ -0,0 +1,20 @@
import Taro from '@tarojs/taro'
import { Button, Text, View } from '@tarojs/components'
type Props = {
title?: string
back?: boolean
}
export function NavBar({ title, back = false }: Props) {
return (
<View className='nav'>
{back && (
<Button className='back' onClick={() => Taro.navigateBack()}>
‹
</Button>
)}
{title && <Text className='nav-title'>{title}</Text>}
</View>
)
}
+81
View File
@@ -0,0 +1,81 @@
.radar {
position: relative;
width: 520px;
height: 470px;
margin: 22px auto 42px;
}
.radar-web,
.radar-fill {
position: absolute;
left: 110px;
top: 58px;
width: 300px;
height: 300px;
clip-path: polygon(50% 0, 98% 36%, 80% 98%, 20% 98%, 2% 36%);
}
.radar-web {
border: 2px solid rgba(255, 219, 117, 0.5);
background: rgba(255, 237, 184, 0.18);
}
.web-1 {
transform: scale(1);
}
.web-2 {
transform: scale(0.68);
}
.web-3 {
transform: scale(0.36);
}
.radar-fill {
background: #13cce9;
opacity: 0.95;
}
.radar-label {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
font-weight: 700;
}
.radar-name {
font-size: 34px;
}
.radar-value {
margin-top: 8px;
font-size: 22px;
}
.radar-label-0 {
left: 50%;
top: 0;
transform: translateX(-50%);
}
.radar-label-1 {
right: 0;
top: 150px;
}
.radar-label-2 {
right: 50px;
bottom: 12px;
}
.radar-label-3 {
left: 48px;
bottom: 12px;
}
.radar-label-4 {
left: 0;
top: 150px;
}
+48
View File
@@ -0,0 +1,48 @@
import { View, Text } from '@tarojs/components'
import { ElementScore } from '../services/request'
import './RadarChart.scss'
type Props = {
scores: ElementScore[]
}
const positions = [
{ left: 50, top: 3 },
{ left: 86, top: 35 },
{ left: 72, top: 84 },
{ left: 28, top: 84 },
{ left: 14, top: 35 }
]
const elementMarks: Record<string, string> = {
土: '●', 金: '✦', 水: '●', 木: '●', 火: '●'
}
export function RadarChart({ scores }: Props) {
const polygon = scores.map((score, index) => {
const point = positions[index]
const scale = Math.max(score.value, 4) / 100
const x = 50 + (point.left - 50) * scale
const y = 50 + (point.top - 50) * scale
return `${x}% ${y}%`
}).join(', ')
return (
<View className='radar'>
<View className='radar-web web-1' />
<View className='radar-web web-2' />
<View className='radar-web web-3' />
<View className='radar-fill' style={{ clipPath: `polygon(${polygon})` }} />
{scores.map((score, index) => (
<View
key={score.name}
className={`radar-label radar-label-${index}`}
style={{ color: score.color }}
>
<Text className='radar-name'>{elementMarks[score.name]} {score.name}</Text>
<Text className='radar-value'>{score.value}%</Text>
</View>
))}
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+51
View File
@@ -0,0 +1,51 @@
.agreement-page {
color: #222;
padding-left: 30px;
padding-right: 30px;
}
.tabs {
margin-top: 210px;
display: flex;
justify-content: space-around;
}
.tab {
position: relative;
color: #8f640d;
font-size: 34px;
font-weight: 700;
padding-bottom: 18px;
}
.current::after {
content: '';
position: absolute;
left: 32px;
right: 32px;
bottom: 0;
height: 8px;
border-radius: 4px;
background: #8f640d;
}
.article {
height: calc(100vh - 350px);
margin-top: 42px;
}
.article-title {
display: block;
color: #252525;
font-size: 40px;
font-weight: 800;
margin-bottom: 26px;
}
.paragraph {
display: block;
color: #2f2f2f;
font-size: 30px;
line-height: 1.95;
margin-bottom: 28px;
}
+34
View File
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react'
import { Button, ScrollView, Text, View } from '@tarojs/components'
import { NavBar } from '../../components/NavBar'
import { api } from '../../services/request'
import './index.scss'
const sampleUser = '为使用小程序软件服务(以下简称“本服务”),您应当阅读并遵守《小程序软件注册协议》以及专项规则等。请您务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的相应条款。'
const samplePrivacy = '欢迎使用那吉。本隐私政策旨在向您说明我们收集、使用、存储、披露个人信息的方式、范围及您享有的相关权利,确保您在充分知情的前提下自愿使用本应用。'
export default function AgreementPage() {
const [active, setActive] = useState<'user' | 'privacy'>('user')
const [body, setBody] = useState(sampleUser)
useEffect(() => {
api.agreement(active).then((res) => setBody(res.body)).catch(() => setBody(active === 'user' ? sampleUser : samplePrivacy))
}, [active])
return (
<View className='page agreement-page'>
<NavBar back title='协议' />
<View className='tabs'>
<Button className={active === 'user' ? 'tab current' : 'tab'} onClick={() => setActive('user')}>用户协议</Button>
<Button className={active === 'privacy' ? 'tab current' : 'tab'} onClick={() => setActive('privacy')}>隐私政策</Button>
</View>
<ScrollView className='article' scrollY>
<Text className='article-title'>{active === 'user' ? '用户协议' : '一、引言'}</Text>
<Text className='paragraph'>{body}</Text>
<Text className='paragraph'>一、协议的范围{'\n'}1.1 本协议是您与那吉之间关于使用那吉软件服务所订立的协议。所有规则均为本协议不可分割的一部分,与协议正文具有同等法律效力。</Text>
<Text className='paragraph'>二、适用范围{'\n'}本政策适用于您通过本应用进行的所有操作及我们因此收集的个人信息,涵盖注册、登录、使用功能、参与互动等场景。</Text>
<Text className='paragraph'>三、个人信息的收集与使用{'\n'}我们遵循合法、正当、必要原则,仅收集为实现服务功能所必需的信息。</Text>
</ScrollView>
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+48
View File
@@ -0,0 +1,48 @@
.contact-page {
min-height: 100vh;
background: #f1f1f1;
box-sizing: border-box;
padding: 92px 30px 120px;
}
.session-subtitle {
position: fixed;
top: 76px;
left: 0;
right: 0;
text-align: center;
color: #555;
font-size: 22px;
z-index: 11;
}
.chat-input {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 104px;
background: #fff;
border-top: 1px solid #e5e5e5;
display: flex;
align-items: center;
gap: 22px;
padding: 16px 32px;
box-sizing: border-box;
}
.chat-input input {
flex: 1;
height: 72px;
border-radius: 12px;
background: #fff;
border: 1px solid #e0e0e0;
padding: 0 28px;
color: #555;
font-size: 28px;
}
.chat-input text {
color: #111;
font-size: 52px;
}
+17
View File
@@ -0,0 +1,17 @@
import { Input, Text, View } from '@tarojs/components'
import { NavBar } from '../../components/NavBar'
import './index.scss'
export default function ContactPage() {
return (
<View className='contact-page'>
<NavBar back title='纳吉云 ›' />
<Text className='session-subtitle'>客服会话</Text>
<View className='chat-input'>
<Input placeholder='可按住 转文字' />
<Text>☺</Text>
<Text>+</Text>
</View>
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+83
View File
@@ -0,0 +1,83 @@
.follow-page {
display: flex;
flex-direction: column;
align-items: center;
}
.follow-title {
margin-top: 210px;
color: #8f640d;
font-size: 52px;
line-height: 1.6;
text-align: center;
font-weight: 800;
white-space: pre-line;
}
.follow-tip {
margin-top: 38px;
color: #9e8b65;
font-size: 28px;
}
.qr-row {
margin-top: 72px;
display: flex;
gap: 36px;
}
.qr-card {
width: 290px;
height: 350px;
border-radius: 30px;
background: #fff;
box-shadow: 0 16px 28px rgba(95, 75, 38, 0.18);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
color: #7a4a20;
font-weight: 700;
font-size: 30px;
}
.qr-image {
width: 224px;
height: 224px;
border-radius: 18px;
}
.search-row {
margin-top: 86px;
display: flex;
gap: 28px;
}
.search-pill {
width: 220px;
height: 68px;
border-radius: 36px;
background: rgba(255, 255, 255, 0.78);
box-shadow: inset 0 0 10px rgba(151, 119, 55, 0.12), 0 8px 20px rgba(98, 80, 49, 0.12);
color: #7a4a20;
font-size: 24px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.other {
margin-top: 40px;
color: #9e8b65;
font-size: 26px;
}
.brand {
margin-top: 72px;
color: #8f640d;
font-size: 50px;
font-weight: 400;
text-align: center;
}
+39
View File
@@ -0,0 +1,39 @@
import { useEffect, useState } from 'react'
import { Image, Text, View } from '@tarojs/components'
import { NavBar } from '../../components/NavBar'
import { api } from '../../services/request'
import redBookQr from '../../assets/follow-redbook.jpg'
import videoQr from '../../assets/follow-video.jpg'
import './index.scss'
export default function FollowPage() {
const [data, setData] = useState<Record<string, string>>({})
useEffect(() => {
api.follow().then(setData).catch(() => setData({}))
}, [])
return (
<View className='page follow-page soft-bg'>
<NavBar back />
<Text className='follow-title'>关注我们{'\n'}一触即吉</Text>
<Text className='follow-tip'>点击图片保存,扫码关注</Text>
<View className='qr-row'>
<View className='qr-card'>
<Image className='qr-image' src={data.redBookQr || redBookQr} mode='aspectFit' />
<Text>小红书</Text>
</View>
<View className='qr-card'>
<Image className='qr-image' src={data.videoQr || videoQr} mode='aspectFit' />
<Text>视频号</Text>
</View>
</View>
<View className='search-row'>
<View className='search-pill'>{data.redBookKeyword || '那小吉'} 🔍</View>
<View className='search-pill'>{data.videoKeyword || '那吉 nagiflow'} 🔍</View>
</View>
<Text className='other'>其他平台搜索我们</Text>
<Text className='brand'>{data.brand || '那吉 NAGI FLOW'}</Text>
</View>
)
}
+5
View File
@@ -0,0 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enableShareAppMessage: true,
enableShareTimeline: true
})
+162
View File
@@ -0,0 +1,162 @@
.tiangong-page {
min-height: 100vh;
position: relative;
overflow-x: hidden;
color: #202126;
background: #fff;
}
.tiangong-content {
min-height: 100vh;
padding: calc(72px + env(safe-area-inset-top)) 48px calc(206px + env(safe-area-inset-bottom));
box-sizing: border-box;
background: #fff;
}
.tiangong-title {
display: block;
text-align: center;
color: #26272b;
font-size: 42px;
font-weight: 700;
line-height: 1.25;
}
.tiangong-subtitle {
display: block;
margin: 16px 0 54px;
text-align: center;
color: #9a9da5;
font-size: 24px;
line-height: 1.5;
}
.deity-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 28px;
row-gap: 38px;
}
.deity-card {
min-width: 0;
height: 246px;
padding: 0;
border: 0;
border-radius: 0;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
background: #fff;
box-shadow: none;
}
.deity-image {
width: 100%;
height: 192px;
display: block;
background: #fff;
object-fit: contain;
}
.deity-card.locked .deity-image {
opacity: 0.28;
filter: grayscale(1);
}
.deity-name {
width: 100%;
height: 50px;
margin-top: 4px;
background: #dceddf;
color: #283b32;
font-size: 24px;
line-height: 50px;
text-align: center;
font-weight: 600;
}
.tiangong-tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
height: calc(166px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
border-radius: 28px 28px 0 0;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -8px 30px rgba(23, 30, 37, 0.1);
backdrop-filter: blur(12px);
display: flex;
align-items: center;
justify-content: space-around;
box-sizing: border-box;
}
.tabbar-item,
.center-tab {
width: 150px;
height: 132px;
color: #5b5e65;
font-size: 30px;
font-weight: 900;
line-height: 1.1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
}
.center-tab {
margin-top: -82px;
color: #ff9b2a;
}
.center-avatar {
width: 118px;
height: 118px;
border-radius: 64px;
border: 12px solid #f3f6fb;
box-shadow: 0 10px 28px rgba(92, 104, 135, 0.2);
}
.home-tab-icon {
width: 42px;
height: 36px;
border-radius: 0 0 18px 18px;
background: currentColor;
position: relative;
}
.home-tab-icon::before {
content: '';
position: absolute;
left: 5px;
top: -22px;
width: 32px;
height: 32px;
border-radius: 18px 18px 10px 10px;
background: currentColor;
}
.mine-tab-icon {
width: 48px;
height: 32px;
border-radius: 24px 24px 14px 14px;
background: currentColor;
position: relative;
}
.mine-tab-icon::before {
content: '';
position: absolute;
left: 13px;
top: -26px;
width: 22px;
height: 22px;
border-radius: 50%;
background: currentColor;
}
+82
View File
@@ -0,0 +1,82 @@
import { useEffect, useState } from 'react'
import Taro from '@tarojs/taro'
import { Button, Image, Text, View } from '@tarojs/components'
import avatarAsset from '../../assets/fortune/avatar.jpg'
import fortuneAsset from '../../assets/tiangong/fortune.png'
import longevityAsset from '../../assets/tiangong/longevity.png'
import matchmakerAsset from '../../assets/tiangong/matchmaker.png'
import wealthAsset from '../../assets/tiangong/wealth.png'
import wenchangAsset from '../../assets/tiangong/wenchang.png'
import { api, TiangongItem } from '../../services/request'
import './index.scss'
const mockTiangong: TiangongItem[] = [
{ id: 'wealth', name: '财神', imageUrl: wealthAsset, unlocked: true },
{ id: 'fortune', name: '禄星', imageUrl: fortuneAsset, unlocked: false },
{ id: 'longevity', name: '寿星', imageUrl: longevityAsset, unlocked: false },
{ id: 'wenchang', name: '文昌星', imageUrl: wenchangAsset, unlocked: false },
{ id: 'matchmaker', name: '月老', imageUrl: matchmakerAsset, unlocked: false }
]
export default function FortunePage() {
const [items, setItems] = useState<TiangongItem[]>(mockTiangong)
useEffect(() => {
api.tiangong()
.then(setItems)
.catch(() => setItems(mockTiangong))
}, [])
const go = (url: string) => Taro.redirectTo({ url })
return (
<View className='tiangong-page'>
<View className='tiangong-content'>
<Text className='tiangong-title'>天宫</Text>
<Text className='tiangong-subtitle'>供奉神祇,收集属于你的福运</Text>
<View className='deity-grid'>
{items.map((item) => (
<Button
className={item.unlocked ? 'deity-card unlocked' : 'deity-card locked'}
key={item.id}
onClick={() => {
if (item.unlocked) {
Taro.navigateTo({ url: `/pages/tiangong-detail/index?id=${item.id}` })
return
}
Taro.showToast({
title: `${item.name}暂未解锁`,
icon: 'none'
})
}}
>
<Image className='deity-image' src={item.imageUrl} mode='aspectFit' />
<View className='deity-name'>
<Text>{item.name}</Text>
</View>
</Button>
))}
</View>
</View>
<View className='tiangong-tabbar'>
<Button className='tabbar-item' onClick={() => go('/pages/home/index')}>
<View className='home-tab-icon' />
<Text>首页</Text>
</Button>
<Button
className='center-tab active'
onClick={() => Taro.showToast({ title: '天宫', icon: 'none' })}
>
<Image className='center-avatar' src={avatarAsset} mode='aspectFill' />
<Text>天宫</Text>
</Button>
<Button className='tabbar-item' onClick={() => go('/pages/my/index')}>
<View className='mine-tab-icon' />
<Text>我的</Text>
</Button>
</View>
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+602
View File
@@ -0,0 +1,602 @@
.home-page {
min-height: 100vh;
padding: calc(216px + env(safe-area-inset-top)) 30px calc(178px + env(safe-area-inset-bottom));
box-sizing: border-box;
color: #29243a;
background:
linear-gradient(180deg, rgba(154, 124, 255, 0.92) 0%, rgba(220, 201, 255, 0.76) 27%, rgba(253, 241, 255, 0.9) 52%, #eef6ff 100%);
overflow-x: hidden;
}
.day-switch {
display: flex;
align-items: center;
gap: 52px;
}
.avatar-orb {
width: 132px;
height: 132px;
flex: none;
border-radius: 70px;
background: #fffef0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 26px rgba(119, 102, 154, 0.12);
}
.water-face {
width: 82px;
height: 96px;
border-radius: 50% 50% 52% 52%;
background: linear-gradient(180deg, #9ff5ff 0%, #39c6ea 100%);
color: #3274c8;
font-size: 42px;
line-height: 78px;
text-align: center;
}
.day-tabs {
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
}
.day-tab {
min-width: 0;
text-align: center;
color: rgba(42, 34, 62, 0.58);
position: relative;
}
.day-tab + .day-tab::before {
content: '';
position: absolute;
left: -2px;
top: 20px;
width: 2px;
height: 12px;
background: rgba(42, 34, 62, 0.4);
}
.day-label,
.day-status {
display: block;
}
.day-label {
font-size: 36px;
line-height: 1.2;
}
.day-status {
width: 112px;
height: 48px;
margin: 12px auto 0;
border-radius: 26px;
font-size: 28px;
line-height: 48px;
}
.day-tab.active {
color: #29243a;
font-weight: 900;
}
.day-tab.active .day-label {
font-size: 46px;
}
.day-tab.active .day-status {
background: #7956f4;
color: #fff;
box-shadow: 0 8px 18px rgba(105, 71, 220, 0.26);
}
.date-row {
margin-top: 52px;
color: #4f485f;
font-size: 28px;
display: flex;
align-items: center;
justify-content: space-between;
}
.month-day {
font-size: 34px;
font-weight: 900;
}
.summary-card {
min-height: 202px;
margin-top: 38px;
border-radius: 18px;
background: linear-gradient(105deg, #7139f3 0%, #8344ff 54%, #d35af3 100%);
color: #fff;
padding: 42px 34px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
overflow: hidden;
position: relative;
}
.summary-card::after {
content: '';
position: absolute;
right: -22px;
top: -36px;
width: 236px;
height: 236px;
border-radius: 50%;
background: radial-gradient(circle, rgba(131, 226, 255, 0.95) 0%, rgba(61, 121, 250, 0.72) 50%, rgba(255, 255, 255, 0) 70%);
filter: blur(1px);
}
.summary-copy {
width: 420px;
font-size: 34px;
line-height: 1.75;
font-weight: 800;
position: relative;
z-index: 1;
}
.index-orb {
width: 178px;
height: 178px;
border-radius: 96px;
background: radial-gradient(circle at 45% 36%, #e8ffff 0%, #86dfff 42%, #5a71ff 100%);
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 12px rgba(255, 255, 255, 0.22);
position: relative;
z-index: 1;
}
.index-number {
font-size: 72px;
line-height: 0.95;
font-weight: 900;
}
.index-orb text:last-child {
margin-top: 10px;
font-size: 24px;
}
.lucky-grid-home {
margin-top: 24px;
display: grid;
grid-template-columns: 1.35fr 0.75fr;
gap: 22px;
}
.lucky-box {
min-height: 90px;
border-radius: 18px;
padding: 0 28px;
background: rgba(255, 255, 255, 0.52);
display: flex;
align-items: center;
box-sizing: border-box;
gap: 18px;
}
.colors-box {
background: rgba(246, 233, 255, 0.72);
}
.number-box {
background: rgba(234, 225, 255, 0.72);
}
.direction-box {
background: rgba(255, 241, 198, 0.82);
}
.hour-box {
background: rgba(196, 235, 255, 0.82);
}
.lucky-label {
flex: none;
color: #29243a;
font-size: 30px;
font-weight: 900;
}
.color-chip,
.hour-chip {
display: flex;
align-items: center;
gap: 10px;
color: #423a52;
font-size: 28px;
white-space: nowrap;
}
.swatch {
width: 48px;
height: 48px;
border-radius: 12px;
}
.number-slots {
display: flex;
gap: 12px;
}
.number-slots text {
width: 48px;
height: 58px;
border-radius: 10px;
background: linear-gradient(180deg, #8f75ff, #7454ef);
color: #fff;
font-size: 48px;
line-height: 58px;
text-align: center;
font-weight: 900;
box-shadow: inset 0 -12px 0 rgba(255, 255, 255, 0.18);
}
.compass-icon {
width: 42px;
height: 42px;
border-radius: 50%;
background: #fff;
color: #ff9f27;
font-size: 30px;
line-height: 42px;
text-align: center;
}
.hour-chip text:first-child {
width: 42px;
height: 42px;
border-radius: 22px;
background: #5bb8df;
color: #fff;
text-align: center;
line-height: 42px;
font-weight: 900;
}
.dimension-head {
margin-top: 46px;
display: flex;
align-items: center;
justify-content: space-between;
}
.dimension-head > text {
color: #29243a;
font-size: 42px;
font-weight: 900;
}
.dimension-head button {
color: #6d6578;
font-size: 28px;
}
.dimension-list {
margin-top: 28px;
display: flex;
flex-direction: column;
gap: 22px;
}
.dimension-card {
min-height: 204px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.84);
padding: 26px 60px 28px 34px;
box-sizing: border-box;
display: flex;
align-items: flex-start;
position: relative;
box-shadow: 0 8px 22px rgba(90, 79, 116, 0.06);
}
.dimension-card.suitable {
background: rgba(255, 255, 255, 0.86);
}
.dimension-card.avoid {
background: linear-gradient(180deg, rgba(255, 249, 236, 0.9) 0%, rgba(255, 255, 255, 0.86) 48%);
}
.dimension-icon {
width: 62px;
height: 62px;
flex: none;
margin-right: 20px;
border-radius: 18px;
position: relative;
}
.icon-heart {
background: radial-gradient(circle at 32% 32%, #ffd2df, #ff79a3 68%, #fff 70%);
transform: rotate(-10deg);
}
.icon-gourd {
background: radial-gradient(circle at 50% 28%, #ffc65c 0 28%, transparent 29%), radial-gradient(circle at 50% 65%, #f0a222 0 48%, transparent 49%);
}
.icon-vase {
background: radial-gradient(circle at 50% 18%, #d5fff0 0 24%, transparent 25%), linear-gradient(180deg, #7df0c9, #2ccda0);
border-radius: 30px 30px 16px 16px;
}
.icon-coin {
border-radius: 50%;
background: radial-gradient(circle, #fff8bb 0 33%, #f1b82d 34% 100%);
}
.icon-fan {
border-radius: 50% 50% 12px 12px;
background: conic-gradient(from 235deg, #c29cff, #fff, #8e63ef, #c29cff);
}
.icon-scroll {
border-radius: 12px;
background: repeating-linear-gradient(90deg, #ffe8aa 0 10px, #f5c868 11px 16px);
}
.icon-bamboo {
background: linear-gradient(135deg, transparent 20%, #30d3b9 21% 38%, transparent 39% 50%, #25bfa8 51% 68%, transparent 69%);
}
.icon-horse {
border-radius: 50% 28px 50% 30px;
background: linear-gradient(135deg, #dbe9ff, #6b94f8);
}
.dimension-copy {
flex: 1;
min-width: 0;
}
.dimension-title-row {
display: flex;
align-items: center;
gap: 20px;
}
.dimension-title {
color: #29243a;
font-size: 34px;
font-weight: 900;
white-space: nowrap;
}
.dimension-headline {
min-width: 0;
color: #5d5667;
font-size: 28px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dimension-detail {
display: block;
margin-top: 30px;
color: #666071;
font-size: 28px;
line-height: 1.7;
}
.dimension-tag {
position: absolute;
right: 34px;
top: 30px;
width: 48px;
height: 48px;
border-radius: 50%;
color: #fff;
font-size: 28px;
line-height: 48px;
text-align: center;
font-weight: 900;
}
.dimension-tag.suitable {
background: #4bc660;
}
.dimension-tag.avoid {
background: #f45b72;
}
.dimension-arrow {
position: absolute;
right: 34px;
bottom: 30px;
color: #3f394a;
font-size: 52px;
line-height: 1;
}
.home-tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
height: calc(158px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
border-radius: 26px 26px 0 0;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -10px 32px rgba(85, 79, 110, 0.1);
display: flex;
align-items: center;
justify-content: space-around;
box-sizing: border-box;
}
.home-tabbar-item,
.home-realm-tab {
width: 150px;
height: 132px;
color: #29243a;
font-size: 28px;
font-weight: 900;
line-height: 1.1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
}
.home-tabbar-item.active {
color: #ff9b2a;
}
.home-tab-icon {
width: 42px;
height: 36px;
border-radius: 0 0 18px 18px;
background: currentColor;
position: relative;
}
.home-tab-icon::before {
content: '';
position: absolute;
left: 5px;
top: -22px;
width: 32px;
height: 32px;
border-radius: 18px 18px 10px 10px;
background: currentColor;
}
.home-mine-icon {
width: 48px;
height: 32px;
border-radius: 24px 24px 14px 14px;
background: currentColor;
position: relative;
}
.home-mine-icon::before {
content: '';
position: absolute;
left: 13px;
top: -26px;
width: 22px;
height: 22px;
border-radius: 50%;
background: currentColor;
}
.home-realm-tab {
margin-top: -78px;
}
.home-realm-avatar {
width: 112px;
height: 112px;
border-radius: 60px;
border: 12px solid #f0f7ff;
box-shadow: 0 10px 24px rgba(121, 129, 164, 0.16);
}
.sort-page {
padding-top: calc(188px + env(safe-area-inset-top));
}
.sort-back {
position: fixed;
left: 30px;
top: calc(48px + env(safe-area-inset-top));
z-index: 3;
width: 58px;
height: 58px;
color: #2d2549;
font-size: 72px;
line-height: 44px;
}
.sort-title {
position: fixed;
left: 0;
right: 0;
top: calc(116px + env(safe-area-inset-top));
z-index: 2;
color: #29243a;
font-size: 42px;
line-height: 1;
font-weight: 900;
text-align: center;
}
.sort-hint-row {
display: flex;
align-items: center;
color: #6e6678;
font-size: 28px;
}
.sort-handle-symbol {
margin: 0 16px;
color: #6e6678;
font-size: 42px;
}
.sort-done {
margin-left: auto;
color: #29243a;
font-size: 30px;
font-weight: 900;
}
.sort-list {
margin-top: 62px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sort-item {
height: 112px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.7);
padding: 0 34px 0 38px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
color: #29243a;
font-size: 32px;
font-weight: 800;
}
.sort-controls {
display: flex;
align-items: center;
gap: 14px;
color: #77717f;
font-size: 38px;
}
.sort-controls button {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(121, 86, 244, 0.1);
color: #6d57da;
font-size: 28px;
line-height: 44px;
}
+306
View File
@@ -0,0 +1,306 @@
import { useEffect, useMemo, useState } from 'react'
import Taro from '@tarojs/taro'
import { Button, Image, Text, View } from '@tarojs/components'
import avatarAsset from '../../assets/fortune/avatar.jpg'
import { api, DimensionItem, HomeOverview } from '../../services/request'
import './index.scss'
type PageMode = 'home' | 'sort'
const weekNames = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const pad = (value: number) => `${value}`.padStart(2, '0')
const formatDate = (date: Date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
const createDate = (offset: number) => {
const date = new Date()
date.setDate(date.getDate() + offset)
return date
}
const createMockHome = (): HomeOverview => {
const yesterday = createDate(-1)
const today = createDate(0)
const tomorrow = createDate(1)
return {
activeDay: '今日',
days: [
{
label: '昨日',
date: formatDate(yesterday),
weekName: weekNames[yesterday.getDay()],
lunarDate: '农历丙午年 九月初二',
status: '小逆'
},
{
label: '今日',
date: formatDate(today),
weekName: weekNames[today.getDay()],
lunarDate: '农历丙午年 九月初三',
status: '小逆'
},
{
label: '明日',
date: formatDate(tomorrow),
weekName: weekNames[tomorrow.getDay()],
lunarDate: '农历丙午年 九月初四',
status: '平日'
}
],
summary: '小逆之日,少言多思,养精蓄锐为上。',
overallIndex: 42,
luckyColors: [
{ name: '绿茶色', value: '#a7ad2a' },
{ name: '瀑布蓝', value: '#50c3cf' }
],
luckyNumbers: ['6', '8'],
luckyDirection: '东北',
luckyHours: [
{ branch: '子', time: '23-01点' },
{ branch: '申', time: '15-17点' }
],
dimensions: [
{
id: 'emotion',
title: '情感',
icon: 'heart',
headline: '默契收集,心流时刻',
detail: '适合捕捉和TA的默契瞬间,让这些小细节成为关系里的锚点。',
suitable: true
},
{
id: 'health',
title: '健康',
icon: 'gourd',
headline: '润肺养身,舒缓秋燥',
detail: '秋日干燥适合润肺养身,舒缓身体的秋燥不适感。',
suitable: true
},
{
id: 'life',
title: '生活',
icon: 'vase',
headline: '收纳旧物,沉淀回忆',
detail: '秋日里适合收纳旧物,在整理中沉淀那些温暖的小回忆。',
suitable: true
},
{
id: 'wealth',
title: '财富',
icon: 'coin',
headline: '勿乱囤货,按需消费',
detail: '财富能量忌盲目囤积,适合避开非必要的批量采购,保持理性消费。',
suitable: false
},
{
id: 'social',
title: '社交',
icon: 'fan',
headline: '勿涉纷争,保持中立',
detail: '易卷入人际是非,适合远离矛盾,保持中立姿态。',
suitable: false
},
{
id: 'study',
title: '学业',
icon: 'scroll',
headline: '基础松散,学业忌跳级',
detail: '底层知识不牢,别急于学习进阶内容。',
suitable: false
},
{
id: 'career',
title: '事业',
icon: 'bamboo',
headline: '敷衍了事,别凑活',
detail: '今日易因松散出错,适合认真对待每一项小任务,别敷衍。',
suitable: false
},
{
id: 'travel',
title: '出行',
icon: 'horse',
headline: '勿负重行,轻装简从',
detail: '今日出行负重易生疲惫,建议轻装出门,避免带过多不必要的物品。',
suitable: false
}
]
}
}
const iconClass = (icon: string) => `dimension-icon icon-${icon}`
export default function HomePage() {
const [overview, setOverview] = useState<HomeOverview>(createMockHome)
const [mode, setMode] = useState<PageMode>('home')
const [orderedIds, setOrderedIds] = useState<string[]>([])
useEffect(() => {
api.homeOverview()
.then((data) => {
setOverview(data)
setOrderedIds(data.dimensions.map((item) => item.id))
})
.catch(() => {
const mock = createMockHome()
setOverview(mock)
setOrderedIds(mock.dimensions.map((item) => item.id))
})
}, [])
const activeDay = overview.days.find((day) => day.label === overview.activeDay) || overview.days[1]
const orderedDimensions = useMemo(() => {
const byId = overview.dimensions.reduce<Record<string, DimensionItem>>((result, item) => {
result[item.id] = item
return result
}, {})
return orderedIds.map((id) => byId[id]).filter(Boolean)
}, [orderedIds, overview.dimensions])
const moveDimension = (index: number, direction: -1 | 1) => {
const target = index + direction
if (target < 0 || target >= orderedIds.length) return
const next = [...orderedIds]
const current = next[index]
next[index] = next[target]
next[target] = current
setOrderedIds(next)
}
const go = (url: string) => Taro.redirectTo({ url })
const renderTabbar = () => (
<View className='home-tabbar'>
<View className='home-tabbar-item active'>
<View className='home-tab-icon' />
<Text>首页</Text>
</View>
<Button className='home-realm-tab' onClick={() => go('/pages/fortune/index')}>
<Image className='home-realm-avatar' src={avatarAsset} mode='aspectFill' />
<Text>天宫</Text>
</Button>
<Button className='home-tabbar-item' onClick={() => go('/pages/my/index')}>
<View className='home-mine-icon' />
<Text>我的</Text>
</Button>
</View>
)
if (mode === 'sort') {
return (
<View className='home-page sort-page'>
<Button className='sort-back' onClick={() => setMode('home')}>‹</Button>
<Text className='sort-title'>排序</Text>
<View className='sort-hint-row'>
<Text>按住右侧</Text>
<Text className='sort-handle-symbol'>☰</Text>
<Text>上下拖动排序</Text>
<Button className='sort-done' onClick={() => setMode('home')}>完成</Button>
</View>
<View className='sort-list'>
{orderedDimensions.map((item, index) => (
<View className='sort-item' key={item.id}>
<Text>{item.title}</Text>
<View className='sort-controls'>
<Button onClick={() => moveDimension(index, -1)}>↑</Button>
<Button onClick={() => moveDimension(index, 1)}>↓</Button>
<Text>☰</Text>
</View>
</View>
))}
</View>
{renderTabbar()}
</View>
)
}
return (
<View className='home-page'>
<View className='day-switch'>
<View className='avatar-orb'>
<View className='water-face'>⌣</View>
</View>
<View className='day-tabs'>
{overview.days.map((day) => (
<View className={day.label === overview.activeDay ? 'day-tab active' : 'day-tab'} key={day.label}>
<Text className='day-label'>{day.label}</Text>
<Text className='day-status'>{day.status}</Text>
</View>
))}
</View>
</View>
<View className='date-row'>
<Text><Text className='month-day'>{Number(activeDay.date.slice(5, 7))}月{Number(activeDay.date.slice(8, 10))}日</Text> {activeDay.weekName}</Text>
<Text>{activeDay.lunarDate}</Text>
</View>
<View className='summary-card'>
<Text className='summary-copy'>{overview.summary}</Text>
<View className='index-orb'>
<Text className='index-number'>{overview.overallIndex}</Text>
<Text>综合指数</Text>
</View>
</View>
<View className='lucky-grid-home'>
<View className='lucky-box colors-box'>
<Text className='lucky-label'>吉色</Text>
{overview.luckyColors.map((color) => (
<View className='color-chip' key={color.name}>
<View className='swatch' style={{ background: color.value }} />
<Text>{color.name}</Text>
</View>
))}
</View>
<View className='lucky-box number-box'>
<Text className='lucky-label'>吉数</Text>
<View className='number-slots'>
{overview.luckyNumbers.map((number) => <Text key={number}>{number}</Text>)}
</View>
</View>
<View className='lucky-box direction-box'>
<Text className='lucky-label'>吉向</Text>
<View className='compass-icon'>⌖</View>
<Text>{overview.luckyDirection}</Text>
</View>
<View className='lucky-box hour-box'>
<Text className='lucky-label'>吉时</Text>
{overview.luckyHours.map((hour) => (
<View className='hour-chip' key={`${hour.branch}-${hour.time}`}>
<Text>{hour.branch}</Text>
<Text>{hour.time}</Text>
</View>
))}
</View>
</View>
<View className='dimension-head'>
<Text>维度解析</Text>
<Button onClick={() => setMode('sort')}>排序 ⇅</Button>
</View>
<View className='dimension-list'>
{orderedDimensions.map((item) => (
<View className={item.suitable ? 'dimension-card suitable' : 'dimension-card avoid'} key={item.id}>
<View className={iconClass(item.icon)} />
<View className='dimension-copy'>
<View className='dimension-title-row'>
<Text className='dimension-title'>{item.title}</Text>
<Text className='dimension-headline'>{item.headline}</Text>
</View>
<Text className='dimension-detail'>{item.detail}</Text>
</View>
<Text className={item.suitable ? 'dimension-tag suitable' : 'dimension-tag avoid'}>
{item.suitable ? '宜' : '忌'}
</Text>
<Text className='dimension-arrow'>›</Text>
</View>
))}
</View>
{renderTabbar()}
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+76
View File
@@ -0,0 +1,76 @@
.login-page {
min-height: 100vh;
padding: 170px 56px 80px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
}
.brand-mark {
width: 180px;
height: 180px;
border-radius: 90px;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 18px 42px rgba(143, 100, 13, 0.14);
color: #8f640d;
font-size: 54px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.login-title {
margin-top: 44px;
font-size: 48px;
font-weight: 800;
color: #8f640d;
}
.login-subtitle {
margin-top: 22px;
font-size: 26px;
color: #9e8b65;
}
.login-button {
width: 100%;
margin-top: 54px;
}
.login-form {
width: 100%;
margin-top: 40px;
display: flex;
flex-direction: column;
gap: 22px;
}
.code-row {
display: flex;
gap: 18px;
}
.code-input {
flex: 1;
}
.send-code {
width: 148px;
height: 78px;
border-radius: 40px;
background: #fff;
color: #8f640d;
box-shadow: 0 10px 24px rgba(143, 100, 13, 0.14);
font-size: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.mock-code {
padding-left: 24px;
font-size: 24px;
color: #b38a32;
}
+49
View File
@@ -0,0 +1,49 @@
import { useState } from 'react'
import Taro from '@tarojs/taro'
import { Button, Input, Text, View } from '@tarojs/components'
import { api } from '../../services/request'
import './index.scss'
export default function LoginPage() {
const [sessionKey, setSessionKey] = useState('')
const [phone, setPhone] = useState('')
const [captcha, setCaptcha] = useState('')
const [mockCode, setMockCode] = useState('')
const wxLogin = async () => {
const login = await Taro.login()
const res = await api.wxLogin(login.code || 'dev-code')
setSessionKey(res.sessionKey)
Taro.showToast({ title: '微信登录成功', icon: 'none' })
}
const sendSms = async () => {
const res = await api.sendSms(phone)
setMockCode(res.mockCode || '')
Taro.showToast({ title: '验证码已发送', icon: 'none' })
}
const bindPhone = async () => {
const res = await api.bindPhone(sessionKey, phone, captcha)
Taro.setStorageSync('token', res.token)
Taro.redirectTo({ url: '/pages/my/index' })
}
return (
<View className='login-page soft-bg'>
<View className='brand-mark'>那吉</View>
<Text className='login-title'>一触即吉</Text>
<Text className='login-subtitle'>微信登录后绑定手机号,开启你的吉运档案</Text>
<Button className='gold-button login-button' onClick={wxLogin}>微信小程序登录</Button>
<View className='login-form'>
<Input className='input-pill' type='number' maxlength={11} placeholder='手机号' value={phone} onInput={(e) => setPhone(String(e.detail.value))} />
<View className='code-row'>
<Input className='input-pill code-input' type='number' maxlength={6} placeholder='验证码' value={captcha} onInput={(e) => setCaptcha(String(e.detail.value))} />
<Button className='send-code' onClick={sendSms}>发送</Button>
</View>
{mockCode && <Text className='mock-code'>开发验证码:{mockCode}</Text>}
</View>
<Button className='gold-button login-button' onClick={bindPhone}>绑定手机号</Button>
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+631
View File
@@ -0,0 +1,631 @@
.my-page {
min-height: 100vh;
box-sizing: border-box;
padding: calc(190px + env(safe-area-inset-top)) 30px calc(178px + env(safe-area-inset-bottom));
background:
linear-gradient(180deg, #9e85f5 0%, #c7b8f5 22%, #e6e8f7 43%, #edf4ff 62%, #e9f2ff 100%);
color: #28243a;
position: relative;
overflow-x: hidden;
}
.profile-area {
display: flex;
align-items: center;
min-height: 140px;
}
.profile-avatar-button {
width: 132px;
height: 132px;
flex: none;
border-radius: 70px;
background: #fffef0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 26px rgba(119, 102, 154, 0.12);
}
.water-avatar,
.element-icon {
width: 82px;
height: 96px;
border-radius: 50% 50% 52% 52%;
background: linear-gradient(180deg, #9ff5ff 0%, #39c6ea 100%);
color: #3274c8;
font-size: 42px;
line-height: 78px;
text-align: center;
transform: rotate(0deg);
}
.profile-info {
flex: 1;
min-width: 0;
margin-left: 34px;
}
.name-row {
display: flex;
align-items: center;
gap: 10px;
}
.profile-name {
max-width: 280px;
color: #262038;
font-size: 40px;
line-height: 1.1;
font-weight: 900;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gender-chip {
width: 40px;
height: 40px;
border-radius: 22px;
background: #5cc8ff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
font-weight: 800;
}
.birthday-row {
margin-top: 20px;
display: flex;
align-items: center;
gap: 12px;
color: #6e6378;
font-size: 26px;
line-height: 1;
background: transparent;
text-align: left;
}
.birthday-icon {
font-size: 28px;
}
.eye-line {
width: 32px;
height: 16px;
border-bottom: 4px solid #756850;
border-radius: 50%;
}
.eye-line.open {
height: 18px;
border: 3px solid #756850;
border-radius: 50%;
position: relative;
}
.eye-line.open::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: #756850;
transform: translate(-50%, -50%);
}
.profile-arrow {
width: 54px;
height: 54px;
color: rgba(38, 32, 56, 0.45);
font-size: 58px;
line-height: 48px;
}
.quick-card {
min-height: 190px;
margin-top: 54px;
border-radius: 24px;
background: rgba(255, 255, 255, 0.76);
display: flex;
align-items: center;
padding: 26px 44px;
box-sizing: border-box;
gap: 52px;
box-shadow: 0 12px 36px rgba(94, 76, 134, 0.08);
}
.quick-entry {
width: 140px;
color: #2b2637;
font-size: 28px;
line-height: 1.25;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
}
.zodiac-icon,
.element-icon {
width: 86px;
height: 86px;
border-radius: 46px;
background: #ece4ff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 18px rgba(138, 106, 194, 0.12);
}
.zodiac-icon {
color: #5c4438;
font-size: 70px;
line-height: 1;
}
.element-icon {
color: #2e75c8;
font-size: 34px;
}
.palace-section {
margin-top: 42px;
}
.section-title-row {
display: flex;
align-items: center;
gap: 14px;
}
.section-title {
color: #262038;
font-size: 40px;
font-weight: 900;
}
.collection-text {
color: #2b2637;
font-size: 28px;
}
.collection-text text {
color: #e74f83;
font-weight: 900;
}
.palace-placeholder {
width: 236px;
height: 302px;
margin-top: 28px;
border-radius: 18px;
padding: 18px 16px 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(180deg, #ffd56e 0%, #ffe49a 54%, #fff3ba 100%);
box-shadow: 0 14px 24px rgba(216, 170, 67, 0.18);
}
.palace-copy {
align-self: flex-start;
width: 28px;
color: #7b5320;
font-size: 24px;
font-weight: 900;
line-height: 1.15;
}
.palace-coin {
width: 158px;
height: 158px;
margin-top: -52px;
mix-blend-mode: multiply;
}
.palace-name {
margin-top: 6px;
color: #7b5320;
font-size: 28px;
font-weight: 900;
}
.settings-card {
margin-top: 42px;
border-radius: 26px;
background: rgba(255, 255, 255, 0.92);
padding: 0 34px;
box-sizing: border-box;
box-shadow: 0 14px 34px rgba(93, 83, 114, 0.08);
}
.settings-row {
min-height: 108px;
color: #28243a;
font-size: 32px;
display: flex;
align-items: center;
gap: 26px;
border-bottom: 2px solid rgba(220, 224, 236, 0.76);
}
.settings-row:last-child {
border-bottom: 0;
}
.settings-row text:nth-child(2) {
flex: 1;
text-align: left;
}
.menu-icon {
width: 48px;
height: 48px;
flex: none;
color: #252238;
position: relative;
box-sizing: border-box;
}
.headset-icon {
border: 5px solid currentColor;
border-bottom-color: transparent;
border-radius: 50% 50% 42% 42%;
}
.headset-icon::before,
.headset-icon::after {
content: '';
position: absolute;
bottom: 4px;
width: 10px;
height: 18px;
border-radius: 8px;
background: currentColor;
}
.headset-icon::before {
left: -3px;
}
.headset-icon::after {
right: -3px;
}
.star-icon {
border: 4px solid currentColor;
border-radius: 14px;
font-size: 26px;
line-height: 40px;
text-align: center;
}
.agreement-icon {
border: 4px solid currentColor;
border-radius: 10px;
}
.agreement-icon::before {
content: '';
position: absolute;
left: 10px;
top: 12px;
width: 22px;
height: 4px;
border-radius: 3px;
background: currentColor;
box-shadow: 0 12px 0 currentColor;
}
.agreement-icon::after {
content: '';
position: absolute;
right: -8px;
bottom: -8px;
width: 22px;
height: 22px;
border-radius: 50%;
border: 4px solid currentColor;
background: #fff;
}
.row-arrow {
flex: none;
color: rgba(40, 36, 58, 0.45);
font-size: 52px;
}
.my-tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
height: calc(158px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
border-radius: 26px 26px 0 0;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -10px 32px rgba(85, 79, 110, 0.1);
display: flex;
align-items: center;
justify-content: space-around;
box-sizing: border-box;
}
.my-tabbar-item,
.realm-tab {
width: 150px;
height: 132px;
color: #29243a;
font-size: 28px;
font-weight: 900;
line-height: 1.1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
}
.my-tabbar-item.active,
.realm-tab.active {
color: #ff9b2a;
}
.home-tab-icon {
width: 42px;
height: 36px;
border-radius: 0 0 18px 18px;
background: currentColor;
position: relative;
}
.home-tab-icon::before {
content: '';
position: absolute;
left: 5px;
top: -22px;
width: 32px;
height: 32px;
border-radius: 18px 18px 10px 10px;
background: currentColor;
}
.mine-tab-icon {
width: 48px;
height: 32px;
border-radius: 24px 24px 14px 14px;
background: currentColor;
position: relative;
}
.mine-tab-icon::before {
content: '';
position: absolute;
left: 13px;
top: -26px;
width: 22px;
height: 22px;
border-radius: 50%;
background: currentColor;
}
.realm-tab {
margin-top: -78px;
}
.realm-avatar {
width: 112px;
height: 112px;
border-radius: 60px;
border: 12px solid #f0f7ff;
box-shadow: 0 10px 24px rgba(121, 129, 164, 0.16);
}
.zodiac-mask {
position: fixed;
inset: 0;
z-index: 40;
background: rgba(37, 34, 53, 0.58);
display: flex;
align-items: center;
justify-content: center;
padding: 0 84px;
box-sizing: border-box;
}
.zodiac-modal {
width: 100%;
min-height: 620px;
border-radius: 34px;
background: linear-gradient(180deg, #fbf7ff 0%, #f7f2ff 100%);
box-shadow: 0 28px 0 -14px rgba(193, 127, 255, 0.52), 26px 30px 0 -18px rgba(114, 160, 255, 0.46);
display: flex;
flex-direction: column;
align-items: center;
padding: 80px 52px 54px;
box-sizing: border-box;
position: relative;
}
.lucky-flower {
position: absolute;
top: -72px;
width: 136px;
height: 136px;
border-radius: 70px;
background: radial-gradient(circle at 50% 30%, #ffd9e9, #ff9bc1 62%, rgba(255, 255, 255, 0.46) 64%);
color: #fff;
font-size: 40px;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 28px rgba(229, 127, 177, 0.22);
}
.zodiac-title {
color: #28243a;
font-size: 42px;
font-weight: 900;
}
.zodiac-large {
margin-top: 46px;
color: #5b4539;
font-size: 150px;
line-height: 1;
}
.zodiac-status {
min-width: 168px;
height: 58px;
margin-top: 22px;
border-radius: 30px;
background: #c8efd9;
color: #28243a;
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.zodiac-summary {
margin-top: 32px;
color: #666071;
font-size: 30px;
line-height: 1.8;
text-align: center;
}
.zodiac-confirm {
width: 252px;
height: 84px;
margin-top: 42px;
border-radius: 44px;
background: #ff982c;
color: #fff;
font-size: 34px;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
}
.element-detail {
padding-top: calc(104px + env(safe-area-inset-top));
}
.element-back {
position: fixed;
left: 30px;
top: calc(48px + env(safe-area-inset-top));
z-index: 4;
width: 58px;
height: 58px;
color: #2d2549;
font-size: 72px;
line-height: 44px;
}
.element-radar {
margin-top: 52px;
}
.element-radar .radar {
margin-bottom: 40px;
}
.element-radar .radar-fill {
background: #ff8fc9;
opacity: 0.72;
}
.element-card {
margin-top: 26px;
border-radius: 18px;
padding: 30px 28px 36px;
background: rgba(255, 255, 255, 0.72);
box-sizing: border-box;
}
.favorable-card {
background: rgba(242, 255, 248, 0.8);
}
.avoid-card {
background: rgba(255, 247, 249, 0.78);
}
.element-title-row {
display: flex;
align-items: center;
gap: 28px;
margin-bottom: 26px;
}
.element-badge {
min-width: 116px;
height: 58px;
border-radius: 30px;
color: #fff;
font-size: 30px;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
}
.element-badge.orange {
background: #ff9b2a;
}
.element-badge.green {
background: #49bb68;
}
.element-badge.pink {
background: #ef5b80;
}
.element-name {
color: #302759;
font-size: 32px;
font-weight: 900;
}
.element-copy,
.element-strong {
display: block;
color: #696474;
font-size: 27px;
line-height: 1.72;
}
.element-strong {
margin-top: 20px;
color: #28243a;
font-size: 32px;
font-weight: 900;
}
.orange-text {
color: #ff8f1f;
font-weight: 900;
}
.green-text {
color: #49bb68;
font-weight: 900;
}
.pink-text {
color: #ef5b80;
font-weight: 900;
}
+228
View File
@@ -0,0 +1,228 @@
import { useEffect, useMemo, useState } from 'react'
import Taro from '@tarojs/taro'
import { Button, Image, Text, View } from '@tarojs/components'
import { RadarChart } from '../../components/RadarChart'
import { api, UserProfile } from '../../services/request'
import avatarAsset from '../../assets/fortune/avatar.jpg'
import coinAsset from '../../assets/fortune/coin.jpg'
import { calculateFiveElements } from '../../utils/fiveElements'
import './index.scss'
type PageMode = 'home' | 'element'
const mockProfile: UserProfile = {
id: 1,
nickname: 'You',
gender: '男',
birthday: '1998-06-18',
birthHour: '辰时',
province: '河南省',
city: '洛阳市',
district: '老城区',
points: 120,
remainingEdits: 5,
elementReport: {
scores: [
{ name: '土', color: '#b46a18', value: 42 },
{ name: '金', color: '#d59a05', value: 22 },
{ name: '水', color: '#35bfe8', value: 8 },
{ name: '木', color: '#36b85b', value: 22 },
{ name: '火', color: '#ed3c43', value: 7 }
],
favorable: '金水',
unfavorable: '木火土',
dayMaster: '壬'
}
}
const formatMaskedBirthday = (birthday: string) => {
const [year, month, day] = birthday.split('-')
if (!year || !month || !day) return '****_**_**'
return '****_**_**'
}
const formatBirthday = (birthday: string) => {
const [year, month, day] = birthday.split('-')
if (!year || !month || !day) return '****_**_**'
return `${year}_${month}_${day}`
}
const genderSymbol = (gender: string) => (gender === '女' ? '♀' : '♂')
const elementTraits: Record<string, string> = {
木: '生长与舒展',
火: '热情与行动',
土: '稳定与承载',
金: '清晰与判断',
水: '变通与洞察'
}
const traitText = (elements: string) => elements
.split('')
.map((element) => elementTraits[element])
.filter(Boolean)
.join('、')
export default function MyPage() {
const [profile, setProfile] = useState<UserProfile>(mockProfile)
const [mode, setMode] = useState<PageMode>('home')
const [zodiacOpen, setZodiacOpen] = useState(false)
const [birthdayVisible, setBirthdayVisible] = useState(false)
const elementReport = useMemo(
() => calculateFiveElements(profile.birthday, profile.birthHour),
[profile.birthday, profile.birthHour]
)
useEffect(() => {
api.profile()
.then(setProfile)
.catch(() => setProfile(mockProfile))
}, [])
const go = (url: string) => Taro.navigateTo({ url })
const openEnterpriseWechat = () => {
// TODO: replace with Enterprise WeChat customer-service config from backend.
Taro.showToast({ title: '企业微信入口待配置', icon: 'none' })
}
const renderTabs = () => (
<View className='my-tabbar'>
<Button className='my-tabbar-item' onClick={() => Taro.redirectTo({ url: '/pages/home/index' })}>
<View className='home-tab-icon' />
<Text>首页</Text>
</Button>
<Button className='realm-tab' onClick={() => go('/pages/fortune/index')}>
<Image className='realm-avatar' src={avatarAsset} mode='aspectFill' />
<Text>天宫</Text>
</Button>
<View className='my-tabbar-item active'>
<View className='mine-tab-icon' />
<Text>我的</Text>
</View>
</View>
)
const renderElement = () => (
<View className='my-page element-detail'>
<Button className='element-back' onClick={() => setMode('home')}>‹</Button>
<View className='element-radar'>
<RadarChart scores={elementReport.scores} />
</View>
<View className='element-card day-master-card'>
<View className='element-title-row'>
<Text className='element-badge orange'>日主</Text>
<Text className='element-name'>{elementReport.dayMaster}</Text>
</View>
<Text className='element-copy'>
<Text className='orange-text'>日主</Text>,即出生日期中的天干,其五行属性直接代表自身能量基础。
</Text>
<Text className='element-strong'>你适应力强,擅长在变化中寻找机会与路径。</Text>
</View>
<View className='element-card favorable-card'>
<View className='element-title-row'>
<Text className='element-badge green'>喜用</Text>
<Text className='element-name'>{elementReport.favorable}</Text>
</View>
<Text className='element-copy'>
<Text className='green-text'>喜用</Text>,即更容易让事情进展顺、做得稳、发挥得好的行为方式。
</Text>
<Text className='element-strong'>你更容易发挥{traitText(elementReport.favorable)}的优势,遇事能找到适合自己的节奏。</Text>
</View>
<View className='element-card avoid-card'>
<View className='element-title-row'>
<Text className='element-badge pink'>忌用</Text>
<Text className='element-name'>{elementReport.unfavorable}</Text>
</View>
<Text className='element-copy'>
<Text className='pink-text'>忌用</Text>,即容易让你更累、更乱,或做了很多却效果不好的行为方式。
</Text>
<Text className='element-strong'>当{traitText(elementReport.unfavorable)}的能量偏旺时,适合放慢节奏,避免陷入无谓消耗。</Text>
</View>
{renderTabs()}
</View>
)
if (mode === 'element') {
return renderElement()
}
return (
<View className='my-page my-home'>
<View className='profile-area'>
<Button className='profile-avatar-button' onClick={() => go('/pages/profile-edit/index')}>
<View className='water-avatar'>⌣</View>
</Button>
<View className='profile-info'>
<View className='name-row'>
<Text className='profile-name'>{profile.nickname}</Text>
<Text className='gender-chip'>{genderSymbol(profile.gender)}</Text>
</View>
<Button className='birthday-row' onClick={() => setBirthdayVisible((visible) => !visible)}>
<Text className='birthday-icon'>▦</Text>
<Text>{birthdayVisible ? formatBirthday(profile.birthday) : formatMaskedBirthday(profile.birthday)}</Text>
<Text className={birthdayVisible ? 'eye-line open' : 'eye-line'} />
</Button>
</View>
<Button className='profile-arrow' onClick={() => go('/pages/profile-edit/index')}>›</Button>
</View>
<View className='quick-card'>
<Button className='quick-entry' onClick={() => setZodiacOpen(true)}>
<View className='zodiac-icon'>⌇</View>
<Text>我的生肖</Text>
</Button>
<Button className='quick-entry' onClick={() => setMode('element')}>
<View className='element-icon'>⌣</View>
<Text>我的元素</Text>
</Button>
</View>
<View className='palace-section'>
<View className='section-title-row'>
<Text className='section-title'>我的天宫</Text>
<Text className='collection-text'>(已收集 <Text>1</Text> 款)</Text>
</View>
<View className='palace-placeholder'>
<Text className='palace-copy'>厚积薄发</Text>
<Image className='palace-coin' src={coinAsset} mode='aspectFit' />
<Text className='palace-name'>山鬼花钱</Text>
</View>
</View>
<View className='settings-card'>
<Button className='settings-row' onClick={openEnterpriseWechat}>
<View className='menu-icon headset-icon' />
<Text>联系我们</Text>
<Text className='row-arrow'>›</Text>
</Button>
<Button className='settings-row' onClick={() => go('/pages/follow/index')}>
<View className='menu-icon star-icon'>✦</View>
<Text>关注我们</Text>
<Text className='row-arrow'>›</Text>
</Button>
<Button className='settings-row' onClick={() => go('/pages/agreement/index')}>
<View className='menu-icon agreement-icon' />
<Text>用户协议</Text>
<Text className='row-arrow'>›</Text>
</Button>
</View>
{renderTabs()}
{zodiacOpen && (
<View className='zodiac-mask' onClick={() => setZodiacOpen(false)}>
<View className='zodiac-modal' onClick={(event) => event.stopPropagation()}>
<View className='lucky-flower'>运</View>
<Text className='zodiac-title'>生肖周运</Text>
<View className='zodiac-large'>⌇</View>
<Text className='zodiac-status'>平稳</Text>
<Text className='zodiac-summary'>本周运势平稳,生活节奏舒缓,安心度日,享受静谧时光。</Text>
<Button className='zodiac-confirm' onClick={() => setZodiacOpen(false)}>知道了</Button>
</View>
</View>
)}
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+113
View File
@@ -0,0 +1,113 @@
.edit-page {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 210px;
}
.edit-avatar {
width: 150px;
height: 150px;
border-radius: 88px;
background: #fbffd7;
color: #17c9ea;
font-size: 60px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.edit-avatar text {
position: absolute;
right: -34px;
bottom: 34px;
font-size: 28px;
font-weight: 700;
}
.name-pill {
width: 420px;
height: 76px;
margin-top: 56px;
border-radius: 40px;
background: #fff7de;
display: flex;
align-items: center;
padding: 0 30px;
gap: 20px;
box-sizing: border-box;
color: #8f640d;
font-size: 38px;
}
.name-pill input {
flex: 1;
text-align: center;
color: #8f640d;
}
.gender-row {
margin-top: 58px;
display: flex;
gap: 86px;
}
.gender {
color: #8f640d;
font-size: 34px;
display: flex;
gap: 12px;
align-items: center;
}
.section-title {
margin-top: 58px;
font-size: 34px;
font-weight: 700;
color: #8f640d;
}
.date-row,
.region-row {
margin-top: 34px;
display: flex;
gap: 18px;
}
.date-part {
width: 150px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.hour {
width: 360px;
margin-top: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.region-row .input-pill {
width: 176px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
padding: 0 12px;
}
.start-button {
width: 500px;
margin-top: 110px;
}
.edit-note {
margin-top: 28px;
color: #4b2d1a;
font-size: 28px;
}
+74
View File
@@ -0,0 +1,74 @@
import { useEffect, useState } from 'react'
import Taro from '@tarojs/taro'
import { Button, Input, Picker, Text, View } from '@tarojs/components'
import { NavBar } from '../../components/NavBar'
import { api, UserProfile } from '../../services/request'
import './index.scss'
const hours = ['子时', '丑时', '寅时', '卯时', '辰时', '巳时', '午时', '未时', '申时', '酉时', '戌时', '亥时']
const regions = [['河南省', '北京市', '上海市'], ['洛阳市', '郑州市', '北京市'], ['老城区', '涧西区', '西工区']]
export default function ProfileEditPage() {
const [form, setForm] = useState<Partial<UserProfile>>({})
useEffect(() => {
api.profile().then(setForm)
}, [])
const save = async () => {
await api.updateProfile(form)
Taro.showToast({ title: '已开启吉运', icon: 'none' })
Taro.navigateBack()
}
return (
<View className='page edit-page'>
<NavBar back />
<View className='edit-avatar'>💧<Text>水</Text></View>
<View className='name-pill'>
<Input value={form.nickname || ''} onInput={(e) => setForm({ ...form, nickname: String(e.detail.value) })} />
<Text>✎</Text>
</View>
<View className='gender-row'>
{['男', '女'].map((gender) => (
<Button key={gender} className='gender' onClick={() => setForm({ ...form, gender })}>
<Text>{form.gender === gender ? '◉' : '○'}</Text>{gender}
</Button>
))}
</View>
<Text className='section-title'>选择你的出生日期(公历)</Text>
<Picker mode='date' value={form.birthday} onChange={(e) => setForm({ ...form, birthday: String(e.detail.value) })}>
<View className='date-row'>
{(form.birthday || '2008-12-28').split('-').map((part, idx) => (
<View className='input-pill date-part' key={idx}>{part}</View>
))}
</View>
</Picker>
<Picker mode='selector' range={hours} value={Math.max(0, hours.indexOf(form.birthHour || '寅时'))} onChange={(e) => setForm({ ...form, birthHour: hours[Number(e.detail.value)] })}>
<View className='input-pill hour'>{form.birthHour || '寅时'}<Text> 时</Text></View>
</Picker>
<Text className='section-title'>选择你的出生地</Text>
<Picker
mode='multiSelector'
range={regions}
value={[
Math.max(0, regions[0].indexOf(form.province || '河南省')),
Math.max(0, regions[1].indexOf(form.city || '洛阳市')),
Math.max(0, regions[2].indexOf(form.district || '老城区'))
]}
onChange={(e) => {
const values = e.detail.value as number[]
setForm({ ...form, province: regions[0][values[0]], city: regions[1][values[1]], district: regions[2][values[2]] })
}}
>
<View className='region-row'>
<View className='input-pill'>{form.province || '河南省'} ▼</View>
<View className='input-pill'>{form.city || '洛阳市'} ▼</View>
<View className='input-pill'>{form.district || '老城区'} ▼</View>
</View>
</Picker>
<Button className='gold-button start-button' onClick={save}>开启吉运</Button>
<Text className='edit-note'>半年内只能修改五次哦</Text>
</View>
)
}
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+69
View File
@@ -0,0 +1,69 @@
.tiangong-detail-page {
height: 100vh;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
background: #fff9eb;
}
.detail-navbar {
position: relative;
z-index: 2;
height: calc(92px + env(safe-area-inset-top));
padding-top: env(safe-area-inset-top);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
flex: none;
background: #fff;
}
.detail-navbar-title {
color: #25262a;
font-size: 30px;
font-weight: 600;
}
.god-scene-wrap {
flex: 1;
min-height: 0;
overflow: hidden;
background: #f8f5e8;
}
.god-scene {
width: 100%;
height: 100%;
display: block;
}
.worship-actions {
height: calc(158px + env(safe-area-inset-bottom));
padding: 20px 34px calc(18px + env(safe-area-inset-bottom));
display: flex;
justify-content: space-between;
box-sizing: border-box;
flex: none;
background: rgba(255, 247, 226, 0.85);
backdrop-filter: blur(8px);
}
.worship-action {
width: 60px;
height: 104px;
padding: 14px 0;
border: 0;
border-radius: 28px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
color: #25282b;
font-size: 25px;
line-height: 1;
background: #e3f0e9;
box-shadow: 0 4px 10px rgba(129, 154, 137, 0.18);
}
+60
View File
@@ -0,0 +1,60 @@
import { useEffect, useState } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { Button, Image, Text, View } from '@tarojs/components'
import wealthDetailSceneAsset from '../../assets/tiangong/wealth-detail-scene.jpg'
import { api, TiangongDetail } from '../../services/request'
import './index.scss'
const mockCashGod: TiangongDetail = {
id: 'wealth',
name: '财神',
title: '敬神',
imageUrl: wealthDetailSceneAsset,
introduction: '',
blessing: ''
}
const actions = ['上香', '祈愿', '回向', '送神', '返回']
export default function TiangongDetailPage() {
const router = useRouter()
const itemId = router.params.id || 'wealth'
const [detail, setDetail] = useState<TiangongDetail>(mockCashGod)
useEffect(() => {
api.tiangongDetail(itemId)
.then(setDetail)
.catch(() => setDetail(mockCashGod))
}, [itemId])
return (
<View className='tiangong-detail-page'>
<View className='detail-navbar'>
<Text className='detail-navbar-title'>{detail.title || '敬神'}</Text>
</View>
<View className='god-scene-wrap'>
<Image className='god-scene' src={detail.imageUrl} mode='aspectFill' />
</View>
<View className='worship-actions'>
{actions.map((action) => (
<Button
className='worship-action'
key={action}
onClick={() => {
if (action === '返回') {
Taro.navigateBack()
return
}
Taro.showToast({ title: `${action}功能即将开启`, icon: 'none' })
}}
>
<Text>{action.slice(0, 1)}</Text>
<Text>{action.slice(1)}</Text>
</Button>
))}
</View>
</View>
)
}
+184
View File
@@ -0,0 +1,184 @@
import Taro from '@tarojs/taro'
const BASE_URL = 'http://localhost:8080'
type ApiResult<T> = {
code: number
message: string
data: T
}
type RequestOptions = Omit<Taro.request.Option, 'url'>
export async function request<T>(url: string, options: RequestOptions = {}): Promise<T> {
const token = Taro.getStorageSync('token')
const headers = {
'content-type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.header || {})
}
const res = await Taro.request<ApiResult<T>>({
url: `${BASE_URL}${url}`,
...options,
header: headers
})
if (res.data.code !== 0) {
throw new Error(res.data.message || '请求失败')
}
return res.data.data
}
export const api = {
wxLogin: (code: string) => request<{ sessionKey: string; phoneBound: boolean }>('/api/auth/wx-login', {
method: 'POST',
data: { code }
}),
sendSms: (phone: string) => request<{ mockCode?: string }>('/api/auth/sms', {
method: 'POST',
data: { phone }
}),
bindPhone: (sessionKey: string, phone: string, captcha: string) => request<{ token: string; user: UserProfile }>('/api/auth/bind-phone', {
method: 'POST',
data: { sessionKey, phone, captcha }
}),
profile: () => request<UserProfile>('/api/user/profile'),
updateProfile: (data: Partial<UserProfile>) => request<UserProfile>('/api/user/profile', {
method: 'PUT',
data
}),
exchangeStatus: () => request<{ points: number; available: boolean; message: string }>('/api/points/exchange-status'),
agreement: (type: string) => request<{ title: string; body: string }>(`/api/content/agreement/${type}`),
follow: () => request<Record<string, string>>('/api/content/follow'),
homeOverview: () => request<HomeOverview>('/api/home/overview'),
tiangong: () => request<TiangongItem[]>('/api/tiangong/items'),
tiangongDetail: (id: string) => request<TiangongDetail>(`/api/tiangong/items/${id}`),
dailyHome: () => request<DailyHome>('/api/daily/home'),
pinDailyCard: (cardId: string) => request<{ pinnedCardId: string | null }>(`/api/daily/cards/${cardId}/pin`, {
method: 'POST'
})
}
export type ElementScore = {
name: string
color: string
value: number
}
export type UserProfile = {
id: number
nickname: string
phone?: string
gender: string
birthday: string
birthHour: string
province: string
city: string
district: string
points: number
remainingEdits: number
elementReport: {
scores: ElementScore[]
favorable: string
unfavorable: string
dayMaster: string
}
}
export type CalendarDay = {
day: number
weekName: string
label: string
}
export type LuckyPanel = {
title: string
value: string
accent: string
}
export type FortuneCard = {
id: string
title: string
category: string
categoryIcon: string
verdict: string
avoid: string
summary: string
detail: string
color: string
gradient: string
mascot: string
}
export type LifeTip = {
id: string
title: string
icon: string
content: string
color: string
mark: string
}
export type DailyHome = {
date: string
lunarDate: string
weekName: string
fortuneLevel: string
greeting: string
dailySummary: string
calendarDays: CalendarDay[]
luckyPanels: LuckyPanel[]
cards: FortuneCard[]
lifeTips: LifeTip[]
pinnedCardId?: string | null
}
export type DayFortune = {
label: '昨日' | '今日' | '明日'
date: string
weekName: string
lunarDate: string
status: string
}
export type LuckyColor = {
name: string
value: string
}
export type DimensionItem = {
id: string
title: string
icon: string
headline: string
detail: string
suitable: boolean
}
export type HomeOverview = {
days: DayFortune[]
activeDay: '昨日' | '今日' | '明日'
summary: string
overallIndex: number
luckyColors: LuckyColor[]
luckyNumbers: string[]
luckyDirection: string
luckyHours: Array<{ branch: string; time: string }>
dimensions: DimensionItem[]
}
export type TiangongItem = {
id: string
name: string
imageUrl: string
unlocked: boolean
}
export type TiangongDetail = {
id: string
name: string
title: string
imageUrl: string
introduction: string
blessing: string
}
+9
View File
@@ -0,0 +1,9 @@
declare module '*.jpg' {
const src: string
export default src
}
declare module '*.png' {
const src: string
export default src
}
+90
View File
@@ -0,0 +1,90 @@
import { Solar } from 'lunar-typescript'
import { ElementScore } from '../services/request'
type ElementName = '木' | '火' | '土' | '金' | '水'
export type FiveElementReport = {
scores: ElementScore[]
favorable: string
unfavorable: string
dayMaster: string
}
const stemElements: Record<string, ElementName> = {
甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土', 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水'
}
const branchElements: Record<string, ElementName> = {
子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火', 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水'
}
const elementMeta: Record<ElementName, { color: string }> = {
木: { color: '#36b85b' },
火: { color: '#ef5a58' },
土: { color: '#b46a18' },
金: { color: '#d59a05' },
水: { color: '#35bfe8' }
}
const scoreOrder: ElementName[] = ['土', '金', '水', '木', '火']
const generationOrder: ElementName[] = ['木', '火', '土', '金', '水']
const hourFromBirthHour = (birthHour: string) => {
const numericHour = Number.parseInt(birthHour, 10)
if (Number.isFinite(numericHour)) return Math.max(0, Math.min(23, numericHour))
const branchHours: Record<string, number> = {
子: 23, 丑: 1, 寅: 3, 卯: 5, 辰: 7, 巳: 9,
午: 11, 未: 13, 申: 15, 酉: 17, 戌: 19, 亥: 21
}
return branchHours[birthHour.slice(0, 1)] ?? 12
}
export function calculateFiveElements(birthday: string, birthHour: string): FiveElementReport {
const date = new Date(`${birthday}T00:00:00`)
if (Number.isNaN(date.getTime())) {
return {
scores: scoreOrder.map((name) => ({ name, color: elementMeta[name].color, value: 20 })),
favorable: '金水',
unfavorable: '木火土',
dayMaster: '壬'
}
}
const solar = Solar.fromYmdHms(
date.getFullYear(),
date.getMonth() + 1,
date.getDate(),
hourFromBirthHour(birthHour),
0,
0
)
const eightChar = solar.getLunar().getEightChar()
const pillars = [eightChar.getYear(), eightChar.getMonth(), eightChar.getDay(), eightChar.getTime()]
const weights: Record<ElementName, number> = { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 }
pillars.forEach((pillar) => {
const [stem, branch] = pillar
if (stemElements[stem]) weights[stemElements[stem]] += 2
if (branchElements[branch]) weights[branchElements[branch]] += 1
})
const total = Object.values(weights).reduce((sum, value) => sum + value, 0)
const scores = scoreOrder.map((name) => ({
name,
color: elementMeta[name].color,
value: Math.round((weights[name] / total) * 100)
}))
const dayMaster = eightChar.getDay().slice(0, 1)
const masterElement = stemElements[dayMaster] || '水'
const masterIndex = generationOrder.indexOf(masterElement)
const resourceElement = generationOrder[(masterIndex + generationOrder.length - 1) % generationOrder.length]
const unfavorable = generationOrder.filter((element) => element !== resourceElement && element !== masterElement).join('')
return {
scores,
favorable: `${resourceElement}${masterElement}`,
unfavorable,
dayMaster
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"removeComments": false,
"preserveConstEnums": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"outDir": "lib",
"noUnusedLocals": false,
"noUnusedParameters": false,
"strictNullChecks": false,
"sourceMap": true,
"rootDir": ".",
"jsx": "react-jsx",
"skipLibCheck": true
},
"include": ["src", "config"]
}