更换 UI
This commit is contained in:
@@ -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 (
|
|
||||||
<div className={`time-range-picker ${className}`}>
|
|
||||||
<button
|
|
||||||
className="time-range-btn"
|
|
||||||
onClick={() => setShowDropdown(!showDropdown)}
|
|
||||||
>
|
|
||||||
{timeRange === '1m' && '近一个月'}
|
|
||||||
{timeRange === '3m' && '近三个月'}
|
|
||||||
{timeRange === '1y' && '近一年'}
|
|
||||||
{timeRange === 'custom' && startDate && endDate && `${startDate} 至 ${endDate}`}
|
|
||||||
{timeRange === 'custom' && (!startDate || !endDate) && '自定义'}
|
|
||||||
<span className="dropdown-arrow">▼</span>
|
|
||||||
</button>
|
|
||||||
{showDropdown && (
|
|
||||||
<div className="time-range-dropdown">
|
|
||||||
<div className="dropdown-section">
|
|
||||||
<h4 className="dropdown-title">自定义范围</h4>
|
|
||||||
<div className="custom-date-range">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={startDate}
|
|
||||||
onChange={handleStartDateChange}
|
|
||||||
max={endDate || new Date().toISOString().split('T')[0]}
|
|
||||||
/>
|
|
||||||
<span>至</span>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={endDate}
|
|
||||||
onChange={handleEndDateChange}
|
|
||||||
min={startDate}
|
|
||||||
max={new Date().toISOString().split('T')[0]}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="apply-btn"
|
|
||||||
onClick={handleCustomRangeApply}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="dropdown-section">
|
|
||||||
<h4 className="dropdown-title">快捷选项</h4>
|
|
||||||
<ul className="dropdown-options">
|
|
||||||
<li
|
|
||||||
className={`dropdown-option ${timeRange === '1m' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleTimeRangeChange('1m')}
|
|
||||||
>
|
|
||||||
近一个月
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
className={`dropdown-option ${timeRange === '3m' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleTimeRangeChange('3m')}
|
|
||||||
>
|
|
||||||
近三个月
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
className={`dropdown-option ${timeRange === '1y' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleTimeRangeChange('1y')}
|
|
||||||
>
|
|
||||||
近一年
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TimeRangePicker
|
|
||||||
Generated
+367
-1
@@ -11,6 +11,7 @@
|
|||||||
"@huolala-tech/page-spy": "^1.5.7",
|
"@huolala-tech/page-spy": "^1.5.7",
|
||||||
"@prisma/adapter-mariadb": "^7.9.1",
|
"@prisma/adapter-mariadb": "^7.9.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
|
"antd-mobile": "^5.42.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
@@ -766,6 +767,31 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/@hono/node-server": {
|
||||||
"version": "1.19.11",
|
"version": "1.19.11",
|
||||||
"resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.11.tgz",
|
"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": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.11.2",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
@@ -2398,6 +2509,12 @@
|
|||||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "26.1.2",
|
"version": "26.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-26.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/node/-/node-26.1.2.tgz",
|
||||||
@@ -2423,6 +2540,24 @@
|
|||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
@@ -2457,6 +2592,28 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/ajv": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz",
|
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz",
|
||||||
@@ -2499,6 +2656,56 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"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": {
|
"node_modules/argparse": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
@@ -2514,6 +2721,12 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -2856,6 +3069,12 @@
|
|||||||
"consola": "^3.2.3"
|
"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": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -3232,6 +3451,12 @@
|
|||||||
"node": ">=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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -3256,6 +3481,15 @@
|
|||||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/deepmerge-ts": {
|
||||||
"version": "7.1.5",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
"resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
@@ -4203,6 +4437,13 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -4259,6 +4500,12 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -4374,6 +4621,12 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -4593,6 +4846,12 @@
|
|||||||
"node": ">=8.0.0"
|
"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": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.16",
|
"version": "3.3.16",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz",
|
||||||
@@ -4973,6 +5232,75 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/rc9": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz",
|
||||||
@@ -5008,6 +5336,12 @@
|
|||||||
"react": "^18.3.1"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
"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==",
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
|
||||||
@@ -5227,6 +5567,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"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": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.8.2",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
|
"resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
@@ -5272,6 +5618,18 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"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": {
|
"node_modules/section-matter": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||||
@@ -5566,6 +5924,15 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -5678,7 +6045,6 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@huolala-tech/page-spy": "^1.5.7",
|
"@huolala-tech/page-spy": "^1.5.7",
|
||||||
"@prisma/adapter-mariadb": "^7.9.1",
|
"@prisma/adapter-mariadb": "^7.9.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
|
"antd-mobile": "^5.42.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|||||||
@@ -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;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
-721
@@ -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 (
|
|
||||||
<div className="container">
|
|
||||||
<Head>
|
|
||||||
<title>数据 - iBoard</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<Navbar />
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<h1 className="title">
|
|
||||||
经济数据
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="description">
|
|
||||||
各种经济数据和图表
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="data-layout">
|
|
||||||
<div className="data-sidebar">
|
|
||||||
{indicators.map((group, groupIndex) => (
|
|
||||||
<div key={groupIndex} className="indicator-group">
|
|
||||||
<h3 className="group-title">{group.group}</h3>
|
|
||||||
<ul className="indicator-list">
|
|
||||||
{group.items.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
className={`indicator-item ${selectedIndicator === item.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSelectedIndicator(item.id)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-content">
|
|
||||||
{selectedIndicator === 'bond-china-treasury' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国国债收益率走势</h2>
|
|
||||||
<p className="indicator-description">展示一年期、五年期、十年期国债收益率的变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaBondData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="oneYear" name="一年期" stroke="#8884d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line type="monotone" dataKey="fiveYear" name="五年期" stroke="#82ca9d" strokeWidth={2} />
|
|
||||||
<Line type="monotone" dataKey="tenYear" name="十年期" stroke="#ffc658" strokeWidth={2} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>一年期(%)</th>
|
|
||||||
<th>五年期(%)</th>
|
|
||||||
<th>十年期(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaBondData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.oneYear.toFixed(2)}</td>
|
|
||||||
<td>{row.fiveYear.toFixed(2)}</td>
|
|
||||||
<td>{row.tenYear.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'bond-us-treasury' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">美国国债收益率走势</h2>
|
|
||||||
<p className="indicator-description">展示一年期、五年期、十年期国债收益率的变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={usBondData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="oneYear" name="一年期" stroke="#ff7300" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line type="monotone" dataKey="fiveYear" name="五年期" stroke="#0088fe" strokeWidth={2} />
|
|
||||||
<Line type="monotone" dataKey="tenYear" name="十年期" stroke="#00C49F" strokeWidth={2} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>一年期(%)</th>
|
|
||||||
<th>五年期(%)</th>
|
|
||||||
<th>十年期(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usBondData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.oneYear.toFixed(2)}</td>
|
|
||||||
<td>{row.fiveYear.toFixed(2)}</td>
|
|
||||||
<td>{row.tenYear.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'loan-china-lpr' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国LPR走势</h2>
|
|
||||||
<p className="indicator-description">展示一年期和五年期贷款市场报价利率(LPR)的变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaLPRData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="oneYear" name="一年期LPR" stroke="#e6194b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line type="monotone" dataKey="fiveYear" name="五年期LPR" stroke="#3cb44b" strokeWidth={2} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>一年期LPR(%)</th>
|
|
||||||
<th>五年期LPR(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaLPRData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.oneYear.toFixed(2)}</td>
|
|
||||||
<td>{row.fiveYear.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-china-gold-monthly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国黄金储备(月度)</h2>
|
|
||||||
<p className="indicator-description">展示中国央行黄金储备的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaGoldReserveMonthly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 20', 'dataMax + 20']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#ffd700" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaGoldReserveMonthly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-us-gold-monthly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">美国黄金储备(月度)</h2>
|
|
||||||
<p className="indicator-description">展示美国央行黄金储备的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={usGoldReserveMonthly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={[8130, 8140]} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#333333" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usGoldReserveMonthly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-china-gold-yearly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国黄金储备(年度)</h2>
|
|
||||||
<p className="indicator-description">展示中国央行黄金储备的年度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaGoldReserveYearly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 50', 'dataMax + 50']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#ffd700" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaGoldReserveYearly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-us-gold-yearly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">美国黄金储备(年度)</h2>
|
|
||||||
<p className="indicator-description">展示美国央行黄金储备的年度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={usGoldReserveYearly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={[8130, 8140]} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#333333" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usGoldReserveYearly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-usd-cny-rate' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">人民币兑美元汇率</h2>
|
|
||||||
<p className="indicator-description">展示人民币兑美元汇率的变化趋势</p>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="loading">加载中...</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="error">{error}</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="chart-header">
|
|
||||||
<TimeRangePicker
|
|
||||||
value={timeRange}
|
|
||||||
onChange={handleTimeRangeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={exchangeRateData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="date" />
|
|
||||||
<YAxis domain={['dataMin - 0.1', 'dataMax + 0.1']} tickFormatter={(value) => value.toFixed(4)} />
|
|
||||||
<Tooltip formatter={(value) => [value.toFixed(4), '汇率']} />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="rate" name="汇率" stroke="#1e88e5" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>日期</th>
|
|
||||||
<th>汇率</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{exchangeRateData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.date}</td>
|
|
||||||
<td>{row.rate.toFixed(4)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'commodity-pork-price' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">猪肉价格</h2>
|
|
||||||
<p className="indicator-description">展示中国猪肉价格的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={porkPriceData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="price" name="猪肉价格(元/公斤)" stroke="#ff6b6b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>猪肉价格(元/公斤)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{porkPriceData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.price.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'commodity-sow-inventory' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">能繁母猪数量</h2>
|
|
||||||
<p className="indicator-description">展示中国能繁母猪数量的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={sowInventoryData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 50', 'dataMax + 50']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="inventory" name="能繁母猪数量(万头)" stroke="#4ecdc4" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>能繁母猪数量(万头)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{sowInventoryData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.inventory}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<div className="container">
|
|
||||||
<Head>
|
|
||||||
<title>策略 - iBoard</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<Navbar />
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<h1 className="title">
|
|
||||||
经济策略
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="description">
|
|
||||||
按照经济理论组织的相关数据和图表的对比
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="strategy-layout">
|
|
||||||
<div className="strategy-sidebar">
|
|
||||||
<h3 className="sidebar-title">策略列表</h3>
|
|
||||||
<ul className="strategy-list">
|
|
||||||
{strategies.map((strategy) => (
|
|
||||||
<li
|
|
||||||
key={strategy.id}
|
|
||||||
className={`strategy-item ${selectedStrategy === strategy.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSelectedStrategy(strategy.id)}
|
|
||||||
>
|
|
||||||
{strategy.name}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="strategy-content">
|
|
||||||
{selectedStrategy === 'us-debt-gdp' && (
|
|
||||||
<div className="strategy-detail">
|
|
||||||
<h2 className="strategy-title">美国政府债务/GDP分析</h2>
|
|
||||||
<p className="strategy-description">
|
|
||||||
分析美国政府债务、GDP、债务/GDP比率以及债务利息/GDP比率的走势
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国政府债务走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 2', 'dataMax + 2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debt" name="美国政府债务(万亿美元)" stroke="#8884d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="gdp" name="美国GDP(万亿美元)" stroke="#82ca9d" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国政府债务/GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 5', 'dataMax + 5']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debtToGdp" name="债务/GDP比率(%)" stroke="#ffc658" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国政府债务利息/GDP</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="interestToGdp" name="债务利息/GDP比率(%)" stroke="#ff7300" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>政府债务(万亿美元)</th>
|
|
||||||
<th>GDP(万亿美元)</th>
|
|
||||||
<th>债务/GDP(%)</th>
|
|
||||||
<th>债务利息/GDP(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usDebtData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.debt.toFixed(1)}</td>
|
|
||||||
<td>{row.gdp.toFixed(1)}</td>
|
|
||||||
<td>{row.debtToGdp.toFixed(1)}</td>
|
|
||||||
<td>{row.interestToGdp.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedStrategy === 'china-debt-gdp' && (
|
|
||||||
<div className="strategy-detail">
|
|
||||||
<h2 className="strategy-title">中国政府债务/GDP分析</h2>
|
|
||||||
<p className="strategy-description">
|
|
||||||
分析中国政府债务、GDP、债务/GDP比率以及债务利息/GDP比率的走势
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国政府债务走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debt" name="中国政府债务(万亿美元)" stroke="#e6194b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="gdp" name="中国GDP(万亿美元)" stroke="#3cb44b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国政府债务/GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 5', 'dataMax + 5']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debtToGdp" name="债务/GDP比率(%)" stroke="#4363d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国政府债务利息/GDP</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="interestToGdp" name="债务利息/GDP比率(%)" stroke="#f58231" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>政府债务(万亿美元)</th>
|
|
||||||
<th>GDP(万亿美元)</th>
|
|
||||||
<th>债务/GDP(%)</th>
|
|
||||||
<th>债务利息/GDP(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaDebtData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.debt.toFixed(1)}</td>
|
|
||||||
<td>{row.gdp.toFixed(1)}</td>
|
|
||||||
<td>{row.debtToGdp.toFixed(1)}</td>
|
|
||||||
<td>{row.interestToGdp.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedStrategy === 'sh300-exchange-rate' && (
|
|
||||||
<div className="strategy-detail">
|
|
||||||
<h2 className="strategy-title">沪深300与汇率对比分析</h2>
|
|
||||||
<p className="strategy-description">
|
|
||||||
对比沪深300指数收盘价与人民币兑美元汇率的走势关系
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{sh300Loading ? (
|
|
||||||
<div className="loading">加载中...</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="chart-header">
|
|
||||||
<TimeRangePicker
|
|
||||||
value={timeRange}
|
|
||||||
onChange={handleTimeRangeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">沪深300与汇率对比(双Y轴)</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={sh300ExchangeData} margin={{ top: 20, right: 60, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="date" />
|
|
||||||
<YAxis yAxisId="left" domain={['auto', 'auto']} allowDataOverflow={true} />
|
|
||||||
<YAxis yAxisId="right" orientation="right" domain={['auto', 'auto']} allowDataOverflow={true} tickFormatter={(value) => value.toFixed(4)} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line yAxisId="left" type="monotone" dataKey="sh300" name="沪深300收盘价" stroke="#8884d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line yAxisId="right" type="monotone" dataKey="exchangeRate" name="汇率" stroke="#82ca9d" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 = [
|
const TABS = [
|
||||||
{ to: '/', label: '首页', icon: '⌂' },
|
{ key: '/', name: 'home', label: '首页' },
|
||||||
{ to: '/macro', label: '宏观', icon: '◫' },
|
{ key: '/macro', name: 'macro', label: '宏观' },
|
||||||
{ to: '/monitor', label: '监控', icon: '◉' },
|
{ key: '/monitor', name: 'monitor', label: '监控' },
|
||||||
{ to: '/me', label: '我', icon: '☺' },
|
{ 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() {
|
export default function BottomNav() {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const activeKey = activeKeyFromPath(pathname)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bottom-nav" aria-label="主导航">
|
<TabBar
|
||||||
{TABS.map((t) => (
|
activeKey={activeKey}
|
||||||
<NavLink
|
safeArea
|
||||||
key={t.to}
|
className="app-tabbar"
|
||||||
to={t.to}
|
onChange={navigate}
|
||||||
end={t.to === '/'}
|
|
||||||
className={({ isActive }) => 'bottom-nav-item' + (isActive ? ' active' : '')}
|
|
||||||
>
|
>
|
||||||
<span className="icon" aria-hidden="true">{t.icon}</span>
|
{TABS.map((t) => (
|
||||||
<span>{t.label}</span>
|
<TabBar.Item
|
||||||
</NavLink>
|
key={t.key}
|
||||||
|
title={t.label}
|
||||||
|
icon={(active) => <TabIcon name={t.name} active={active} />}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</TabBar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useRef, useState, useEffect } from 'react'
|
import { useRef, useState, useEffect } from 'react'
|
||||||
|
import { Tabs } from 'antd-mobile'
|
||||||
|
|
||||||
// 滑屏容器 + 顶栏 tabs
|
// 滑屏容器 + 顶栏 tabs
|
||||||
|
// - 顶部: antd-mobile Tabs (带底部激活线动画, 仿原生)
|
||||||
|
// - 下方: 横向滑屏容器, 手势切换 tab
|
||||||
// props:
|
// props:
|
||||||
// tabs: [{ key, label, render }]
|
// tabs: [{ key, label, render }]
|
||||||
// active: 当前激活 key
|
// active: 当前激活 key
|
||||||
@@ -12,10 +15,21 @@ export default function SwipeTabs({ tabs, active, onChange }) {
|
|||||||
const [animOff, setAnimOff] = useState(false)
|
const [animOff, setAnimOff] = useState(false)
|
||||||
const startX = useRef(0)
|
const startX = useRef(0)
|
||||||
const containerRef = useRef(null)
|
const containerRef = useRef(null)
|
||||||
|
const widthRef = useRef(0)
|
||||||
|
|
||||||
// idx 变化时关闭拖动动画 (避免快速切换时滑动残影)
|
// idx 变化时关闭拖动动画 (避免快速切换时滑动残影)
|
||||||
useEffect(() => { setOffsetPx(0); setAnimOff(false) }, [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) {
|
function onTouchStart(e) {
|
||||||
startX.current = e.touches[0].clientX
|
startX.current = e.touches[0].clientX
|
||||||
setDragging(true)
|
setDragging(true)
|
||||||
@@ -24,7 +38,7 @@ export default function SwipeTabs({ tabs, active, onChange }) {
|
|||||||
function onTouchMove(e) {
|
function onTouchMove(e) {
|
||||||
if (!dragging) return
|
if (!dragging) return
|
||||||
const dx = e.touches[0].clientX - startX.current
|
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)
|
const limited = Math.max(Math.min(dx, w * 0.4), -w * 0.4)
|
||||||
setOffsetPx(limited)
|
setOffsetPx(limited)
|
||||||
@@ -33,7 +47,7 @@ export default function SwipeTabs({ tabs, active, onChange }) {
|
|||||||
if (!dragging) return
|
if (!dragging) return
|
||||||
setDragging(false)
|
setDragging(false)
|
||||||
setAnimOff(false)
|
setAnimOff(false)
|
||||||
const w = containerRef.current?.offsetWidth || window.innerWidth
|
const w = widthRef.current || window.innerWidth
|
||||||
const dx = offsetPx
|
const dx = offsetPx
|
||||||
let next = idx
|
let next = idx
|
||||||
if (dx < -w * 0.18 && idx < tabs.length - 1) next = idx + 1
|
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 作为基础位移, 再叠加拖动偏移
|
// 用 -33.33% × idx 作为基础位移, 再叠加拖动偏移
|
||||||
const basePct = idx * 33.3333
|
const basePct = idx * 33.3333
|
||||||
const dragPct = containerRef.current
|
const dragPct = widthRef.current
|
||||||
? (offsetPx / containerRef.current.offsetWidth) * 100
|
? (offsetPx / widthRef.current) * 100
|
||||||
: 0
|
: 0
|
||||||
const translate = `calc(-${basePct}% + ${dragPct}%)`
|
const translate = `calc(-${basePct}% + ${dragPct}%)`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="swipe-tabs-wrap">
|
||||||
<div className="top-tabs" role="tablist">
|
<Tabs activeKey={active} onChange={onChange}>
|
||||||
{tabs.map((t) => (
|
{tabs.map((t) => (
|
||||||
<button
|
<Tabs.Tab key={t.key} title={t.label} />
|
||||||
key={t.key}
|
|
||||||
role="tab"
|
|
||||||
aria-selected={t.key === active}
|
|
||||||
className={'top-tabs-item' + (t.key === active ? ' active' : '')}
|
|
||||||
onClick={() => onChange(t.key)}
|
|
||||||
>
|
|
||||||
{t.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</Tabs>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="swipe-container"
|
className="swipe-container"
|
||||||
@@ -74,13 +80,13 @@ export default function SwipeTabs({ tabs, active, onChange }) {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={'swipe-track' + (animOff || dragging ? ' no-anim' : '')}
|
className={'swipe-track' + (animOff || dragging ? ' no-anim' : '')}
|
||||||
style={{ '--swipe-offset': translate, transform: `translateX(${translate})` }}
|
style={{ transform: `translateX(${translate})` }}
|
||||||
>
|
>
|
||||||
{tabs.map((t) => (
|
{tabs.map((t) => (
|
||||||
<div key={t.key} className="swipe-pane">{t.render()}</div>
|
<div key={t.key} className="swipe-pane">{t.render()}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// 底部 TabBar 图标: 简洁线性 SVG, 仿原生 App 风格
|
||||||
|
// props: name 字符串, active 布尔
|
||||||
|
// 返回对应的 24x24 SVG 节点
|
||||||
|
const ICONS = {
|
||||||
|
home: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M3 11l9-7 9 7" />
|
||||||
|
<path d="M5 10v10h14V10" />
|
||||||
|
{active ? <path d="M10 20v-5h4v5" fill="currentColor" stroke="none" /> : null}
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
macro: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="3" y="3" width="7" height="9" rx="1" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
||||||
|
<rect x="14" y="3" width="7" height="5" rx="1" />
|
||||||
|
<rect x="14" y="12" width="7" height="9" rx="1" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
||||||
|
<rect x="3" y="16" width="7" height="5" rx="1" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
monitor: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M3 12h4l2-5 4 10 2-5h6" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.12 : 0} />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
me: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="8" r="4" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
||||||
|
<path d="M4 20c1.5-4 4.5-6 8-6s6.5 2 8 6" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TabIcon({ name, active }) {
|
||||||
|
const render = ICONS[name]
|
||||||
|
if (!render) return null
|
||||||
|
return <span className="tab-icon">{render(!!active)}</span>
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
|
import { SafeArea } from 'antd-mobile'
|
||||||
import BottomNav from '../components/BottomNav.jsx'
|
import BottomNav from '../components/BottomNav.jsx'
|
||||||
|
|
||||||
// 移动端应用外壳: 底栏 + 滚动主区
|
// 移动端应用外壳
|
||||||
|
// - 顶部 SafeArea: 让出手机状态栏高度
|
||||||
|
// - 中间 <main>: flex:1 可滚动主区
|
||||||
|
// - 底部 TabBar: antd-mobile TabBar 自带底部安全区
|
||||||
|
// 解决: 状态栏遮挡/底部 TabBar 遮挡/左右贴边
|
||||||
export default function MobileShell() {
|
export default function MobileShell() {
|
||||||
return (
|
return (
|
||||||
<div className="app-root">
|
<div className="app-root">
|
||||||
|
<SafeArea position="top" />
|
||||||
<main className="app-main">
|
<main className="app-main">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// 远程调试: 仅在 Tauri WebView 内启用, web 模式自动跳过
|
// 远程调试: 仅在 Tauri WebView 内启用, web 模式自动跳过
|
||||||
import './debug/pagespy.js'
|
import './debug/pagespy.js'
|
||||||
|
|
||||||
|
// 移动端 UI 框架: 全局样式 + 主题 (antd-mobile 5)
|
||||||
|
import 'antd-mobile/es/global'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
|||||||
+10
-4
@@ -1,10 +1,16 @@
|
|||||||
|
import { ErrorBlock, NavBar } from 'antd-mobile'
|
||||||
|
|
||||||
// 首页: 暂留占位, 后续接入监控告警 / 个性化推荐
|
// 首页: 暂留占位, 后续接入监控告警 / 个性化推荐
|
||||||
|
// 使用 antd-mobile ErrorBlock 仿原生空状态页
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="placeholder-page">
|
<div className="page-with-navbar">
|
||||||
<div className="icon">⌂</div>
|
<NavBar back={null}>首页</NavBar>
|
||||||
<div>首页</div>
|
<ErrorBlock
|
||||||
<div style={{ fontSize: 12, opacity: .7 }}>后续接入: 监控告警 / 个性化推荐 / 收藏的指标</div>
|
status="empty"
|
||||||
|
title="首页"
|
||||||
|
description="后续接入: 监控告警 / 个性化推荐 / 收藏的指标"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+41
-11
@@ -1,7 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate, useLocation, Navigate } from 'react-router-dom'
|
import { useNavigate, useLocation, Navigate } from 'react-router-dom'
|
||||||
|
import { Button, Card, Checkbox, Input, Toast } from 'antd-mobile'
|
||||||
import { useAuth } from '../auth/AuthContext.jsx'
|
import { useAuth } from '../auth/AuthContext.jsx'
|
||||||
|
|
||||||
|
// 登录页 (未走 MobileShell, 独立全屏布局)
|
||||||
|
// 使用 antd-mobile: Input / Checkbox / Button / Card / Toast
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { user, login } = useAuth()
|
const { user, login } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -22,6 +25,7 @@ export default function LoginPage() {
|
|||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
await login(email.trim(), password, remember)
|
await login(email.trim(), password, remember)
|
||||||
|
Toast.show({ icon: 'success', content: '登录成功' })
|
||||||
navigate(from, { replace: true })
|
navigate(from, { replace: true })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(e.message)
|
setErr(e.message)
|
||||||
@@ -32,29 +36,55 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<form className="login-card" onSubmit={onSubmit}>
|
<form className="login-form" onSubmit={onSubmit}>
|
||||||
<div className="login-title">iBoard</div>
|
<div className="login-brand">iBoard</div>
|
||||||
<div className="login-subtitle">登录后继续</div>
|
<div className="login-subtitle">登录后继续</div>
|
||||||
|
<Card className="login-card">
|
||||||
{err && <div className="login-error">{err}</div>}
|
{err && <div className="login-error">{err}</div>}
|
||||||
<div className="login-field">
|
<div className="login-field">
|
||||||
<label htmlFor="email">邮箱</label>
|
<label htmlFor="email">邮箱</label>
|
||||||
<input id="email" type="email" autoComplete="username" required value={email} onChange={(e) => setEmail(e.target.value)} />
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="username"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={setEmail}
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="login-field">
|
<div className="login-field">
|
||||||
<label htmlFor="password">密码</label>
|
<label htmlFor="password">密码</label>
|
||||||
<input id="password" type="password" autoComplete="current-password" required value={password} onChange={(e) => setPassword(e.target.value)} />
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={setPassword}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="login-options">
|
<div className="login-options">
|
||||||
<input type="checkbox" checked={remember} onChange={(e) => setRemember(e.target.checked)} />
|
<Checkbox checked={remember} onChange={setRemember} block>
|
||||||
<span>记住登录 (7 天免登录)</span>
|
记住登录 (7 天免登录)
|
||||||
</label>
|
</Checkbox>
|
||||||
<button className="login-btn" type="submit" disabled={busy}>
|
</div>
|
||||||
{busy ? '登录中...' : '登录'}
|
<Button
|
||||||
</button>
|
block
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
loading={busy}
|
||||||
|
loadingText="登录中"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
<div className="login-hint">
|
<div className="login-hint">
|
||||||
默认账号: admin@iboard.local<br />
|
默认账号: admin@iboard.local<br />
|
||||||
首次登录后请尽快修改密码
|
首次登录后请尽快修改密码
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+30
-17
@@ -1,36 +1,49 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Avatar, List, NavBar, Toast } from 'antd-mobile'
|
||||||
import { useAuth } from '../auth/AuthContext.jsx'
|
import { useAuth } from '../auth/AuthContext.jsx'
|
||||||
|
|
||||||
|
// 我的页: 顶部 NavBar + 用户头像卡片 + 信息列表
|
||||||
|
// 使用 antd-mobile: NavBar / List / Avatar / Toast
|
||||||
export default function MePage() {
|
export default function MePage() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
async function onLogout() {
|
async function onLogout() {
|
||||||
if (!confirm('确定退出登录?')) return
|
const confirmed = window.confirm('确定退出登录?')
|
||||||
|
if (!confirmed) return
|
||||||
await logout()
|
await logout()
|
||||||
|
Toast.show({ icon: 'success', content: '已退出' })
|
||||||
navigate('/login', { replace: true })
|
navigate('/login', { replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="me-page">
|
||||||
<div className="me-header">
|
<NavBar back={null} className="me-navbar">我的</NavBar>
|
||||||
|
<div className="me-profile">
|
||||||
|
<Avatar
|
||||||
|
className="me-avatar"
|
||||||
|
src={user?.avatar}
|
||||||
|
style={{ '--size': '64px', '--border-radius': '50%' }}
|
||||||
|
>
|
||||||
|
{(user?.name || '?').slice(0, 1).toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
<div className="me-name">{user?.name || '用户'}</div>
|
<div className="me-name">{user?.name || '用户'}</div>
|
||||||
<div className="me-email">{user?.email}</div>
|
<div className="me-email">{user?.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="me-list">
|
<List className="me-list" mode="card">
|
||||||
<div className="list-item">
|
<List.Item extra={'#' + (user?.id || '-')}>用户 ID</List.Item>
|
||||||
<span className="label">用户 ID</span>
|
<List.Item extra={user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}>
|
||||||
<span className="value">#{user?.id}</span>
|
注册时间
|
||||||
</div>
|
</List.Item>
|
||||||
<div className="list-item">
|
<List.Item
|
||||||
<span className="label">注册时间</span>
|
onClick={onLogout}
|
||||||
<span className="value">{user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}</span>
|
clickable
|
||||||
</div>
|
arrowIcon={false}
|
||||||
<div className="list-item" onClick={onLogout} role="button" style={{ cursor: 'pointer' }}>
|
className="me-logout"
|
||||||
<span className="label danger">退出登录</span>
|
>
|
||||||
<span className="value">›</span>
|
退出登录
|
||||||
</div>
|
</List.Item>
|
||||||
</div>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import { ErrorBlock, NavBar } from 'antd-mobile'
|
||||||
|
|
||||||
// 监控: 暂留占位
|
// 监控: 暂留占位
|
||||||
|
// 使用 antd-mobile ErrorBlock 仿原生空状态页
|
||||||
export default function MonitorPage() {
|
export default function MonitorPage() {
|
||||||
return (
|
return (
|
||||||
<div className="placeholder-page">
|
<div className="page-with-navbar">
|
||||||
<div className="icon">◉</div>
|
<NavBar back={null}>监控</NavBar>
|
||||||
<div>监控</div>
|
<ErrorBlock
|
||||||
<div style={{ fontSize: 12, opacity: .7 }}>后续接入: 指标阈值告警 / 时序趋势</div>
|
status="empty"
|
||||||
|
title="监控"
|
||||||
|
description="后续接入: 指标阈值告警 / 时序趋势"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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'
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
// 文章详情: /macro/articles/:slug
|
// 文章详情: /macro/articles/:slug
|
||||||
|
// 使用 antd-mobile NavBar 提供原生风格的返回按钮
|
||||||
export default function ArticleDetail() {
|
export default function ArticleDetail() {
|
||||||
const { slug } = useParams()
|
const { slug } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -20,16 +22,19 @@ export default function ArticleDetail() {
|
|||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [slug])
|
}, [slug])
|
||||||
|
|
||||||
if (loading) return <div className="loading">加载中...</div>
|
|
||||||
if (err) return <div className="error-banner">{err}</div>
|
|
||||||
if (!article) return <div className="empty">文章不存在</div>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="article-page">
|
||||||
|
<NavBar onBack={() => navigate(-1)}>文章详情</NavBar>
|
||||||
|
{loading && <div className="loading">加载中...</div>}
|
||||||
|
{err && <div className="error-banner">{err}</div>}
|
||||||
|
{!loading && !err && !article && <div className="empty">文章不存在</div>}
|
||||||
|
{!loading && !err && article && (
|
||||||
<div className="article-detail">
|
<div className="article-detail">
|
||||||
<div className="article-back" onClick={() => navigate(-1)} role="button">‹ 返回</div>
|
|
||||||
<h1>{article.title}</h1>
|
<h1>{article.title}</h1>
|
||||||
<div className="meta">{article.date}</div>
|
<div className="meta">{article.date}</div>
|
||||||
<div className="article-content" dangerouslySetInnerHTML={{ __html: article.contentHtml }} />
|
<div className="article-content" dangerouslySetInnerHTML={{ __html: article.contentHtml }} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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'
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
// 经济文章: 列表, 点进 /macro/articles/:slug
|
// 经济文章: 列表, 点进 /macro/articles/:slug
|
||||||
|
// 使用 antd-mobile List 仿原生设置/通讯录样式
|
||||||
export default function ArticlesView() {
|
export default function ArticlesView() {
|
||||||
const [articles, setArticles] = useState([])
|
const [articles, setArticles] = useState([])
|
||||||
const [err, setErr] = useState('')
|
const [err, setErr] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -25,15 +28,23 @@ export default function ArticlesView() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-title">经济文章</div>
|
<div className="page-title">经济文章</div>
|
||||||
<div className="list">
|
<List className="articles-list" mode="card">
|
||||||
{articles.map((a) => (
|
{articles.map((a) => (
|
||||||
<Link key={a.id} to={`/macro/articles/${encodeURIComponent(a.slug)}`} className="list-item">
|
<List.Item
|
||||||
<div className="list-item-title">{a.title}</div>
|
key={a.id}
|
||||||
|
title={a.title}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
<div className="list-item-meta">{a.date}</div>
|
<div className="list-item-meta">{a.date}</div>
|
||||||
<div className="list-item-excerpt">{a.excerpt}</div>
|
<div className="list-item-excerpt">{a.excerpt}</div>
|
||||||
</Link>
|
</>
|
||||||
|
}
|
||||||
|
arrowIcon
|
||||||
|
clickable
|
||||||
|
onClick={() => navigate('/macro/articles/' + encodeURIComponent(a.slug))}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||||
|
import { CapsuleTabs, Card } from 'antd-mobile'
|
||||||
import { api } from '../../api/client.js'
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
// 经济数据: 从 /api/indicators 拉所有指标, 顶上一个 chip 行切换, 下面渲染图表+表格
|
// 经济数据: 顶部 CapsuleTabs 选指标, 下面渲染图表 + 表格
|
||||||
|
// 使用 antd-mobile: CapsuleTabs / Card
|
||||||
|
// 多 groupName 的指标: 在 chip 文本中携带分组前缀以便区分
|
||||||
export default function DataView() {
|
export default function DataView() {
|
||||||
const [indicators, setIndicators] = useState([])
|
const [indicators, setIndicators] = useState([])
|
||||||
const [selected, setSelected] = useState(null)
|
const [selected, setSelected] = useState(null)
|
||||||
@@ -27,37 +30,32 @@ export default function DataView() {
|
|||||||
if (err) return <div className="error-banner">{err}</div>
|
if (err) return <div className="error-banner">{err}</div>
|
||||||
if (!indicators.length) return <div className="empty">暂无指标数据, 请先执行 npm run db:seed</div>
|
if (!indicators.length) return <div className="empty">暂无指标数据, 请先执行 npm run db:seed</div>
|
||||||
|
|
||||||
// 按 groupName 分组, chip 行按组排列
|
// 为 chip label 计算: 多个 group 时显示 "分组 / 标题", 单个 group 只显示标题
|
||||||
const groups = groupBy(indicators, 'groupName')
|
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 cur = indicators.find((i) => i.name === selected)
|
||||||
const chartData = cur ? buildMonthlySeries(cur.data) : []
|
const chartData = cur ? buildMonthlySeries(cur.data) : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-title">经济数据</div>
|
<div className="page-title">经济数据</div>
|
||||||
{Object.entries(groups).map(([groupName, list]) => (
|
<CapsuleTabs activeKey={selected || undefined} onChange={setSelected}>
|
||||||
<div key={groupName}>
|
{indicators.map((i) => (
|
||||||
<div className="section-title">{groupName}</div>
|
<CapsuleTabs.Tab key={i.name} title={labelOf(i)} />
|
||||||
<div className="chip-row">
|
|
||||||
{list.map((i) => (
|
|
||||||
<button
|
|
||||||
key={i.name}
|
|
||||||
className={'chip' + (selected === i.name ? ' active' : '')}
|
|
||||||
onClick={() => setSelected(i.name)}
|
|
||||||
>
|
|
||||||
{shortTitle(i)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
</CapsuleTabs>
|
||||||
|
|
||||||
{cur && (
|
{cur && (
|
||||||
<div style={{ padding: '0 12px 12px' }}>
|
<div style={{ padding: '0 12px 12px' }}>
|
||||||
<div className="card">
|
<Card className="chart-card">
|
||||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{cur.data.title || cur.name}</div>
|
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
||||||
单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'}
|
{labelOf(cur)} · 单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'}
|
||||||
</div>
|
</div>
|
||||||
<div className="chart-wrap">
|
<div className="chart-wrap">
|
||||||
<ResponsiveContainer width="100%" height={260}>
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
@@ -67,7 +65,7 @@ export default function DataView() {
|
|||||||
<YAxis tick={{ fontSize: 11 }} />
|
<YAxis tick={{ fontSize: 11 }} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Line type="monotone" dataKey="value" name={cur.data.unit || '值'} stroke="#2563eb" strokeWidth={2} dot={{ r: 3 }} />
|
<Line type="monotone" dataKey="value" name={cur.data.unit || '值'} stroke="#1677ff" strokeWidth={2} dot={{ r: 3 }} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,26 +81,13 @@ export default function DataView() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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}, ...] 直接返回, 服务端已经按月排好
|
// 把 data.rows: [{month, value}, ...] 直接返回, 服务端已经按月排好
|
||||||
function buildMonthlySeries(data) {
|
function buildMonthlySeries(data) {
|
||||||
return (data && data.rows) || []
|
return (data && data.rows) || []
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||||
|
import { CapsuleTabs, Card } from 'antd-mobile'
|
||||||
import { api } from '../../api/client.js'
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
// 策略研究: 顶部 chip 行选策略, 下面显示 4 张图 (debt / gdp / debtToGdp / interestToGdp) + 表格
|
// 策略研究: 顶部 CapsuleTabs 选策略, 下面 4 张图 + 表格
|
||||||
|
// 使用 antd-mobile: CapsuleTabs / Card
|
||||||
const SERIES = [
|
const SERIES = [
|
||||||
{ key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' },
|
{ key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' },
|
||||||
{ key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' },
|
{ key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' },
|
||||||
@@ -40,22 +42,14 @@ export default function StrategyView() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-title">策略研究</div>
|
<div className="page-title">策略研究</div>
|
||||||
<div className="chip-row" style={{ paddingLeft: 12 }}>
|
<CapsuleTabs activeKey={selected || undefined} onChange={setSelected}>
|
||||||
{strategies.map((s) => (
|
{strategies.map((s) => (
|
||||||
<button
|
<CapsuleTabs.Tab key={s.name} title={s.description || s.name} />
|
||||||
key={s.name}
|
|
||||||
className={'chip' + (selected === s.name ? ' active' : '')}
|
|
||||||
onClick={() => setSelected(s.name)}
|
|
||||||
>
|
|
||||||
{s.description || s.name}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</CapsuleTabs>
|
||||||
|
|
||||||
{cur && (
|
{cur && (
|
||||||
<div style={{ padding: '0 12px 12px' }}>
|
<div style={{ padding: '0 12px 12px' }}>
|
||||||
<div className="card">
|
<Card className="chart-card">
|
||||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{cur.description || cur.name}</div>
|
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
||||||
数据 {rows.length} 条 · 来源 {cur.data.source || 'seed'}
|
数据 {rows.length} 条 · 来源 {cur.data.source || 'seed'}
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +94,7 @@ export default function StrategyView() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+105
-652
@@ -1,513 +1,20 @@
|
|||||||
* {
|
/* ================== 移动端优先布局 (mobile-first, antd-mobile) ================== */
|
||||||
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) ================== */
|
|
||||||
|
|
||||||
|
/* 主题变量: 与 antd-mobile 的 --adm-* 保持一致, 便于组件统一 */
|
||||||
:root {
|
:root {
|
||||||
--color-bg: #f5f5f5;
|
--color-bg: #f5f5f5;
|
||||||
--color-card: #ffffff;
|
--color-card: #ffffff;
|
||||||
--color-text: #333333;
|
--color-text: #333333;
|
||||||
--color-muted: #666666;
|
--color-muted: #666666;
|
||||||
--color-border: #e5e7eb;
|
--color-border: #eeeeee;
|
||||||
--color-primary: #1f2937;
|
--color-primary: #1677ff;
|
||||||
--color-accent: #2563eb;
|
--color-accent: #1677ff;
|
||||||
--color-danger: #dc2626;
|
--color-danger: #ff3141;
|
||||||
--bottom-nav-h: 56px;
|
--safe-top: env(safe-area-inset-top, 0px);
|
||||||
--top-tabs-h: 44px;
|
|
||||||
--safe-bottom: env(safe-area-inset-bottom, 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; }
|
* { 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; }
|
button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
|
||||||
input, textarea, select { font: inherit; }
|
input, textarea, select { font: inherit; }
|
||||||
|
|
||||||
/* 应用主框架 */
|
/* 应用主框架: 全屏 flex 列布局, 不再 max-width 居中 (解决"左右不贴边") */
|
||||||
.app-root {
|
.app-root {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
min-height: 100dvh;
|
height: 100dvh;
|
||||||
max-width: 768px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
max-width: 100%;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-left: var(--safe-left);
|
||||||
|
padding-right: var(--safe-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding-bottom: calc(var(--bottom-nav-h) + var(--safe-bottom) + 8px);
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom));
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main.has-top-tabs {
|
/* 带有 antd-mobile NavBar 的页面 (Home/Monitor/Me/Article) */
|
||||||
padding-top: var(--top-tabs-h);
|
.page-with-navbar { display: flex; flex-direction: column; min-height: 100%; }
|
||||||
}
|
.page-with-navbar .adm-error-block { flex: 1; }
|
||||||
|
|
||||||
/* 顶栏 tabs (宏观页用) */
|
/* 底部 antd-mobile TabBar 定位 */
|
||||||
.top-tabs {
|
.app-tabbar.adm-tab-bar {
|
||||||
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 {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
height: calc(var(--bottom-nav-h) + var(--safe-bottom));
|
|
||||||
padding-bottom: var(--safe-bottom);
|
|
||||||
display: flex;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
max-width: 768px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
.tab-icon { display: inline-flex; }
|
||||||
|
|
||||||
.bottom-nav-item {
|
/* 宏观页 SwipeTabs */
|
||||||
flex: 1;
|
.swipe-tabs-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
height: 100%;
|
||||||
justify-content: center;
|
min-height: 0;
|
||||||
gap: 2px;
|
}
|
||||||
font-size: 11px;
|
.swipe-container {
|
||||||
color: var(--color-muted);
|
position: relative;
|
||||||
user-select: none;
|
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; }
|
|
||||||
|
|
||||||
/* 通用卡片 */
|
/* 图表卡片: 用于 DataView/StrategyView */
|
||||||
.card { background: #fff; border-radius: 10px; padding: 14px 16px; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
|
.chart-card.adm-card { padding: 12px; }
|
||||||
.card + .card { margin-top: 10px; }
|
.chart-card.adm-card .adm-card-body { padding: 0; }
|
||||||
|
|
||||||
.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; }
|
|
||||||
|
|
||||||
/* 文章详情 */
|
/* 文章详情 */
|
||||||
|
.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 { background: #fff; padding: 18px 16px 24px; }
|
||||||
.article-detail h1 { font-size: 20px; line-height: 1.35; margin-bottom: 8px; }
|
.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); }
|
.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 p { margin-bottom: 0.9em; }
|
||||||
.article-content ul, .article-content ol { margin: 0 0 1em 1.4em; }
|
.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-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 行) */
|
/* 文章列表项 (用于 ArticlesView) */
|
||||||
.chip-row { display: flex; gap: 8px; overflow-x: auto; padding: 8px 12px; }
|
.articles-list .adm-list-item-description { color: var(--color-muted); }
|
||||||
.chip-row::-webkit-scrollbar { display: none; }
|
.list-item-meta { font-size: 12px; color: var(--color-muted); margin-bottom: 4px; }
|
||||||
.chip { padding: 7px 12px; background: #fff; border: 1px solid var(--color-border); border-radius: 999px; font-size: 13px; white-space: nowrap; }
|
.list-item-excerpt { font-size: 13px; color: var(--color-muted); line-height: 1.5; }
|
||||||
.chip.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
|
|
||||||
|
/* 图表 / 表格 */
|
||||||
|
.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); }
|
||||||
Reference in New Issue
Block a user