feat: 添加首页、交易页面及相关组件,并引入Confetti效果。

This commit is contained in:
2026-01-26 07:13:09 +08:00
parent 94a28aa31a
commit c7e9ce5825
8 changed files with 587 additions and 184 deletions

436
package-lock.json generated
View File

@@ -17,9 +17,12 @@
"decimal.js": "^10.6.0",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"framer-motion": "^12.29.0",
"react": "^19.2.0",
"react-confetti": "^6.4.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.12.0"
"react-router-dom": "^7.12.0",
"react-use": "^17.6.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -353,7 +356,6 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1396,7 +1398,6 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -1794,27 +1795,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
@@ -1884,14 +1864,6 @@
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1969,6 +1941,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2491,6 +2469,12 @@
"integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==",
"license": "MIT"
},
"node_modules/@xobotyi/scrollbar-width": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2541,17 +2525,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -2847,6 +2820,15 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"license": "MIT",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2862,6 +2844,15 @@
"node": ">= 8"
}
},
"node_modules/css-in-js-utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
"license": "MIT",
"dependencies": {
"hyphenate-style-name": "^1.0.3"
}
},
"node_modules/css-tree": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
@@ -2913,7 +2904,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/data-urls": {
@@ -2989,14 +2979,6 @@
"node": ">=6"
}
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -3066,6 +3048,15 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"license": "MIT",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -3480,6 +3471,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-shallow-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
},
"node_modules/fastest-stable-stringify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
"integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -3592,6 +3594,39 @@
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "12.29.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.0.tgz",
"integrity": "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.29.0",
"motion-utils": "^12.27.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/framer-motion/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3807,6 +3842,12 @@
"node": ">= 14"
}
},
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3854,6 +3895,15 @@
"node": ">=8"
}
},
"node_modules/inline-style-prefixer": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
"license": "MIT",
"dependencies": {
"css-in-js-utils": "^3.1.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3891,6 +3941,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4065,17 +4121,6 @@
"yallist": "^3.0.2"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4146,6 +4191,21 @@
"node": "*"
}
},
"node_modules/motion-dom": {
"version": "12.29.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.0.tgz",
"integrity": "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.27.2"
}
},
"node_modules/motion-utils": {
"version": "12.27.2",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.27.2.tgz",
"integrity": "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q==",
"license": "MIT"
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -4162,6 +4222,54 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nano-css": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz",
"integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==",
"license": "Unlicense",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"css-tree": "^1.1.2",
"csstype": "^3.1.2",
"fastest-stable-stringify": "^2.0.2",
"inline-style-prefixer": "^7.0.1",
"rtl-css-js": "^1.16.1",
"stacktrace-js": "^2.0.2",
"stylis": "^4.3.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/nano-css/node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/nano-css/node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/nano-css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4423,36 +4531,6 @@
"node": ">=6.0.0"
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -4495,6 +4573,21 @@
"node": ">=0.10.0"
}
},
"node_modules/react-confetti": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz",
"integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==",
"license": "MIT",
"dependencies": {
"tween-functions": "^1.2.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
@@ -4507,14 +4600,6 @@
"react": "^19.2.3"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -4563,6 +4648,41 @@
"react-dom": ">=18"
}
},
"node_modules/react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
"integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==",
"peerDependencies": {
"react": "*",
"tslib": "*"
}
},
"node_modules/react-use": {
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz",
"integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==",
"license": "Unlicense",
"dependencies": {
"@types/js-cookie": "^2.2.6",
"@xobotyi/scrollbar-width": "^1.9.5",
"copy-to-clipboard": "^3.3.1",
"fast-deep-equal": "^3.1.3",
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^2.2.1",
"nano-css": "^5.6.2",
"react-universal-interface": "^0.6.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.1.0",
"set-harmonic-interval": "^1.0.1",
"throttle-debounce": "^3.0.1",
"ts-easing": "^0.2.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -4587,6 +4707,12 @@
"node": ">=0.10.0"
}
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4642,6 +4768,15 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rtl-css-js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
"integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.1.2"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -4661,6 +4796,18 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -4677,6 +4824,15 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/set-harmonic-interval": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz",
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==",
"license": "Unlicense",
"engines": {
"node": ">=6.9"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4728,6 +4884,15 @@
"integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==",
"license": "ISC"
},
"node_modules/source-map": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4737,6 +4902,15 @@
"node": ">=0.10.0"
}
},
"node_modules/stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
"license": "MIT",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -4744,6 +4918,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
"license": "MIT"
},
"node_modules/stacktrace-gps": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
"license": "MIT",
"dependencies": {
"source-map": "0.5.6",
"stackframe": "^1.3.4"
}
},
"node_modules/stacktrace-js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
"license": "MIT",
"dependencies": {
"error-stack-parser": "^2.0.6",
"stack-generator": "^2.0.5",
"stacktrace-gps": "^3.0.4"
}
},
"node_modules/std-env": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
@@ -4777,6 +4978,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stylis": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4812,6 +5019,15 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/throttle-debounce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
"integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -4876,6 +5092,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
"license": "MIT"
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -4925,12 +5147,24 @@
"typescript": ">=4.8.4"
}
},
"node_modules/ts-easing": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==",
"license": "Unlicense"
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/tween-functions": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
"integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==",
"license": "BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -25,9 +25,12 @@
"decimal.js": "^10.6.0",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"framer-motion": "^12.29.0",
"react": "^19.2.0",
"react-confetti": "^6.4.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.12.0"
"react-router-dom": "^7.12.0",
"react-use": "^17.6.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",

View File

@@ -0,0 +1,36 @@
import { useEffect, useState } from 'react';
import ReactConfetti from 'react-confetti';
import { useWindowSize } from 'react-use';
export interface ConfettiProps {
active: boolean;
onComplete?: () => void;
recycle?: boolean;
}
export function Confetti({ active, onComplete, recycle = false }: ConfettiProps) {
const { width, height } = useWindowSize();
const [show, setShow] = useState(active);
useEffect(() => {
setShow(active);
}, [active]);
if (!show) return null;
return (
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 9999 }}>
<ReactConfetti
width={width}
height={height}
recycle={recycle}
numberOfPieces={500}
gravity={0.15}
onConfettiComplete={() => {
if (onComplete) onComplete();
setShow(false);
}}
/>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Confetti';

View File

@@ -197,6 +197,7 @@
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);

View File

@@ -8,103 +8,166 @@
padding-bottom: 2rem;
}
/* Header */
/* Header New Structure */
.home-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
align-items: flex-start;
margin-bottom: var(--spacing-xl);
padding: var(--spacing-md) 0;
}
.home-greeting {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.greeting-title-wrapper {
display: flex;
align-items: baseline;
gap: var(--spacing-md);
cursor: pointer;
transition: opacity 0.2s;
}
.greeting-title-wrapper:hover {
opacity: 0.8;
}
.greeting-text {
font-size: 2.5rem;
font-weight: 800;
margin: 0;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
color: var(--accent-primary);
line-height: 1.1;
letter-spacing: -1px;
}
.current-ledger-badge {
.greeting-top-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.8rem;
background: var(--bg-hover);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
transition: all 0.3s ease;
gap: var(--spacing-md);
margin-bottom: 4px;
}
.greeting-title-wrapper:hover .current-ledger-badge {
.greeting-pill {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--glass-panel-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(8px);
}
.greeting-pill:hover {
background: var(--bg-hover);
border-color: var(--accent-primary);
color: var(--accent-primary);
transform: translateY(-1px);
}
.chevron-icon {
opacity: 0.5;
transition: transform 0.3s ease;
.greeting-text {
font-family: 'Outfit', sans-serif;
font-size: 2rem;
font-weight: 700;
margin: 0;
color: var(--text-primary);
line-height: 1.2;
}
.greeting-title-wrapper:hover .chevron-icon {
transform: translateY(2px);
opacity: 1;
.greeting-highlight {
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.home-date {
color: var(--text-muted);
font-size: 1rem;
margin: 0.5rem 0 0 0;
.greeting-insight {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
color: var(--text-secondary);
margin: 4px 0 0 0;
font-weight: 500;
letter-spacing: 0.5px;
}
.insight-icon {
color: var(--accent-primary);
}
/* Header Actions & Health Score */
.header-actions {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.health-score-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
}
.health-ring {
width: 48px;
height: 48px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.health-ring svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.ring-bg {
fill: none;
stroke: var(--bg-tertiary);
stroke-width: 2.5;
}
.ring-fill {
fill: none;
stroke: var(--color);
stroke-width: 2.5;
stroke-linecap: round;
transition: stroke-dasharray 1s ease-out;
}
.health-val {
position: absolute;
font-family: 'Outfit', sans-serif;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-primary);
}
.health-label {
font-size: 0.7rem;
font-weight: 600;
color: var(--text-secondary);
}
.quick-action-btn-small {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: var(--accent-primary);
color: var(--text-inverse);
border: none;
padding: 0.6rem 1.2rem;
background: var(--glass-panel-bg);
color: var(--accent-primary);
border: 1px solid var(--accent-primary);
border-radius: var(--radius-full);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-glow-sm);
box-shadow: var(--shadow-sm);
}
.quick-action-btn-small:hover {
background: var(--accent-primary);
color: white;
transform: translateY(-2px);
background: var(--accent-primary-hover);
box-shadow: var(--shadow-glow-md);
box-shadow: var(--shadow-glow-sm);
}
@keyframes meshGradient {
0% {
background-position: 0% 50%;

View File

@@ -14,8 +14,10 @@ import VoiceInputModal from '../../components/ai/VoiceInputModal/VoiceInputModal
import { CreateFirstAccountModal } from '../../components/account/CreateFirstAccountModal/CreateFirstAccountModal';
import { AccountForm } from '../../components/account/AccountForm/AccountForm';
import { createAccount } from '../../services/accountService';
import { Confetti } from '../../components/common/Confetti';
import type { Account, Transaction, Category, Ledger, UserSettings } from '../../types';
/**
* Home Page Component
* Displays account balance overview and recent transactions
@@ -35,9 +37,11 @@ function Home() {
const [ledgerSelectorOpen, setLedgerSelectorOpen] = useState(false);
const [voiceModalOpen, setVoiceModalOpen] = useState(false);
const [showAccountForm, setShowAccountForm] = useState(false);
const [showConfetti, setShowConfetti] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
@@ -169,15 +173,50 @@ function Home() {
}
};
const getGreeting = () => {
// Phase 3: Daily Briefing Logic
const getDailyBriefing = () => {
const hour = new Date().getHours();
if (hour < 5) return '夜深了';
if (hour < 11) return '早上好';
if (hour < 13) return '中午好';
if (hour < 18) return '下午好';
return '晚上好';
const todaySpend = recentTransactions
.filter(t => {
const tDate = new Date(t.transactionDate);
const today = new Date();
return tDate.getDate() === today.getDate() &&
tDate.getMonth() === today.getMonth() &&
tDate.getFullYear() === today.getFullYear() &&
t.type === 'expense';
})
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
let greeting = '你好';
if (hour < 5) greeting = '夜深了';
else if (hour < 11) greeting = '早上好';
else if (hour < 13) greeting = '中午好';
else if (hour < 18) greeting = '下午好';
else greeting = '晚上好';
let insight = '今天还没有记账哦';
if (todaySpend > 0) {
insight = `今天已支出 ${formatCurrency(todaySpend)}`;
}
return { greeting, insight };
};
const { greeting, insight } = getDailyBriefing();
// Phase 3: Financial Health Score (Mock Logic)
// In a real app, this would be complex. Here we use a simple placeholder derived from net worth/assets ratio
const calculateHealthScore = () => {
if (totalAssets === 0) return 60; // Baseline
const ratio = (totalAssets - totalLiabilities) / totalAssets;
let score = Math.round(ratio * 100);
if (score < 40) score = 40;
if (score > 98) score = 98;
return score;
};
const healthScore = calculateHealthScore();
if (loading) {
return (
<div className="home-page">
@@ -221,23 +260,46 @@ function Home() {
return (
<div className="home-page">
<header className="home-header">
<div className="home-greeting">
<div className="greeting-title-wrapper" onClick={() => setLedgerSelectorOpen(true)}>
<h1 className="greeting-text">{getGreeting()}</h1>
{currentLedger && (
<div className="current-ledger-badge">
<Icon icon="solar: notebook-minimalistic-bold-duotone" width="14" />
<span>{currentLedger.name}</span>
<Icon icon="solar:alt-arrow-down-bold" width="12" className="chevron-icon" />
<header className="home-header">
<div className="home-greeting animate-slide-up">
<div className="greeting-top-row">
<div className="greeting-pill" onClick={() => setLedgerSelectorOpen(true)}>
{currentLedger && (
<>
<Icon icon="solar:notebook-minimalistic-bold-duotone" width="14" />
<span>{currentLedger.name}</span>
<Icon icon="solar:alt-arrow-down-bold" width="10" className="chevron-icon" />
</>
)}
</div>
)}
<span className="home-date">{new Date().toLocaleDateString('zh-CN', { weekday: 'short', month: 'long', day: 'numeric' })}</span>
</div>
<h1 className="greeting-text">
{greeting}<span className="greeting-highlight"></span>
</h1>
<p className="greeting-insight animate-slide-up delay-100">
<Icon icon="solar:bell-bing-bold-duotone" width="16" className="insight-icon" />
{insight}
</p>
</div>
<p className="home-date">{new Date().toLocaleDateString('zh-CN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</p>
</div>
<button className="quick-action-btn-small" onClick={handleQuickTransaction}>
<Icon icon="solar:add-circle-bold-duotone" width="20" />
<span></span>
</button>
<div className="header-actions animate-slide-up delay-200">
<button className="health-score-btn" onClick={() => setShowConfetti(true)}>
<div className="health-ring" style={{ '--score': `${healthScore}%`, '--color': 'var(--accent-success)' } as any}>
<svg viewBox="0 0 36 36">
<path className="ring-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path className="ring-fill" strokeDasharray={`${healthScore}, 100`} d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
</svg>
<span className="health-val">{healthScore}</span>
</div>
<span className="health-label"></span>
</button>
<button className="quick-action-btn-small" onClick={handleQuickTransaction}>
<Icon icon="solar:add-circle-bold-duotone" width="20" />
<span></span>
</button>
</div>
</header>
</header>
@@ -418,7 +480,10 @@ function Home() {
</div>
</div>
)}
<Confetti active={showConfetti} recycle={false} onComplete={() => setShowConfetti(false)} />
</div>
);
}