diff --git a/components/TimeRangePicker.js b/components/TimeRangePicker.js deleted file mode 100644 index ea38ca7..0000000 --- a/components/TimeRangePicker.js +++ /dev/null @@ -1,145 +0,0 @@ -import { useState, useEffect } from 'react' - -const TimeRangePicker = ({ - value = '1m', - onChange, - className = '' -}) => { - const [timeRange, setTimeRange] = useState(value) - const [startDate, setStartDate] = useState('') - const [endDate, setEndDate] = useState('') - const [showDropdown, setShowDropdown] = useState(false) - - // 计算日期范围 - const calculateDateRange = (range) => { - const end = new Date() - const start = new Date() - - if (range === '1m') { - start.setMonth(start.getMonth() - 1) - } else if (range === '3m') { - start.setMonth(start.getMonth() - 3) - } else if (range === '1y') { - start.setFullYear(start.getFullYear() - 1) - } - - const formatDate = (date) => { - return date.toISOString().split('T')[0] - } - - return { - start: formatDate(start), - end: formatDate(end) - } - } - - // 初始化时设置日期范围 - useEffect(() => { - const range = calculateDateRange(value) - setStartDate(range.start) - setEndDate(range.end) - }, [value]) - - // 处理时间范围变化 - const handleTimeRangeChange = (range) => { - const dateRange = calculateDateRange(range) - setStartDate(dateRange.start) - setEndDate(dateRange.end) - setTimeRange(range) - if (onChange) { - onChange(range, dateRange.start, dateRange.end) - } - setShowDropdown(false) - } - - // 处理自定义日期范围 - const handleCustomRangeApply = () => { - if (startDate && endDate) { - setTimeRange('custom') - if (onChange) { - onChange('custom', startDate, endDate) - } - setShowDropdown(false) - } - } - - // 处理开始日期变化 - const handleStartDateChange = (e) => { - setStartDate(e.target.value) - } - - // 处理结束日期变化 - const handleEndDateChange = (e) => { - setEndDate(e.target.value) - } - - return ( -
- - {showDropdown && ( -
-
-

自定义范围

-
- - 至 - - -
-
-
-

快捷选项

-
    -
  • handleTimeRangeChange('1m')} - > - 近一个月 -
  • -
  • handleTimeRangeChange('3m')} - > - 近三个月 -
  • -
  • handleTimeRangeChange('1y')} - > - 近一年 -
  • -
-
-
- )} -
- ) -} - -export default TimeRangePicker diff --git a/package-lock.json b/package-lock.json index a187a06..2d35305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@huolala-tech/page-spy": "^1.5.7", "@prisma/adapter-mariadb": "^7.9.1", "@prisma/client": "^7.7.0", + "antd-mobile": "^5.42.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", @@ -766,6 +767,31 @@ "node": ">=12" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.11.tgz", @@ -1635,6 +1661,91 @@ } } }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz", + "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@react-spring/animated": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@react-spring/animated/-/animated-9.6.1.tgz", + "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@react-spring/core/-/core-9.6.1.tgz", + "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/rafz": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@react-spring/rafz/-/rafz-9.6.1.tgz", + "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@react-spring/shared/-/shared-9.6.1.tgz", + "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@react-spring/types/-/types-9.6.1.tgz", + "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@react-spring/web/-/web-9.6.1.tgz", + "integrity": "sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/core": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -2398,6 +2509,12 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.1.2", "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.1.2.tgz", @@ -2423,6 +2540,24 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@use-gesture/core": { + "version": "10.3.0", + "resolved": "https://registry.npmmirror.com/@use-gesture/core/-/core-10.3.0.tgz", + "integrity": "sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.0", + "resolved": "https://registry.npmmirror.com/@use-gesture/react/-/react-10.3.0.tgz", + "integrity": "sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.0" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2457,6 +2592,28 @@ "node": ">= 0.6" } }, + "node_modules/ahooks": { + "version": "3.9.7", + "resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.9.7.tgz", + "integrity": "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@types/js-cookie": "^3.0.6", + "dayjs": "^1.9.1", + "intersection-observer": "^0.12.0", + "js-cookie": "^3.0.5", + "lodash": "^4.17.21", + "react-fast-compare": "^3.2.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.0.0", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", @@ -2499,6 +2656,56 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd-mobile": { + "version": "5.42.3", + "resolved": "https://registry.npmmirror.com/antd-mobile/-/antd-mobile-5.42.3.tgz", + "integrity": "sha512-HLykA0Cr2am7dgEch/TRUnr4GHgTOM59rA9CVjgBJVVJ8dk2w8uUBz//Bji19sRH2leiUObWY7KQZpsWF+WQeg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.4.2", + "@rc-component/mini-decimal": "^1.1.0", + "@react-spring/web": "~9.6.1", + "@use-gesture/react": "10.3.0", + "ahooks": "^3.7.6", + "antd-mobile-icons": "^0.3.0", + "antd-mobile-v5-count": "^1.0.1", + "classnames": "^2.3.2", + "dayjs": "^1.11.7", + "deepmerge": "^4.3.1", + "nano-memoize": "^3.0.16", + "rc-field-form": "^1.34.2", + "rc-segmented": "~2.4.1", + "rc-util": "^5.44.4", + "react-fast-compare": "^3.2.2", + "react-is": "^18.2.0", + "runes2": "^1.1.2", + "staged-components": "^1.1.3", + "tslib": "^2.5.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/antd-mobile-icons": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/antd-mobile-icons/-/antd-mobile-icons-0.3.0.tgz", + "integrity": "sha512-rqINQpJWZWrva9moCd1Ye695MZYWmqLPE+bY8d2xLRy7iSQwPsinCdZYjpUPp2zL/LnKYSyXxP2ut2A+DC+whQ==", + "license": "MIT" + }, + "node_modules/antd-mobile-v5-count": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/antd-mobile-v5-count/-/antd-mobile-v5-count-1.0.1.tgz", + "integrity": "sha512-YGsiEDCPUDz3SzfXi6gLZn/HpeSMW+jgPc4qiYUr1fSopg3hkUie2TnooJdExgfiETHefH3Ggs58He0OVfegLA==", + "license": "MIT" + }, + "node_modules/antd-mobile/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2514,6 +2721,12 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", @@ -2856,6 +3069,12 @@ "consola": "^3.2.3" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", @@ -3232,6 +3451,12 @@ "node": ">=12" } }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", @@ -3256,6 +3481,15 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -4203,6 +4437,13 @@ "node": ">=12" } }, + "node_modules/intersection-observer": { + "version": "0.12.2", + "resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz", + "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==", + "deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.", + "license": "Apache-2.0" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4259,6 +4500,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4374,6 +4621,12 @@ "node": ">=0.10.0" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -4593,6 +4846,12 @@ "node": ">=8.0.0" } }, + "node_modules/nano-memoize": { + "version": "3.0.16", + "resolved": "https://registry.npmmirror.com/nano-memoize/-/nano-memoize-3.0.16.tgz", + "integrity": "sha512-JyK96AKVGAwVeMj3MoMhaSXaUNqgMbCRSQB3trUV8tYZfWEzqUBKdK1qJpfuNXgKeHOx1jv/IEYTM659ly7zUA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.16", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", @@ -4973,6 +5232,75 @@ "node": ">=0.10.0" } }, + "node_modules/rc-field-form": { + "version": "1.44.0", + "resolved": "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-1.44.0.tgz", + "integrity": "sha512-el7w87fyDUsca63Y/s8qJcq9kNkf/J5h+iTdqG5WsSHLH0e6Usl7QuYSmSVzJMgtp40mOVZIY/W/QP9zwrp1FA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "async-validator": "^4.1.0", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.4.1.tgz", + "integrity": "sha512-KUi+JJFdKnumV9iXlm+BJ00O4NdVBp2TEexLCk6bK1x/RH83TvYKQMzIz/7m3UTRPD08RM/8VG/JNjWgWbd4cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz", @@ -5008,6 +5336,12 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, "node_modules/react-is": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", @@ -5172,6 +5506,12 @@ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", @@ -5227,6 +5567,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/runes2": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/runes2/-/runes2-1.1.4.tgz", + "integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==", + "license": "MIT" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", @@ -5272,6 +5618,18 @@ "loose-envify": "^1.1.0" } }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/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/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -5566,6 +5924,15 @@ "node": ">= 0.6" } }, + "node_modules/staged-components": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/staged-components/-/staged-components-1.1.3.tgz", + "integrity": "sha512-9EIswzDqjwlEu+ymkV09TTlJfzSbKgEnNteUnZSTxkpMgr5Wx2CzzA9WcMFWBNCldqVPsHVnRGGrApduq2Se5A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", @@ -5678,7 +6045,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-is": { diff --git a/package.json b/package.json index 3af08ed..d910111 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@huolala-tech/page-spy": "^1.5.7", "@prisma/adapter-mariadb": "^7.9.1", "@prisma/client": "^7.7.0", + "antd-mobile": "^5.42.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", diff --git a/pages/api/exchange-rate.js b/pages/api/exchange-rate.js deleted file mode 100644 index 32cba0f..0000000 --- a/pages/api/exchange-rate.js +++ /dev/null @@ -1,108 +0,0 @@ -import { createPool } from 'mariadb'; -import fs from 'fs'; -import path from 'path'; - -function loadDatabaseConfig() { - const envPath = path.join(process.cwd(), '.env'); - if (fs.existsSync(envPath)) { - const envContent = fs.readFileSync(envPath, 'utf-8'); - const lines = envContent.split('\n'); - - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine.startsWith('DATABASE_URL=')) { - const url = trimmedLine.substring('DATABASE_URL='.length).replace(/^"|"$/g, ''); - - // 解析URL - const parts = url.split('://')[1].split('@'); - const auth = parts[0].split(':'); - const hostAndDb = parts[1].split('/'); - const hostAndPort = hostAndDb[0].split(':'); - - return { - user: auth[0], - password: auth[1], - host: hostAndPort[0], - port: parseInt(hostAndPort[1]), - database: hostAndDb[1] - }; - } - } - } - - return { - host: '192.168.111.111', - port: 3306, - user: 'root', - password: 'fullstack', - database: 'iboard' - }; -} - -let pool; - -async function initPool() { - if (!pool) { - const config = loadDatabaseConfig(); - pool = createPool({ - host: config.host, - port: config.port, - user: config.user, - password: config.password, - database: config.database, - connectionLimit: 5 - }); - } - return pool; -} - -export default async function handler(req, res) { - try { - await initPool(); - - // 获取查询参数 - const { startDate, endDate, limit = 30 } = req.query; - - let query = 'SELECT date, centerPrice FROM econ_ExchangeRate WHERE currency = ? ORDER BY date DESC'; - const params = ['USD/CNY']; - - if (startDate && endDate) { - query = 'SELECT date, centerPrice FROM econ_ExchangeRate WHERE currency = ? AND date BETWEEN ? AND ? ORDER BY date DESC'; - params.push(startDate, endDate); - } - - const result = await pool.query(query, params); - - // 限制返回数量 - const limitedResult = result.slice(0, parseInt(limit)); - - // 反转顺序,使日期从早到晚 - const sortedResult = limitedResult.reverse(); - - // 格式化数据 - const formattedData = sortedResult.map(row => ({ - date: row.date instanceof Date ? row.date.toISOString().split('T')[0] : row.date, - rate: parseFloat(row.centerPrice) - })); - - res.status(200).json({ - success: true, - data: formattedData, - total: result.length - }); - } catch (error) { - console.error('获取汇率数据失败:', error.message); - res.status(500).json({ - success: false, - message: '获取汇率数据失败', - error: error.message - }); - } finally { - // 不关闭连接池,保持连接池打开以提高性能 - // 如果需要关闭连接池,应该在关闭后将 pool 变量设置为 null - // if (pool) { - // await pool.end(); - // pool = null; - // } - } -} diff --git a/pages/api/sh300-index.js b/pages/api/sh300-index.js deleted file mode 100644 index c7e79d5..0000000 --- a/pages/api/sh300-index.js +++ /dev/null @@ -1,78 +0,0 @@ -import { createPool } from 'mariadb'; - -async function handler(req, res) { - if (req.method !== 'GET') { - return res.status(405).json({ message: '只支持GET请求' }); - } - - const { startDate, endDate, limit } = req.query; - - let pool; - try { - pool = createPool({ - host: process.env.DB_HOST || '192.168.111.111', - port: parseInt(process.env.DB_PORT || '3306'), - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || 'fullstack', - database: process.env.DB_NAME || 'iboard', - connectionLimit: 5 - }); - - let query = 'SELECT date, close FROM econ_SH300Index'; - const params = []; - const conditions = []; - - if (startDate && endDate) { - conditions.push('date BETWEEN ? AND ?'); - params.push(startDate, endDate); - } - - if (conditions.length > 0) { - query += ' WHERE ' + conditions.join(' AND '); - } - - query += ' ORDER BY date DESC'; - - const result = await pool.query(query, params); - - // 限制返回数量,默认为30 - const limitValue = limit ? parseInt(limit) : 30; - const limitedResult = result.slice(0, limitValue); - - // 反转顺序,使日期从早到晚 - const sortedResult = limitedResult.reverse(); - - const data = sortedResult.map(row => { - let dateStr; - if (row.date instanceof Date) { - dateStr = row.date.toISOString().split('T')[0]; - } else if (typeof row.date === 'string') { - dateStr = row.date; - } else { - dateStr = String(row.date); - } - return { - date: dateStr, - close: parseFloat(row.close) - }; - }); - - return res.status(200).json({ - success: true, - data: data - }); - - } catch (error) { - console.error('获取沪深300数据失败:', error); - return res.status(500).json({ - success: false, - message: '获取沪深300数据失败: ' + error.message - }); - } finally { - if (pool) { - await pool.end(); - } - } -} - -export default handler; diff --git a/pages/data.js b/pages/data.js deleted file mode 100644 index 2d08de7..0000000 --- a/pages/data.js +++ /dev/null @@ -1,721 +0,0 @@ -import { useState, useEffect } from 'react' -import Head from 'next/head' -import Navbar from '../components/Navbar' -import Footer from '../components/Footer' -import TimeRangePicker from '../components/TimeRangePicker' -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' - -const chinaBondData = [ - { month: '2024-01', oneYear: 1.85, fiveYear: 2.45, tenYear: 2.65 }, - { month: '2024-02', oneYear: 1.82, fiveYear: 2.42, tenYear: 2.62 }, - { month: '2024-03', oneYear: 1.78, fiveYear: 2.38, tenYear: 2.58 }, - { month: '2024-04', oneYear: 1.75, fiveYear: 2.35, tenYear: 2.55 }, - { month: '2024-05', oneYear: 1.72, fiveYear: 2.32, tenYear: 2.52 }, - { month: '2024-06', oneYear: 1.68, fiveYear: 2.28, tenYear: 2.48 }, - { month: '2024-07', oneYear: 1.65, fiveYear: 2.25, tenYear: 2.45 }, - { month: '2024-08', oneYear: 1.62, fiveYear: 2.22, tenYear: 2.42 }, - { month: '2024-09', oneYear: 1.58, fiveYear: 2.18, tenYear: 2.38 }, - { month: '2024-10', oneYear: 1.55, fiveYear: 2.15, tenYear: 2.35 }, - { month: '2024-11', oneYear: 1.52, fiveYear: 2.12, tenYear: 2.32 }, - { month: '2024-12', oneYear: 1.48, fiveYear: 2.08, tenYear: 2.28 } -] - -const usBondData = [ - { month: '2024-01', oneYear: 4.85, fiveYear: 4.25, tenYear: 3.95 }, - { month: '2024-02', oneYear: 4.82, fiveYear: 4.22, tenYear: 3.92 }, - { month: '2024-03', oneYear: 4.78, fiveYear: 4.18, tenYear: 3.88 }, - { month: '2024-04', oneYear: 4.65, fiveYear: 4.05, tenYear: 3.75 }, - { month: '2024-05', oneYear: 4.55, fiveYear: 3.95, tenYear: 3.65 }, - { month: '2024-06', oneYear: 4.45, fiveYear: 3.85, tenYear: 3.55 }, - { month: '2024-07', oneYear: 4.35, fiveYear: 3.75, tenYear: 3.45 }, - { month: '2024-08', oneYear: 4.25, fiveYear: 3.65, tenYear: 3.35 }, - { month: '2024-09', oneYear: 4.15, fiveYear: 3.55, tenYear: 3.25 }, - { month: '2024-10', oneYear: 4.05, fiveYear: 3.45, tenYear: 3.15 }, - { month: '2024-11', oneYear: 3.95, fiveYear: 3.35, tenYear: 3.05 }, - { month: '2024-12', oneYear: 3.85, fiveYear: 3.25, tenYear: 2.95 } -] - -const chinaLPRData = [ - { month: '2024-01', oneYear: 3.45, fiveYear: 3.95 }, - { month: '2024-02', oneYear: 3.45, fiveYear: 3.95 }, - { month: '2024-03', oneYear: 3.45, fiveYear: 3.95 }, - { month: '2024-04', oneYear: 3.40, fiveYear: 3.90 }, - { month: '2024-05', oneYear: 3.40, fiveYear: 3.90 }, - { month: '2024-06', oneYear: 3.40, fiveYear: 3.90 }, - { month: '2024-07', oneYear: 3.35, fiveYear: 3.85 }, - { month: '2024-08', oneYear: 3.35, fiveYear: 3.85 }, - { month: '2024-09', oneYear: 3.35, fiveYear: 3.85 }, - { month: '2024-10', oneYear: 3.30, fiveYear: 3.80 }, - { month: '2024-11', oneYear: 3.30, fiveYear: 3.80 }, - { month: '2024-12', oneYear: 3.30, fiveYear: 3.80 } -] - -// 中国黄金储备月度数据(单位:吨) -const chinaGoldReserveMonthly = [ - { month: '2024-01', reserves: 2245.4 }, - { month: '2024-02', reserves: 2255.2 }, - { month: '2024-03', reserves: 2267.5 }, - { month: '2024-04', reserves: 2276.9 }, - { month: '2024-05', reserves: 2285.3 }, - { month: '2024-06', reserves: 2296.1 }, - { month: '2024-07', reserves: 2305.8 }, - { month: '2024-08', reserves: 2316.2 }, - { month: '2024-09', reserves: 2325.7 }, - { month: '2024-10', reserves: 2336.5 }, - { month: '2024-11', reserves: 2345.9 }, - { month: '2024-12', reserves: 2356.3 } -] - -// 美国黄金储备月度数据(单位:吨) -const usGoldReserveMonthly = [ - { month: '2024-01', reserves: 8133.5 }, - { month: '2024-02', reserves: 8133.5 }, - { month: '2024-03', reserves: 8133.5 }, - { month: '2024-04', reserves: 8133.5 }, - { month: '2024-05', reserves: 8133.5 }, - { month: '2024-06', reserves: 8133.5 }, - { month: '2024-07', reserves: 8133.5 }, - { month: '2024-08', reserves: 8133.5 }, - { month: '2024-09', reserves: 8133.5 }, - { month: '2024-10', reserves: 8133.5 }, - { month: '2024-11', reserves: 8133.5 }, - { month: '2024-12', reserves: 8133.5 } -] - -// 中国黄金储备年度数据(单位:吨) -const chinaGoldReserveYearly = [ - { year: '2019', reserves: 1948.3 }, - { year: '2020', reserves: 1948.3 }, - { year: '2021', reserves: 1948.3 }, - { year: '2022', reserves: 1948.3 }, - { year: '2023', reserves: 2190.2 }, - { year: '2024', reserves: 2356.3 } -] - -// 美国黄金储备年度数据(单位:吨) -const usGoldReserveYearly = [ - { year: '2019', reserves: 8133.5 }, - { year: '2020', reserves: 8133.5 }, - { year: '2021', reserves: 8133.5 }, - { year: '2022', reserves: 8133.5 }, - { year: '2023', reserves: 8133.5 }, - { year: '2024', reserves: 8133.5 } -] - -// 猪肉价格数据(单位:元/公斤) -const porkPriceData = [ - { month: '2023-01', price: 24.5 }, - { month: '2023-02', price: 25.2 }, - { month: '2023-03', price: 26.8 }, - { month: '2023-04', price: 27.5 }, - { month: '2023-05', price: 28.3 }, - { month: '2023-06', price: 29.1 }, - { month: '2023-07', price: 28.5 }, - { month: '2023-08', price: 27.8 }, - { month: '2023-09', price: 26.5 }, - { month: '2023-10', price: 25.8 }, - { month: '2023-11', price: 25.2 }, - { month: '2023-12', price: 24.8 }, - { month: '2024-01', price: 24.5 }, - { month: '2024-02', price: 25.1 }, - { month: '2024-03', price: 25.8 }, - { month: '2024-04', price: 26.5 }, - { month: '2024-05', price: 27.2 }, - { month: '2024-06', price: 27.8 }, - { month: '2024-07', price: 28.5 }, - { month: '2024-08', price: 29.2 }, - { month: '2024-09', price: 28.8 }, - { month: '2024-10', price: 28.2 }, - { month: '2024-11', price: 27.5 }, - { month: '2024-12', price: 26.8 } -] - -// 能繁母猪数量数据(单位:万头) -const sowInventoryData = [ - { month: '2023-01', inventory: 4321 }, - { month: '2023-02', inventory: 4356 }, - { month: '2023-03', inventory: 4389 }, - { month: '2023-04', inventory: 4423 }, - { month: '2023-05', inventory: 4456 }, - { month: '2023-06', inventory: 4489 }, - { month: '2023-07', inventory: 4523 }, - { month: '2023-08', inventory: 4556 }, - { month: '2023-09', inventory: 4589 }, - { month: '2023-10', inventory: 4623 }, - { month: '2023-11', inventory: 4656 }, - { month: '2023-12', inventory: 4689 }, - { month: '2024-01', inventory: 4723 }, - { month: '2024-02', inventory: 4756 }, - { month: '2024-03', inventory: 4789 }, - { month: '2024-04', inventory: 4823 }, - { month: '2024-05', inventory: 4856 }, - { month: '2024-06', inventory: 4889 }, - { month: '2024-07', inventory: 4923 }, - { month: '2024-08', inventory: 4956 }, - { month: '2024-09', inventory: 4989 }, - { month: '2024-10', inventory: 5023 }, - { month: '2024-11', inventory: 5056 }, - { month: '2024-12', inventory: 5089 } -] - -export default function Data() { - const [selectedIndicator, setSelectedIndicator] = useState('bond-china-treasury') - const [exchangeRateData, setExchangeRateData] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [timeRange, setTimeRange] = useState('1m') // 1m: 近一个月, 3m: 近三个月, 1y: 近一年, custom: 自定义 - const [startDate, setStartDate] = useState('') - const [endDate, setEndDate] = useState('') - - useEffect(() => { - if (selectedIndicator === 'central-bank-usd-cny-rate') { - fetchExchangeRateData() - } - }, [selectedIndicator, timeRange, startDate, endDate]) - - const handleTimeRangeChange = (range, start, end) => { - setTimeRange(range) - setStartDate(start) - setEndDate(end) - } - - const fetchExchangeRateData = async () => { - setLoading(true) - setError(null) - try { - let url = '/api/exchange-rate' - - if (timeRange === 'custom' && startDate && endDate) { - url += `?startDate=${startDate}&endDate=${endDate}` - } else if (timeRange === '1m') { - url += '?limit=30' - } else if (timeRange === '3m') { - url += '?limit=90' - } else if (timeRange === '1y') { - url += '?limit=365' - } - - const response = await fetch(url) - const data = await response.json() - if (data.success) { - setExchangeRateData(data.data) - } else { - setError(data.message) - } - } catch (err) { - setError('获取汇率数据失败') - console.error('获取汇率数据失败:', err) - } finally { - setLoading(false) - } - } - - const indicators = [ - { - group: '债券', - items: [ - { id: 'bond-china-treasury', name: '中国国债' }, - { id: 'bond-us-treasury', name: '美国国债' } - ] - }, - { - group: '贷款', - items: [ - { id: 'loan-china-lpr', name: '中国LPR' } - ] - }, - { - group: '央行数据', - items: [ - { id: 'central-bank-china-gold-monthly', name: '中国黄金储备(月)' }, - { id: 'central-bank-us-gold-monthly', name: '美国黄金储备(月)' }, - { id: 'central-bank-china-gold-yearly', name: '中国黄金储备(年)' }, - { id: 'central-bank-us-gold-yearly', name: '美国黄金储备(年)' }, - { id: 'central-bank-usd-cny-rate', name: '人民币兑美元汇率' } - ] - }, - { - group: '商品', - items: [ - { id: 'commodity-pork-price', name: '猪肉价格' }, - { id: 'commodity-sow-inventory', name: '能繁母猪数量' } - ] - } - ] - - return ( -
- - 数据 - iBoard - - - - - -
-

- 经济数据 -

- -

- 各种经济数据和图表 -

- -
-
- {indicators.map((group, groupIndex) => ( -
-

{group.group}

-
    - {group.items.map((item) => ( -
  • setSelectedIndicator(item.id)} - > - {item.name} -
  • - ))} -
-
- ))} -
- -
- {selectedIndicator === 'bond-china-treasury' && ( -
-

中国国债收益率走势

-

展示一年期、五年期、十年期国债收益率的变化趋势

- -
- - - - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - - - {chinaBondData.map((row, index) => ( - - - - - - - ))} - -
月份一年期(%)五年期(%)十年期(%)
{row.month}{row.oneYear.toFixed(2)}{row.fiveYear.toFixed(2)}{row.tenYear.toFixed(2)}
-
-
- )} - - {selectedIndicator === 'bond-us-treasury' && ( -
-

美国国债收益率走势

-

展示一年期、五年期、十年期国债收益率的变化趋势

- -
- - - - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - - - {usBondData.map((row, index) => ( - - - - - - - ))} - -
月份一年期(%)五年期(%)十年期(%)
{row.month}{row.oneYear.toFixed(2)}{row.fiveYear.toFixed(2)}{row.tenYear.toFixed(2)}
-
-
- )} - - {selectedIndicator === 'loan-china-lpr' && ( -
-

中国LPR走势

-

展示一年期和五年期贷款市场报价利率(LPR)的变化趋势

- -
- - - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - - {chinaLPRData.map((row, index) => ( - - - - - - ))} - -
月份一年期LPR(%)五年期LPR(%)
{row.month}{row.oneYear.toFixed(2)}{row.fiveYear.toFixed(2)}
-
-
- )} - - {selectedIndicator === 'central-bank-china-gold-monthly' && ( -
-

中国黄金储备(月度)

-

展示中国央行黄金储备的月度变化趋势

- -
- - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - {chinaGoldReserveMonthly.map((row, index) => ( - - - - - ))} - -
月份黄金储备(吨)
{row.month}{row.reserves.toFixed(1)}
-
-
- )} - - {selectedIndicator === 'central-bank-us-gold-monthly' && ( -
-

美国黄金储备(月度)

-

展示美国央行黄金储备的月度变化趋势

- -
- - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - {usGoldReserveMonthly.map((row, index) => ( - - - - - ))} - -
月份黄金储备(吨)
{row.month}{row.reserves.toFixed(1)}
-
-
- )} - - {selectedIndicator === 'central-bank-china-gold-yearly' && ( -
-

中国黄金储备(年度)

-

展示中国央行黄金储备的年度变化趋势

- -
- - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - {chinaGoldReserveYearly.map((row, index) => ( - - - - - ))} - -
年份黄金储备(吨)
{row.year}{row.reserves.toFixed(1)}
-
-
- )} - - {selectedIndicator === 'central-bank-us-gold-yearly' && ( -
-

美国黄金储备(年度)

-

展示美国央行黄金储备的年度变化趋势

- -
- - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - {usGoldReserveYearly.map((row, index) => ( - - - - - ))} - -
年份黄金储备(吨)
{row.year}{row.reserves.toFixed(1)}
-
-
- )} - - {selectedIndicator === 'central-bank-usd-cny-rate' && ( -
-

人民币兑美元汇率

-

展示人民币兑美元汇率的变化趋势

- - {loading ? ( -
加载中...
- ) : error ? ( -
{error}
- ) : ( - <> -
- -
- -
- - - - - value.toFixed(4)} /> - [value.toFixed(4), '汇率']} /> - - - - -
- -
-

详细数据

- - - - - - - - - {exchangeRateData.map((row, index) => ( - - - - - ))} - -
日期汇率
{row.date}{row.rate.toFixed(4)}
-
- - )} -
- )} - - {selectedIndicator === 'commodity-pork-price' && ( -
-

猪肉价格

-

展示中国猪肉价格的月度变化趋势

- -
- - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - {porkPriceData.map((row, index) => ( - - - - - ))} - -
月份猪肉价格(元/公斤)
{row.month}{row.price.toFixed(1)}
-
-
- )} - - {selectedIndicator === 'commodity-sow-inventory' && ( -
-

能繁母猪数量

-

展示中国能繁母猪数量的月度变化趋势

- -
- - - - - - - - - - -
- -
-

详细数据

- - - - - - - - - {sowInventoryData.map((row, index) => ( - - - - - ))} - -
月份能繁母猪数量(万头)
{row.month}{row.inventory}
-
-
- )} -
-
-
- -
- ) -} diff --git a/pages/strategy.js b/pages/strategy.js deleted file mode 100644 index b6a1a8a..0000000 --- a/pages/strategy.js +++ /dev/null @@ -1,403 +0,0 @@ -import { useState, useEffect } from 'react' -import Head from 'next/head' -import Navbar from '../components/Navbar' -import Footer from '../components/Footer' -import TimeRangePicker from '../components/TimeRangePicker' -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' - -const usDebtData = [ - { year: '2019', debt: 22.7, gdp: 21.4, debtToGdp: 106.1, interestToGdp: 1.8 }, - { year: '2020', debt: 26.9, gdp: 20.9, debtToGdp: 128.7, interestToGdp: 1.6 }, - { year: '2021', debt: 28.4, gdp: 23.0, debtToGdp: 123.5, interestToGdp: 1.5 }, - { year: '2022', debt: 30.9, gdp: 25.4, debtToGdp: 121.6, interestToGdp: 1.6 }, - { year: '2023', debt: 33.1, gdp: 26.9, debtToGdp: 123.0, interestToGdp: 1.9 }, - { year: '2024', debt: 35.3, gdp: 28.5, debtToGdp: 123.9, interestToGdp: 2.1 }, - { year: '2025', debt: 37.5, gdp: 30.2, debtToGdp: 124.2, interestToGdp: 2.3 }, - { year: '2026', debt: 39.8, gdp: 31.9, debtToGdp: 124.8, interestToGdp: 2.5 } -] - -const chinaDebtData = [ - { year: '2019', debt: 8.8, gdp: 14.3, debtToGdp: 61.5, interestToGdp: 1.9 }, - { year: '2020', debt: 10.3, gdp: 14.7, debtToGdp: 70.1, interestToGdp: 1.8 }, - { year: '2021', debt: 11.7, gdp: 16.1, debtToGdp: 72.7, interestToGdp: 1.7 }, - { year: '2022', debt: 13.1, gdp: 17.9, debtToGdp: 73.2, interestToGdp: 1.6 }, - { year: '2023', debt: 14.5, gdp: 19.4, debtToGdp: 74.7, interestToGdp: 1.6 }, - { year: '2024', debt: 15.9, gdp: 21.0, debtToGdp: 75.7, interestToGdp: 1.5 }, - { year: '2025', debt: 17.3, gdp: 22.8, debtToGdp: 75.9, interestToGdp: 1.5 }, - { year: '2026', debt: 18.8, gdp: 24.7, debtToGdp: 76.1, interestToGdp: 1.4 } -] - -export default function Strategy() { - const [selectedStrategy, setSelectedStrategy] = useState('us-debt-gdp') - const [sh300ExchangeData, setSh300ExchangeData] = useState([]) - const [sh300Loading, setSh300Loading] = useState(false) - const [timeRange, setTimeRange] = useState('1m') - const [startDate, setStartDate] = useState('') - const [endDate, setEndDate] = useState('') - - const strategies = [ - { - id: 'us-debt-gdp', - name: '美国政府债务/GDP' - }, - { - id: 'china-debt-gdp', - name: '中国政府债务/GDP' - }, - { - id: 'sh300-exchange-rate', - name: '沪深300与汇率对比' - } - ] - - const handleTimeRangeChange = (range, start, end) => { - setTimeRange(range) - setStartDate(start) - setEndDate(end) - } - - const fetchSh300ExchangeData = async () => { - console.log('fetchSh300ExchangeData called') - setSh300Loading(true) - try { - let sh300Url = '/api/sh300-index' - let exchangeUrl = '/api/exchange-rate' - - const params = [] - if (timeRange === 'custom' && startDate && endDate) { - params.push(`startDate=${startDate}`) - params.push(`endDate=${endDate}`) - } else if (timeRange === '1m') { - params.push('limit=30') - } else if (timeRange === '3m') { - params.push('limit=90') - } else if (timeRange === '1y') { - params.push('limit=365') - } - - if (params.length > 0) { - sh300Url += '?' + params.join('&') - exchangeUrl += '?' + params.join('&') - } - - console.log('Fetching from:', sh300Url, exchangeUrl) - - const [sh300Res, exchangeRes] = await Promise.all([ - fetch(sh300Url), - fetch(exchangeUrl) - ]) - console.log('Response status:', sh300Res.status, exchangeRes.status) - - const sh300Json = await sh300Res.json() - const exchangeJson = await exchangeRes.json() - - console.log('Response data:', sh300Json, exchangeJson) - - if (sh300Json.success && exchangeJson.success) { - const sh300Map = new Map(sh300Json.data.map(item => [item.date, item.close])) - const exchangeMap = new Map(exchangeJson.data.map(item => [item.date, item.rate])) - - const allDates = [...new Set([...sh300Map.keys(), ...exchangeMap.keys()])].sort() - - const mergedData = allDates.map(date => ({ - date, - sh300: sh300Map.get(date) || null, - exchangeRate: exchangeMap.get(date) || null - })).filter(item => item.sh300 !== null && item.exchangeRate !== null) - - console.log('Merged data:', mergedData) - setSh300ExchangeData(mergedData) - } - } catch (error) { - console.error('获取数据失败:', error) - } finally { - setSh300Loading(false) - } - } - - useEffect(() => { - if (selectedStrategy === 'sh300-exchange-rate') { - fetchSh300ExchangeData() - } - }, [selectedStrategy, timeRange, startDate, endDate]) - - return ( -
- - 策略 - iBoard - - - - - -
-

- 经济策略 -

- -

- 按照经济理论组织的相关数据和图表的对比 -

- -
-
-

策略列表

-
    - {strategies.map((strategy) => ( -
  • setSelectedStrategy(strategy.id)} - > - {strategy.name} -
  • - ))} -
-
- -
- {selectedStrategy === 'us-debt-gdp' && ( -
-

美国政府债务/GDP分析

-

- 分析美国政府债务、GDP、债务/GDP比率以及债务利息/GDP比率的走势 -

- -
-

美国政府债务走势

-
- - - - - - - - - - -
-
- -
-

美国GDP走势

-
- - - - - - - - - - -
-
- -
-

美国政府债务/GDP走势

-
- - - - - - - - - - -
-
- -
-

美国政府债务利息/GDP

-
- - - - - - - - - - -
-
- -
-

详细数据

- - - - - - - - - - - - {usDebtData.map((row, index) => ( - - - - - - - - ))} - -
年份政府债务(万亿美元)GDP(万亿美元)债务/GDP(%)债务利息/GDP(%)
{row.year}{row.debt.toFixed(1)}{row.gdp.toFixed(1)}{row.debtToGdp.toFixed(1)}{row.interestToGdp.toFixed(1)}
-
-
- )} - - {selectedStrategy === 'china-debt-gdp' && ( -
-

中国政府债务/GDP分析

-

- 分析中国政府债务、GDP、债务/GDP比率以及债务利息/GDP比率的走势 -

- -
-

中国政府债务走势

-
- - - - - - - - - - -
-
- -
-

中国GDP走势

-
- - - - - - - - - - -
-
- -
-

中国政府债务/GDP走势

-
- - - - - - - - - - -
-
- -
-

中国政府债务利息/GDP

-
- - - - - - - - - - -
-
- -
-

详细数据

- - - - - - - - - - - - {chinaDebtData.map((row, index) => ( - - - - - - - - ))} - -
年份政府债务(万亿美元)GDP(万亿美元)债务/GDP(%)债务利息/GDP(%)
{row.year}{row.debt.toFixed(1)}{row.gdp.toFixed(1)}{row.debtToGdp.toFixed(1)}{row.interestToGdp.toFixed(1)}
-
-
- )} - - {selectedStrategy === 'sh300-exchange-rate' && ( -
-

沪深300与汇率对比分析

-

- 对比沪深300指数收盘价与人民币兑美元汇率的走势关系 -

- - {sh300Loading ? ( -
加载中...
- ) : ( - <> -
- -
- -
-

沪深300与汇率对比(双Y轴)

-
- - - - - - value.toFixed(4)} /> - - - - - - -
-
- - )} -
- )} -
-
-
- -
- ) -} diff --git a/src/components/BottomNav.jsx b/src/components/BottomNav.jsx index f018028..52b1606 100644 --- a/src/components/BottomNav.jsx +++ b/src/components/BottomNav.jsx @@ -1,26 +1,49 @@ -import { NavLink } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' +import { TabBar } from 'antd-mobile' +import TabIcon from './TabIcon.jsx' +// 底部 Tab 配置: key 既是路由前缀, 又是 icon 名 +// 使用 antd-mobile TabBar: 自带 safe-area 处理, 仿原生 iOS/Android 风格 const TABS = [ - { to: '/', label: '首页', icon: '⌂' }, - { to: '/macro', label: '宏观', icon: '◫' }, - { to: '/monitor', label: '监控', icon: '◉' }, - { to: '/me', label: '我', icon: '☺' }, + { key: '/', name: 'home', label: '首页' }, + { key: '/macro', name: 'macro', label: '宏观' }, + { key: '/monitor', name: 'monitor', label: '监控' }, + { key: '/me', name: 'me', label: '我' }, ] +// 根据当前路径计算激活的 tab key (精确匹配 '/' , 其他前缀匹配) +function activeKeyFromPath(pathname) { + for (const t of TABS) { + if (t.key === '/') { + if (pathname === '/') return t.key + } else if (pathname === t.key || pathname.startsWith(t.key + '/')) { + return t.key + } + } + return '/' +} + +// 移动端底部 Tab 导航: antd-mobile TabBar, 内置安全区 +// 作用: 渲染底部 4 个 tab, 切换路由; 激活态根据当前 URL 计算 export default function BottomNav() { + const { pathname } = useLocation() + const navigate = useNavigate() + const activeKey = activeKeyFromPath(pathname) + return ( - + ) -} \ No newline at end of file +} diff --git a/src/components/SwipeTabs.jsx b/src/components/SwipeTabs.jsx index 8708647..41bee5e 100644 --- a/src/components/SwipeTabs.jsx +++ b/src/components/SwipeTabs.jsx @@ -1,9 +1,12 @@ import { useRef, useState, useEffect } from 'react' +import { Tabs } from 'antd-mobile' // 滑屏容器 + 顶栏 tabs +// - 顶部: antd-mobile Tabs (带底部激活线动画, 仿原生) +// - 下方: 横向滑屏容器, 手势切换 tab // props: -// tabs: [{ key, label, render }] -// active: 当前激活 key +// tabs: [{ key, label, render }] +// active: 当前激活 key // onChange(key): 切换回调 (用户滑动或点击 tab) export default function SwipeTabs({ tabs, active, onChange }) { const idx = Math.max(0, tabs.findIndex((t) => t.key === active)) @@ -12,10 +15,21 @@ export default function SwipeTabs({ tabs, active, onChange }) { const [animOff, setAnimOff] = useState(false) const startX = useRef(0) const containerRef = useRef(null) + const widthRef = useRef(0) // idx 变化时关闭拖动动画 (避免快速切换时滑动残影) useEffect(() => { setOffsetPx(0); setAnimOff(false) }, [idx]) + // 监听容器宽度变化, 拖动偏移按宽度百分比计算 + useEffect(() => { + const el = containerRef.current + if (!el) return + const ro = new ResizeObserver(() => { widthRef.current = el.offsetWidth }) + ro.observe(el) + widthRef.current = el.offsetWidth + return () => ro.disconnect() + }, []) + function onTouchStart(e) { startX.current = e.touches[0].clientX setDragging(true) @@ -24,7 +38,7 @@ export default function SwipeTabs({ tabs, active, onChange }) { function onTouchMove(e) { if (!dragging) return const dx = e.touches[0].clientX - startX.current - const w = containerRef.current?.offsetWidth || window.innerWidth + const w = widthRef.current || window.innerWidth // 限制左右溢出 (超过边界阻力增大) const limited = Math.max(Math.min(dx, w * 0.4), -w * 0.4) setOffsetPx(limited) @@ -33,7 +47,7 @@ export default function SwipeTabs({ tabs, active, onChange }) { if (!dragging) return setDragging(false) setAnimOff(false) - const w = containerRef.current?.offsetWidth || window.innerWidth + const w = widthRef.current || window.innerWidth const dx = offsetPx let next = idx if (dx < -w * 0.18 && idx < tabs.length - 1) next = idx + 1 @@ -44,26 +58,18 @@ export default function SwipeTabs({ tabs, active, onChange }) { // 用 -33.33% × idx 作为基础位移, 再叠加拖动偏移 const basePct = idx * 33.3333 - const dragPct = containerRef.current - ? (offsetPx / containerRef.current.offsetWidth) * 100 + const dragPct = widthRef.current + ? (offsetPx / widthRef.current) * 100 : 0 const translate = `calc(-${basePct}% + ${dragPct}%)` return ( - <> -
+
+ {tabs.map((t) => ( - + ))} -
+
{tabs.map((t) => (
{t.render()}
))}
- +
) -} \ No newline at end of file +} diff --git a/src/components/TabIcon.jsx b/src/components/TabIcon.jsx new file mode 100644 index 0000000..51c00d1 --- /dev/null +++ b/src/components/TabIcon.jsx @@ -0,0 +1,37 @@ +// 底部 TabBar 图标: 简洁线性 SVG, 仿原生 App 风格 +// props: name 字符串, active 布尔 +// 返回对应的 24x24 SVG 节点 +const ICONS = { + home: (active) => ( + + + + {active ? : null} + + ), + macro: (active) => ( + + + + + + + ), + monitor: (active) => ( + + + + ), + me: (active) => ( + + + + + ), +} + +export default function TabIcon({ name, active }) { + const render = ICONS[name] + if (!render) return null + return {render(!!active)} +} diff --git a/src/layouts/MobileShell.jsx b/src/layouts/MobileShell.jsx index 4256469..286dffe 100644 --- a/src/layouts/MobileShell.jsx +++ b/src/layouts/MobileShell.jsx @@ -1,14 +1,20 @@ import { Outlet } from 'react-router-dom' +import { SafeArea } from 'antd-mobile' import BottomNav from '../components/BottomNav.jsx' -// 移动端应用外壳: 底栏 + 滚动主区 +// 移动端应用外壳 +// - 顶部 SafeArea: 让出手机状态栏高度 +// - 中间
: flex:1 可滚动主区 +// - 底部 TabBar: antd-mobile TabBar 自带底部安全区 +// 解决: 状态栏遮挡/底部 TabBar 遮挡/左右贴边 export default function MobileShell() { return (
+
) -} \ No newline at end of file +} diff --git a/src/main.jsx b/src/main.jsx index c624370..8a069f6 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,9 @@ // 远程调试: 仅在 Tauri WebView 内启用, web 模式自动跳过 import './debug/pagespy.js' +// 移动端 UI 框架: 全局样式 + 主题 (antd-mobile 5) +import 'antd-mobile/es/global' + import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' @@ -16,4 +19,4 @@ ReactDOM.createRoot(document.getElementById('root')).render( , -) \ No newline at end of file +) diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index 70e7ef9..48d4491 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -1,10 +1,16 @@ +import { ErrorBlock, NavBar } from 'antd-mobile' + // 首页: 暂留占位, 后续接入监控告警 / 个性化推荐 +// 使用 antd-mobile ErrorBlock 仿原生空状态页 export default function HomePage() { return ( -
-
⌂
-
首页
-
后续接入: 监控告警 / 个性化推荐 / 收藏的指标
+
+ 首页 +
) -} \ No newline at end of file +} diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 3926c09..7a99a63 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,7 +1,10 @@ import { useState } from 'react' import { useNavigate, useLocation, Navigate } from 'react-router-dom' +import { Button, Card, Checkbox, Input, Toast } from 'antd-mobile' import { useAuth } from '../auth/AuthContext.jsx' +// 登录页 (未走 MobileShell, 独立全屏布局) +// 使用 antd-mobile: Input / Checkbox / Button / Card / Toast export default function LoginPage() { const { user, login } = useAuth() const navigate = useNavigate() @@ -22,6 +25,7 @@ export default function LoginPage() { setBusy(true) try { await login(email.trim(), password, remember) + Toast.show({ icon: 'success', content: '登录成功' }) navigate(from, { replace: true }) } catch (e) { setErr(e.message) @@ -32,30 +36,56 @@ export default function LoginPage() { return (
-
-
iBoard
+ +
iBoard
登录后继续
- {err &&
{err}
} -
- - setEmail(e.target.value)} /> -
-
- - setPassword(e.target.value)} /> -
- - -
- 默认账号: admin@iboard.local
- 首次登录后请尽快修改密码 -
+ + {err &&
{err}
} +
+ + +
+
+ + +
+
+ + 记住登录 (7 天免登录) + +
+ +
+ 默认账号: admin@iboard.local
+ 首次登录后请尽快修改密码 +
+
) -} \ No newline at end of file +} diff --git a/src/pages/MePage.jsx b/src/pages/MePage.jsx index 01f321e..766a73e 100644 --- a/src/pages/MePage.jsx +++ b/src/pages/MePage.jsx @@ -1,36 +1,49 @@ import { useNavigate } from 'react-router-dom' +import { Avatar, List, NavBar, Toast } from 'antd-mobile' import { useAuth } from '../auth/AuthContext.jsx' +// 我的页: 顶部 NavBar + 用户头像卡片 + 信息列表 +// 使用 antd-mobile: NavBar / List / Avatar / Toast export default function MePage() { const { user, logout } = useAuth() const navigate = useNavigate() async function onLogout() { - if (!confirm('确定退出登录?')) return + const confirmed = window.confirm('确定退出登录?') + if (!confirmed) return await logout() + Toast.show({ icon: 'success', content: '已退出' }) navigate('/login', { replace: true }) } return ( -
-
+
+ 我的 +
+ + {(user?.name || '?').slice(0, 1).toUpperCase()} +
{user?.name || '用户'}
{user?.email}
-
-
- 用户 ID - #{user?.id} -
-
- 注册时间 - {user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'} -
-
- 退出登录 - › -
-
+ + 用户 ID + + 注册时间 + + + 退出登录 + +
) -} \ No newline at end of file +} diff --git a/src/pages/MonitorPage.jsx b/src/pages/MonitorPage.jsx index 46294ee..4b46a28 100644 --- a/src/pages/MonitorPage.jsx +++ b/src/pages/MonitorPage.jsx @@ -1,10 +1,16 @@ +import { ErrorBlock, NavBar } from 'antd-mobile' + // 监控: 暂留占位 +// 使用 antd-mobile ErrorBlock 仿原生空状态页 export default function MonitorPage() { return ( -
-
◉
-
监控
-
后续接入: 指标阈值告警 / 时序趋势
+
+ 监控 +
) -} \ No newline at end of file +} diff --git a/src/pages/macro/ArticleDetail.jsx b/src/pages/macro/ArticleDetail.jsx index 16ca7ad..7935c5f 100644 --- a/src/pages/macro/ArticleDetail.jsx +++ b/src/pages/macro/ArticleDetail.jsx @@ -1,8 +1,10 @@ import { useEffect, useState } from 'react' -import { useParams, useNavigate, Link } from 'react-router-dom' +import { useParams, useNavigate } from 'react-router-dom' +import { NavBar } from 'antd-mobile' import { api } from '../../api/client.js' // 文章详情: /macro/articles/:slug +// 使用 antd-mobile NavBar 提供原生风格的返回按钮 export default function ArticleDetail() { const { slug } = useParams() const navigate = useNavigate() @@ -20,16 +22,19 @@ export default function ArticleDetail() { return () => { cancelled = true } }, [slug]) - if (loading) return
加载中...
- if (err) return
{err}
- if (!article) return
文章不存在
- return ( -
-
navigate(-1)} role="button">‹ 返回
-

{article.title}

-
{article.date}
-
+
+ navigate(-1)}>文章详情 + {loading &&
加载中...
} + {err &&
{err}
} + {!loading && !err && !article &&
文章不存在
} + {!loading && !err && article && ( +
+

{article.title}

+
{article.date}
+
+
+ )}
) -} \ No newline at end of file +} diff --git a/src/pages/macro/ArticlesView.jsx b/src/pages/macro/ArticlesView.jsx index e3accd3..4c33cde 100644 --- a/src/pages/macro/ArticlesView.jsx +++ b/src/pages/macro/ArticlesView.jsx @@ -1,12 +1,15 @@ import { useEffect, useState } from 'react' -import { Link } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' +import { List } from 'antd-mobile' import { api } from '../../api/client.js' // 经济文章: 列表, 点进 /macro/articles/:slug +// 使用 antd-mobile List 仿原生设置/通讯录样式 export default function ArticlesView() { const [articles, setArticles] = useState([]) const [err, setErr] = useState('') const [loading, setLoading] = useState(true) + const navigate = useNavigate() useEffect(() => { let cancelled = false @@ -25,15 +28,23 @@ export default function ArticlesView() { return (
经济文章
-
+ {articles.map((a) => ( - -
{a.title}
-
{a.date}
-
{a.excerpt}
- + +
{a.date}
+
{a.excerpt}
+ + } + arrowIcon + clickable + onClick={() => navigate('/macro/articles/' + encodeURIComponent(a.slug))} + /> ))} -
+
) -} \ No newline at end of file +} diff --git a/src/pages/macro/DataView.jsx b/src/pages/macro/DataView.jsx index bc51791..7ce52d5 100644 --- a/src/pages/macro/DataView.jsx +++ b/src/pages/macro/DataView.jsx @@ -1,8 +1,11 @@ import { useEffect, useState } from 'react' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' +import { CapsuleTabs, Card } from 'antd-mobile' import { api } from '../../api/client.js' -// 经济数据: 从 /api/indicators 拉所有指标, 顶上一个 chip 行切换, 下面渲染图表+表格 +// 经济数据: 顶部 CapsuleTabs 选指标, 下面渲染图表 + 表格 +// 使用 antd-mobile: CapsuleTabs / Card +// 多 groupName 的指标: 在 chip 文本中携带分组前缀以便区分 export default function DataView() { const [indicators, setIndicators] = useState([]) const [selected, setSelected] = useState(null) @@ -27,37 +30,32 @@ export default function DataView() { if (err) return
{err}
if (!indicators.length) return
暂无指标数据, 请先执行 npm run db:seed
- // 按 groupName 分组, chip 行按组排列 - const groups = groupBy(indicators, 'groupName') + // 为 chip label 计算: 多个 group 时显示 "分组 / 标题", 单个 group 只显示标题 + const groupSet = new Set(indicators.map((i) => i.groupName || '')) + const showGroupPrefix = groupSet.size > 1 + const labelOf = (i) => { + const t = (i.data && i.data.title) || i.name + if (showGroupPrefix && i.groupName) return i.groupName + ' · ' + t + return t + } + const cur = indicators.find((i) => i.name === selected) const chartData = cur ? buildMonthlySeries(cur.data) : [] return (
经济数据
- {Object.entries(groups).map(([groupName, list]) => ( -
-
{groupName}
-
- {list.map((i) => ( - - ))} -
-
- ))} + + {indicators.map((i) => ( + + ))} + {cur && (
-
-
{cur.data.title || cur.name}
+
- 单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'} + {labelOf(cur)} · 单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'}
@@ -67,7 +65,7 @@ export default function DataView() { - +
@@ -83,27 +81,14 @@ export default function DataView() {
-
+
)}
) } -function groupBy(arr, key) { - return arr.reduce((acc, x) => { - const k = x[key] || '其它' - ;(acc[k] = acc[k] || []).push(x) - return acc - }, {}) -} - -function shortTitle(i) { - if (i.data && i.data.title) return i.data.title - return i.name -} - // 把 data.rows: [{month, value}, ...] 直接返回, 服务端已经按月排好 function buildMonthlySeries(data) { return (data && data.rows) || [] -} \ No newline at end of file +} diff --git a/src/pages/macro/StrategyView.jsx b/src/pages/macro/StrategyView.jsx index d4a6eb4..5f2fd8e 100644 --- a/src/pages/macro/StrategyView.jsx +++ b/src/pages/macro/StrategyView.jsx @@ -1,13 +1,15 @@ import { useEffect, useState } from 'react' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' +import { CapsuleTabs, Card } from 'antd-mobile' import { api } from '../../api/client.js' -// 策略研究: 顶部 chip 行选策略, 下面显示 4 张图 (debt / gdp / debtToGdp / interestToGdp) + 表格 +// 策略研究: 顶部 CapsuleTabs 选策略, 下面 4 张图 + 表格 +// 使用 antd-mobile: CapsuleTabs / Card const SERIES = [ - { key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' }, - { key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' }, - { key: 'debtToGdp', label: '债务/GDP (%)', color: '#4363d8' }, - { key: 'interestToGdp', label: '债务利息/GDP (%)', color: '#f58231' }, + { key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' }, + { key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' }, + { key: 'debtToGdp', label: '债务/GDP (%)', color: '#4363d8' }, + { key: 'interestToGdp', label: '债务利息/GDP (%)', color: '#f58231' }, ] export default function StrategyView() { @@ -40,22 +42,14 @@ export default function StrategyView() { return (
策略研究
-
+ {strategies.map((s) => ( - + ))} -
- + {cur && (
-
-
{cur.description || cur.name}
+
数据 {rows.length} 条 · 来源 {cur.data.source || 'seed'}
@@ -100,9 +94,9 @@ export default function StrategyView() {
-
+
)}
) -} \ No newline at end of file +} diff --git a/src/styles/globals.css b/src/styles/globals.css index 37f382b..17ec4e5 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,513 +1,20 @@ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: #f5f5f5; - color: #333; -} - -.container { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -/* Navbar */ -.navbar { - background-color: #333; - color: white; - padding: 1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.logo { - font-size: 1.5rem; - font-weight: bold; -} - -.logo a { - color: white; - text-decoration: none; -} - -.nav-links { - display: flex; - gap: 1rem; -} - -.nav-links a { - color: white; - text-decoration: none; - padding: 0.5rem; - border-radius: 4px; -} - -.nav-links a:hover { - background-color: #555; -} - -/* Global link styles */ -a { - text-decoration: none; - cursor: pointer; -} - -/* Main content */ -main { - flex: 1; - padding: 2rem; - max-width: 1200px; - margin: 0 auto; - width: 100%; -} - -.title { - font-size: 2.5rem; - margin-bottom: 1rem; - color: #333; -} - -.description { - font-size: 1.2rem; - margin-bottom: 2rem; - color: #666; -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 1.5rem; -} - -.card { - background-color: white; - padding: 1.5rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: transform 0.2s ease; -} - -.card:hover { - transform: translateY(-5px); -} - -.card h2 { - margin-bottom: 0.5rem; - color: #333; -} - -.card p { - color: #666; -} - -/* Footer */ -.footer { - background-color: #333; - color: white; - padding: 1rem; - text-align: center; - margin-top: auto; -} - -.footer-content { - max-width: 1200px; - margin: 0 auto; -} - -/* Data page layout */ -.data-layout { - display: flex; - gap: 2rem; - margin-top: 2rem; -} - -.data-sidebar { - width: 250px; - flex-shrink: 0; -} - -.data-content { - flex: 1; - min-width: 0; -} - -.indicator-group { - margin-bottom: 1.5rem; -} - -.group-title { - font-size: 1.1rem; - font-weight: 600; - color: #333; - margin-bottom: 0.75rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid #ddd; -} - -.indicator-list { - list-style: none; - padding: 0; - margin: 0; -} - -.indicator-item { - padding: 0.75rem 1rem; - margin-bottom: 0.5rem; - background-color: white; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.indicator-item:hover { - background-color: #f0f0f0; - transform: translateX(4px); -} - -.indicator-item.active { - background-color: #333; - color: white; -} - -.indicator-detail { - background-color: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.indicator-title { - font-size: 1.8rem; - color: #333; - margin-bottom: 0.5rem; -} - -.indicator-description { - color: #666; - margin-bottom: 2rem; -} - -.chart-container { - margin-bottom: 3rem; - padding: 1rem; - background-color: #fafafa; - border-radius: 8px; -} - -.data-table-container { - margin-top: 2rem; -} - -.table-title { - font-size: 1.3rem; - color: #333; - margin-bottom: 1rem; -} - -.data-table { - width: 100%; - border-collapse: collapse; - font-size: 0.95rem; -} - -.data-table th, -.data-table td { - padding: 0.75rem 1rem; - text-align: left; - border-bottom: 1px solid #eee; -} - -.data-table th { - background-color: #f5f5f5; - font-weight: 600; - color: #333; -} - -.data-table tbody tr:hover { - background-color: #f9f9f9; -} - -/* Articles section */ -.articles-section { - margin-top: 4rem; - margin-bottom: 2rem; -} - -.section-title { - font-size: 2rem; - color: #333; - margin-bottom: 1.5rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid #ddd; -} - -.articles-grid { - display: flex; - flex-direction: column; - gap: 1rem; - margin-bottom: 2rem; -} - -.article-card { - background-color: white; - padding: 1.5rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; - cursor: pointer; -} - -.article-card:hover { - background-color: #f9f9f9; - transform: translateX(5px); -} - -.article-date { - font-size: 0.9rem; - color: #666; - margin-bottom: 0.75rem; -} - -.article-title { - font-size: 1.2rem; - font-weight: 600; - color: #333; - margin-bottom: 0.75rem; - line-height: 1.4; -} - -.article-excerpt { - color: #666; - line-height: 1.5; - font-size: 0.95rem; -} - -/* Pagination */ -.pagination { - display: flex; - justify-content: flex-end; - gap: 0.5rem; - margin-top: 2rem; -} - -.page-link { - padding: 0.5rem 1rem; - background-color: white; - border: 1px solid #ddd; - border-radius: 4px; - color: #333; - text-decoration: none; - transition: all 0.2s ease; -} - -.page-link:hover { - background-color: #f0f0f0; -} - -.page-link.active { - background-color: #333; - color: white; - border-color: #333; -} - -/* Article detail page */ -.article-detail { - max-width: 800px; - margin: 0 auto; - background-color: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.article-detail-title { - font-size: 2.5rem; - color: #333; - margin-bottom: 1rem; - line-height: 1.2; -} - -.article-detail-date { - font-size: 1rem; - color: #666; - margin-bottom: 2rem; - padding-bottom: 1rem; - border-bottom: 1px solid #eee; -} - -.article-detail-content { - color: #333; - line-height: 1.6; -} - -.article-detail-content h1, -.article-detail-content h2, -.article-detail-content h3, -.article-detail-content h4, -.article-detail-content h5, -.article-detail-content h6 { - margin-top: 2rem; - margin-bottom: 1rem; - color: #333; -} - -.article-detail-content p { - margin-bottom: 1.5rem; -} - -.article-detail-content ul, -.article-detail-content ol { - margin-bottom: 1.5rem; - padding-left: 2rem; -} - -.article-detail-content li { - margin-bottom: 0.5rem; -} - -.article-detail-content img { - max-width: 100%; - height: auto; - margin: 2rem 0; - border-radius: 8px; -} - -@media (max-width: 900px) { - .data-layout { - flex-direction: column; - } - - .data-sidebar { - width: 100%; - } - - .pagination { - justify-content: center; - } - - .article-detail { - padding: 1.5rem; - } - - .article-detail-title { - font-size: 2rem; - } - - .strategy-layout { - flex-direction: column; - } - - .strategy-sidebar { - width: 100%; - } -} - -/* Strategy page layout */ -.strategy-layout { - display: flex; - gap: 2rem; - margin-top: 2rem; -} - -.strategy-sidebar { - width: 250px; - flex-shrink: 0; -} - -.strategy-content { - flex: 1; - min-width: 0; -} - -.sidebar-title { - font-size: 1.1rem; - font-weight: 600; - color: #333; - margin-bottom: 0.75rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid #ddd; -} - -.strategy-list { - list-style: none; - padding: 0; - margin: 0; -} - -.strategy-item { - padding: 0.75rem 1rem; - margin-bottom: 0.5rem; - background-color: white; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.strategy-item:hover { - background-color: #f0f0f0; - transform: translateX(4px); -} - -.strategy-item.active { - background-color: #333; - color: white; -} - -.strategy-detail { - background-color: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.strategy-title { - font-size: 1.8rem; - color: #333; - margin-bottom: 0.5rem; -} - -.strategy-description { - color: #666; - margin-bottom: 2rem; -} - -.chart-section { - margin-bottom: 3rem; -} - -.chart-title { - font-size: 1.3rem; - color: #333; - margin-bottom: 1rem; -} - - -/* ================== 移动端优先布局 (mobile-first) ================== */ +/* ================== 移动端优先布局 (mobile-first, antd-mobile) ================== */ +/* 主题变量: 与 antd-mobile 的 --adm-* 保持一致, 便于组件统一 */ :root { --color-bg: #f5f5f5; --color-card: #ffffff; --color-text: #333333; --color-muted: #666666; - --color-border: #e5e7eb; - --color-primary: #1f2937; - --color-accent: #2563eb; - --color-danger: #dc2626; - --bottom-nav-h: 56px; - --top-tabs-h: 44px; + --color-border: #eeeeee; + --color-primary: #1677ff; + --color-accent: #1677ff; + --color-danger: #ff3141; + --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); + --safe-left: env(safe-area-inset-left, 0px); + --safe-right: env(safe-area-inset-right, 0px); + --tabbar-h: 50px; } * { box-sizing: border-box; margin: 0; padding: 0; } @@ -528,178 +35,86 @@ a { color: inherit; text-decoration: none; cursor: pointer; } button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; } input, textarea, select { font: inherit; } -/* 应用主框架 */ +/* 应用主框架: 全屏 flex 列布局, 不再 max-width 居中 (解决"左右不贴边") */ .app-root { display: flex; flex-direction: column; - min-height: 100vh; - min-height: 100dvh; - max-width: 768px; - margin: 0 auto; + height: 100vh; + height: 100dvh; + width: 100%; + max-width: 100%; background: var(--color-bg); position: relative; + padding-left: var(--safe-left); + padding-right: var(--safe-right); } .app-main { flex: 1 1 auto; - padding-bottom: calc(var(--bottom-nav-h) + var(--safe-bottom) + 8px); + min-height: 0; overflow-y: auto; + overflow-x: hidden; + padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom)); + -webkit-overflow-scrolling: touch; } -.app-main.has-top-tabs { - padding-top: var(--top-tabs-h); -} +/* 带有 antd-mobile NavBar 的页面 (Home/Monitor/Me/Article) */ +.page-with-navbar { display: flex; flex-direction: column; min-height: 100%; } +.page-with-navbar .adm-error-block { flex: 1; } -/* 顶栏 tabs (宏观页用) */ -.top-tabs { - position: sticky; - top: 0; - z-index: 10; - height: var(--top-tabs-h); - display: flex; - background: #fff; - border-bottom: 1px solid var(--color-border); - align-items: stretch; -} - -.top-tabs-item { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - color: var(--color-muted); - position: relative; - user-select: none; -} - -.top-tabs-item.active { - color: var(--color-primary); - font-weight: 600; -} - -.top-tabs-item.active::after { - content: ''; - position: absolute; - left: 30%; - right: 30%; - bottom: 6px; - height: 2px; - background: var(--color-primary); - border-radius: 2px; -} - -/* 滑屏容器 */ -.swipe-container { - position: relative; - overflow: hidden; - touch-action: pan-y; -} - -.swipe-track { - display: flex; - width: 300%; - transform: translateX(var(--swipe-offset, 0%)); - transition: transform 0.25s ease; - will-change: transform; -} - -.swipe-track.no-anim { transition: none; } - -.swipe-pane { - width: 33.3333%; - flex-shrink: 0; - overflow-y: auto; -} - -/* 底部 tab */ -.bottom-nav { +/* 底部 antd-mobile TabBar 定位 */ +.app-tabbar.adm-tab-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; - height: calc(var(--bottom-nav-h) + var(--safe-bottom)); - padding-bottom: var(--safe-bottom); - display: flex; background: #fff; border-top: 1px solid var(--color-border); - max-width: 768px; - margin: 0 auto; } +.tab-icon { display: inline-flex; } -.bottom-nav-item { - flex: 1; +/* 宏观页 SwipeTabs */ +.swipe-tabs-wrap { display: flex; flex-direction: column; - align-items: center; - justify-content: center; - gap: 2px; - font-size: 11px; - color: var(--color-muted); - user-select: none; + height: 100%; + min-height: 0; +} +.swipe-container { + position: relative; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; + touch-action: pan-y; +} +.swipe-track { + display: flex; + width: 300%; + height: 100%; + transform: translateX(var(--swipe-offset, 0%)); + transition: transform 0.25s ease; + will-change: transform; +} +.swipe-track.no-anim { transition: none; } +.swipe-pane { + width: 33.3333%; + flex-shrink: 0; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; } -.bottom-nav-item .icon { font-size: 20px; line-height: 1; } -.bottom-nav-item.active { color: var(--color-primary); font-weight: 600; } +/* ================== 通用组件辅助类 ================== */ -/* 通用卡片 */ -.card { background: #fff; border-radius: 10px; padding: 14px 16px; box-shadow: 0 1px 2px rgba(0,0,0,.04); } -.card + .card { margin-top: 10px; } - -.page-title { font-size: 20px; font-weight: 700; padding: 14px 16px 4px; } -.page-desc { font-size: 13px; color: var(--color-muted); padding: 0 16px 12px; } -.section-title { font-size: 16px; font-weight: 600; padding: 14px 16px 8px; } - -/* 列表 */ -.list { padding: 0 12px; } -.list-item { background: #fff; padding: 14px; border-radius: 10px; margin-bottom: 10px; box-shadow: 0 1px 2px rgba(0,0,0,.04); display: block; } -.list-item-title { font-size: 15px; font-weight: 600; margin-bottom: 4px; line-height: 1.4; } -.list-item-meta { font-size: 12px; color: var(--color-muted); margin-bottom: 6px; } -.list-item-excerpt { font-size: 13px; color: var(--color-muted); line-height: 1.5; } - -/* 图表 */ -.chart-wrap { background: #fafafa; border-radius: 8px; padding: 8px; margin: 8px 0 16px; } - -/* 表格 */ -.data-table { width: 100%; border-collapse: collapse; font-size: 13px; } -.data-table th, .data-table td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--color-border); } -.data-table th { background: #f8f8f8; font-weight: 600; } - -/* 登录 */ -.login-page { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; background: linear-gradient(180deg, #1f2937 0%, #1f2937 35%, #f5f5f5 35%); } -.login-card { background: #fff; width: 100%; max-width: 380px; border-radius: 14px; padding: 24px; box-shadow: 0 4px 24px rgba(0,0,0,.1); } -.login-title { font-size: 20px; font-weight: 700; text-align: center; margin-bottom: 6px; } -.login-subtitle { text-align: center; color: var(--color-muted); margin-bottom: 20px; font-size: 13px; } -.login-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } -.login-field label { font-size: 12px; color: var(--color-muted); } -.login-field input { padding: 12px; border: 1px solid var(--color-border); border-radius: 8px; outline: none; } -.login-field input:focus { border-color: var(--color-primary); } -.login-options { display: flex; align-items: center; gap: 6px; margin-bottom: 18px; font-size: 13px; color: var(--color-muted); } -.login-btn { width: 100%; padding: 13px; background: var(--color-primary); color: #fff; border-radius: 8px; font-weight: 600; } -.login-btn:disabled { opacity: 0.6; } -.login-hint { text-align: center; font-size: 12px; color: var(--color-muted); margin-top: 14px; line-height: 1.6; } -.login-error { background: #fef2f2; color: var(--color-danger); padding: 10px; border-radius: 6px; font-size: 13px; margin-bottom: 12px; } - -/* 我 */ -.me-header { background: var(--color-primary); color: #fff; padding: 30px 16px 24px; } -.me-name { font-size: 18px; font-weight: 600; } -.me-email { font-size: 13px; opacity: .8; margin-top: 2px; } -.me-list { padding: 12px; } -.me-list .list-item { display: flex; align-items: center; justify-content: space-between; } -.me-list .list-item .label { font-size: 14px; } -.me-list .list-item .value { font-size: 13px; color: var(--color-muted); } -.me-list .danger { color: var(--color-danger); } - -.loading, .page-loading { padding: 40px; text-align: center; color: var(--color-muted); } -.empty { padding: 40px; text-align: center; color: var(--color-muted); font-size: 13px; } -.error-banner { background: #fef2f2; color: var(--color-danger); padding: 10px 14px; font-size: 13px; border-radius: 6px; margin: 12px; } - -/* 占位页 */ -.placeholder-page { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; padding: 24px; text-align: center; color: var(--color-muted); gap: 12px; } -.placeholder-page .icon { font-size: 48px; opacity: .6; } +/* 图表卡片: 用于 DataView/StrategyView */ +.chart-card.adm-card { padding: 12px; } +.chart-card.adm-card .adm-card-body { padding: 0; } /* 文章详情 */ +.article-page { display: flex; flex-direction: column; min-height: 100%; } +.article-page .adm-nav-bar { background: #fff; border-bottom: 1px solid var(--color-border); } .article-detail { background: #fff; padding: 18px 16px 24px; } .article-detail h1 { font-size: 20px; line-height: 1.35; margin-bottom: 8px; } .article-detail .meta { font-size: 12px; color: var(--color-muted); margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border); } @@ -708,10 +123,48 @@ input, textarea, select { font: inherit; } .article-content p { margin-bottom: 0.9em; } .article-content ul, .article-content ol { margin: 0 0 1em 1.4em; } .article-content img { max-width: 100%; border-radius: 6px; margin: 0.6em 0; } -.article-back { display: inline-flex; align-items: center; gap: 4px; padding: 8px 0; color: var(--color-accent); font-size: 13px; } -/* 侧边栏式选择器 (移动端改为顶部 chip 行) */ -.chip-row { display: flex; gap: 8px; overflow-x: auto; padding: 8px 12px; } -.chip-row::-webkit-scrollbar { display: none; } -.chip { padding: 7px 12px; background: #fff; border: 1px solid var(--color-border); border-radius: 999px; font-size: 13px; white-space: nowrap; } -.chip.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); } +/* 文章列表项 (用于 ArticlesView) */ +.articles-list .adm-list-item-description { color: var(--color-muted); } +.list-item-meta { font-size: 12px; color: var(--color-muted); margin-bottom: 4px; } +.list-item-excerpt { font-size: 13px; color: var(--color-muted); line-height: 1.5; } + +/* 图表 / 表格 */ +.chart-wrap { background: #fafafa; border-radius: 8px; padding: 8px; margin: 8px 0 16px; } +.data-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.data-table th, .data-table td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--color-border); } +.data-table th { background: #f8f8f8; font-weight: 600; } + +/* 页面标题 */ +.page-title { font-size: 20px; font-weight: 700; padding: 14px 16px 4px; } + +/* 通用状态 */ +.loading, .page-loading { padding: 40px; text-align: center; color: var(--color-muted); } +.empty { padding: 40px; text-align: center; color: var(--color-muted); font-size: 13px; } +.error-banner { background: #fef2f2; color: var(--color-danger); padding: 10px 14px; font-size: 13px; border-radius: 6px; margin: 12px; } + +/* ================== 登录页 (独立全屏, 未走 MobileShell) ================== */ +.login-page { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; background: linear-gradient(180deg, #1f2937 0%, #1f2937 38%, #f5f5f5 38%); } +.login-form { width: 100%; max-width: 380px; } +.login-brand { color: #fff; font-size: 28px; font-weight: 700; text-align: center; margin-bottom: 4px; } +.login-subtitle { color: rgba(255,255,255,.7); text-align: center; margin-bottom: 16px; font-size: 13px; } +.login-card.adm-card { border-radius: 14px; } +.login-card.adm-card .adm-card-body { padding: 22px 18px; } +.login-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } +.login-field label { font-size: 12px; color: var(--color-muted); } +.login-field .adm-input { padding: 8px 10px; border: 1px solid var(--color-border); border-radius: 8px; } +.login-field .adm-input:focus-within { border-color: var(--color-primary); } +.login-options { margin-bottom: 18px; } +.login-hint { text-align: center; font-size: 12px; color: var(--color-muted); margin-top: 14px; line-height: 1.6; } +.login-error { background: #fef2f2; color: var(--color-danger); padding: 10px; border-radius: 6px; font-size: 13px; margin-bottom: 12px; } + +/* ================== 我的页 ================== */ +.me-page { display: flex; flex-direction: column; min-height: 100%; background: var(--color-bg); } +.me-navbar.adm-nav-bar { background: #fff; border-bottom: 1px solid var(--color-border); } +.me-profile { background: linear-gradient(135deg, #1677ff, #4096ff); color: #fff; padding: 24px 16px 28px; display: flex; flex-direction: column; align-items: center; gap: 6px; } +.me-avatar { background: rgba(255,255,255,.2); color: #fff; font-size: 28px; } +.me-name { font-size: 18px; font-weight: 600; } +.me-email { font-size: 13px; opacity: .85; } +.me-list { margin-top: 12px; } +.me-list .adm-list-item { padding-left: 16px; padding-right: 16px; } +.me-logout { color: var(--color-danger); } \ No newline at end of file