mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-02 06:43:23 +08:00
Merge pull request #317 from awesomeYG/feat-dashboard-time-filter
feat: add timerange filter
This commit is contained in:
21
ui/components.json
Normal file
21
ui/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,18 @@
|
||||
"@mui/lab": "6.0.0-beta.19",
|
||||
"@mui/material": "^6.4.12",
|
||||
"@yokowu/modelkit-ui": "0.7.1",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"decimal.js": "^10.5.0",
|
||||
"echarts": "^5.6.0",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^19.1.0",
|
||||
"react-activity-calendar": "^2.7.12",
|
||||
@@ -39,6 +45,8 @@
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"zod": "^4.0.17"
|
||||
@@ -57,6 +65,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"shiki": "^3.7.0",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
|
||||
445
ui/pnpm-lock.yaml
generated
445
ui/pnpm-lock.yaml
generated
@@ -32,6 +32,9 @@ importers:
|
||||
'@mui/material':
|
||||
specifier: ^6.4.12
|
||||
version: 6.5.0(@emotion/react@11.14.0(@types/react@19.1.10)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.10)(react@19.1.1))(@types/react@19.1.10)(react@19.1.1))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.12
|
||||
version: 4.1.12(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1))
|
||||
'@yokowu/modelkit-ui':
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1(8525073519bd78ecc27b6ab63a7efdab)
|
||||
@@ -41,6 +44,18 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.9.0
|
||||
version: 1.11.0
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
date-fns-tz:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(date-fns@4.1.0)
|
||||
dayjs:
|
||||
specifier: ^1.11.7
|
||||
version: 1.11.13
|
||||
@@ -53,6 +68,9 @@ importers:
|
||||
lottie-react:
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
lucide-react:
|
||||
specifier: ^0.542.0
|
||||
version: 0.542.0(react@19.1.1)
|
||||
monaco-editor:
|
||||
specifier: ^0.52.2
|
||||
version: 0.52.2
|
||||
@@ -92,12 +110,18 @@ importers:
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
tailwindcss:
|
||||
specifier: ^4.1.12
|
||||
version: 4.1.12
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
vite-plugin-dts:
|
||||
specifier: ^4.5.4
|
||||
version: 4.5.4(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1))
|
||||
version: 4.5.4(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1))
|
||||
zod:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17
|
||||
@@ -122,7 +146,7 @@ importers:
|
||||
version: 15.5.13
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.4.1
|
||||
version: 4.7.0(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1))
|
||||
version: 4.7.0(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1))
|
||||
dotenv:
|
||||
specifier: ^17.2.1
|
||||
version: 17.2.1
|
||||
@@ -141,6 +165,9 @@ importers:
|
||||
shiki:
|
||||
specifier: ^3.7.0
|
||||
version: 3.9.2
|
||||
tw-animate-css:
|
||||
specifier: ^1.3.7
|
||||
version: 1.3.7
|
||||
typescript:
|
||||
specifier: ~5.8.3
|
||||
version: 5.8.3
|
||||
@@ -149,7 +176,7 @@ importers:
|
||||
version: 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
|
||||
vite:
|
||||
specifier: ^6.3.5
|
||||
version: 6.3.5(@types/node@24.2.1)(jiti@2.5.1)
|
||||
version: 6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -576,9 +603,16 @@ packages:
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -938,6 +972,100 @@ packages:
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@tailwindcss/node@4.1.12':
|
||||
resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==}
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.12':
|
||||
resolution: {integrity: sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.12':
|
||||
resolution: {integrity: sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.12':
|
||||
resolution: {integrity: sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.12':
|
||||
resolution: {integrity: sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12':
|
||||
resolution: {integrity: sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.12':
|
||||
resolution: {integrity: sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.12':
|
||||
resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.12':
|
||||
resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.12':
|
||||
resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.12':
|
||||
resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
bundledDependencies:
|
||||
- '@napi-rs/wasm-runtime'
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@tybys/wasm-util'
|
||||
- '@emnapi/wasi-threads'
|
||||
- tslib
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.12':
|
||||
resolution: {integrity: sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.12':
|
||||
resolution: {integrity: sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide@4.1.12':
|
||||
resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@tailwindcss/vite@4.1.12':
|
||||
resolution: {integrity: sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==}
|
||||
peerDependencies:
|
||||
vite: ^5.2.0 || ^6 || ^7
|
||||
|
||||
'@types/argparse@1.0.38':
|
||||
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
|
||||
|
||||
@@ -1258,9 +1386,16 @@ packages:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
citty@0.1.6:
|
||||
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1343,6 +1478,11 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
date-fns-tz@3.2.0:
|
||||
resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==}
|
||||
peerDependencies:
|
||||
date-fns: ^3.0.0 || ^4.0.0
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
@@ -1384,6 +1524,10 @@ packages:
|
||||
destr@2.0.5:
|
||||
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||
|
||||
detect-libc@2.0.4:
|
||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
@@ -1407,6 +1551,10 @@ packages:
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
enhanced-resolve@5.18.3:
|
||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
@@ -1861,6 +2009,74 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.30.1:
|
||||
resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.30.1:
|
||||
resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||
resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.1:
|
||||
resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.1:
|
||||
resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.30.1:
|
||||
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
@@ -1907,6 +2123,11 @@ packages:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lucide-react@0.542.0:
|
||||
resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
@@ -2076,6 +2297,19 @@ packages:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minizlib@3.0.2:
|
||||
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mlly@1.7.4:
|
||||
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
|
||||
|
||||
@@ -2550,6 +2784,20 @@ packages:
|
||||
resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==}
|
||||
hasBin: true
|
||||
|
||||
tailwind-merge@3.3.1:
|
||||
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||
|
||||
tailwindcss@4.1.12:
|
||||
resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==}
|
||||
|
||||
tapable@2.2.3:
|
||||
resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar@7.4.3:
|
||||
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyexec@1.0.1:
|
||||
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
|
||||
|
||||
@@ -2585,6 +2833,9 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tw-animate-css@1.3.7:
|
||||
resolution: {integrity: sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2745,6 +2996,10 @@ packages:
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yaml@1.10.2:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3172,11 +3427,20 @@ snapshots:
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.30
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.30
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
@@ -3519,6 +3783,77 @@ snapshots:
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@tailwindcss/node@4.1.12':
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
enhanced-resolve: 5.18.3
|
||||
jiti: 2.5.1
|
||||
lightningcss: 1.30.1
|
||||
magic-string: 0.30.17
|
||||
source-map-js: 1.2.1
|
||||
tailwindcss: 4.1.12
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.12':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide@4.1.12':
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
tar: 7.4.3
|
||||
optionalDependencies:
|
||||
'@tailwindcss/oxide-android-arm64': 4.1.12
|
||||
'@tailwindcss/oxide-darwin-arm64': 4.1.12
|
||||
'@tailwindcss/oxide-darwin-x64': 4.1.12
|
||||
'@tailwindcss/oxide-freebsd-x64': 4.1.12
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.12
|
||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.12
|
||||
'@tailwindcss/oxide-linux-arm64-musl': 4.1.12
|
||||
'@tailwindcss/oxide-linux-x64-gnu': 4.1.12
|
||||
'@tailwindcss/oxide-linux-x64-musl': 4.1.12
|
||||
'@tailwindcss/oxide-wasm32-wasi': 4.1.12
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.12
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.12
|
||||
|
||||
'@tailwindcss/vite@4.1.12(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.12
|
||||
'@tailwindcss/oxide': 4.1.12
|
||||
tailwindcss: 4.1.12
|
||||
vite: 6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)
|
||||
|
||||
'@types/argparse@1.0.38': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
@@ -3697,7 +4032,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1))':
|
||||
'@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
||||
@@ -3705,7 +4040,7 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
vite: 6.3.5(@types/node@24.2.1)(jiti@2.5.1)
|
||||
vite: 6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3930,10 +4265,16 @@ snapshots:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
citty@0.1.6:
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -4008,6 +4349,10 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
date-fns-tz@3.2.0(date-fns@4.1.0):
|
||||
dependencies:
|
||||
date-fns: 4.1.0
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
@@ -4034,6 +4379,8 @@ snapshots:
|
||||
|
||||
destr@2.0.5: {}
|
||||
|
||||
detect-libc@2.0.4: {}
|
||||
|
||||
devlop@1.1.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
@@ -4060,6 +4407,11 @@ snapshots:
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
enhanced-resolve@5.18.3:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.3
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
@@ -4564,6 +4916,51 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.1:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.30.1:
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
optionalDependencies:
|
||||
lightningcss-darwin-arm64: 1.30.1
|
||||
lightningcss-darwin-x64: 1.30.1
|
||||
lightningcss-freebsd-x64: 1.30.1
|
||||
lightningcss-linux-arm-gnueabihf: 1.30.1
|
||||
lightningcss-linux-arm64-gnu: 1.30.1
|
||||
lightningcss-linux-arm64-musl: 1.30.1
|
||||
lightningcss-linux-x64-gnu: 1.30.1
|
||||
lightningcss-linux-x64-musl: 1.30.1
|
||||
lightningcss-win32-arm64-msvc: 1.30.1
|
||||
lightningcss-win32-x64-msvc: 1.30.1
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
local-pkg@1.1.1:
|
||||
@@ -4609,6 +5006,10 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
lucide-react@0.542.0(react@19.1.1):
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -4991,6 +5392,14 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
minizlib@3.0.2:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
mlly@1.7.4:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
@@ -5551,6 +5960,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
tailwind-merge@3.3.1: {}
|
||||
|
||||
tailwindcss@4.1.12: {}
|
||||
|
||||
tapable@2.2.3: {}
|
||||
|
||||
tar@7.4.3:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
minipass: 7.1.2
|
||||
minizlib: 3.0.2
|
||||
mkdirp: 3.0.1
|
||||
yallist: 5.0.0
|
||||
|
||||
tinyexec@1.0.1: {}
|
||||
|
||||
tinyglobby@0.2.14:
|
||||
@@ -5578,6 +6002,8 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tw-animate-css@1.3.7: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -5663,7 +6089,7 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-plugin-dts@4.5.4(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)):
|
||||
vite-plugin-dts@4.5.4(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.52.10(@types/node@24.2.1)
|
||||
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
|
||||
@@ -5676,13 +6102,13 @@ snapshots:
|
||||
magic-string: 0.30.17
|
||||
typescript: 5.8.3
|
||||
optionalDependencies:
|
||||
vite: 6.3.5(@types/node@24.2.1)(jiti@2.5.1)
|
||||
vite: 6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1):
|
||||
vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.9
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
@@ -5694,6 +6120,7 @@ snapshots:
|
||||
'@types/node': 24.2.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.5.1
|
||||
lightningcss: 1.30.1
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
@@ -5726,6 +6153,8 @@ snapshots:
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yaml@1.10.2: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
@@ -1594,69 +1594,53 @@ export interface V1CliCreateParams {
|
||||
}
|
||||
|
||||
export interface GetCategoryStatDashboardParams {
|
||||
/**
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 持续时间 (小时或天数)` */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 结束时间, 时间范围优先级高于精度选择 */
|
||||
end_at?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision?: string;
|
||||
/** 开始时间, 时间范围优先级高于精度选择 */
|
||||
start_at?: number;
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface GetTimeStatDashboardParams {
|
||||
/**
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 持续时间 (小时或天数)` */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 结束时间, 时间范围优先级高于精度选择 */
|
||||
end_at?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision?: string;
|
||||
/** 开始时间, 时间范围优先级高于精度选择 */
|
||||
start_at?: number;
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface GetUserCodeRankDashboardParams {
|
||||
/**
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 持续时间 (小时或天数)` */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 结束时间, 时间范围优先级高于精度选择 */
|
||||
end_at?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision?: string;
|
||||
/** 开始时间, 时间范围优先级高于精度选择 */
|
||||
start_at?: number;
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface GetUserEventsDashboardParams {
|
||||
/**
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 持续时间 (小时或天数)` */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 结束时间, 时间范围优先级高于精度选择 */
|
||||
end_at?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision?: string;
|
||||
/** 开始时间, 时间范围优先级高于精度选择 */
|
||||
start_at?: number;
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
@@ -1667,18 +1651,14 @@ export interface GetUserHeatmapDashboardParams {
|
||||
}
|
||||
|
||||
export interface GetUserStatDashboardParams {
|
||||
/**
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 持续时间 (小时或天数)` */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 结束时间, 时间范围优先级高于精度选择 */
|
||||
end_at?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision?: string;
|
||||
/** 开始时间, 时间范围优先级高于精度选择 */
|
||||
start_at?: number;
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
@@ -1880,35 +1860,27 @@ export interface GetUserListCompletionRecordParams {
|
||||
}
|
||||
|
||||
export interface GetUserDashboardEventsParams {
|
||||
/**
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 持续时间 (小时或天数)` */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 结束时间, 时间范围优先级高于精度选择 */
|
||||
end_at?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision?: string;
|
||||
/** 开始时间, 时间范围优先级高于精度选择 */
|
||||
start_at?: number;
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface GetUserDashboardStatParams {
|
||||
/**
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 持续时间 (小时或天数)` */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 结束时间, 时间范围优先级高于精度选择 */
|
||||
end_at?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision?: string;
|
||||
/** 开始时间, 时间范围优先级高于精度选择 */
|
||||
start_at?: number;
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
117
ui/src/components/ui/button-1.tsx
Normal file
117
ui/src/components/ui/button-1.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React from "react";
|
||||
import { Spinner } from "@/components/ui/spinner-1";
|
||||
import clsx from "clsx";
|
||||
|
||||
const sizes = [
|
||||
{
|
||||
tiny: "px-1.5 h-6 text-sm",
|
||||
small: "px-1.5 h-8 text-sm",
|
||||
medium: "px-2.5 h-10 text-sm",
|
||||
large: "px-3.5 h-12 text-base"
|
||||
},
|
||||
{
|
||||
tiny: "w-6 h-6 text-sm",
|
||||
small: "w-8 h-8 text-sm",
|
||||
medium: "w-10 h-10 text-sm",
|
||||
large: "w-12 h-12 text-base"
|
||||
}
|
||||
];
|
||||
|
||||
const types = {
|
||||
primary: "bg-gray-1000 hover:bg-gray-1000-h text-background-100 fill-background-100",
|
||||
secondary: "bg-background-100 hover:bg-gray-alpha-200 text-gray-1000 fill-gray-1000 border border-gray-alpha-400",
|
||||
tertiary: "bg-none hover:bg-gray-alpha-200 text-gray-1000 fill-gray-1000",
|
||||
error: "bg-red-800 hover:bg-red-900 text-white fill-white",
|
||||
warning: "bg-amber-800 hover:bg-amber-850 text-black fill-black"
|
||||
};
|
||||
|
||||
const shapes = {
|
||||
square: {
|
||||
tiny: "rounded",
|
||||
small: "rounded-md",
|
||||
medium: "rounded-md",
|
||||
large: "rounded-lg"
|
||||
},
|
||||
circle: {
|
||||
tiny: "rounded-[100%]",
|
||||
small: "rounded-[100%]",
|
||||
medium: "rounded-[100%]",
|
||||
large: "rounded-[100%]"
|
||||
},
|
||||
rounded: {
|
||||
tiny: "rounded-[100px]",
|
||||
small: "rounded-[100px]",
|
||||
medium: "rounded-[100px]",
|
||||
large: "rounded-[100px]"
|
||||
}
|
||||
};
|
||||
|
||||
export interface ButtonProps {
|
||||
size?: keyof typeof sizes[0];
|
||||
type?: keyof typeof types;
|
||||
variant?: "styled" | "unstyled";
|
||||
shape?: keyof typeof shapes;
|
||||
svgOnly?: boolean;
|
||||
children?: React.ReactNode;
|
||||
prefix?: React.ReactNode;
|
||||
suffix?: React.ReactNode;
|
||||
shadow?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
fullWidth?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
ref?: React.Ref<HTMLButtonElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
size = "medium",
|
||||
type = "primary",
|
||||
variant = "styled",
|
||||
shape = "square",
|
||||
svgOnly = false,
|
||||
children,
|
||||
prefix,
|
||||
suffix,
|
||||
shadow = false,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
fullWidth = false,
|
||||
onClick,
|
||||
ref,
|
||||
className,
|
||||
...rest
|
||||
}: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="submit"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
"flex justify-center items-center gap-0.5 duration-150",
|
||||
sizes[+svgOnly][size],
|
||||
(disabled || loading) ? "bg-gray-100 text-gray-700 border border-gray-400 cursor-not-allowed" : types[type],
|
||||
shapes[shape][size],
|
||||
shadow && "shadow-border-small border-none",
|
||||
fullWidth && "w-full",
|
||||
variant === "unstyled" ? "outline-none px-0 h-fit bg-transparent hover:bg-transparent text-gray-1000" : "focus:shadow-focus-ring focus:outline-0",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{loading
|
||||
? <Spinner size={size === "large" ? 24 : 16} />
|
||||
: prefix
|
||||
}
|
||||
<span className={clsx(
|
||||
"relative overflow-hidden whitespace-nowrap overflow-ellipsis font-sans",
|
||||
size !== "tiny" && variant !== "unstyled" && "px-1.5"
|
||||
)}>
|
||||
{children}
|
||||
</span>
|
||||
{!loading && suffix}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
870
ui/src/components/ui/calendar.tsx
Normal file
870
ui/src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,870 @@
|
||||
import {
|
||||
addDays,
|
||||
addMonths, endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek, format, isEqual,
|
||||
isSameDay,
|
||||
isSameMonth, isToday,
|
||||
isValid,
|
||||
isWithinInterval, parse, startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek, sub,
|
||||
subDays, subHours, subMinutes,
|
||||
subMonths,
|
||||
subWeeks, subYears
|
||||
} from "date-fns";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button-1";
|
||||
import { Material } from "@/components/ui/material-1";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select-1";
|
||||
import { formatInTimeZone, fromZonedTime } from "date-fns-tz";
|
||||
import { useClickOutside } from "@/components/ui/use-click-outside";
|
||||
import clsx from "clsx";
|
||||
import { enUS } from "date-fns/locale";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type SecondTimeRange = {
|
||||
start_at: number;
|
||||
end_at: number;
|
||||
}
|
||||
const ClockIcon = () => (
|
||||
<svg height="16" strokeLinejoin="round" viewBox="0 0 16 16" width="16">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8.75 4.75V4H7.25V4.75V7.875C7.25 8.18976 7.39819 8.48615 7.65 8.675L9.55 10.1L10.15 10.55L11.05 9.35L10.45 8.9L8.75 7.625V4.75Z"
|
||||
className="fill-gray-1000"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ArrowBottomIcon = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
className={clsx("fill-gray-1000", className)}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.0607 5.49999L13.5303 6.03032L8.7071 10.8535C8.31658 11.2441 7.68341 11.2441 7.29289 10.8535L2.46966 6.03032L1.93933 5.49999L2.99999 4.43933L3.53032 4.96966L7.99999 9.43933L12.4697 4.96966L13 4.43933L14.0607 5.49999Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ArrowLeftIcon = () => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.5 14.0607L9.96966 13.5303L5.14644 8.7071C4.75592 8.31658 4.75592 7.68341 5.14644 7.29289L9.96966 2.46966L10.5 1.93933L11.5607 2.99999L11.0303 3.53032L6.56065 7.99999L11.0303 12.4697L11.5607 13L10.5 14.0607Z"
|
||||
className="fill-gray-700"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ArrowRightIcon = () => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.50001 1.93933L6.03034 2.46966L10.8536 7.29288C11.2441 7.68341 11.2441 8.31657 10.8536 8.7071L6.03034 13.5303L5.50001 14.0607L4.43935 13L4.96968 12.4697L9.43935 7.99999L4.96968 3.53032L4.43935 2.99999L5.50001 1.93933Z"
|
||||
className="fill-gray-700"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CalendarIcon = () => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.5 0.5V1.25V2H10.5V1.25V0.5H12V1.25V2H14H15.5V3.5V13.5C15.5 14.8807 14.3807 16 13 16H3C1.61929 16 0.5 14.8807 0.5 13.5V3.5V2H2H4V1.25V0.5H5.5ZM2 3.5H14V6H2V3.5ZM2 7.5V13.5C2 14.0523 2.44772 14.5 3 14.5H13C13.5523 14.5 14 14.0523 14 13.5V7.5H2Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ClearIcon = () => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.4697 13.5303L13 14.0607L14.0607 13L13.5303 12.4697L9.06065 7.99999L13.5303 3.53032L14.0607 2.99999L13 1.93933L12.4697 2.46966L7.99999 6.93933L3.53032 2.46966L2.99999 1.93933L1.93933 2.99999L2.46966 3.53032L6.93933 7.99999L2.46966 12.4697L1.93933 13L2.99999 14.0607L3.53032 13.5303L7.99999 9.06065L12.4697 13.5303Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const parseRelativeDate = (input: string) => {
|
||||
const regex = /(\d+)\s*(day|week|month|year|hour)s?/i;
|
||||
const match = input.match(regex);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2].toLowerCase() + "s";
|
||||
|
||||
const now = new Date();
|
||||
const start = startOfDay(sub(now, { [unit]: value }));
|
||||
const end = endOfDay(now);
|
||||
|
||||
return {
|
||||
[input]: { text: input, start, end }
|
||||
};
|
||||
};
|
||||
|
||||
const parseFixedRange = (input: string) => {
|
||||
const rangePattern = /(.+)\s*[-–]\s*(.+)/;
|
||||
|
||||
const match = input.match(rangePattern);
|
||||
if (!match) {
|
||||
return parseExactDate(input);
|
||||
}
|
||||
|
||||
const [, startStr, endStr] = match;
|
||||
if (!startStr || !endStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const possibleFormats = ["d MMM yyyy", "d MMM", "yyyy-MM-dd"];
|
||||
|
||||
for (const format of possibleFormats) {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
|
||||
const start = parse(startStr, format, now, { locale: enUS });
|
||||
const end = parse(endStr, format, now, { locale: enUS });
|
||||
|
||||
const finalStart = isValid(start) ? startOfDay(start) : null;
|
||||
const finalEnd = isValid(end) ? endOfDay(end) : null;
|
||||
|
||||
if (finalStart && finalEnd) {
|
||||
if (format === "d MMM") {
|
||||
finalStart.setFullYear(year);
|
||||
finalEnd.setFullYear(year);
|
||||
}
|
||||
return {
|
||||
[input]: { text: input, start: finalStart, end: finalEnd }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseExactDate = (input: string) => {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
|
||||
const dateFormats = ["d MMM yyyy", "d MMM", "yyyy-MM-dd"];
|
||||
|
||||
for (const format of dateFormats) {
|
||||
const date = parse(input.trim(), format, now, { locale: enUS });
|
||||
|
||||
if (isValid(date)) {
|
||||
if (format === "d MMM") {
|
||||
date.setFullYear(currentYear);
|
||||
}
|
||||
|
||||
return {
|
||||
[input]: {
|
||||
text: input,
|
||||
start: startOfDay(date),
|
||||
end: endOfDay(date)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseDateInput = (input: string) => {
|
||||
const relative = parseRelativeDate(input);
|
||||
if (relative) return relative;
|
||||
|
||||
const fixedRange = parseFixedRange(input);
|
||||
if (fixedRange) return fixedRange;
|
||||
|
||||
const exact = parseExactDate(input);
|
||||
if (exact) return exact;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const filterPresets = (obj: Record<string, any>, search: string) => {
|
||||
if (!search) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const searchWords = search.toLowerCase().split("-").filter(Boolean);
|
||||
|
||||
const filtered = Object.fromEntries(
|
||||
Object.entries(obj).filter(([_, value]) => {
|
||||
const keyLower = value.text.toLowerCase();
|
||||
return searchWords.every(word => keyLower.includes(word));
|
||||
})
|
||||
);
|
||||
|
||||
if (Object.entries(filtered).length > 0) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
const parsed = parseDateInput(search);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const numberMatch = search.match(/\d+/);
|
||||
if (!numberMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const n = parseInt(numberMatch[0], 10);
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
[`last-${n}-days`]: {
|
||||
text: `Last ${n} Days`,
|
||||
start: startOfDay(subDays(now, n)),
|
||||
end: endOfDay(now)
|
||||
},
|
||||
[`last-${n}-weeks`]: {
|
||||
text: `Last ${n} Weeks`,
|
||||
start: startOfDay(subWeeks(now, n)),
|
||||
end: endOfDay(now)
|
||||
},
|
||||
[`last-${n}-months`]: {
|
||||
text: `Last ${n} Months`,
|
||||
start: startOfDay(subMonths(now, n)),
|
||||
end: endOfDay(now)
|
||||
},
|
||||
[`last-${n}-years`]: {
|
||||
text: `Last ${n} Years`,
|
||||
start: startOfDay(subYears(now, n)),
|
||||
end: endOfDay(now)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const formatDateRange = (start: Date, end: Date, timezone: string) => {
|
||||
const isStartMidnight = isEqual(start, startOfDay(start));
|
||||
const isEndEOD = isEqual(end, endOfDay(end));
|
||||
const sameDay = isSameDay(start, end);
|
||||
|
||||
const formatSingle = (date: Date) =>
|
||||
formatInTimeZone(
|
||||
date,
|
||||
timezone,
|
||||
isStartMidnight ? "EEE, MMM d" : "EEE, MMM d, HH:mm"
|
||||
);
|
||||
|
||||
const formatMonth = (date: Date) => formatInTimeZone(date, timezone, "MMM");
|
||||
const formatDay = (date: Date) => formatInTimeZone(date, timezone, "d");
|
||||
const formatYear = (date: Date) => formatInTimeZone(date, timezone, "yy");
|
||||
|
||||
const formatDateWithTimeIfNeeded = (date: Date, showTime: boolean) =>
|
||||
formatInTimeZone(date, timezone, showTime ? "MMM d, HH:mm" : "MMM d");
|
||||
|
||||
if (sameDay) {
|
||||
return formatSingle(start);
|
||||
}
|
||||
|
||||
const sameMonth = formatMonth(start) === formatMonth(end) && formatYear(start) === formatYear(end);
|
||||
const sameYear = formatYear(start) === formatYear(end);
|
||||
|
||||
const startHasTime = !isStartMidnight;
|
||||
const endHasTime = !isEndEOD;
|
||||
|
||||
if (startHasTime || endHasTime) {
|
||||
const startFormatted = formatDateWithTimeIfNeeded(start, startHasTime);
|
||||
const endFormatted = formatDateWithTimeIfNeeded(end, endHasTime);
|
||||
return `${startFormatted} - ${endFormatted}`;
|
||||
}
|
||||
|
||||
if (sameMonth) {
|
||||
return `${formatMonth(start)} ${formatDay(start)} - ${formatDay(end)}`;
|
||||
}
|
||||
|
||||
if (sameYear) {
|
||||
return `${formatMonth(start)} ${formatDay(start)} - ${formatMonth(end)} ${formatDay(end)}`;
|
||||
}
|
||||
|
||||
return `${formatMonth(start)} ${formatDay(start)} '${formatYear(start)} - ${formatMonth(end)} ${formatDay(end)} '${formatYear(end)}`;
|
||||
};
|
||||
|
||||
const typeRelativeTimes = [
|
||||
{
|
||||
text: "45分钟",
|
||||
start: subMinutes(new Date(), 45),
|
||||
end: new Date()
|
||||
},
|
||||
{
|
||||
text: "12 小时",
|
||||
start: subHours(new Date(), 12),
|
||||
end: new Date()
|
||||
},
|
||||
{
|
||||
text: "10 天",
|
||||
start: startOfDay(subDays(new Date(), 10)),
|
||||
end: endOfDay(new Date())
|
||||
},
|
||||
{
|
||||
text: "2 周",
|
||||
start: startOfDay(subWeeks(new Date(), 2)),
|
||||
end: endOfDay(new Date())
|
||||
},
|
||||
{
|
||||
text: "1 月",
|
||||
start: startOfDay(subMonths(new Date(), 1)),
|
||||
end: endOfDay(new Date())
|
||||
},
|
||||
{
|
||||
text: "昨天",
|
||||
start: startOfDay(subDays(new Date(), 1)),
|
||||
end: endOfDay(subDays(new Date(), 1))
|
||||
},
|
||||
{
|
||||
text: "今天",
|
||||
start: startOfDay(new Date()),
|
||||
end: endOfDay(new Date())
|
||||
}
|
||||
];
|
||||
|
||||
interface CalendarComboboxProps {
|
||||
stacked: boolean;
|
||||
compact: boolean;
|
||||
disabled: boolean;
|
||||
value: RangeValue | null;
|
||||
onChange: (date: RangeValue | null) => void;
|
||||
presets: {
|
||||
[key: string]: {
|
||||
text: string;
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
};
|
||||
presetIndex?: number;
|
||||
}
|
||||
|
||||
const CalendarCombobox = ({
|
||||
stacked,
|
||||
compact,
|
||||
value,
|
||||
onChange,
|
||||
presets,
|
||||
presetIndex,
|
||||
disabled
|
||||
}: CalendarComboboxProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [inputValue, setInputValue] = useState<string>("");
|
||||
const [currentPreset, setCurrentPreset] = useState<any | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onFocus = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onChangeInputValue = (value: string) => {
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
const onClick = (value: any) => {
|
||||
setInputValue(value.text);
|
||||
setCurrentPreset(value);
|
||||
onChange({ start: value.start, end: value.end });
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const filteredPresets = filterPresets(presets, inputValue);
|
||||
|
||||
useClickOutside(ref, () => setIsOpen(false));
|
||||
|
||||
useEffect(() => {
|
||||
const array = Object.entries(presets);
|
||||
if (presetIndex !== undefined && presetIndex >= 0 && presetIndex < array.length) {
|
||||
setInputValue(array[presetIndex][1].text);
|
||||
setCurrentPreset(array[presetIndex][1]);
|
||||
onChange({ start: array[presetIndex][1].start, end: array[presetIndex][1].end });
|
||||
}
|
||||
}, [presetIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPreset) {
|
||||
if (currentPreset.start !== value?.start || currentPreset.end !== value?.end) {
|
||||
setCurrentPreset(null);
|
||||
setInputValue("");
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={twMerge(clsx(
|
||||
"inline-block text-sm font-sans",
|
||||
compact ? "w-[180px] absolute left-[38px]" : "w-[250px] relative",
|
||||
compact && !isOpen && "pl-[140px]",
|
||||
compact && (isOpen || (currentPreset && currentPreset?.start === value?.start && currentPreset?.end === value?.end)) && "pl-0"
|
||||
))}
|
||||
>
|
||||
<Input
|
||||
prefix={compact ? undefined : <ClockIcon />}
|
||||
prefixStyling={"pl-2.5"}
|
||||
suffix={<ArrowBottomIcon className={clsx("duration-200", isOpen && "rotate-180")} />}
|
||||
suffixStyling={clsx(
|
||||
"cursor-pointer",
|
||||
compact && !isOpen && (!currentPreset || (currentPreset?.start !== value?.start && currentPreset?.end !== value?.end)) && "w-10 !px-0"
|
||||
)}
|
||||
placeholder="选择时间段"
|
||||
disabled={disabled}
|
||||
onFocus={onFocus}
|
||||
value={inputValue}
|
||||
onChange={onChangeInputValue}
|
||||
wrapperClassName={clsx(
|
||||
"hover:z-10",
|
||||
stacked && !compact && "rounded-b-none",
|
||||
!stacked && !compact && "rounded-r-none",
|
||||
compact && "rounded-l-none",
|
||||
(isOpen || (compact && currentPreset && currentPreset?.start === value?.start && currentPreset?.end === value?.end)) && "z-10"
|
||||
)}
|
||||
className={clsx(
|
||||
"pl-2 placeholder:!text-gray-1000 placeholder:!opacity-100",
|
||||
compact && !isOpen && (!currentPreset || (currentPreset?.start !== value?.start && currentPreset?.end !== value?.end)) && "!w-0 !px-0"
|
||||
)}
|
||||
/>
|
||||
<Material
|
||||
type="menu"
|
||||
className={clsx(
|
||||
"absolute z-50 top-12 left-0",
|
||||
compact ? "w-full" : "grid grid-cols-2 w-[200%]",
|
||||
isOpen && "opacity-100",
|
||||
!isOpen && "opacity-0 pointer-events-none duration-200"
|
||||
)}
|
||||
>
|
||||
<ul className="p-2 border-r border-r-gray-200">
|
||||
{Object.entries(filteredPresets).length > 0 ? Object.entries(filteredPresets).map(([key, value]) => (
|
||||
<li
|
||||
key={key}
|
||||
className="flex items-center cursor-pointer px-2 w-full h-9 rounded-md hover:bg-gray-alpha-300 active:bg-gray-alpha-300 font-sans text-sm text-gray-1000"
|
||||
onClick={() => onClick(value)}
|
||||
>
|
||||
{value.text}
|
||||
</li>
|
||||
)) : (
|
||||
<li
|
||||
className="flex items-center cursor-pointer px-2 w-full h-9 rounded-md hover:bg-gray-alpha-300 active:bg-gray-alpha-300 font-sans text-sm text-gray-1000">
|
||||
{inputValue}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{!compact && (
|
||||
<div className="p-4 pr-[30px]">
|
||||
<div className="font-sans text-gray-900 text-sm">输入相对时间</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{typeRelativeTimes.map((value) => (
|
||||
<button
|
||||
key={value.text}
|
||||
className="font-mono text-[13px] text-gray-1000 px-1.5 h-5 inline-flex items-center bg-accents-2 border-none rounded cursor-pointer"
|
||||
onClick={() => onClick(value)}
|
||||
>
|
||||
{value.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* <div className="font-sans text-gray-900 text-sm mt-4">选择固定时间</div> */}
|
||||
{/* <div className="mt-2 flex flex-wrap gap-1">
|
||||
{typeFixedTimes.map((value) => (
|
||||
<button
|
||||
key={value.text}
|
||||
className="font-mono text-[13px] text-gray-1000 px-1.5 h-5 inline-flex items-center bg-accents-2 border-none rounded cursor-pointer"
|
||||
>
|
||||
{value.text}
|
||||
</button>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
</Material>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface RangeValue {
|
||||
start: Date | null;
|
||||
end: Date | null;
|
||||
}
|
||||
|
||||
interface CalendarProps {
|
||||
allowClear?: boolean;
|
||||
compact?: boolean;
|
||||
isDocsPage?: boolean;
|
||||
stacked?: boolean;
|
||||
disabled: boolean;
|
||||
horizontalLayout?: boolean;
|
||||
showTimeInput?: boolean;
|
||||
popoverAlignment?: "start" | "center" | "end";
|
||||
value: RangeValue | null;
|
||||
onChange: (date: RangeValue | null) => void;
|
||||
presets?: {
|
||||
[key: string]: {
|
||||
text: string;
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
};
|
||||
presetIndex?: number;
|
||||
minValue?: Date;
|
||||
maxValue?: Date;
|
||||
}
|
||||
|
||||
export const Calendar = ({
|
||||
allowClear = false,
|
||||
compact = false,
|
||||
isDocsPage = false,
|
||||
stacked = false,
|
||||
horizontalLayout = false,
|
||||
showTimeInput = true,
|
||||
popoverAlignment = "start",
|
||||
value,
|
||||
disabled = false,
|
||||
onChange,
|
||||
presets,
|
||||
presetIndex,
|
||||
minValue,
|
||||
maxValue,
|
||||
}: CalendarProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||
const [hoverDate, setHoverDate] = useState<Date | null>(null);
|
||||
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
||||
const timezones = useMemo(() => ([
|
||||
{
|
||||
value: "UTC",
|
||||
label: "UTC"
|
||||
},
|
||||
{
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
label: `Local (${Intl.DateTimeFormat().resolvedOptions().timeZone})`
|
||||
}
|
||||
|
||||
]), []);
|
||||
const [selectedTimezone, setSelectedTimezone] = useState(timezones[1].value);
|
||||
const [startDate, setStartDate] = useState<string>(formatInTimeZone(value?.start || new Date(), selectedTimezone, "MMM dd, yyyy"));
|
||||
const [startTime, setStartTime] = useState<string>(formatInTimeZone(startOfDay(value?.start || new Date()), selectedTimezone, "HH:mm"));
|
||||
const [endDate, setEndDate] = useState<string>(formatInTimeZone(value?.end || new Date(), selectedTimezone, "MMM dd, yyyy"));
|
||||
const [endTime, setEndTime] = useState<string>(formatInTimeZone(endOfDay(value?.end || new Date()), selectedTimezone, "HH:mm"));
|
||||
const [startDateError, setStartDateError] = useState<boolean>(false);
|
||||
const [startTimeError, setStartTimeError] = useState<boolean>(false);
|
||||
const [endDateError, setEndDateError] = useState<boolean>(false);
|
||||
const [endTimeError, setEndTimeError] = useState<boolean>(false);
|
||||
const calendarRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useClickOutside(calendarRef, () => setIsOpen(false));
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", () => setIsOpen(false));
|
||||
window.addEventListener("scroll", () => setIsOpen(false));
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", () => setIsOpen(false));
|
||||
window.removeEventListener("scroll", () => setIsOpen(false));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const prevMonth = () => setCurrentDate(subMonths(currentDate, 1));
|
||||
const nextMonth = () => setCurrentDate(addMonths(currentDate, 1));
|
||||
|
||||
const daysArray = [];
|
||||
let day = startOfWeek(startOfMonth(currentDate), { weekStartsOn: 1 });
|
||||
while (day <= endOfWeek(endOfMonth(currentDate), { weekStartsOn: 1 })) {
|
||||
daysArray.push(day);
|
||||
day = addDays(day, 1);
|
||||
}
|
||||
|
||||
const handleDateClick = (day: Date) => {
|
||||
if (!value?.start || (value.start && value.end)) {
|
||||
onChange({ start: startOfDay(day), end: null });
|
||||
setHoverDate(day);
|
||||
setIsSelecting(true);
|
||||
} else if (isSelecting) {
|
||||
if (day > value.start) {
|
||||
onChange({ ...value, end: endOfDay(day) });
|
||||
} else {
|
||||
onChange({ start: startOfDay(day), end: endOfDay(value.start) });
|
||||
}
|
||||
setIsSelecting(false);
|
||||
setHoverDate(null);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter = (day: Date) => {
|
||||
if (value?.start && !value.end) {
|
||||
setHoverDate(day);
|
||||
}
|
||||
};
|
||||
|
||||
const onApply = () => {
|
||||
const parsedStartDate = parse(startDate, "MMM dd, yyyy", new Date());
|
||||
const parsedStartTime = parse(startTime || "", "HH:mm", new Date());
|
||||
const parsedEndDate = parse(endDate, "MMM dd, yyyy", new Date());
|
||||
const parsedEndTime = parse(endTime || "", "HH:mm", new Date());
|
||||
|
||||
if (
|
||||
parsedStartDate.toString() === "Invalid Date" ||
|
||||
parsedStartTime.toString() === "Invalid Date" ||
|
||||
parsedEndDate.toString() === "Invalid Date" ||
|
||||
parsedEndTime.toString() === "Invalid Date"
|
||||
) {
|
||||
setStartDateError(parsedStartDate.toString() === "Invalid Date");
|
||||
setStartTimeError(parsedStartTime.toString() === "Invalid Date");
|
||||
setEndDateError(parsedEndDate.toString() === "Invalid Date");
|
||||
setEndTimeError(parsedEndTime.toString() === "Invalid Date");
|
||||
} else {
|
||||
setStartDateError(false);
|
||||
setStartTimeError(false);
|
||||
setEndDateError(false);
|
||||
setEndTimeError(false);
|
||||
const parsedStart = parse(`${startDate} ${startTime}`, "MMM d, yyyy HH:mm", new Date());
|
||||
const parsedEnd = parse(`${endDate} ${endTime}`, "MMM d, yyyy HH:mm", new Date());
|
||||
onChange({
|
||||
start: fromZonedTime(parsedStart, selectedTimezone),
|
||||
end: fromZonedTime(parsedEnd, selectedTimezone)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setStartDate(formatInTimeZone(value?.start || new Date(), selectedTimezone, "MMM dd, yyyy"));
|
||||
setStartTime(formatInTimeZone(value?.start || startOfDay(new Date()), selectedTimezone, "HH:mm"));
|
||||
setEndDate(formatInTimeZone(value?.end || new Date(), selectedTimezone, "MMM dd, yyyy"));
|
||||
setEndTime(formatInTimeZone(value?.end || endOfDay(new Date()), selectedTimezone, "HH:mm"));
|
||||
}, [isOpen, value]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={clsx(
|
||||
presets && "flex",
|
||||
presets && stacked && "flex-col",
|
||||
compact && "w-[220px]"
|
||||
)}>
|
||||
{presets && (
|
||||
<div>
|
||||
<CalendarCombobox
|
||||
stacked={stacked}
|
||||
compact={compact}
|
||||
disabled={disabled}
|
||||
presets={presets}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
presetIndex={presetIndex}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="relative">
|
||||
<Button
|
||||
disabled={disabled}
|
||||
className={clsx(
|
||||
"!justify-start focus:!border-transparent focus:!shadow-focus-input",
|
||||
presets && !stacked && !compact && "rounded-l-none -ml-[1px]",
|
||||
presets && stacked && !compact && "rounded-t-none -mt-[1px]",
|
||||
presets && compact && "rounded-r-none -mr-[1px]",
|
||||
compact ? "w-[180px] gap-1.5" : "w-[250px]"
|
||||
)}
|
||||
prefix={<CalendarIcon />}
|
||||
type="secondary"
|
||||
onClick={() => setIsOpen((prevState) => !prevState)}
|
||||
>
|
||||
<div className="truncate pr-4">
|
||||
{value?.start && value?.end ?
|
||||
formatDateRange(value.start, value.end, selectedTimezone)
|
||||
: "选择时间范围"
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
{value?.start && value?.end && (
|
||||
<Button
|
||||
aria-label="Clear input value"
|
||||
svgOnly
|
||||
variant="unstyled"
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 fill-gray-700 hover:fill-gray-1000"
|
||||
onClick={() => onChange(null)}
|
||||
>
|
||||
<ClearIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<Material
|
||||
ref={calendarRef}
|
||||
type="menu"
|
||||
className={twMerge(clsx(
|
||||
"p-3 font-sans absolute top-12 z-10",
|
||||
horizontalLayout ? "w-[462px]" : "w-[280px]",
|
||||
presets && !stacked && !compact && "left-[250px]",
|
||||
presets && stacked && "top-[88px]",
|
||||
popoverAlignment === "center" && "left-[125px] -translate-x-1/2",
|
||||
popoverAlignment === "end" && "left-[250px] -translate-x-full"
|
||||
))}
|
||||
>
|
||||
<div className={clsx(horizontalLayout && "flex gap-5")}>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h2 className="text-sm text-gray-1000 font-medium">
|
||||
{formatInTimeZone(currentDate, selectedTimezone, "MMMM yyyy")}
|
||||
</h2>
|
||||
<div className="flex gap-0.5">
|
||||
<Button variant="unstyled" onClick={prevMonth}><ArrowLeftIcon /></Button>
|
||||
<Button variant="unstyled" onClick={nextMonth}><ArrowRightIcon /></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 text-center text-xs text-gray-900 uppercase mb-2">
|
||||
<div>一</div>
|
||||
<div>二</div>
|
||||
<div>三</div>
|
||||
<div>四</div>
|
||||
<div>五</div>
|
||||
<div>六</div>
|
||||
<div>日</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 items-center gap-y-2">
|
||||
{daysArray.map((day) => {
|
||||
const isStart = value?.start && isSameDay(day, value.start);
|
||||
const isEnd = value?.end && isSameDay(day, value.end);
|
||||
const currentHover = hoverDate && isSelecting && isSameDay(day, hoverDate);
|
||||
const isInRange =
|
||||
value?.start &&
|
||||
((value.end && isWithinInterval(day, { start: value.start, end: value.end })) ||
|
||||
(hoverDate && isWithinInterval(day, { start: value.start, end: hoverDate })));
|
||||
const isAllowedDate = (minValue ? day >= minValue : true) && (maxValue ? day <= maxValue : true);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={day.toString()}
|
||||
className={clsx(
|
||||
"flex items-center justify-center text-sm text-center rounded transition",
|
||||
isSameMonth(day, currentDate) && isAllowedDate ? "bg-background-100 text-gray-1000" : "bg-background-100 text-gray-700",
|
||||
isInRange && !isStart && !isEnd && !currentHover && "!bg-accents-2 rounded-none",
|
||||
isAllowedDate ? "cursor-pointer" : "cursor-not-allowed"
|
||||
)}
|
||||
onMouseEnter={() => isAllowedDate && handleMouseEnter(day)}
|
||||
onClick={() => isAllowedDate && handleDateClick(day)}
|
||||
>
|
||||
<div className={clsx(
|
||||
"h-8 w-8 flex items-center justify-center rounded",
|
||||
(isStart || isEnd || currentHover) && isAllowedDate && " !bg-gray-1000 !text-background-100",
|
||||
!isStart && !isEnd && !currentHover && !isToday(day) && isAllowedDate && "hover:text-gray-1000 hover:border hover:border-gray-alpha-500",
|
||||
currentHover && isAllowedDate && " !shadow-focus-calendar-date",
|
||||
isToday(day) && " !bg-blue-900 !text-background-100"
|
||||
)}>
|
||||
{format(day, "d")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className={clsx(
|
||||
"flex flex-col gap-2",
|
||||
horizontalLayout ? "justify-between" : "mt-3 -mx-3 px-3 pt-2.5 border-t border-gray-alpha-100"
|
||||
)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<div className="text-[13px] text-gray-900 capitalize">Start</div>
|
||||
<div className="grid grid-cols-3 gap-2 mt-1">
|
||||
<div className={showTimeInput ? "col-span-2" : "col-span-3"}>
|
||||
<Input
|
||||
size="small"
|
||||
value={startDate}
|
||||
onChange={(value) => setStartDate(value)}
|
||||
error={startDateError}
|
||||
/>
|
||||
</div>
|
||||
{showTimeInput && (
|
||||
<Input
|
||||
size="small"
|
||||
value={startTime}
|
||||
onChange={(value) => setStartTime(value)}
|
||||
error={startTimeError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[13px] text-gray-900 capitalize">End</div>
|
||||
<div className="grid grid-cols-3 gap-2 mt-1">
|
||||
<div className={showTimeInput ? "col-span-2" : "col-span-3"}>
|
||||
<Input
|
||||
size="small"
|
||||
value={endDate}
|
||||
onChange={(value) => setEndDate(value)}
|
||||
error={endDateError}
|
||||
/>
|
||||
</div>
|
||||
{showTimeInput && (
|
||||
<Input
|
||||
size="small"
|
||||
value={endTime}
|
||||
onChange={(value) => setEndTime(value)}
|
||||
error={endTimeError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="font-medium flex flex-col">
|
||||
<Button
|
||||
type="secondary"
|
||||
size="small"
|
||||
suffix={<span className="mt-1 text-xs">↵</span>}
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-fit self-center">
|
||||
<Select
|
||||
size="xsmall"
|
||||
variant="ghost"
|
||||
options={timezones}
|
||||
value={selectedTimezone}
|
||||
onChange={(event) => setSelectedTimezone(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</Material>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
81
ui/src/components/ui/error.tsx
Normal file
81
ui/src/components/ui/error.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
|
||||
const ErrorIcon = () => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.30761 1.5L1.5 5.30761L1.5 10.6924L5.30761 14.5H10.6924L14.5 10.6924V5.30761L10.6924 1.5H5.30761ZM5.10051 0C4.83529 0 4.58094 0.105357 4.3934 0.292893L0.292893 4.3934C0.105357 4.58094 0 4.83529 0 5.10051V10.8995C0 11.1647 0.105357 11.4191 0.292894 11.6066L4.3934 15.7071C4.58094 15.8946 4.83529 16 5.10051 16H10.8995C11.1647 16 11.4191 15.8946 11.6066 15.7071L15.7071 11.6066C15.8946 11.4191 16 11.1647 16 10.8995V5.10051C16 4.83529 15.8946 4.58093 15.7071 4.3934L11.6066 0.292893C11.4191 0.105357 11.1647 0 10.8995 0H5.10051ZM8.75 3.75V4.5V8L8.75 8.75H7.25V8V4.5V3.75H8.75ZM8 12C8.55229 12 9 11.5523 9 11C9 10.4477 8.55229 10 8 10C7.44772 10 7 10.4477 7 11C7 11.5523 7.44772 12 8 12Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ErrorLinkIcon = () => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.5 9.75V11.25C11.5 11.3881 11.3881 11.5 11.25 11.5H4.75C4.61193 11.5 4.5 11.3881 4.5 11.25L4.5 4.75C4.5 4.61193 4.61193 4.5 4.75 4.5H6.25H7V3H6.25H4.75C3.7835 3 3 3.7835 3 4.75V11.25C3 12.2165 3.7835 13 4.75 13H11.25C12.2165 13 13 12.2165 13 11.25V9.75V9H11.5V9.75ZM8.5 3H9.25H12.2495C12.6637 3 12.9995 3.33579 12.9995 3.75V6.75V7.5H11.4995V6.75V5.56066L8.53033 8.52978L8 9.06011L6.93934 7.99945L7.46967 7.46912L10.4388 4.5H9.25H8.5V3Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface Error {
|
||||
message: string;
|
||||
action: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface ErrorProps {
|
||||
error?: Error;
|
||||
label?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Error = ({ error, label, size = "medium", children }: ErrorProps) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`flex items-center gap-2 text-red-900 fill-red-900 font-sans
|
||||
${{
|
||||
small: "text-[13px] leading-5",
|
||||
medium: "text-sm",
|
||||
large: "text-base"
|
||||
}[size]}`
|
||||
}
|
||||
// @ts-ignore
|
||||
style={{ "--geist-link-color": "var(--ds-red-900)" }}
|
||||
>
|
||||
<ErrorIcon />
|
||||
{error ? (
|
||||
<>
|
||||
{error.message}
|
||||
<a
|
||||
className="font-medium flex items-center gap-0.5 -ml-1 hover:no-underline hover:opacity-60 duration-150 relative after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-[1px] after:bg-red-900"
|
||||
href={error.link}
|
||||
target="_blank"
|
||||
>
|
||||
{error.action}
|
||||
<ErrorLinkIcon />
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{label && <span className="font-medium">{label}:</span>}
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
120
ui/src/components/ui/input.tsx
Normal file
120
ui/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Error } from "@/components/ui/error";
|
||||
import clsx from "clsx";
|
||||
|
||||
const sizes = {
|
||||
xSmall: "h-6 text-xs rounded-md",
|
||||
small: "h-8 text-sm rounded-md",
|
||||
mediumSmall: "h-10 text-sm rounded-md",
|
||||
medium: "h-10 text-sm rounded-md",
|
||||
large: "h-12 text-base rounded-lg"
|
||||
};
|
||||
|
||||
interface InputProps {
|
||||
placeholder?: string;
|
||||
size?: keyof typeof sizes;
|
||||
prefix?: React.ReactNode | string;
|
||||
suffix?: React.ReactNode | string;
|
||||
prefixStyling?: boolean | string;
|
||||
suffixStyling?: boolean | string;
|
||||
disabled?: boolean;
|
||||
error?: string | boolean;
|
||||
label?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
ref?: React.RefObject<HTMLInputElement | null>;
|
||||
className?: string;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
export const Input = ({
|
||||
placeholder,
|
||||
size = "medium",
|
||||
prefix,
|
||||
suffix,
|
||||
prefixStyling = true,
|
||||
suffixStyling = true,
|
||||
disabled = false,
|
||||
error,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
ref,
|
||||
className,
|
||||
wrapperClassName,
|
||||
...rest
|
||||
}: InputProps) => {
|
||||
const [_value, set_value] = useState(value || "");
|
||||
const _ref = ref ? ref : useRef<HTMLInputElement>(null);
|
||||
|
||||
const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
set_value(e.target.value);
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined) {
|
||||
set_value(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2" onClick={() => _ref.current?.focus()}>
|
||||
{label && (
|
||||
<div className="capitalize text-[13px] text-gray-900">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
<div className={clsx(
|
||||
"flex items-center duration-150 font-sans",
|
||||
error ? "shadow-error-input hover:shadow-error-input-hover" : "border border-gray-alpha-400 hover:border-gray-alpha-500 focus-within:border-transparent focus-within:shadow-focus-input",
|
||||
sizes[size],
|
||||
disabled ? "cursor-not-allowed bg-gray-100" : "bg-background-100",
|
||||
wrapperClassName
|
||||
)}>
|
||||
{prefix && (
|
||||
<div
|
||||
className={clsx(
|
||||
"text-gray-700 fill-gray-700 h-full flex items-center justify-center",
|
||||
prefixStyling === true ? "bg-background-200 border-r border-gray-alpha-400 px-3" : `pl-3${!prefixStyling ? "" : ` ${prefixStyling}`}`,
|
||||
size === "large" ? "rounded-l-lg" : "rounded-l-md"
|
||||
)}>
|
||||
{prefix}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
className={clsx(
|
||||
"w-full inline-flex appearance-none placeholder:text-gray-900 placeholder:opacity-70 outline-none",
|
||||
(size === "xSmall" || size === "mediumSmall") ? "px-2" : "px-3",
|
||||
disabled ? "cursor-not-allowed bg-gray-100 text-gray-700" : "bg-background-100 text-geist-foreground",
|
||||
className
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
value={_value}
|
||||
onChange={_onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
ref={_ref}
|
||||
{...rest}
|
||||
/>
|
||||
{suffix && (
|
||||
<div className={clsx(
|
||||
"text-gray-700 fill-gray-700 h-full flex items-center justify-center",
|
||||
suffixStyling === true ? "bg-background-200 border-l border-gray-alpha-400 px-3" : `pr-3 ${!suffixStyling ? "" : ` ${suffixStyling}`}`,
|
||||
size === "large" ? "rounded-r-lg" : "rounded-r-md"
|
||||
)}>
|
||||
{suffix}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{typeof error === "string" && <Error size={size === "large" ? "large" : "small"}>{error}</Error>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
ui/src/components/ui/material-1.tsx
Normal file
39
ui/src/components/ui/material-1.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
const types = {
|
||||
base: "rounded-md shadow-border",
|
||||
small: "rounded-md shadow-border-small",
|
||||
medium: "rounded-xl shadow-border-medium",
|
||||
large: "rounded-xl shadow-border-large",
|
||||
tooltip: "rounded-md shadow-tooltip",
|
||||
menu: "rounded-xl shadow-menu",
|
||||
modal: "rounded-xl shadow-modal",
|
||||
fullscreen: "rounded-2xl shadow-fullscreen"
|
||||
};
|
||||
|
||||
interface MaterialProps {
|
||||
type: keyof typeof types;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
style?: React.CSSProperties;
|
||||
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export const Material = ({ type, children, className, ref, style, onClick }: MaterialProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-background-100",
|
||||
types[type],
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
140
ui/src/components/ui/select-1.tsx
Normal file
140
ui/src/components/ui/select-1.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from "react";
|
||||
import { Error } from "@/components/ui/error";
|
||||
import clsx from "clsx";
|
||||
|
||||
const sizes = [
|
||||
{
|
||||
xsmall: "h-6 text-xs pl-1.5 pr-[22px]",
|
||||
small: "h-8 text-sm pl-3 pr-9",
|
||||
medium: "h-10 text-sm pl-3 pr-9",
|
||||
large: "h-12 text-base pl-3 pr-9 rounded-lg"
|
||||
},
|
||||
{
|
||||
xsmall: "h-6 text-xs px-[22px]",
|
||||
small: "h-8 text-sm px-9",
|
||||
medium: "h-10 text-sm px-9",
|
||||
large: "h-12 text-base px-9 rounded-lg"
|
||||
}
|
||||
];
|
||||
|
||||
const variants = {
|
||||
default: "",
|
||||
ghost: ""
|
||||
};
|
||||
|
||||
export interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
variant?: keyof typeof variants;
|
||||
options?: Option[];
|
||||
label?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
size?: keyof typeof sizes[0];
|
||||
prefix?: React.ReactNode;
|
||||
suffix?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
onChange?: React.ChangeEventHandler<HTMLSelectElement>;
|
||||
}
|
||||
|
||||
const ArrowBottom = () => (
|
||||
<svg
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.0607 5.49999L13.5303 6.03032L8.7071 10.8535C8.31658 11.2441 7.68341 11.2441 7.29289 10.8535L2.46966 6.03032L1.93933 5.49999L2.99999 4.43933L3.53032 4.96966L7.99999 9.43933L12.4697 4.96966L13 4.43933L14.0607 5.49999Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Select = ({
|
||||
variant = "default",
|
||||
options,
|
||||
label,
|
||||
value,
|
||||
placeholder,
|
||||
size = "medium",
|
||||
suffix,
|
||||
prefix,
|
||||
disabled = false,
|
||||
error,
|
||||
onChange
|
||||
}: SelectProps) => {
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor="select"
|
||||
className="cursor-text block font-sans text-[13px] text-gray-900 capitalize mb-2"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className={clsx(
|
||||
"relative flex items-center",
|
||||
disabled ? "fill-[#8f8f8f]" : "fill-[#666666] dark:fill-[#a1a1a1] hover:fill-[#171717] hover:dark:fill-[#ededed]"
|
||||
)}>
|
||||
<style>
|
||||
{`
|
||||
.xsmallIconContainer svg {
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
}
|
||||
.smallIconContainer, .mediumIconContainer, .largeIconContainer svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<select
|
||||
id="select"
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={clsx(
|
||||
"font-sans appearance-none w-full border rounded-[5px] duration-200 outline-none",
|
||||
sizes[prefix ? 1 : 0][size],
|
||||
disabled ? "cursor-not-allowed bg-gray-100 text-gray-700" : variant === "default" ? "text-gray-1000 bg-background-100 cursor-pointer" : "bg-transparent text-accents-5",
|
||||
error ? "border-error ring-red-900-alpha-160 ring-opacity-100 ring-[3px]" : `ring-gray-alpha-500 ring-opacity-100 focus:ring-[3px] ${variant === "default" ? "border-gray-alpha-400" : "border-transparent ring-none"}`
|
||||
)}
|
||||
>
|
||||
{placeholder && <option value="" disabled selected>{placeholder}</option>}
|
||||
{options && options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{prefix && (
|
||||
<span className={clsx(
|
||||
`inline-flex absolute pointer-events-none duration-150 ${size}IconContainer`,
|
||||
size === "xsmall" ? "left-[5px]" : "left-3"
|
||||
)}>
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={clsx(
|
||||
`inline-flex absolute pointer-events-none duration-150 ${size}IconContainer`,
|
||||
size === "xsmall" ? "right-[5px]" : "right-3"
|
||||
)}>
|
||||
{suffix ? suffix : <ArrowBottom />}
|
||||
</span>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="mt-2">
|
||||
<Error size={size === "large" ? "large" : "small"}>{error}</Error>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
73
ui/src/components/ui/spinner-1.tsx
Normal file
73
ui/src/components/ui/spinner-1.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
|
||||
interface SpinnerProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const bars = [
|
||||
{
|
||||
animationDelay: "-1.2s",
|
||||
transform: "rotate(.0001deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-1.1s",
|
||||
transform: "rotate(30deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-1.0s",
|
||||
transform: "rotate(60deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.9s",
|
||||
transform: "rotate(90deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.8s",
|
||||
transform: "rotate(120deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.7s",
|
||||
transform: "rotate(150deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.6s",
|
||||
transform: "rotate(180deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.5s",
|
||||
transform: "rotate(210deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.4s",
|
||||
transform: "rotate(240deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.3s",
|
||||
transform: "rotate(270deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.2s",
|
||||
transform: "rotate(300deg) translate(146%)"
|
||||
},
|
||||
{
|
||||
animationDelay: "-0.1s",
|
||||
transform: "rotate(330deg) translate(146%)"
|
||||
}
|
||||
];
|
||||
|
||||
export const Spinner = ({ size = 20, color = "#8f8f8f" }: SpinnerProps) => {
|
||||
return (
|
||||
<div style={{ width: size, height: size }}>
|
||||
<div className="relative top-1/2 left-1/2" style={{ width: size, height: size }}>
|
||||
{bars.map((item) => (
|
||||
<div
|
||||
key={item.transform}
|
||||
className="absolute h-[8%] w-[24%] -left-[10%] -top-[3.9%] rounded-[5px] animate-fade-spin"
|
||||
style={{ backgroundColor: color, ...item }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
19
ui/src/components/ui/use-click-outside.tsx
Normal file
19
ui/src/components/ui/use-click-outside.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { RefObject, useEffect } from "react";
|
||||
|
||||
export const useClickOutside = (ref: RefObject<any>, callback: () => void) => {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: Event) {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("touchstart", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("touchstart", handleClickOutside);
|
||||
};
|
||||
}, [ref]);
|
||||
};
|
||||
321
ui/src/index.css
321
ui/src/index.css
@@ -1,3 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
/* ---break---*/
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -5,15 +10,99 @@ html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@layer base{
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-gilory: 'gilroy';
|
||||
--font-HarmonyOS: 'HarmonyOS';
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
--context-card-border: hsla(0, 0%, 92%, 1);
|
||||
--ds-blue-100: oklch(97.32% 0.0141 251.56);
|
||||
--ds-blue-900: oklch(53.18% 0.2399 256.9900584162342);
|
||||
--ds-red-100: oklch(96.5% 0.0223 13.09);
|
||||
--ds-red-900: oklch(54.99% 0.232 25.29);
|
||||
--ds-amber-100: oklch(97.48% 0.0331 85.79);
|
||||
--ds-amber-900: oklch(52.79% 0.1496 54.65);
|
||||
--ds-gray-100: hsla(0, 0%, 95%, 1);
|
||||
--ds-gray-1000: hsla(0, 0%, 9%, 1);
|
||||
--ds-gray-alpha-400: hsla(0, 0%, 0%, 0.08);
|
||||
--ds-background-100: hsla(0, 0%, 100%, 1);
|
||||
--color-red-900: oklch(54.99% 0.232 25.29);
|
||||
--ds-shadow-border: 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
--ds-shadow-small: 0px 2px 2px rgba(0, 0, 0, 0.04);
|
||||
--ds-shadow-border-small: var(--ds-shadow-border), var(--ds-shadow-small);
|
||||
--ds-shadow-medium: 0px 2px 2px rgba(0, 0, 0, 0.04), 0px 8px 8px -8px rgba(0, 0, 0, 0.04);
|
||||
--ds-shadow-border-medium: var(--ds-shadow-border), var(--ds-shadow-medium);
|
||||
--ds-shadow-large: 0px 2px 2px rgba(0, 0, 0, 0.04), 0px 8px 16px -4px rgba(0, 0, 0, 0.04);
|
||||
--ds-shadow-border-large: var(--ds-shadow-border), var(--ds-shadow-large);
|
||||
--ds-shadow-tooltip: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 4px 8px rgba(0, 0, 0, 0.04);
|
||||
--ds-shadow-menu: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 4px 8px -4px rgba(0, 0, 0, 0.04), 0px 16px 24px -8px rgba(0, 0, 0, 0.06);
|
||||
--ds-shadow-modal: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 8px 16px -4px rgba(0, 0, 0, 0.04), 0px 24px 32px -8px rgba(0, 0, 0, 0.06);
|
||||
--ds-shadow-fullscreen: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 8px 16px -4px rgba(0, 0, 0, 0.04), 0px 24px 32px -8px rgba(0, 0, 0, 0.06);
|
||||
--ds-red-800: oklch(58.19% 0.2482 25.15);
|
||||
--ds-blue-700: oklch(57.61% 0.2508 258.23);
|
||||
--ds-amber-800: oklch(77.21% 0.1991 64.28);
|
||||
--ds-amber-850: hsl(33, 96%, 42%);
|
||||
--ds-gray-400: hsla(0, 0%, 92%, 1);
|
||||
--ds-gray-700: hsla(0, 0%, 56%, 1);
|
||||
--ds-gray-1000-h: hsl(0, 0%, 22%);
|
||||
--ds-gray-alpha-200: hsla(0, 0%, 0%, 0.08);
|
||||
--ds-focus-color: var(--ds-blue-700);
|
||||
--ds-focus-ring: 0 0 0 2px var(--ds-background-100), 0 0 0 4px var(--ds-focus-color);
|
||||
--ds-red-300: oklch(94.33% 0.0369 15.011509923860523);
|
||||
--ds-red-500: oklch(84.47% 0.1018 17.71);
|
||||
--ds-gray-900: hsla(0, 0%, 40%, 1);
|
||||
--ds-gray-alpha-500: hsla(0, 0%, 0%, 0.21);
|
||||
--ds-gray-alpha-600: hsla(0, 0%, 0%, 0.34);
|
||||
--ds-background-200: hsla(0, 0%, 98%, 1);
|
||||
--geist-foreground: #000;
|
||||
--ds-input-ring: 0 0 0 1px var(--ds-gray-alpha-600), 0 0 0 4px rgba(0, 0, 0, .16);
|
||||
--ds-input-error-ring: 0 0 0 1px var(--ds-red-900), 0 0 0 4px var(--ds-red-300);
|
||||
--ds-input-error-hover-ring: 0 0 0 1px var(--ds-red-900), 0 0 0 4px var(--ds-red-500);
|
||||
--ds-red-900-alpha-160: hsla(358,66%,48%,.16);
|
||||
--geist-error: #ee0000;
|
||||
--ds-gray-200: hsla(0, 0%, 92%, 1);
|
||||
--ds-gray-alpha-100: rgba(0, 0, 0, 0.05);
|
||||
--ds-gray-alpha-300: hsla(0, 0%, 0%, 0.1);
|
||||
--accents-2: #eaeaea;
|
||||
--ds-focus-calendar-date-ring: 0 0 0 1px var(--ds-background-100), 0 0 0 2.5px var(--ds-gray-1000);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -78,3 +167,227 @@ select:-webkit-autofill {
|
||||
-webkit-text-fill-color: var(--mui-palette-text-primary) !important;
|
||||
transition: background-color 5000s ease-in-out 0s !important;
|
||||
}
|
||||
|
||||
/* ---break---*/
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-context-card-border: var(--context-card-border);
|
||||
--animate-fade-spin: fade-spin 1.2s linear infinite;
|
||||
--color-ds-background-100: var(----ds-background-100);
|
||||
--color-ds-gray-1000: var(----ds-gray-1000);
|
||||
--color-background-100: var(--ds-background-100);
|
||||
--shadow-border: var(--ds-shadow-border);
|
||||
--shadow-border-small: var(--ds-shadow-border-small);
|
||||
--shadow-border-medium: var(--ds-shadow-border-medium);
|
||||
--shadow-border-large: var(--ds-shadow-border-large);
|
||||
--shadow-tooltip: var(--ds-shadow-tooltip);
|
||||
--shadow-menu: var(--ds-shadow-menu);
|
||||
--shadow-modal: var(--ds-shadow-modal);
|
||||
--shadow-fullscreen: var(--ds-shadow-fullscreen);
|
||||
--color-red-800: var(--ds-red-800);
|
||||
--color-red-900: var(--ds-red-900);
|
||||
--color-amber-800: var(--ds-amber-800);
|
||||
--color-amber-850: var(--ds-amber-850);
|
||||
--color-gray-100: var(--ds-gray-100);
|
||||
--color-gray-400: var(--ds-gray-400);
|
||||
--color-gray-700: var(--ds-gray-700);
|
||||
--color-gray-1000: var(--ds-gray-1000);
|
||||
--color-gray-1000-h: var(--ds-gray-1000-h);
|
||||
--color-gray-alpha-200: var(--ds-gray-alpha-200);
|
||||
--color-gray-alpha-400: var(--ds-gray-alpha-400);
|
||||
--shadow-focus-ring: var(--ds-focus-ring);
|
||||
--color-gray-900: var(--ds-gray-900);
|
||||
--color-gray-alpha-500: var(--ds-gray-alpha-500);
|
||||
--color-background-200: var(--ds-background-200);
|
||||
--color-geist-foreground: var(--geist-foreground);
|
||||
--shadow-focus-input: var(--ds-input-ring);
|
||||
--shadow-error-input: var(--ds-input-error-ring);
|
||||
--shadow-error-input-hover: var(--ds-input-error-hover-ring);
|
||||
--color-red-900-alpha-160: var(--ds-red-900-alpha-160);
|
||||
--color-error: var(--geist-error);
|
||||
--color-color-red-900: var(----color-red-900);
|
||||
--color-blue-900: var(--ds-blue-900);
|
||||
--color-gray-200: var(--ds-gray-200);
|
||||
--color-gray-alpha-100: var(--ds-gray-alpha-100);
|
||||
--color-gray-alpha-300: var(--ds-gray-alpha-300);
|
||||
--color-accents-2: var(--accents-2);
|
||||
--shadow-focus-calendar-date: var(--ds-focus-calendar-date-ring);
|
||||
--ds-focus-calendar-date-ring: var(----ds-focus-calendar-date-ring);
|
||||
--color-ds-gray-alpha-300: var(----ds-gray-alpha-300);
|
||||
--color-ds-gray-alpha-100: var(----ds-gray-alpha-100);
|
||||
--color-ds-gray-200: var(----ds-gray-200);
|
||||
--color-geist-error: var(----geist-error);
|
||||
--color-ds-red-900-alpha-160: var(----ds-red-900-alpha-160);
|
||||
--ds-input-error-hover-ring: var(----ds-input-error-hover-ring);
|
||||
--ds-input-error-ring: var(----ds-input-error-ring);
|
||||
--ds-input-ring: var(----ds-input-ring);
|
||||
--color-ds-background-200: var(----ds-background-200);
|
||||
--color-ds-gray-alpha-600: var(----ds-gray-alpha-600);
|
||||
--color-ds-gray-alpha-500: var(----ds-gray-alpha-500);
|
||||
--color-ds-gray-900: var(----ds-gray-900);
|
||||
--color-ds-red-500: var(----ds-red-500);
|
||||
--color-ds-red-300: var(----ds-red-300);
|
||||
--ds-focus-ring: var(----ds-focus-ring);
|
||||
--ds-focus-color: var(----ds-focus-color);
|
||||
--color-ds-gray-alpha-200: var(----ds-gray-alpha-200);
|
||||
--color-ds-gray-1000-h: var(----ds-gray-1000-h);
|
||||
--color-ds-gray-700: var(----ds-gray-700);
|
||||
--color-ds-gray-400: var(----ds-gray-400);
|
||||
--color-ds-amber-850: var(----ds-amber-850);
|
||||
--color-ds-amber-800: var(----ds-amber-800);
|
||||
--color-ds-blue-700: var(----ds-blue-700);
|
||||
--color-ds-red-800: var(----ds-red-800);
|
||||
--ds-shadow-fullscreen: var(----ds-shadow-fullscreen);
|
||||
--ds-shadow-modal: var(----ds-shadow-modal);
|
||||
--ds-shadow-menu: var(----ds-shadow-menu);
|
||||
--ds-shadow-tooltip: var(----ds-shadow-tooltip);
|
||||
--ds-shadow-border-large: var(----ds-shadow-border-large);
|
||||
--ds-shadow-large: var(----ds-shadow-large);
|
||||
--ds-shadow-border-medium: var(----ds-shadow-border-medium);
|
||||
--ds-shadow-medium: var(----ds-shadow-medium);
|
||||
--ds-shadow-border-small: var(----ds-shadow-border-small);
|
||||
--ds-shadow-small: var(----ds-shadow-small);
|
||||
--ds-shadow-border: var(----ds-shadow-border);
|
||||
--color-ds-gray-alpha-400: var(----ds-gray-alpha-400);
|
||||
--color-ds-gray-100: var(----ds-gray-100);
|
||||
--color-ds-amber-900: var(----ds-amber-900);
|
||||
--color-ds-amber-100: var(----ds-amber-100);
|
||||
--color-ds-red-900: var(----ds-red-900);
|
||||
--color-ds-red-100: var(----ds-red-100);
|
||||
--color-ds-blue-900: var(----ds-blue-900);
|
||||
--color-ds-blue-100: var(----ds-blue-100);
|
||||
@keyframes fade-spin {
|
||||
0% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---break---*/
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
--context-card-border: hsla(0, 0%, 18%, 1);
|
||||
--ds-blue-100: oklch(22.17% 0.069 259.89);
|
||||
--ds-blue-900: oklch(71.7% 0.1648 250.79360374054167);
|
||||
--ds-red-100: oklch(22.1% 0.0657 15.11);
|
||||
--ds-red-900: oklch(69.96% 0.2136 22.03);
|
||||
--ds-amber-100: oklch(22.46% 0.0538 76.04);
|
||||
--ds-amber-900: oklch(77.21% 0.1991 64.28);
|
||||
--ds-gray-100: hsla(0, 0%, 10%, 1);
|
||||
--ds-gray-1000: hsla(0, 0%, 93%, 1);
|
||||
--ds-gray-alpha-400: hsla(0, 0%, 100%, 0.14);
|
||||
--ds-background-100: hsla(0,0%,4%, 1);
|
||||
--color-red-900: oklch(69.96% 0.2136 22.03);
|
||||
--ds-shadow-border: 0 0 0 1px rgba(255, 255, 255, 0.145);
|
||||
--ds-shadow-small: 0px 1px 2px rgba(0, 0, 0, 0.16);
|
||||
--ds-shadow-border-small: var(--ds-shadow-border), 0px 1px 2px rgba(0, 0, 0, 0.16);
|
||||
--ds-shadow-medium: 0px 2px 2px rgba(0, 0, 0, 0.32), 0px 8px 8px -8px rgba(0, 0, 0, 0.16);
|
||||
--ds-shadow-border-medium: var(--ds-shadow-border), 0px 2px 2px rgba(0, 0, 0, 0.32), 0px 8px 8px -8px rgba(0, 0, 0, 0.16);
|
||||
--ds-shadow-large: 0px 2px 2px rgba(0, 0, 0, 0.04), 0px 8px 16px -4px rgba(0, 0, 0, 0.04);
|
||||
--ds-shadow-border-large: var(--ds-shadow-border), 0px 2px 2px rgba(0, 0, 0, 0.04), 0px 8px 16px -4px rgba(0, 0, 0, 0.04);
|
||||
--ds-shadow-tooltip: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 4px 8px rgba(0, 0, 0, 0.04);
|
||||
--ds-shadow-menu: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 4px 8px -4px rgba(0, 0, 0, 0.04), 0px 16px 24px -8px rgba(0, 0, 0, 0.06);
|
||||
--ds-shadow-modal: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 8px 16px -4px rgba(0, 0, 0, 0.04), 0px 24px 32px -8px rgba(0, 0, 0, 0.06);
|
||||
--ds-shadow-fullscreen: var(--ds-shadow-border), 0px 1px 1px rgba(0, 0, 0, 0.02), 0px 8px 16px -4px rgba(0, 0, 0, 0.04), 0px 24px 32px -8px rgba(0, 0, 0, 0.06);
|
||||
--ds-red-800: oklch(58.01% 0.227 25.12);
|
||||
--ds-blue-700: oklch(57.61% 0.2321 258.23);
|
||||
--ds-amber-800: oklch(77.21% 0.1991 64.28);
|
||||
--ds-gray-400: hsla(0, 0%, 18%, 1);
|
||||
--ds-gray-700: hsla(0, 0%, 56%, 1);
|
||||
--ds-gray-1000-h: hsl(0, 0%, 80%);
|
||||
--ds-gray-alpha-200: hsla(0, 0%, 100%, 0.09);
|
||||
--ds-red-300: oklch(31.47% 0.1105 20.96);
|
||||
--ds-red-500: oklch(40.68% 0.1479 23.16);
|
||||
--ds-gray-900: hsla(0, 0%, 63%, 1);
|
||||
--ds-gray-alpha-500: hsla(0, 0%, 100%, 0.24);
|
||||
--ds-gray-alpha-600: hsla(0, 0%, 100%, 0.51);
|
||||
--ds-background-200: hsla(0, 0%, 0%, 1);
|
||||
--geist-foreground: #fff;
|
||||
--ds-input-ring: 0 0 0 1px var(--ds-gray-alpha-600), 0 0 0 4px rgba(255, 255, 255, .24);
|
||||
--ds-red-900-alpha-160: hsla(358,100%,69%,.16);
|
||||
--geist-error: #ff0000;
|
||||
--ds-gray-200: hsla(0, 0%, 12%, 1);
|
||||
--ds-gray-alpha-100: rgba(255, 255, 255, 0.06);
|
||||
--ds-gray-alpha-300: hsla(0, 0%, 100%, 0.13);
|
||||
--accents-2: #333333;
|
||||
}
|
||||
|
||||
/* ---break---*/
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
6
ui/src/lib/utils.ts
Normal file
6
ui/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import '@/assets/fonts/iconfont';
|
||||
import './index.css';
|
||||
import '@/assets/styles/markdown.css';
|
||||
import { ThemeProvider } from '@c-x/ui';
|
||||
import { zhCN } from 'date-fns/locale/zh-CN';
|
||||
import { setDefaultOptions } from 'date-fns';
|
||||
|
||||
// 配置 Monaco Editor 环境
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
@@ -51,7 +53,7 @@ import { getRedirectUrl } from './utils';
|
||||
dayjs.locale('zh-cn');
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
setDefaultOptions({ locale: zhCN });
|
||||
const App = () => {
|
||||
const [user, setUser] = useState<DomainUser | DomainAdminUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Grid2 as Grid, styled } from '@mui/material';
|
||||
import {
|
||||
getStatisticsDashboard,
|
||||
getUserCodeRankDashboard,
|
||||
getTimeStatDashboard,
|
||||
getCategoryStatDashboard,
|
||||
getStatisticsDashboard,
|
||||
getTimeStatDashboard,
|
||||
getUserCodeRankDashboard,
|
||||
} from '@/api/Dashboard';
|
||||
import { SecondTimeRange } from '@/components/ui/calendar';
|
||||
import {
|
||||
getRecent60MinutesData,
|
||||
getRecentDaysData,
|
||||
getRecent24HoursData as getRecentHoursData,
|
||||
getTimeRange,
|
||||
} from '@/utils';
|
||||
import { Grid2 as Grid, styled } from '@mui/material';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { UserCard } from './statisticCard';
|
||||
import { useMemo } from 'react';
|
||||
import BarCharts from './barCharts';
|
||||
import LineCharts from './lineCharts';
|
||||
import PieCharts from './pieCharts';
|
||||
import BarCharts from './barCharts';
|
||||
import { ContributionCard } from './statisticCard';
|
||||
import {
|
||||
getRecent90DaysData,
|
||||
getRecent60MinutesData,
|
||||
getRecent24HoursData,
|
||||
} from '@/utils';
|
||||
import { TimeRange } from '../index';
|
||||
|
||||
interface TimeDuration {
|
||||
duration: number;
|
||||
precision: 'day' | 'hour';
|
||||
}
|
||||
import { ContributionCard, UserCard } from './statisticCard';
|
||||
|
||||
export const StyledHighlight = styled('span')(({ theme }) => ({
|
||||
fontSize: 12,
|
||||
@@ -30,12 +25,11 @@ export const StyledHighlight = styled('span')(({ theme }) => ({
|
||||
padding: '0 4px',
|
||||
}));
|
||||
|
||||
const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
const [timeDuration, settimeDuration] = useState<TimeDuration>({
|
||||
duration: timeRange === '90d' ? 90 : 24,
|
||||
precision: timeRange === '90d' ? 'day' : 'hour',
|
||||
});
|
||||
|
||||
const GlobalStatistic = ({
|
||||
timeDuration,
|
||||
}: {
|
||||
timeDuration: SecondTimeRange;
|
||||
}) => {
|
||||
const { data: statisticsData } = useRequest(getStatisticsDashboard);
|
||||
|
||||
const { data: userCodeRankData } = useRequest(
|
||||
@@ -60,15 +54,15 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
} = useRequest(() => getCategoryStatDashboard(timeDuration), {
|
||||
refreshDeps: [timeDuration],
|
||||
});
|
||||
|
||||
const precision = useMemo(() => getTimeRange(timeDuration), [timeDuration]);
|
||||
const getRangeData = (
|
||||
data: Record<string, number>[],
|
||||
timeRange: TimeRange,
|
||||
precision: 'day' | 'hour',
|
||||
label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' }
|
||||
) => {
|
||||
return timeRange === '90d'
|
||||
? getRecent90DaysData(data, label)
|
||||
: getRecent24HoursData(data, label);
|
||||
return precision === 'day'
|
||||
? getRecentDaysData(data, label)
|
||||
: getRecentHoursData(data, label);
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -89,21 +83,14 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
} = timeStatData || {};
|
||||
const label = { valueLabel: 'value' };
|
||||
return {
|
||||
userActiveChartData: getRangeData(active_users, timeRange, label),
|
||||
chatChartData: getRangeData(chats, timeRange, label),
|
||||
codeCompletionChartData: getRangeData(code_completions, timeRange, label),
|
||||
codeLineChartData: getRangeData(lines_of_code, timeRange, label),
|
||||
userActiveChartData: getRangeData(active_users, precision, label),
|
||||
chatChartData: getRangeData(chats, precision, label),
|
||||
codeCompletionChartData: getRangeData(code_completions, precision, label),
|
||||
codeLineChartData: getRangeData(lines_of_code, precision, label),
|
||||
realTimeTokenChartData: getRecent60MinutesData(real_time_tokens, label),
|
||||
acceptedPerChartData: getRangeData(accepted_per, timeRange, label),
|
||||
acceptedPerChartData: getRangeData(accepted_per, precision, label),
|
||||
};
|
||||
}, [timeStatData]);
|
||||
|
||||
useEffect(() => {
|
||||
settimeDuration({
|
||||
duration: timeRange === '90d' ? 90 : 24,
|
||||
precision: timeRange === '90d' ? 'day' : 'hour',
|
||||
});
|
||||
}, [timeRange]);
|
||||
}, [timeStatData, precision]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -123,7 +110,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
data={userActiveChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
共
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_users || 0}
|
||||
</StyledHighlight>
|
||||
@@ -133,20 +120,18 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<ContributionCard data={userCodeRankData} timeRange={timeRange} />
|
||||
<ContributionCard data={userCodeRankData} />
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<PieCharts
|
||||
title='工作模式-对话任务'
|
||||
data={categoryStatData.work_mode || []}
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<PieCharts
|
||||
title='编程语言'
|
||||
data={categoryStatData.program_language || []}
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
@@ -162,7 +147,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
data={chatChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
共
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_chats || 0}
|
||||
</StyledHighlight>
|
||||
@@ -177,7 +162,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
data={codeCompletionChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
共
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_completions || 0}
|
||||
</StyledHighlight>
|
||||
@@ -192,7 +177,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
data={codeLineChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改
|
||||
共修改
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_lines_of_code || 0}
|
||||
</StyledHighlight>
|
||||
@@ -208,7 +193,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
|
||||
formatValueTooltip={(value) => `${value.toFixed(2)}%`}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为
|
||||
平均采纳率为
|
||||
<StyledHighlight>
|
||||
{(timeStatData?.total_accepted_per || 0).toFixed(2)}
|
||||
</StyledHighlight>
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import {
|
||||
DomainUser,
|
||||
DomainUserHeatmap,
|
||||
DomainUserHeatmapResp,
|
||||
} from '@/api/types';
|
||||
import Avatar from '@/components/avatar';
|
||||
import Card from '@/components/card';
|
||||
import dayjs from 'dayjs';
|
||||
import { Icon } from '@c-x/ui';
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ActivityCalendar } from 'react-activity-calendar';
|
||||
import { DomainUserHeatmap, DomainUserHeatmapResp } from '@/api/types';
|
||||
import { DomainUser } from '@/api/types';
|
||||
import Avatar from '@/components/avatar';
|
||||
import { Icon } from '@c-x/ui';
|
||||
|
||||
const getRecent1YearData = (
|
||||
data: DomainUserHeatmap[] = [],
|
||||
@@ -58,30 +62,34 @@ const useActivityCalendarAutoScroll = () => {
|
||||
'.react-activity-calendar [style*="overflow"]',
|
||||
'.react-activity-calendar > div:first-child',
|
||||
];
|
||||
|
||||
|
||||
let scrollContainer: HTMLElement | null = null;
|
||||
|
||||
|
||||
// 按优先级尝试找到滚动容器
|
||||
for (const selector of selectors) {
|
||||
scrollContainer = document.querySelector(selector);
|
||||
if (scrollContainer && scrollContainer.scrollWidth > scrollContainer.clientWidth) {
|
||||
if (
|
||||
scrollContainer &&
|
||||
scrollContainer.scrollWidth > scrollContainer.clientWidth
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (scrollContainer) {
|
||||
// 滚动到最右侧(最新数据)
|
||||
scrollContainer.scrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth;
|
||||
scrollContainer.scrollLeft =
|
||||
scrollContainer.scrollWidth - scrollContainer.clientWidth;
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const setupAutoScroll = useCallback(() => {
|
||||
// 延迟执行确保组件完全渲染
|
||||
const timeoutId = setTimeout(scrollToLatest, 100);
|
||||
|
||||
|
||||
// 使用ResizeObserver监听容器大小变化
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
|
||||
const setupResizeObserver = () => {
|
||||
const container = document.querySelector('.react-activity-calendar');
|
||||
if (container && 'ResizeObserver' in window) {
|
||||
@@ -94,10 +102,10 @@ const useActivityCalendarAutoScroll = () => {
|
||||
window.addEventListener('resize', scrollToLatest);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 延迟设置ResizeObserver确保DOM已渲染
|
||||
const observerTimeoutId = setTimeout(setupResizeObserver, 150);
|
||||
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -109,33 +117,36 @@ const useActivityCalendarAutoScroll = () => {
|
||||
}
|
||||
};
|
||||
}, [scrollToLatest]);
|
||||
|
||||
|
||||
return { setupAutoScroll };
|
||||
};
|
||||
|
||||
// 简化的blockSize计算Hook - 保持原有大小
|
||||
const useBlockSize = (containerRef: React.RefObject<HTMLElement>) => {
|
||||
const [blockSize, setBlockSize] = useState(8);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const calculateBlockSize = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const baseWidth = 980;
|
||||
const blockIncrement = 54;
|
||||
|
||||
|
||||
// 只在桌面端进行计算,保持原有逻辑
|
||||
const increment = Math.max(0, Math.ceil((containerWidth - baseWidth) / blockIncrement));
|
||||
const increment = Math.max(
|
||||
0,
|
||||
Math.ceil((containerWidth - baseWidth) / blockIncrement)
|
||||
);
|
||||
setBlockSize(increment + 8);
|
||||
};
|
||||
|
||||
|
||||
// 初始计算
|
||||
calculateBlockSize();
|
||||
|
||||
|
||||
// 使用ResizeObserver监听容器大小变化
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
|
||||
if ('ResizeObserver' in window && containerRef.current) {
|
||||
resizeObserver = new ResizeObserver(calculateBlockSize);
|
||||
resizeObserver.observe(containerRef.current);
|
||||
@@ -143,7 +154,7 @@ const useBlockSize = (containerRef: React.RefObject<HTMLElement>) => {
|
||||
// 降级方案
|
||||
window.addEventListener('resize', calculateBlockSize);
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
@@ -152,7 +163,7 @@ const useBlockSize = (containerRef: React.RefObject<HTMLElement>) => {
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
|
||||
|
||||
return blockSize;
|
||||
};
|
||||
|
||||
@@ -171,19 +182,35 @@ const MemberInfo = ({
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const [searchUser, setSearchUser] = useState('');
|
||||
const [filterUser, setFilterUser] = useState<DomainUser[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchUser) {
|
||||
setFilterUser(
|
||||
userList?.filter((item) =>
|
||||
(item.username || '')
|
||||
.toLowerCase()
|
||||
?.includes(searchUser.toLowerCase())
|
||||
) || []
|
||||
);
|
||||
} else {
|
||||
setFilterUser(userList || []);
|
||||
}
|
||||
}, [searchUser, userList]);
|
||||
// 使用自定义Hooks
|
||||
const blockSize = useBlockSize(ref as React.RefObject<HTMLElement>);
|
||||
const { setupAutoScroll } = useActivityCalendarAutoScroll();
|
||||
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSearchUser('');
|
||||
};
|
||||
|
||||
|
||||
// 设置自动滚动
|
||||
useEffect(() => {
|
||||
const cleanup = setupAutoScroll();
|
||||
@@ -218,9 +245,30 @@ const MemberInfo = ({
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
autoFocus={false}
|
||||
disableAutoFocusItem={false}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
p: 1,
|
||||
maxHeight: '200px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{userList?.map((item) => (
|
||||
<TextField
|
||||
label='搜索'
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setSearchUser(e.target.value);
|
||||
}}
|
||||
value={searchUser}
|
||||
sx={{ mb: 1, position: 'sticky', top: '10px', zIndex: 1000 }}
|
||||
/>
|
||||
{filterUser?.map((item) => (
|
||||
<MenuItem
|
||||
autoFocus={false}
|
||||
key={item.id}
|
||||
selected={memberData?.id === item.id}
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,48 +1,41 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Grid2 as Grid } from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import MemberInfo from './memberInfo';
|
||||
import PieCharts from './pieCharts';
|
||||
import LineCharts from './lineCharts';
|
||||
import { RecentActivityCard } from './statisticCard';
|
||||
import { useRequest } from 'ahooks';
|
||||
import {
|
||||
getUserEventsDashboard,
|
||||
getUserStatDashboard,
|
||||
getUserHeatmapDashboard,
|
||||
getUserStatDashboard,
|
||||
} from '@/api/Dashboard';
|
||||
import { StyledHighlight } from './globalStatistic';
|
||||
import { getRecent90DaysData, getRecent24HoursData } from '@/utils';
|
||||
import { DomainUser } from '@/api/types';
|
||||
import { TimeRange } from '../index';
|
||||
import { SecondTimeRange } from '@/components/ui/calendar';
|
||||
import { getRecent24HoursData, getRecentDaysData, getTimeRange } from '@/utils';
|
||||
import { Grid2 as Grid } from '@mui/material';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { StyledHighlight } from './globalStatistic';
|
||||
import LineCharts from './lineCharts';
|
||||
import MemberInfo from './memberInfo';
|
||||
import PieCharts from './pieCharts';
|
||||
import { RecentActivityCard } from './statisticCard';
|
||||
|
||||
interface TimeDuration {
|
||||
duration: number;
|
||||
precision: 'day' | 'hour';
|
||||
}
|
||||
type Precision = 'day' | 'hour';
|
||||
|
||||
const MemberStatistic = ({
|
||||
memberData,
|
||||
userList,
|
||||
onMemberChange,
|
||||
timeRange,
|
||||
timeDuration,
|
||||
}: {
|
||||
memberData: DomainUser | null;
|
||||
userList: DomainUser[];
|
||||
onMemberChange: (data: DomainUser) => void;
|
||||
timeRange: TimeRange;
|
||||
timeDuration: SecondTimeRange;
|
||||
}) => {
|
||||
const [timeDuration, setTimeDuration] = useState<TimeDuration>({
|
||||
duration: timeRange === '90d' ? 90 : 24,
|
||||
precision: timeRange === '90d' ? 'day' : 'hour',
|
||||
});
|
||||
|
||||
const { id } = useParams();
|
||||
const precision = useMemo(() => getTimeRange(timeDuration), [timeDuration]);
|
||||
const { data: userEvents } = useRequest(
|
||||
() =>
|
||||
getUserEventsDashboard({
|
||||
user_id: id || '',
|
||||
precision: timeDuration.precision,
|
||||
precision,
|
||||
}),
|
||||
{
|
||||
refreshDeps: [id],
|
||||
@@ -74,20 +67,13 @@ const MemberStatistic = ({
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeDuration({
|
||||
duration: timeRange === '90d' ? 90 : 24,
|
||||
precision: timeRange === '90d' ? 'day' : 'hour',
|
||||
});
|
||||
}, [timeRange]);
|
||||
|
||||
const getRangeData = (
|
||||
data: Record<string, number>[],
|
||||
timeRange: TimeRange,
|
||||
precision: Precision,
|
||||
label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' }
|
||||
) => {
|
||||
return timeRange === '90d'
|
||||
? getRecent90DaysData(data, label)
|
||||
return precision === 'day'
|
||||
? getRecentDaysData(data, label)
|
||||
: getRecent24HoursData(data, label);
|
||||
};
|
||||
|
||||
@@ -104,14 +90,14 @@ const MemberStatistic = ({
|
||||
lines_of_code = [],
|
||||
} = userStat || {};
|
||||
const label = { valueLabel: 'value' };
|
||||
const chatChartData = getRangeData(chats, timeRange, label);
|
||||
const chatChartData = getRangeData(chats, precision, label);
|
||||
const codeCompletionChartData = getRangeData(
|
||||
code_completions,
|
||||
timeRange,
|
||||
precision,
|
||||
label
|
||||
);
|
||||
const codeLineChartData = getRangeData(lines_of_code, timeRange, label);
|
||||
const acceptedPerChartData = getRangeData(accepted_per, timeRange, label);
|
||||
const codeLineChartData = getRangeData(lines_of_code, precision, label);
|
||||
const acceptedPerChartData = getRangeData(accepted_per, precision, label);
|
||||
return {
|
||||
chatChartData,
|
||||
codeCompletionChartData,
|
||||
@@ -140,16 +126,11 @@ const MemberStatistic = ({
|
||||
<Grid size={6}>
|
||||
<PieCharts
|
||||
title='工作模式-对话任务'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
data={userStat?.work_mode || []}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<PieCharts
|
||||
title='编程语言'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
data={userStat?.program_language || []}
|
||||
/>
|
||||
<PieCharts title='编程语言' data={userStat?.program_language || []} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
@@ -161,8 +142,7 @@ const MemberStatistic = ({
|
||||
data={chatChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>{userStat?.total_chats || 0}</StyledHighlight>
|
||||
共<StyledHighlight>{userStat?.total_chats || 0}</StyledHighlight>
|
||||
个对话任务
|
||||
</>
|
||||
}
|
||||
@@ -174,7 +154,7 @@ const MemberStatistic = ({
|
||||
data={codeCompletionChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
共
|
||||
<StyledHighlight>
|
||||
{userStat?.total_completions || 0}
|
||||
</StyledHighlight>
|
||||
@@ -189,7 +169,7 @@ const MemberStatistic = ({
|
||||
data={codeLineChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改
|
||||
共修改
|
||||
<StyledHighlight>
|
||||
{userStat?.total_lines_of_code || 0}
|
||||
</StyledHighlight>
|
||||
@@ -205,7 +185,7 @@ const MemberStatistic = ({
|
||||
formatValueTooltip={(value) => `${value.toFixed(2)}%`}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为
|
||||
平均采纳率为
|
||||
<StyledHighlight>
|
||||
{(userStat?.total_accepted_per || 0).toFixed(2)}
|
||||
</StyledHighlight>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { styled, Stack, Box } from '@mui/material';
|
||||
import { Empty } from '@c-x/ui';
|
||||
import { Box, Stack, styled } from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TimeRange } from '../index';
|
||||
import ContributionModal from './contributionModal';
|
||||
|
||||
import Card from '@/components/card';
|
||||
import {
|
||||
DomainStatistics,
|
||||
DomainUserCodeRank,
|
||||
DomainUserEvent,
|
||||
} from '@/api/types';
|
||||
import Avatar from '@/components/avatar';
|
||||
import Card from '@/components/card';
|
||||
|
||||
const StyledCardLabel = styled('div')(({ theme }) => ({
|
||||
fontSize: '14px',
|
||||
@@ -58,10 +57,8 @@ export const StyledSerialNumber = styled('span')<{ num: number }>(
|
||||
|
||||
export const ContributionCard = ({
|
||||
data = [],
|
||||
timeRange,
|
||||
}: {
|
||||
data?: DomainUserCodeRank[];
|
||||
timeRange: TimeRange;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [contributionModalOpen, setContributionModalOpen] = useState(false);
|
||||
@@ -92,9 +89,6 @@ export const ContributionCard = ({
|
||||
查看更多
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box sx={{ fontSize: 12, color: 'text.tertiary' }}>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getListUser } from '@/api/User';
|
||||
import { Stack, MenuItem, Select } from '@mui/material';
|
||||
import { Stack, MenuItem, Select, Box } from '@mui/material';
|
||||
import { CusTabs } from '@c-x/ui';
|
||||
import GlobalStatistic from './components/globalStatistic';
|
||||
import { useRequest } from 'ahooks';
|
||||
@@ -9,7 +9,41 @@ import { useParams } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { DomainUser } from '@/api/types';
|
||||
import { v1LicenseList } from '@/api';
|
||||
import { Calendar, RangeValue } from '@/components/ui/calendar';
|
||||
import { endOfDay, startOfDay, subDays, subMonths, subWeeks } from 'date-fns';
|
||||
|
||||
const get24HoursRange = () => {
|
||||
return {
|
||||
start: startOfDay(subDays(new Date(), 1)),
|
||||
end: endOfDay(new Date()),
|
||||
};
|
||||
};
|
||||
const presets = {
|
||||
'last-1-days': {
|
||||
text: '最近 24 小时',
|
||||
...get24HoursRange(),
|
||||
},
|
||||
'last-3-days': {
|
||||
text: '最近 3 天',
|
||||
start: startOfDay(subDays(new Date(), 3)),
|
||||
end: endOfDay(new Date()),
|
||||
},
|
||||
'last-7-days': {
|
||||
text: '最近 7 天',
|
||||
start: startOfDay(subWeeks(new Date(), 1)),
|
||||
end: endOfDay(new Date()),
|
||||
},
|
||||
'last-14-days': {
|
||||
text: '最近 14 天',
|
||||
start: startOfDay(subWeeks(new Date(), 2)),
|
||||
end: endOfDay(new Date()),
|
||||
},
|
||||
'last-month': {
|
||||
text: '最近 1 月',
|
||||
start: startOfDay(subMonths(new Date(), 1)),
|
||||
end: endOfDay(new Date()),
|
||||
},
|
||||
};
|
||||
export type TimeRange = '90d' | '24h';
|
||||
|
||||
const Dashboard = () => {
|
||||
@@ -17,10 +51,13 @@ const Dashboard = () => {
|
||||
const { tab, id } = useParams();
|
||||
const [tabValue, setTabValue] = useState(tab || 'global');
|
||||
const [memberData, setMemberData] = useState<DomainUser | null>(null);
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('24h');
|
||||
const [timeRange, setTimeRange] = useState<RangeValue>(
|
||||
presets['last-1-days']
|
||||
);
|
||||
|
||||
const license = useRequest(() => {
|
||||
return v1LicenseList({})
|
||||
}).data
|
||||
return v1LicenseList({});
|
||||
}).data;
|
||||
const { data: userData, refresh } = useRequest(
|
||||
() =>
|
||||
getListUser({
|
||||
@@ -57,10 +94,24 @@ const Dashboard = () => {
|
||||
setTabValue(value);
|
||||
navigate(`/dashboard/${value}`);
|
||||
};
|
||||
|
||||
const handleTimeRangeChange = (value: any) => {
|
||||
if (value) {
|
||||
setTimeRange(value);
|
||||
} else {
|
||||
setTimeRange(get24HoursRange());
|
||||
}
|
||||
};
|
||||
const secondValue = useMemo(() => {
|
||||
return {
|
||||
start_at: timeRange.start
|
||||
? Math.floor(timeRange.start?.getTime() / 1000)
|
||||
: 0,
|
||||
end_at: timeRange.end ? Math.floor(timeRange.end?.getTime() / 1000) : 0,
|
||||
};
|
||||
}, [timeRange]);
|
||||
return (
|
||||
<Stack gap={2} sx={{ height: '100%' }}>
|
||||
<Stack direction='row' justifyContent='space-between' alignItems='center'>
|
||||
<Stack direction='row' gap={2} alignItems='center'>
|
||||
<CusTabs
|
||||
value={tabValue}
|
||||
onChange={onTabChange}
|
||||
@@ -84,26 +135,24 @@ const Dashboard = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
labelId='time-range-label'
|
||||
value={timeRange}
|
||||
size='small'
|
||||
onChange={(e) => setTimeRange(e.target.value as TimeRange)}
|
||||
sx={{ fontSize: 14 }}
|
||||
disabled={license?.edition !== 2}
|
||||
>
|
||||
<MenuItem value='24h'>最近 24 小时</MenuItem>
|
||||
<MenuItem value='90d'>最近 90 天</MenuItem>
|
||||
</Select>
|
||||
<Box sx={{ py: '4px', pr: 5 }}>
|
||||
<Calendar
|
||||
isDocsPage
|
||||
disabled={license?.edition !== 2}
|
||||
onChange={handleTimeRangeChange}
|
||||
presets={presets}
|
||||
value={timeRange}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{tabValue === 'global' && <GlobalStatistic timeRange={timeRange} />}
|
||||
{tabValue === 'global' && <GlobalStatistic timeDuration={secondValue} />}
|
||||
{tabValue === 'member' && (
|
||||
<MemberStatistic
|
||||
memberData={memberData}
|
||||
userList={userList}
|
||||
onMemberChange={onMemberChange}
|
||||
timeRange={timeRange}
|
||||
timeDuration={secondValue}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Select,
|
||||
Chip,
|
||||
SelectChangeEvent,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Table, Modal, message } from '@c-x/ui';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
@@ -33,9 +34,18 @@ const GroupList = () => {
|
||||
const groupData = useRequest(() => getListUserGroup({page: 1, size: 999}));
|
||||
const userData = useRequest(() => getListUser({}));
|
||||
const adminData = useRequest(() => getListAdminUser({}));
|
||||
|
||||
const [searchUser, setSearchUser] = useState('');
|
||||
const [data, setData] = useState<DomainUserGroup[]>([]);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
useEffect(()=>{
|
||||
if(searchUser){
|
||||
setData(groupData?.data?.groups?.filter((item)=>
|
||||
(item.name ||'').toLowerCase()?.includes(searchUser.toLowerCase())) || []);
|
||||
}else {
|
||||
setData(groupData?.data?.groups || []);
|
||||
}
|
||||
},[searchUser, groupData])
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
record: DomainUserGroup
|
||||
@@ -275,6 +285,7 @@ const GroupList = () => {
|
||||
>
|
||||
<Box sx={{ fontWeight: 700 }}>成员组</Box>
|
||||
<Stack direction='row' gap={1}>
|
||||
<TextField label='搜索' size='small' onChange={(e)=>setSearchUser(e.target.value)} />
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
@@ -285,7 +296,7 @@ const GroupList = () => {
|
||||
<Table
|
||||
columns={columns}
|
||||
height='calc(100% - 53px)'
|
||||
dataSource={groupData.data?.groups || []}
|
||||
dataSource={data}
|
||||
sx={{ mx: -2 }}
|
||||
pagination={false}
|
||||
rowKey='id'
|
||||
|
||||
@@ -147,13 +147,21 @@ const ResetPasswordModal = ({
|
||||
};
|
||||
|
||||
const MemberManage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [resetPasswordOpen, setResetPasswordOpen] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<DomainUser | null>(null);
|
||||
const { data, loading, refresh } = useRequest(() => getListUser({page: 1, size: 999}));
|
||||
const { data: originData, loading, refresh } = useRequest(() => getListUser({page: 1, size: 999}));
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const [searchUser, setSearchUser] = useState('');
|
||||
const [data, setData] = useState<DomainUser[]>([]);
|
||||
useEffect(()=>{
|
||||
if(searchUser){
|
||||
setData(originData?.users?.filter((item)=>
|
||||
(item.username ||'').toLowerCase()?.includes(searchUser.toLowerCase())) || []);
|
||||
}else {
|
||||
setData(originData?.users || []);
|
||||
}
|
||||
},[searchUser, originData])
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
record: DomainUser
|
||||
@@ -332,7 +340,7 @@ const MemberManage = () => {
|
||||
>
|
||||
<Box sx={{ fontWeight: 700 }}>成员列表</Box>
|
||||
<Stack direction='row' gap={1}>
|
||||
<TextField label='搜索' size='small' />
|
||||
<TextField label='搜索' size='small' onChange={(e)=>setSearchUser(e.target.value)} />
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
@@ -345,7 +353,7 @@ const MemberManage = () => {
|
||||
<Table
|
||||
columns={columns}
|
||||
height='calc(100% - 53px)'
|
||||
dataSource={data?.users || []}
|
||||
dataSource={data || []}
|
||||
sx={{ mx: -2 }}
|
||||
pagination={false}
|
||||
rowKey='id'
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Stack, Box, styled } from '@mui/material';
|
||||
import LineCharts from './lineCharts';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { getGetTokenUsage } from '@/api/Model';
|
||||
import { addCommasToNumber, getRecent90DaysData } from '@/utils';
|
||||
import { addCommasToNumber, getRecentDaysData } from '@/utils';
|
||||
import { DomainModelTokenUsage } from '@/api/types';
|
||||
|
||||
export const StyledHighlight = styled('span')(({ theme }) => ({
|
||||
@@ -35,10 +35,10 @@ const TokenUsage = () => {
|
||||
|
||||
const { llmInputData, llmOutputData } = useMemo(() => {
|
||||
return {
|
||||
llmInputData: getRecent90DaysData(
|
||||
llmInputData: getRecentDaysData(
|
||||
(llmModelData?.input_usage as Required<DomainModelTokenUsage>[]) || []
|
||||
),
|
||||
llmOutputData: getRecent90DaysData(
|
||||
llmOutputData: getRecentDaysData(
|
||||
(llmModelData?.output_usage as Required<DomainModelTokenUsage>[]) || []
|
||||
),
|
||||
};
|
||||
@@ -46,10 +46,10 @@ const TokenUsage = () => {
|
||||
|
||||
const { coderInputData, coderOutputData } = useMemo(() => {
|
||||
return {
|
||||
coderInputData: getRecent90DaysData(
|
||||
coderInputData: getRecentDaysData(
|
||||
(coderModelData?.input_usage as Required<DomainModelTokenUsage>[]) || []
|
||||
),
|
||||
coderOutputData: getRecent90DaysData(
|
||||
coderOutputData: getRecentDaysData(
|
||||
(coderModelData?.output_usage as Required<DomainModelTokenUsage>[]) ||
|
||||
[]
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
getUserDashboardStat,
|
||||
} from '@/api/UserDashboard';
|
||||
import { StyledHighlight } from '@/pages/dashboard/components/globalStatistic';
|
||||
import { getRecent90DaysData, getRecent24HoursData } from '@/utils';
|
||||
import { getRecentDaysData, getRecent24HoursData } from '@/utils';
|
||||
import { DomainUser } from '@/api/types';
|
||||
import { TimeRange } from '../index';
|
||||
|
||||
@@ -68,7 +68,7 @@ const MemberStatistic = ({
|
||||
label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' }
|
||||
) => {
|
||||
return timeRange === '90d'
|
||||
? getRecent90DaysData(data, label)
|
||||
? getRecentDaysData(data, label)
|
||||
: getRecent24HoursData(data, label);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Decimal } from 'decimal.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { SecondTimeRange } from '@/components/ui/calendar';
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
@@ -154,7 +155,7 @@ export const getRedirectUrl = (source: 'user' | 'admin' = 'admin') => {
|
||||
return redirectUrl as URL;
|
||||
};
|
||||
|
||||
export const getRecent90DaysData = (
|
||||
export const getRecentDaysData = (
|
||||
data: Record<string, number>[] = [],
|
||||
label: { keyLabel?: string; valueLabel?: string } = {}
|
||||
) => {
|
||||
@@ -265,3 +266,11 @@ export const getBaseLanguageId = (languageId: string): string => {
|
||||
};
|
||||
return map[languageId] || languageId;
|
||||
};
|
||||
|
||||
export const getTimeRange = (timeDuration: SecondTimeRange) => {
|
||||
const diff = timeDuration.end_at - timeDuration.start_at;
|
||||
if (diff > 24 * 60 * 60 * 1000) {
|
||||
return 'day';
|
||||
}
|
||||
return 'hour';
|
||||
};
|
||||
|
||||
@@ -3,5 +3,11 @@
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
@@ -8,7 +9,7 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
|
||||
Reference in New Issue
Block a user