From 268b7c3155c464cbb2d2760193371febf11de020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E7=BA=B2?= Date: Fri, 29 Aug 2025 14:03:02 +0800 Subject: [PATCH] feat: add timerange filter --- ui/components.json | 21 + ui/package.json | 9 + ui/pnpm-lock.yaml | 445 ++++++++- ui/src/api/types.ts | 126 +-- ui/src/components/ui/button-1.tsx | 117 +++ ui/src/components/ui/calendar.tsx | 870 ++++++++++++++++++ ui/src/components/ui/error.tsx | 81 ++ ui/src/components/ui/input.tsx | 120 +++ ui/src/components/ui/material-1.tsx | 39 + ui/src/components/ui/select-1.tsx | 140 +++ ui/src/components/ui/spinner-1.tsx | 73 ++ ui/src/components/ui/use-click-outside.tsx | 19 + ui/src/index.css | 321 ++++++- ui/src/lib/utils.ts | 6 + ui/src/main.tsx | 4 +- .../dashboard/components/globalStatistic.tsx | 87 +- .../pages/dashboard/components/memberInfo.tsx | 116 ++- .../dashboard/components/memberStatistic.tsx | 78 +- .../dashboard/components/statisticCard.tsx | 12 +- ui/src/pages/dashboard/index.tsx | 87 +- ui/src/pages/memberManage/groupList.tsx | 15 +- ui/src/pages/memberManage/memberManage.tsx | 18 +- ui/src/pages/model/components/tokenUsage.tsx | 10 +- .../dashboard/components/memberStatistic.tsx | 4 +- ui/src/utils/index.ts | 11 +- ui/tsconfig.json | 8 +- ui/vite.config.ts | 3 +- 27 files changed, 2571 insertions(+), 269 deletions(-) create mode 100644 ui/components.json create mode 100644 ui/src/components/ui/button-1.tsx create mode 100644 ui/src/components/ui/calendar.tsx create mode 100644 ui/src/components/ui/error.tsx create mode 100644 ui/src/components/ui/input.tsx create mode 100644 ui/src/components/ui/material-1.tsx create mode 100644 ui/src/components/ui/select-1.tsx create mode 100644 ui/src/components/ui/spinner-1.tsx create mode 100644 ui/src/components/ui/use-click-outside.tsx create mode 100644 ui/src/lib/utils.ts diff --git a/ui/components.json b/ui/components.json new file mode 100644 index 0000000..1f2ec01 --- /dev/null +++ b/ui/components.json @@ -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" + } +} diff --git a/ui/package.json b/ui/package.json index caa2850..8423656 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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" diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 1f16c2a..e59dc4e 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -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: {} diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 53155c4..e2966ea 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -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; } diff --git a/ui/src/components/ui/button-1.tsx b/ui/src/components/ui/button-1.tsx new file mode 100644 index 0000000..1e01915 --- /dev/null +++ b/ui/src/components/ui/button-1.tsx @@ -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; + ref?: React.Ref; + 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 ( + + ); +}; \ No newline at end of file diff --git a/ui/src/components/ui/calendar.tsx b/ui/src/components/ui/calendar.tsx new file mode 100644 index 0000000..90706de --- /dev/null +++ b/ui/src/components/ui/calendar.tsx @@ -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 = () => ( + + + +); + +const ArrowBottomIcon = ({ className }: { className?: string }) => ( + + + +); + +const ArrowLeftIcon = () => ( + + + +); + +const ArrowRightIcon = () => ( + + + +); + +const CalendarIcon = () => ( + + + +); + +const ClearIcon = () => ( + + + +); + +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, 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(false); + const [inputValue, setInputValue] = useState(""); + const [currentPreset, setCurrentPreset] = useState(null); + const ref = useRef(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 ( +
+ } + prefixStyling={"pl-2.5"} + suffix={} + 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" + )} + /> + +
    + {Object.entries(filteredPresets).length > 0 ? Object.entries(filteredPresets).map(([key, value]) => ( +
  • onClick(value)} + > + {value.text} +
  • + )) : ( +
  • + {inputValue} +
  • + )} +
+ {!compact && ( +
+
输入相对时间
+
+ {typeRelativeTimes.map((value) => ( + + ))} +
+ {/*
选择固定时间
*/} + {/*
+ {typeFixedTimes.map((value) => ( + + ))} +
*/} +
+ )} +
+
+ ); +}; + +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(false); + const [currentDate, setCurrentDate] = useState(new Date()); + const [hoverDate, setHoverDate] = useState(null); + const [isSelecting, setIsSelecting] = useState(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(formatInTimeZone(value?.start || new Date(), selectedTimezone, "MMM dd, yyyy")); + const [startTime, setStartTime] = useState(formatInTimeZone(startOfDay(value?.start || new Date()), selectedTimezone, "HH:mm")); + const [endDate, setEndDate] = useState(formatInTimeZone(value?.end || new Date(), selectedTimezone, "MMM dd, yyyy")); + const [endTime, setEndTime] = useState(formatInTimeZone(endOfDay(value?.end || new Date()), selectedTimezone, "HH:mm")); + const [startDateError, setStartDateError] = useState(false); + const [startTimeError, setStartTimeError] = useState(false); + const [endDateError, setEndDateError] = useState(false); + const [endTimeError, setEndTimeError] = useState(false); + const calendarRef = useRef(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 ( +
+
+ {presets && ( +
+ +
+ )} +
+
+ + {value?.start && value?.end && ( + + )} +
+
+
+ {isOpen && ( + +
+
+
+

+ {formatInTimeZone(currentDate, selectedTimezone, "MMMM yyyy")} +

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ {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 ( +
isAllowedDate && handleMouseEnter(day)} + onClick={() => isAllowedDate && handleDateClick(day)} + > +
+ {format(day, "d")} +
+
+ ); + })} +
+
+ {/*
+
+
+
Start
+
+
+ setStartDate(value)} + error={startDateError} + /> +
+ {showTimeInput && ( + setStartTime(value)} + error={startTimeError} + /> + )} +
+
+
+
End
+
+
+ setEndDate(value)} + error={endDateError} + /> +
+ {showTimeInput && ( + setEndTime(value)} + error={endTimeError} + /> + )} +
+
+
+
+
+ +
+
+ + {suffix && ( +
+ {suffix} +
+ )} +
+ {typeof error === "string" && {error}} +
+ ); +}; \ No newline at end of file diff --git a/ui/src/components/ui/material-1.tsx b/ui/src/components/ui/material-1.tsx new file mode 100644 index 0000000..a2424d1 --- /dev/null +++ b/ui/src/components/ui/material-1.tsx @@ -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; + style?: React.CSSProperties; + onClick?: (event: React.MouseEvent) => void; +} + +export const Material = ({ type, children, className, ref, style, onClick }: MaterialProps) => { + return ( +
+ {children} +
+ ); +}; \ No newline at end of file diff --git a/ui/src/components/ui/select-1.tsx b/ui/src/components/ui/select-1.tsx new file mode 100644 index 0000000..db0ad13 --- /dev/null +++ b/ui/src/components/ui/select-1.tsx @@ -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; +} + +const ArrowBottom = () => ( + + + +); + +export const Select = ({ + variant = "default", + options, + label, + value, + placeholder, + size = "medium", + suffix, + prefix, + disabled = false, + error, + onChange +}: SelectProps) => { + return ( +
+ {label && ( + + )} +
+ + + {prefix && ( + + {prefix} + + )} + + {suffix ? suffix : } + +
+ {error && ( +
+ {error} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/ui/src/components/ui/spinner-1.tsx b/ui/src/components/ui/spinner-1.tsx new file mode 100644 index 0000000..684be69 --- /dev/null +++ b/ui/src/components/ui/spinner-1.tsx @@ -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 ( +
+
+ {bars.map((item) => ( +
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/ui/src/components/ui/use-click-outside.tsx b/ui/src/components/ui/use-click-outside.tsx new file mode 100644 index 0000000..3905ce5 --- /dev/null +++ b/ui/src/components/ui/use-click-outside.tsx @@ -0,0 +1,19 @@ +import { RefObject, useEffect } from "react"; + +export const useClickOutside = (ref: RefObject, 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]); +}; \ No newline at end of file diff --git a/ui/src/index.css b/ui/src/index.css index a724d06..7703eda 100644 --- a/ui/src/index.css +++ b/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; + } +} \ No newline at end of file diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/ui/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 08a3bf6..0af0a5d 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -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(null); const [loading, setLoading] = useState(true); diff --git a/ui/src/pages/dashboard/components/globalStatistic.tsx b/ui/src/pages/dashboard/components/globalStatistic.tsx index b2b295d..675810e 100644 --- a/ui/src/pages/dashboard/components/globalStatistic.tsx +++ b/ui/src/pages/dashboard/components/globalStatistic.tsx @@ -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({ - 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[], - 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 ( { data={userActiveChartData} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 + 共 {timeStatData?.total_users || 0} @@ -133,20 +120,18 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => { /> - + @@ -162,7 +147,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => { data={chatChartData} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 + 共 {timeStatData?.total_chats || 0} @@ -177,7 +162,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => { data={codeCompletionChartData} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 + 共 {timeStatData?.total_completions || 0} @@ -192,7 +177,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => { data={codeLineChartData} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改 + 共修改 {timeStatData?.total_lines_of_code || 0} @@ -208,7 +193,7 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => { formatValueTooltip={(value) => `${value.toFixed(2)}%`} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为 + 平均采纳率为 {(timeStatData?.total_accepted_per || 0).toFixed(2)} diff --git a/ui/src/pages/dashboard/components/memberInfo.tsx b/ui/src/pages/dashboard/components/memberInfo.tsx index 7d01be5..14895a2 100644 --- a/ui/src/pages/dashboard/components/memberInfo.tsx +++ b/ui/src/pages/dashboard/components/memberInfo.tsx @@ -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) => { 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) => { // 降级方案 window.addEventListener('resize', calculateBlockSize); } - + return () => { if (resizeObserver) { resizeObserver.disconnect(); @@ -152,7 +163,7 @@ const useBlockSize = (containerRef: React.RefObject) => { } }; }, [containerRef]); - + return blockSize; }; @@ -171,19 +182,35 @@ const MemberInfo = ({ const ref = useRef(null); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - + const [searchUser, setSearchUser] = useState(''); + const [filterUser, setFilterUser] = useState([]); + + 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); const { setupAutoScroll } = useActivityCalendarAutoScroll(); - + const handleClick = (event: React.MouseEvent) => { 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) => ( + { + e.stopPropagation(); + setSearchUser(e.target.value); + }} + value={searchUser} + sx={{ mb: 1, position: 'sticky', top: '10px', zIndex: 1000 }} + /> + {filterUser?.map((item) => ( { diff --git a/ui/src/pages/dashboard/components/memberStatistic.tsx b/ui/src/pages/dashboard/components/memberStatistic.tsx index 472f531..a7062bc 100644 --- a/ui/src/pages/dashboard/components/memberStatistic.tsx +++ b/ui/src/pages/dashboard/components/memberStatistic.tsx @@ -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({ - 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[], - 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 = ({ - + @@ -161,8 +142,7 @@ const MemberStatistic = ({ data={chatChartData} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 - {userStat?.total_chats || 0} + 共{userStat?.total_chats || 0} 个对话任务 } @@ -174,7 +154,7 @@ const MemberStatistic = ({ data={codeCompletionChartData} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 + 共 {userStat?.total_completions || 0} @@ -189,7 +169,7 @@ const MemberStatistic = ({ data={codeLineChartData} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改 + 共修改 {userStat?.total_lines_of_code || 0} @@ -205,7 +185,7 @@ const MemberStatistic = ({ formatValueTooltip={(value) => `${value.toFixed(2)}%`} extra={ <> - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为 + 平均采纳率为 {(userStat?.total_accepted_per || 0).toFixed(2)} diff --git a/ui/src/pages/dashboard/components/statisticCard.tsx b/ui/src/pages/dashboard/components/statisticCard.tsx index 420fab5..083abff 100644 --- a/ui/src/pages/dashboard/components/statisticCard.tsx +++ b/ui/src/pages/dashboard/components/statisticCard.tsx @@ -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 = ({ 查看更多 - - {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'} - { + 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(null); - const [timeRange, setTimeRange] = useState('24h'); + const [timeRange, setTimeRange] = useState( + 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 ( - + { }, }} /> - + + + - {tabValue === 'global' && } + {tabValue === 'global' && } {tabValue === 'member' && ( )} diff --git a/ui/src/pages/memberManage/groupList.tsx b/ui/src/pages/memberManage/groupList.tsx index 6bfe2a5..9afd10a 100644 --- a/ui/src/pages/memberManage/groupList.tsx +++ b/ui/src/pages/memberManage/groupList.tsx @@ -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([]); const [anchorEl, setAnchorEl] = useState(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, record: DomainUserGroup @@ -275,6 +285,7 @@ const GroupList = () => { > 成员组 + setSearchUser(e.target.value)} />