mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
170 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1ecd5b78e6 | ||
![]() |
fca2e3c51a | ||
![]() |
95ea761b2d | ||
![]() |
6b3bfa1ee9 | ||
![]() |
df3e302a9d | ||
![]() |
c88a68c9a8 | ||
![]() |
92d01b9cdd | ||
![]() |
fe04fa5986 | ||
![]() |
c382f541b4 | ||
![]() |
f420527207 | ||
![]() |
e0c83ebf79 | ||
![]() |
c7fb18fc08 | ||
![]() |
2db8ab937d | ||
![]() |
819f5dd8e5 | ||
![]() |
d4a8ed735e | ||
![]() |
f07e3bb4d5 | ||
![]() |
fa5ef0c221 | ||
![]() |
da7499ec0b | ||
![]() |
d2f4327e44 | ||
![]() |
2eba640180 | ||
![]() |
29ae55f340 | ||
![]() |
3d2bca3f9f | ||
![]() |
7fd8c0c822 | ||
![]() |
a9e9c81505 | ||
![]() |
e8cc68bdea | ||
![]() |
9e51a661a4 | ||
![]() |
a167aaf55f | ||
![]() |
a54ecbcaa0 | ||
![]() |
788462cdfa | ||
![]() |
45c5965b99 | ||
![]() |
ce7614de46 | ||
![]() |
9f78e1ce1e | ||
![]() |
2c7b0625e8 | ||
![]() |
c3a5da9be1 | ||
![]() |
ca796e1920 | ||
![]() |
7ce04cf781 | ||
![]() |
024a3eb760 | ||
![]() |
1702f429b4 | ||
![]() |
96d79cf495 | ||
![]() |
a6a11a7026 | ||
![]() |
970a49e2a5 | ||
![]() |
2e013ed4f5 | ||
![]() |
f8c396b1fe | ||
![]() |
b54870cb60 | ||
![]() |
84318acb18 | ||
![]() |
a11a042b93 | ||
![]() |
8a8aa8f62c | ||
![]() |
93f78f4db5 | ||
![]() |
404bfdd5e6 | ||
![]() |
e4577dc2f1 | ||
![]() |
5c932e5a27 | ||
![]() |
4bd63c6267 | ||
![]() |
aabe24f903 | ||
![]() |
69cebd7fbc | ||
![]() |
8da371176a | ||
![]() |
dd08adf1d1 | ||
![]() |
2f67bef139 | ||
![]() |
8968c51cdc | ||
![]() |
f2fdcc9289 | ||
![]() |
aa3a575cbe | ||
![]() |
11816d038d | ||
![]() |
6a990edb38 | ||
![]() |
fa12865924 | ||
![]() |
ecdd717742 | ||
![]() |
6851334af9 | ||
![]() |
9051b29565 | ||
![]() |
95c7d3dfbd | ||
![]() |
bc1148c00a | ||
![]() |
d4556d9299 | ||
![]() |
5d389a2359 | ||
![]() |
305116874b | ||
![]() |
b08a29897f | ||
![]() |
b59c1d9122 | ||
![]() |
adb9cea701 | ||
![]() |
5e148d2e82 | ||
![]() |
a0d780558e | ||
![]() |
ad56065a4e | ||
![]() |
f5dee80b6e | ||
![]() |
9cc75881b8 | ||
![]() |
593fb13b61 | ||
![]() |
fca90592d6 | ||
![]() |
d6848e2855 | ||
![]() |
7539a4129f | ||
![]() |
5402574266 | ||
![]() |
853175aa1a | ||
![]() |
feb84809ec | ||
![]() |
a812c568e4 | ||
![]() |
11db25e355 | ||
![]() |
ecd2fba629 | ||
![]() |
a6763cf5a1 | ||
![]() |
c9e91a9b94 | ||
![]() |
43fb62c5bd | ||
![]() |
cb8727d487 | ||
![]() |
a94e03e2fd | ||
![]() |
425c3c6432 | ||
![]() |
89b9610016 | ||
![]() |
62fe88f868 | ||
![]() |
11a7f5fade | ||
![]() |
fbde997f7c | ||
![]() |
26734a35ef | ||
![]() |
715c4ac534 | ||
![]() |
bd4b0885a1 | ||
![]() |
e3c7af3d91 | ||
![]() |
a7ee21bfd8 | ||
![]() |
d0f51d92ac | ||
![]() |
e6dc148ea2 | ||
![]() |
514ab6637f | ||
![]() |
377794abe8 | ||
![]() |
0f3251f35b | ||
![]() |
8002dc5bc5 | ||
![]() |
c75a13dcf4 | ||
![]() |
91d153bb9d | ||
![]() |
b32f9fa397 | ||
![]() |
80593730ae | ||
![]() |
090d54a78d | ||
![]() |
b7d1fb181c | ||
![]() |
6e56693ca7 | ||
![]() |
7403db9b20 | ||
![]() |
9d167cd883 | ||
![]() |
197eec40ad | ||
![]() |
07819a6618 | ||
![]() |
b72156866d | ||
![]() |
59a7d12a8c | ||
![]() |
179351b23a | ||
![]() |
790809e8e5 | ||
![]() |
1414a8a8c9 | ||
![]() |
9ab41734a5 | ||
![]() |
03cace2ea1 | ||
![]() |
c7371ab869 | ||
![]() |
b32d4b618c | ||
![]() |
3a27f37686 | ||
![]() |
fe2d21979d | ||
![]() |
48b1f3d4f0 | ||
![]() |
93ed589ac7 | ||
![]() |
96de9e2c16 | ||
![]() |
b25f9d3bec | ||
![]() |
15854c605b | ||
![]() |
ac193cc94a | ||
![]() |
d626f872e6 | ||
![]() |
3eb66fa34a | ||
![]() |
0fdd0175b7 | ||
![]() |
dec9b477e0 | ||
![]() |
a0a4b0dd1d | ||
![]() |
8dc6da56a7 | ||
![]() |
b4e07aacfe | ||
![]() |
19b47f0f42 | ||
![]() |
f9ef3d63c7 | ||
![]() |
2b574d33b5 | ||
![]() |
6039e9bb46 | ||
![]() |
adfd4b043f | ||
![]() |
719189be55 | ||
![]() |
ef9907f4b6 | ||
![]() |
16b7447df1 | ||
![]() |
4157746478 | ||
![]() |
5120786708 | ||
![]() |
0176fa75ef | ||
![]() |
e6968f2d80 | ||
![]() |
c0dd8a53e8 | ||
![]() |
3cb3135235 | ||
![]() |
28182cac64 | ||
![]() |
73b80d2482 | ||
![]() |
f22eb22409 | ||
![]() |
4a95b17a47 | ||
![]() |
f4a71159fd | ||
![]() |
c0431e3dc2 | ||
![]() |
7f87cee282 | ||
![]() |
c24c704439 | ||
![]() |
232e5d55b8 | ||
![]() |
da24ae7e1c | ||
![]() |
8fc13f8a8f |
@@ -12,7 +12,7 @@ insert_final_newline = true
|
|||||||
# Set default charset
|
# Set default charset
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|
||||||
# 2 space indentation
|
# 4 space indentation
|
||||||
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Develop
|
# Develop
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
out/
|
out/
|
||||||
dist/
|
dist/
|
||||||
/src/core.lib/common/
|
/src/core.lib/common/
|
||||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -5,5 +5,6 @@
|
|||||||
".env.universal": ".env.*",
|
".env.universal": ".env.*",
|
||||||
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts",
|
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts",
|
||||||
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
|
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
|
||||||
}
|
},
|
||||||
|
"css.customData": [".vscode/tailwindcss.json"],
|
||||||
}
|
}
|
55
.vscode/tailwindcss.json
vendored
Normal file
55
.vscode/tailwindcss.json
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"version": 1.1,
|
||||||
|
"atDirectives": [
|
||||||
|
{
|
||||||
|
"name": "@tailwind",
|
||||||
|
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@apply",
|
||||||
|
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@responsive",
|
||||||
|
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@screen",
|
||||||
|
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@variants",
|
||||||
|
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -64,4 +64,4 @@ NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进
|
|||||||
|
|
||||||
## 开源附加
|
## 开源附加
|
||||||
|
|
||||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
|
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
|
||||||
|
@@ -1,70 +1,32 @@
|
|||||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
import eslint from '@eslint/js';
|
||||||
import _import from "eslint-plugin-import";
|
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
|
||||||
import { fixupPluginRules } from "@eslint/compat";
|
import tsEslintParser from '@typescript-eslint/parser';
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import tsParser from "@typescript-eslint/parser";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import js from "@eslint/js";
|
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url);
|
|
||||||
const dirname = path.dirname(filename);
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: dirname,
|
|
||||||
recommendedConfig: js.configs.recommended,
|
|
||||||
allConfig: js.configs.all
|
|
||||||
});
|
|
||||||
|
|
||||||
export default [{
|
|
||||||
ignores: ["src/core/proto/"],
|
|
||||||
}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), {
|
|
||||||
plugins: {
|
|
||||||
"@typescript-eslint": typescriptEslint,
|
|
||||||
import: fixupPluginRules(_import),
|
|
||||||
},
|
|
||||||
|
|
||||||
|
const customTsFlatConfig = [
|
||||||
|
{
|
||||||
|
name: 'typescript-eslint/base',
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
parser: tsEslintParser,
|
||||||
|
sourceType: 'module',
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.node,
|
...globals.node,
|
||||||
},
|
NodeJS: 'readonly', // 添加 NodeJS 全局变量
|
||||||
|
|
||||||
parser: tsParser,
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
"import/parsers": {
|
|
||||||
"@typescript-eslint/parser": [".ts"],
|
|
||||||
},
|
|
||||||
|
|
||||||
"import/resolver": {
|
|
||||||
typescript: {
|
|
||||||
alwaysTryTypes: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
indent: ["error", 4],
|
...tsEslintPlugin.configs.recommended.rules,
|
||||||
semi: ["error", "always"],
|
'quotes': ['error', 'single'], // 使用单引号
|
||||||
"no-unused-vars": "off",
|
'semi': ['error', 'always'], // 强制使用分号
|
||||||
"no-async-promise-executor": "off",
|
'indent': ['error', 4], // 使用 4 空格缩进
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"object-curly-spacing": ["error", "always"],
|
|
||||||
},
|
},
|
||||||
}, {
|
plugins: {
|
||||||
files: ["**/.eslintrc.{js,cjs}"],
|
'@typescript-eslint': tsEslintPlugin,
|
||||||
|
},
|
||||||
|
ignores: ['src/webui/**'], // 忽略 src/webui/ 目录所有文件
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
languageOptions: {
|
export default [eslint.configs.recommended, ...customTsFlatConfig];
|
||||||
globals: {
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
ecmaVersion: 5,
|
|
||||||
sourceType: "commonjs",
|
|
||||||
},
|
|
||||||
}];
|
|
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.4.12",
|
"version": "4.5.22",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@@ -4,12 +4,16 @@
|
|||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host=0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint -c eslint.config.mjs ./src/**/**/*.{ts,tsx} --fix",
|
"lint": "eslint -c eslint.config.mjs ./src/**/**/*.{ts,tsx} --fix",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@heroui/accordion": "^2.2.8",
|
||||||
"@heroui/avatar": "2.2.7",
|
"@heroui/avatar": "2.2.7",
|
||||||
"@heroui/breadcrumbs": "2.2.7",
|
"@heroui/breadcrumbs": "2.2.7",
|
||||||
"@heroui/button": "2.2.10",
|
"@heroui/button": "2.2.10",
|
||||||
@@ -26,80 +30,88 @@
|
|||||||
"@heroui/listbox": "2.3.10",
|
"@heroui/listbox": "2.3.10",
|
||||||
"@heroui/modal": "2.2.8",
|
"@heroui/modal": "2.2.8",
|
||||||
"@heroui/navbar": "2.2.9",
|
"@heroui/navbar": "2.2.9",
|
||||||
|
"@heroui/pagination": "^2.2.9",
|
||||||
"@heroui/popover": "2.3.10",
|
"@heroui/popover": "2.3.10",
|
||||||
"@heroui/select": "2.4.10",
|
"@heroui/select": "2.4.10",
|
||||||
|
"@heroui/skeleton": "^2.2.6",
|
||||||
"@heroui/slider": "2.4.8",
|
"@heroui/slider": "2.4.8",
|
||||||
"@heroui/snippet": "2.2.11",
|
"@heroui/snippet": "2.2.11",
|
||||||
"@heroui/spinner": "2.2.7",
|
"@heroui/spinner": "2.2.7",
|
||||||
"@heroui/switch": "2.2.9",
|
"@heroui/switch": "2.2.9",
|
||||||
"@heroui/system": "2.4.7",
|
"@heroui/system": "2.4.7",
|
||||||
|
"@heroui/table": "^2.2.9",
|
||||||
"@heroui/tabs": "2.2.8",
|
"@heroui/tabs": "2.2.8",
|
||||||
"@heroui/theme": "2.4.6",
|
"@heroui/theme": "2.4.6",
|
||||||
"@heroui/tooltip": "2.2.8",
|
"@heroui/tooltip": "2.2.8",
|
||||||
"@monaco-editor/loader": "^1.4.0",
|
"@monaco-editor/loader": "^1.4.0",
|
||||||
"@monaco-editor/react": "4.7.0-rc.0",
|
"@monaco-editor/react": "4.7.0-rc.0",
|
||||||
"@react-aria/visually-hidden": "3.8.18",
|
"@react-aria/visually-hidden": "^3.8.19",
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.5.1",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"clsx": "2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"event-source-polyfill": "^1.0.31",
|
"event-source-polyfill": "^1.0.31",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^12.0.6",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"motion": "^11.15.0",
|
"motion": "^12.0.6",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"qface": "^1.4.1",
|
"qface": "^1.4.1",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
"react": "19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-color": "^2.19.3",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-dropzone": "^14.3.5",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-error-boundary": "^5.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
|
"react-photo-view": "^1.2.7",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-responsive": "^10.0.0",
|
"react-responsive": "^10.0.0",
|
||||||
"react-router-dom": "7.1.0",
|
"react-router-dom": "^7.1.4",
|
||||||
"react-use-websocket": "^4.11.1",
|
"react-use-websocket": "^4.11.1",
|
||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"tailwind-variants": "0.3.0",
|
"tailwind-variants": "^0.3.0",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@react-types/shared": "^3.26.0",
|
"@react-types/shared": "^3.26.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"@types/event-source-polyfill": "^1.0.5",
|
"@types/event-source-polyfill": "^1.0.5",
|
||||||
"@types/fabric": "^5.3.9",
|
"@types/fabric": "^5.3.9",
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "^22.12.0",
|
||||||
"@types/react": "19.0.2",
|
"@types/path-browserify": "^1.0.3",
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react": "^19.0.8",
|
||||||
|
"@types/react-dom": "^19.0.3",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@typescript-eslint/eslint-plugin": "8.18.1",
|
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||||
"@typescript-eslint/parser": "8.18.1",
|
"@typescript-eslint/parser": "^8.22.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-prettier": "5.2.1",
|
"eslint-plugin-prettier": "5.2.3",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-unused-imports": "4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "^8.5.1",
|
||||||
"prettier": "3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "5.7.2",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^6.0.5",
|
"vite": "^6.0.5",
|
||||||
"vite-plugin-static-copy": "^2.2.0",
|
"vite-plugin-static-copy": "^2.2.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
BIN
napcat.webui/public/fonts/AaCute.woff
Normal file
BIN
napcat.webui/public/fonts/AaCute.woff
Normal file
Binary file not shown.
Binary file not shown.
BIN
napcat.webui/public/fonts/JetBrainsMono-Italic.ttf
Normal file
BIN
napcat.webui/public/fonts/JetBrainsMono-Italic.ttf
Normal file
Binary file not shown.
BIN
napcat.webui/public/fonts/JetBrainsMono.ttf
Normal file
BIN
napcat.webui/public/fonts/JetBrainsMono.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,6 +16,16 @@ import store from '@/store'
|
|||||||
const WebLoginPage = lazy(() => import('@/pages/web_login'))
|
const WebLoginPage = lazy(() => import('@/pages/web_login'))
|
||||||
const IndexPage = lazy(() => import('@/pages/index'))
|
const IndexPage = lazy(() => import('@/pages/index'))
|
||||||
const QQLoginPage = lazy(() => import('@/pages/qq_login'))
|
const QQLoginPage = lazy(() => import('@/pages/qq_login'))
|
||||||
|
const DashboardIndexPage = lazy(() => import('@/pages/dashboard'))
|
||||||
|
const AboutPage = lazy(() => import('@/pages/dashboard/about'))
|
||||||
|
const ConfigPage = lazy(() => import('@/pages/dashboard/config'))
|
||||||
|
const DebugPage = lazy(() => import('@/pages/dashboard/debug'))
|
||||||
|
const HttpDebug = lazy(() => import('@/pages/dashboard/debug/http'))
|
||||||
|
const WSDebug = lazy(() => import('@/pages/dashboard/debug/websocket'))
|
||||||
|
const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'))
|
||||||
|
const LogsPage = lazy(() => import('@/pages/dashboard/logs'))
|
||||||
|
const NetworkPage = lazy(() => import('@/pages/dashboard/network'))
|
||||||
|
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'))
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -58,9 +68,21 @@ function AuthChecker({ children }: { children: React.ReactNode }) {
|
|||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<IndexPage />} path="/*" />
|
<Route path="/" element={<IndexPage />}>
|
||||||
<Route element={<QQLoginPage />} path="/qq_login" />
|
<Route index element={<DashboardIndexPage />} />
|
||||||
<Route element={<WebLoginPage />} path="/web_login" />
|
<Route path="network" element={<NetworkPage />} />
|
||||||
|
<Route path="config" element={<ConfigPage />} />
|
||||||
|
<Route path="logs" element={<LogsPage />} />
|
||||||
|
<Route path="debug" element={<DebugPage />}>
|
||||||
|
<Route path="ws" element={<WSDebug />} />
|
||||||
|
<Route path="http" element={<HttpDebug />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="file_manager" element={<FileManagerPage />} />
|
||||||
|
<Route path="terminal" element={<TerminalPage />} />
|
||||||
|
<Route path="about" element={<AboutPage />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/qq_login" element={<QQLoginPage />} />
|
||||||
|
<Route path="/web_login" element={<WebLoginPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
36
napcat.webui/src/components/ColorPicker.tsx
Normal file
36
napcat.webui/src/components/ColorPicker.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
||||||
|
import React from 'react'
|
||||||
|
import { ColorResult, SketchPicker } from 'react-color'
|
||||||
|
|
||||||
|
// 假定 heroui 提供的 Popover组件
|
||||||
|
|
||||||
|
interface ColorPickerProps {
|
||||||
|
color: string
|
||||||
|
onChange: (color: ColorResult) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
|
||||||
|
const handleChange = (colorResult: ColorResult) => {
|
||||||
|
onChange(colorResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover triggerScaleOnOpen={false}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<div
|
||||||
|
className="w-36 h-8 rounded-md cursor-pointer border border-content4"
|
||||||
|
style={{ background: color }}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<SketchPicker
|
||||||
|
color={color}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="!bg-transparent !shadow-none"
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColorPicker
|
@@ -231,7 +231,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
|
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
|
||||||
)}
|
)}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="danger"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||||
>
|
>
|
||||||
|
@@ -33,7 +33,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
startContent={<IoAddCircleOutline className="text-2xl" />}
|
startContent={<IoAddCircleOutline className="text-2xl" />}
|
||||||
>
|
>
|
||||||
新建
|
新建
|
||||||
|
@@ -1,21 +1,29 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button'
|
||||||
|
import clsx from 'clsx'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io'
|
||||||
|
|
||||||
export interface SaveButtonsProps {
|
export interface SaveButtonsProps {
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
reset: () => void
|
reset: () => void
|
||||||
refresh: () => void
|
refresh?: () => void
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SaveButtons: React.FC<SaveButtonsProps> = ({
|
const SaveButtons: React.FC<SaveButtonsProps> = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
reset,
|
reset,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
refresh
|
refresh,
|
||||||
|
className
|
||||||
}) => (
|
}) => (
|
||||||
<div className="max-w-full mx-3 w-96 flex flex-col justify-center gap-3">
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'max-w-full mx-3 w-96 flex flex-col justify-center gap-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-center gap-2 mt-5">
|
<div className="flex items-center justify-center gap-2 mt-5">
|
||||||
<Button
|
<Button
|
||||||
color="default"
|
color="default"
|
||||||
@@ -33,6 +41,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
|||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
|
{refresh && (
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -42,6 +51,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
|||||||
>
|
>
|
||||||
<IoMdRefresh size={24} />
|
<IoMdRefresh size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@@ -110,7 +110,7 @@ const AudioInsert = () => {
|
|||||||
<Tooltip content="发送音频">
|
<Tooltip content="发送音频">
|
||||||
<div className="max-w-fit">
|
<div className="max-w-fit">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||||
<IoMic className="text-xl" />
|
<IoMic className="text-xl" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -120,7 +120,7 @@ const AudioInsert = () => {
|
|||||||
<Tooltip content="上传音频">
|
<Tooltip content="上传音频">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -137,7 +137,7 @@ const AudioInsert = () => {
|
|||||||
<PopoverTrigger tooltip="输入音频地址">
|
<PopoverTrigger tooltip="输入音频地址">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -154,7 +154,7 @@ const AudioInsert = () => {
|
|||||||
placeholder="请输入音频地址"
|
placeholder="请输入音频地址"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -177,7 +177,7 @@ const AudioInsert = () => {
|
|||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -190,7 +190,7 @@ const AudioInsert = () => {
|
|||||||
<PopoverContent className="flex-col gap-2 p-4">
|
<PopoverContent className="flex-col gap-2 p-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
color={isRecording ? 'danger' : 'danger'}
|
color={isRecording ? 'primary' : 'primary'}
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={isRecording ? stopRecording : startRecording}
|
onPress={isRecording ? stopRecording : startRecording}
|
||||||
>
|
>
|
||||||
@@ -198,7 +198,7 @@ const AudioInsert = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
{showPreview && audioPreview && (
|
{showPreview && audioPreview && (
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={handleShowPreview}
|
onPress={handleShowPreview}
|
||||||
>
|
>
|
||||||
@@ -212,7 +212,7 @@ const AudioInsert = () => {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4 rounded-full',
|
'w-4 h-4 rounded-full',
|
||||||
isRecording
|
isRecording
|
||||||
? 'animate-pulse bg-danger-400'
|
? 'animate-pulse bg-primary-400'
|
||||||
: 'bg-success-400'
|
: 'bg-success-400'
|
||||||
)}
|
)}
|
||||||
></span>
|
></span>
|
||||||
|
@@ -10,7 +10,7 @@ const DiceInsert = () => {
|
|||||||
return (
|
return (
|
||||||
<Tooltip content="发送骰子">
|
<Tooltip content="发送骰子">
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
|
@@ -55,7 +55,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
|||||||
<Tooltip content="插入表情">
|
<Tooltip content="插入表情">
|
||||||
<div className="max-w-fit">
|
<div className="max-w-fit">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||||
<MdEmojiEmotions className="text-xl" />
|
<MdEmojiEmotions className="text-xl" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -65,7 +65,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
|||||||
{visibleEmojis.map((emoji) => (
|
{visibleEmojis.map((emoji) => (
|
||||||
<Button
|
<Button
|
||||||
key={emoji.id}
|
key={emoji.id}
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
|
@@ -35,7 +35,7 @@ const FileInsert = () => {
|
|||||||
<Tooltip content="发送文件">
|
<Tooltip content="发送文件">
|
||||||
<div className="max-w-fit">
|
<div className="max-w-fit">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||||
<FaFolder className="text-lg" />
|
<FaFolder className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -45,7 +45,7 @@ const FileInsert = () => {
|
|||||||
<Tooltip content="上传文件">
|
<Tooltip content="上传文件">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -62,7 +62,7 @@ const FileInsert = () => {
|
|||||||
<PopoverTrigger tooltip="输入文件地址">
|
<PopoverTrigger tooltip="输入文件地址">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -79,7 +79,7 @@ const FileInsert = () => {
|
|||||||
placeholder="请输入文件地址"
|
placeholder="请输入文件地址"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
|
@@ -23,7 +23,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
|||||||
<Tooltip content="插入图片">
|
<Tooltip content="插入图片">
|
||||||
<div className="max-w-fit">
|
<div className="max-w-fit">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||||
<MdImage className="text-xl" />
|
<MdImage className="text-xl" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -33,7 +33,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
|||||||
<Tooltip content="上传图片">
|
<Tooltip content="上传图片">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -50,7 +50,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
|||||||
<PopoverTrigger tooltip="输入图片地址">
|
<PopoverTrigger tooltip="输入图片地址">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -67,7 +67,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
|||||||
placeholder="请输入图片地址"
|
placeholder="请输入图片地址"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
|
@@ -80,7 +80,7 @@ const MusicInsert = () => {
|
|||||||
<Tooltip content="发送音乐">
|
<Tooltip content="发送音乐">
|
||||||
<div className="max-w-fit">
|
<div className="max-w-fit">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||||
<IoMusicalNotes className="text-xl" />
|
<IoMusicalNotes className="text-xl" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -132,7 +132,7 @@ const MusicInsert = () => {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
size="lg"
|
size="lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -236,7 +236,7 @@ const MusicInsert = () => {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
size="lg"
|
size="lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@@ -19,7 +19,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
|||||||
<Tooltip content="回复消息">
|
<Tooltip content="回复消息">
|
||||||
<div className="max-w-fit">
|
<div className="max-w-fit">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||||
<BsChatQuoteFill className="text-lg" />
|
<BsChatQuoteFill className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -38,7 +38,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
@@ -10,7 +10,7 @@ const RPSInsert = () => {
|
|||||||
return (
|
return (
|
||||||
<Tooltip content="发送猜拳">
|
<Tooltip content="发送猜拳">
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
|
@@ -35,7 +35,7 @@ const VideoInsert = () => {
|
|||||||
<Tooltip content="发送视频">
|
<Tooltip content="发送视频">
|
||||||
<div className="max-w-fit">
|
<div className="max-w-fit">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||||
<IoVideocam className="text-xl" />
|
<IoVideocam className="text-xl" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -45,7 +45,7 @@ const VideoInsert = () => {
|
|||||||
<Tooltip content="上传视频">
|
<Tooltip content="上传视频">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -62,7 +62,7 @@ const VideoInsert = () => {
|
|||||||
<PopoverTrigger tooltip="输入视频地址">
|
<PopoverTrigger tooltip="输入视频地址">
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
color="danger"
|
color="primary"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
@@ -79,7 +79,7 @@ const VideoInsert = () => {
|
|||||||
placeholder="请输入视频地址"
|
placeholder="请输入视频地址"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
|
@@ -190,7 +190,7 @@ const ChatInput = () => {
|
|||||||
<DiceInsert />
|
<DiceInsert />
|
||||||
<RPSInsert />
|
<RPSInsert />
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const messages = getChatMessage()
|
const messages = getChatMessage()
|
||||||
showStructuredMessage(messages)
|
showStructuredMessage(messages)
|
||||||
|
@@ -15,7 +15,7 @@ export default function ChatInputModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||||
构造聊天消息
|
构造聊天消息
|
||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -36,7 +36,7 @@ export default function ChatInputModal() {
|
|||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="danger" onPress={onClose} variant="flat">
|
<Button color="primary" onPress={onClose} variant="flat">
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
@@ -78,7 +78,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
|||||||
{debug ? '关闭调试' : '开启调试'}
|
{debug ? '关闭调试' : '开启调试'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
startContent={<MdDeleteForever />}
|
startContent={<MdDeleteForever />}
|
||||||
onPress={handleDelete}
|
onPress={handleDelete}
|
||||||
>
|
>
|
||||||
|
@@ -19,7 +19,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'bg-opacity-60 shadow-sm md:rounded-3xl',
|
'bg-opacity-60 shadow-sm md:rounded-3xl',
|
||||||
size === 'md'
|
size === 'md'
|
||||||
? 'col-span-8 md:col-span-2 bg-danger-50 shadow-danger-100'
|
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
|
||||||
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
||||||
)}
|
)}
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
@@ -27,7 +27,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
|||||||
<CardBody className="items-center md:gap-1 p-1 md:p-2">
|
<CardBody className="items-center md:gap-1 p-1 md:p-2">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'font-outfit flex-1',
|
'flex-1',
|
||||||
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
||||||
title({
|
title({
|
||||||
color: size === 'md' ? 'pink' : 'yellow',
|
color: size === 'md' ? 'pink' : 'yellow',
|
||||||
|
166
napcat.webui/src/components/file_icon.tsx
Normal file
166
napcat.webui/src/components/file_icon.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import {
|
||||||
|
FaFile,
|
||||||
|
FaFileAudio,
|
||||||
|
FaFileCode,
|
||||||
|
FaFileCsv,
|
||||||
|
FaFileExcel,
|
||||||
|
FaFileImage,
|
||||||
|
FaFileLines,
|
||||||
|
FaFilePdf,
|
||||||
|
FaFilePowerpoint,
|
||||||
|
FaFileVideo,
|
||||||
|
FaFileWord,
|
||||||
|
FaFileZipper,
|
||||||
|
FaFolderClosed
|
||||||
|
} from 'react-icons/fa6'
|
||||||
|
|
||||||
|
export interface FileIconProps {
|
||||||
|
name?: string
|
||||||
|
isDirectory?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileIcon = (props: FileIconProps) => {
|
||||||
|
const { name, isDirectory = false } = props
|
||||||
|
if (isDirectory) {
|
||||||
|
return <FaFolderClosed className="text-yellow-500" />
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = name?.split('.').pop() || ''
|
||||||
|
if (ext) {
|
||||||
|
switch (ext.toLowerCase()) {
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'png':
|
||||||
|
case 'gif':
|
||||||
|
case 'svg':
|
||||||
|
case 'bmp':
|
||||||
|
case 'ico':
|
||||||
|
case 'webp':
|
||||||
|
case 'tiff':
|
||||||
|
case 'tif':
|
||||||
|
case 'heic':
|
||||||
|
case 'heif':
|
||||||
|
case 'avif':
|
||||||
|
case 'apng':
|
||||||
|
case 'flif':
|
||||||
|
case 'ai':
|
||||||
|
case 'psd':
|
||||||
|
case 'xcf':
|
||||||
|
case 'sketch':
|
||||||
|
case 'fig':
|
||||||
|
case 'xd':
|
||||||
|
case 'svgz':
|
||||||
|
return <FaFileImage className="text-green-500" />
|
||||||
|
case 'pdf':
|
||||||
|
return <FaFilePdf className="text-red-500" />
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
return <FaFileWord className="text-blue-500" />
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx':
|
||||||
|
return <FaFileExcel className="text-green-500" />
|
||||||
|
case 'csv':
|
||||||
|
return <FaFileCsv className="text-green-500" />
|
||||||
|
case 'ppt':
|
||||||
|
case 'pptx':
|
||||||
|
return <FaFilePowerpoint className="text-red-500" />
|
||||||
|
case 'zip':
|
||||||
|
case 'rar':
|
||||||
|
case '7z':
|
||||||
|
case 'tar':
|
||||||
|
case 'gz':
|
||||||
|
case 'bz2':
|
||||||
|
case 'xz':
|
||||||
|
case 'lz':
|
||||||
|
case 'lzma':
|
||||||
|
case 'zst':
|
||||||
|
case 'zstd':
|
||||||
|
case 'z':
|
||||||
|
case 'taz':
|
||||||
|
case 'tz':
|
||||||
|
case 'tzo':
|
||||||
|
return <FaFileZipper className="text-green-500" />
|
||||||
|
case 'txt':
|
||||||
|
return <FaFileLines className="text-gray-500" />
|
||||||
|
case 'mp3':
|
||||||
|
case 'wav':
|
||||||
|
case 'flac':
|
||||||
|
return <FaFileAudio className="text-green-500" />
|
||||||
|
case 'mp4':
|
||||||
|
case 'avi':
|
||||||
|
case 'mov':
|
||||||
|
case 'wmv':
|
||||||
|
return <FaFileVideo className="text-red-500" />
|
||||||
|
case 'html':
|
||||||
|
case 'css':
|
||||||
|
case 'js':
|
||||||
|
case 'ts':
|
||||||
|
case 'jsx':
|
||||||
|
case 'tsx':
|
||||||
|
case 'json':
|
||||||
|
case 'xml':
|
||||||
|
case 'yaml':
|
||||||
|
case 'yml':
|
||||||
|
case 'md':
|
||||||
|
case 'sh':
|
||||||
|
case 'py':
|
||||||
|
case 'java':
|
||||||
|
case 'c':
|
||||||
|
case 'cpp':
|
||||||
|
case 'cs':
|
||||||
|
case 'go':
|
||||||
|
case 'php':
|
||||||
|
case 'rb':
|
||||||
|
case 'pl':
|
||||||
|
case 'swift':
|
||||||
|
case 'kt':
|
||||||
|
case 'rs':
|
||||||
|
case 'sql':
|
||||||
|
case 'r':
|
||||||
|
case 'scala':
|
||||||
|
case 'groovy':
|
||||||
|
case 'dart':
|
||||||
|
case 'lua':
|
||||||
|
case 'perl':
|
||||||
|
case 'h':
|
||||||
|
case 'm':
|
||||||
|
case 'mm':
|
||||||
|
case 'makefile':
|
||||||
|
case 'cmake':
|
||||||
|
case 'dockerfile':
|
||||||
|
case 'gradle':
|
||||||
|
case 'properties':
|
||||||
|
case 'ini':
|
||||||
|
case 'conf':
|
||||||
|
case 'env':
|
||||||
|
case 'bat':
|
||||||
|
case 'cmd':
|
||||||
|
case 'ps1':
|
||||||
|
case 'psm1':
|
||||||
|
case 'psd1':
|
||||||
|
case 'ps1xml':
|
||||||
|
case 'psc1':
|
||||||
|
case 'pssc':
|
||||||
|
case 'nuspec':
|
||||||
|
case 'resx':
|
||||||
|
case 'resw':
|
||||||
|
case 'csproj':
|
||||||
|
case 'vbproj':
|
||||||
|
case 'vcxproj':
|
||||||
|
case 'fsproj':
|
||||||
|
case 'sln':
|
||||||
|
case 'suo':
|
||||||
|
case 'user':
|
||||||
|
case 'userosscache':
|
||||||
|
case 'sln.docstates':
|
||||||
|
case 'dll':
|
||||||
|
return <FaFileCode className="text-blue-500" />
|
||||||
|
default:
|
||||||
|
return <FaFile className="text-gray-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FaFile className="text-gray-500" />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileIcon
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { Button, ButtonGroup } from '@heroui/button'
|
||||||
|
import { Input } from '@heroui/input'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader
|
||||||
|
} from '@heroui/modal'
|
||||||
|
|
||||||
|
interface CreateFileModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
fileType: 'file' | 'directory'
|
||||||
|
newFileName: string
|
||||||
|
onTypeChange: (type: 'file' | 'directory') => void
|
||||||
|
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
onClose: () => void
|
||||||
|
onCreate: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateFileModal({
|
||||||
|
isOpen,
|
||||||
|
fileType,
|
||||||
|
newFileName,
|
||||||
|
onTypeChange,
|
||||||
|
onNameChange,
|
||||||
|
onClose,
|
||||||
|
onCreate
|
||||||
|
}: CreateFileModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>新建</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ButtonGroup color="primary">
|
||||||
|
<Button
|
||||||
|
variant={fileType === 'file' ? 'solid' : 'flat'}
|
||||||
|
onPress={() => onTypeChange('file')}
|
||||||
|
>
|
||||||
|
文件
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={fileType === 'directory' ? 'solid' : 'flat'}
|
||||||
|
onPress={() => onTypeChange('directory')}
|
||||||
|
>
|
||||||
|
目录
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Input label="名称" value={newFileName} onChange={onNameChange} />
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" variant="flat" onPress={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onPress={onCreate}>
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
94
napcat.webui/src/components/file_manage/file_edit_modal.tsx
Normal file
94
napcat.webui/src/components/file_manage/file_edit_modal.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import { Code } from '@heroui/code'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader
|
||||||
|
} from '@heroui/modal'
|
||||||
|
|
||||||
|
import CodeEditor from '@/components/code_editor'
|
||||||
|
|
||||||
|
interface FileEditModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
file: { path: string; content: string } | null
|
||||||
|
onClose: () => void
|
||||||
|
onSave: () => void
|
||||||
|
onContentChange: (newContent?: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FileEditModal({
|
||||||
|
isOpen,
|
||||||
|
file,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
onContentChange
|
||||||
|
}: FileEditModalProps) {
|
||||||
|
// 根据文件后缀返回对应语言
|
||||||
|
const getLanguage = (filePath: string) => {
|
||||||
|
if (filePath.endsWith('.js')) return 'javascript'
|
||||||
|
if (filePath.endsWith('.ts')) return 'typescript'
|
||||||
|
if (filePath.endsWith('.tsx')) return 'tsx'
|
||||||
|
if (filePath.endsWith('.jsx')) return 'jsx'
|
||||||
|
if (filePath.endsWith('.vue')) return 'vue'
|
||||||
|
if (filePath.endsWith('.svelte')) return 'svelte'
|
||||||
|
if (filePath.endsWith('.json')) return 'json'
|
||||||
|
if (filePath.endsWith('.html')) return 'html'
|
||||||
|
if (filePath.endsWith('.css')) return 'css'
|
||||||
|
if (filePath.endsWith('.scss')) return 'scss'
|
||||||
|
if (filePath.endsWith('.less')) return 'less'
|
||||||
|
if (filePath.endsWith('.md')) return 'markdown'
|
||||||
|
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) return 'yaml'
|
||||||
|
if (filePath.endsWith('.xml')) return 'xml'
|
||||||
|
if (filePath.endsWith('.sql')) return 'sql'
|
||||||
|
if (filePath.endsWith('.sh')) return 'shell'
|
||||||
|
if (filePath.endsWith('.bat')) return 'bat'
|
||||||
|
if (filePath.endsWith('.php')) return 'php'
|
||||||
|
if (filePath.endsWith('.java')) return 'java'
|
||||||
|
if (filePath.endsWith('.c')) return 'c'
|
||||||
|
if (filePath.endsWith('.cpp')) return 'cpp'
|
||||||
|
if (filePath.endsWith('.h')) return 'h'
|
||||||
|
if (filePath.endsWith('.hpp')) return 'hpp'
|
||||||
|
if (filePath.endsWith('.go')) return 'go'
|
||||||
|
if (filePath.endsWith('.py')) return 'python'
|
||||||
|
if (filePath.endsWith('.rb')) return 'ruby'
|
||||||
|
if (filePath.endsWith('.cs')) return 'csharp'
|
||||||
|
if (filePath.endsWith('.swift')) return 'swift'
|
||||||
|
if (filePath.endsWith('.vb')) return 'vb'
|
||||||
|
if (filePath.endsWith('.lua')) return 'lua'
|
||||||
|
if (filePath.endsWith('.pl')) return 'perl'
|
||||||
|
if (filePath.endsWith('.r')) return 'r'
|
||||||
|
return 'plaintext'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size="full" isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader className="flex items-center gap-2 bg-content2 bg-opacity-50">
|
||||||
|
<span>编辑文件</span>
|
||||||
|
<Code className="text-xs">{file?.path}</Code>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="p-0">
|
||||||
|
<div className="h-full">
|
||||||
|
<CodeEditor
|
||||||
|
height="100%"
|
||||||
|
value={file?.content || ''}
|
||||||
|
onChange={onContentChange}
|
||||||
|
options={{ wordWrap: 'on' }}
|
||||||
|
language={file?.path ? getLanguage(file.path) : 'plaintext'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" variant="flat" onPress={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onPress={onSave}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,92 @@
|
|||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader
|
||||||
|
} from '@heroui/modal'
|
||||||
|
import { Spinner } from '@heroui/spinner'
|
||||||
|
import { useRequest } from 'ahooks'
|
||||||
|
import path from 'path-browserify'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import FileManager from '@/controllers/file_manager'
|
||||||
|
|
||||||
|
interface FilePreviewModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
filePath: string
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const videoExts = ['.mp4', '.webm']
|
||||||
|
export const audioExts = ['.mp3', '.wav']
|
||||||
|
|
||||||
|
export const supportedPreviewExts = [...videoExts, ...audioExts]
|
||||||
|
|
||||||
|
export default function FilePreviewModal({
|
||||||
|
isOpen,
|
||||||
|
filePath,
|
||||||
|
onClose
|
||||||
|
}: FilePreviewModalProps) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase()
|
||||||
|
const { data, loading, error, run } = useRequest(
|
||||||
|
async () => FileManager.downloadToURL(filePath),
|
||||||
|
{
|
||||||
|
refreshDeps: [filePath],
|
||||||
|
manual: true,
|
||||||
|
refreshDepsAction: () => {
|
||||||
|
const ext = path.extname(filePath).toLowerCase()
|
||||||
|
if (!filePath || !supportedPreviewExts.includes(ext)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filePath) {
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
}, [filePath])
|
||||||
|
|
||||||
|
let contentElement = null
|
||||||
|
if (!supportedPreviewExts.includes(ext)) {
|
||||||
|
contentElement = <div>暂不支持预览此文件类型</div>
|
||||||
|
} else if (error) {
|
||||||
|
contentElement = <div>读取文件失败</div>
|
||||||
|
} else if (loading || !data) {
|
||||||
|
contentElement = (
|
||||||
|
<div className="flex justify-center items-center h-full">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (videoExts.includes(ext)) {
|
||||||
|
contentElement = <video src={data} controls className="max-w-full" />
|
||||||
|
} else if (audioExts.includes(ext)) {
|
||||||
|
contentElement = <audio src={data} controls className="w-full" />
|
||||||
|
} else {
|
||||||
|
contentElement = (
|
||||||
|
<div className="flex justify-center items-center h-full">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside" size="3xl">
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>文件预览</ModalHeader>
|
||||||
|
<ModalBody className="flex justify-center items-center">
|
||||||
|
{contentElement}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" variant="flat" onPress={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
245
napcat.webui/src/components/file_manage/file_table.tsx
Normal file
245
napcat.webui/src/components/file_manage/file_table.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { Button, ButtonGroup } from '@heroui/button'
|
||||||
|
import { Pagination } from '@heroui/pagination'
|
||||||
|
import { Spinner } from '@heroui/spinner'
|
||||||
|
import {
|
||||||
|
type Selection,
|
||||||
|
type SortDescriptor,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableColumn,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@heroui/table'
|
||||||
|
import path from 'path-browserify'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { BiRename } from 'react-icons/bi'
|
||||||
|
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi'
|
||||||
|
import { PhotoSlider } from 'react-photo-view'
|
||||||
|
|
||||||
|
import FileIcon from '@/components/file_icon'
|
||||||
|
|
||||||
|
import type { FileInfo } from '@/controllers/file_manager'
|
||||||
|
|
||||||
|
import { supportedPreviewExts } from './file_preview_modal'
|
||||||
|
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button'
|
||||||
|
|
||||||
|
export interface FileTableProps {
|
||||||
|
files: FileInfo[]
|
||||||
|
currentPath: string
|
||||||
|
loading: boolean
|
||||||
|
sortDescriptor: SortDescriptor
|
||||||
|
onSortChange: (descriptor: SortDescriptor) => void
|
||||||
|
selectedFiles: Selection
|
||||||
|
onSelectionChange: (selected: Selection) => void
|
||||||
|
onDirectoryClick: (dirPath: string) => void
|
||||||
|
onEdit: (filePath: string) => void
|
||||||
|
onPreview: (filePath: string) => void
|
||||||
|
onRenameRequest: (name: string) => void
|
||||||
|
onMoveRequest: (name: string) => void
|
||||||
|
onCopyPath: (fileName: string) => void
|
||||||
|
onDelete: (filePath: string) => void
|
||||||
|
onDownload: (filePath: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export default function FileTable({
|
||||||
|
files,
|
||||||
|
currentPath,
|
||||||
|
loading,
|
||||||
|
sortDescriptor,
|
||||||
|
onSortChange,
|
||||||
|
selectedFiles,
|
||||||
|
onSelectionChange,
|
||||||
|
onDirectoryClick,
|
||||||
|
onEdit,
|
||||||
|
onPreview,
|
||||||
|
onRenameRequest,
|
||||||
|
onMoveRequest,
|
||||||
|
onCopyPath,
|
||||||
|
onDelete,
|
||||||
|
onDownload
|
||||||
|
}: FileTableProps) {
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const pages = Math.ceil(files.length / PAGE_SIZE) || 1
|
||||||
|
const start = (page - 1) * PAGE_SIZE
|
||||||
|
const end = start + PAGE_SIZE
|
||||||
|
const displayFiles = files.slice(start, end)
|
||||||
|
const [showImage, setShowImage] = useState(false)
|
||||||
|
const [previewIndex, setPreviewIndex] = useState(0)
|
||||||
|
const [previewImages, setPreviewImages] = useState<PreviewImage[]>([])
|
||||||
|
|
||||||
|
const addPreviewImage = useCallback((image: PreviewImage) => {
|
||||||
|
setPreviewImages((prev) => {
|
||||||
|
const exists = prev.some((p) => p.key === image.key)
|
||||||
|
if (exists) return prev
|
||||||
|
return [...prev, image]
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPreviewImages([])
|
||||||
|
setPreviewIndex(0)
|
||||||
|
setShowImage(false)
|
||||||
|
}, [currentPath])
|
||||||
|
|
||||||
|
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
||||||
|
const index = images.findIndex((image) => image.key === name)
|
||||||
|
if (index === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPreviewIndex(index)
|
||||||
|
setShowImage(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PhotoSlider
|
||||||
|
images={previewImages}
|
||||||
|
visible={showImage}
|
||||||
|
onClose={() => setShowImage(false)}
|
||||||
|
index={previewIndex}
|
||||||
|
onIndexChange={setPreviewIndex}
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
aria-label="文件列表"
|
||||||
|
sortDescriptor={sortDescriptor}
|
||||||
|
onSortChange={onSortChange}
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
defaultSelectedKeys={[]}
|
||||||
|
selectedKeys={selectedFiles}
|
||||||
|
selectionMode="multiple"
|
||||||
|
bottomContent={
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<Pagination
|
||||||
|
isCompact
|
||||||
|
showControls
|
||||||
|
showShadow
|
||||||
|
color="primary"
|
||||||
|
page={page}
|
||||||
|
total={pages}
|
||||||
|
onChange={(page) => setPage(page)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn key="name" allowsSorting>
|
||||||
|
名称
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn key="type" allowsSorting>
|
||||||
|
类型
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn key="size" allowsSorting>
|
||||||
|
大小
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn key="mtime" allowsSorting>
|
||||||
|
修改时间
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn key="actions">操作</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody
|
||||||
|
isLoading={loading}
|
||||||
|
loadingContent={
|
||||||
|
<div className="flex justify-center items-center h-full">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{displayFiles.map((file: FileInfo) => {
|
||||||
|
const filePath = path.join(currentPath, file.name)
|
||||||
|
const ext = path.extname(file.name).toLowerCase()
|
||||||
|
const previewable = supportedPreviewExts.includes(ext)
|
||||||
|
const images = previewImages
|
||||||
|
return (
|
||||||
|
<TableRow key={file.name}>
|
||||||
|
<TableCell>
|
||||||
|
{imageExts.includes(ext) ? (
|
||||||
|
<ImageNameButton
|
||||||
|
name={file.name}
|
||||||
|
filePath={filePath}
|
||||||
|
onPreview={() => onPreviewImage(file.name, images)}
|
||||||
|
onAddPreview={addPreviewImage}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onPress={() =>
|
||||||
|
file.isDirectory
|
||||||
|
? onDirectoryClick(file.name)
|
||||||
|
: previewable
|
||||||
|
? onPreview(filePath)
|
||||||
|
: onEdit(filePath)
|
||||||
|
}
|
||||||
|
className="text-left justify-start"
|
||||||
|
startContent={
|
||||||
|
<FileIcon
|
||||||
|
name={file.name}
|
||||||
|
isDirectory={file.isDirectory}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isNaN(file.size) || file.isDirectory
|
||||||
|
? '-'
|
||||||
|
: `${file.size} 字节`}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<ButtonGroup size="sm">
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => onRenameRequest(file.name)}
|
||||||
|
>
|
||||||
|
<BiRename />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => onMoveRequest(file.name)}
|
||||||
|
>
|
||||||
|
<FiMove />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => onCopyPath(file.name)}
|
||||||
|
>
|
||||||
|
<FiCopy />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => onDownload(filePath)}
|
||||||
|
>
|
||||||
|
<FiDownload />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => onDelete(filePath)}
|
||||||
|
>
|
||||||
|
<FiTrash2 />
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,88 @@
|
|||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import { Image } from '@heroui/image'
|
||||||
|
import { Spinner } from '@heroui/spinner'
|
||||||
|
import { useRequest } from 'ahooks'
|
||||||
|
import path from 'path-browserify'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import FileManager from '@/controllers/file_manager'
|
||||||
|
|
||||||
|
import FileIcon from '../file_icon'
|
||||||
|
|
||||||
|
export interface PreviewImage {
|
||||||
|
key: string
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
}
|
||||||
|
export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
|
||||||
|
|
||||||
|
export interface ImageNameButtonProps {
|
||||||
|
name: string
|
||||||
|
filePath: string
|
||||||
|
onPreview: () => void
|
||||||
|
onAddPreview: (image: PreviewImage) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImageNameButton({
|
||||||
|
name,
|
||||||
|
filePath,
|
||||||
|
onPreview,
|
||||||
|
onAddPreview
|
||||||
|
}: ImageNameButtonProps) {
|
||||||
|
const { data, loading, error, run } = useRequest(
|
||||||
|
async () => FileManager.downloadToURL(filePath),
|
||||||
|
{
|
||||||
|
refreshDeps: [filePath],
|
||||||
|
manual: true,
|
||||||
|
refreshDepsAction: () => {
|
||||||
|
const ext = path.extname(filePath).toLowerCase()
|
||||||
|
if (!filePath || !imageExts.includes(ext)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
onAddPreview({
|
||||||
|
key: name,
|
||||||
|
src: data,
|
||||||
|
alt: name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [data, name, onAddPreview])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filePath) {
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
className="text-left justify-start"
|
||||||
|
onPress={onPreview}
|
||||||
|
startContent={
|
||||||
|
error ? (
|
||||||
|
<FileIcon name={name} isDirectory={false} />
|
||||||
|
) : loading || !data ? (
|
||||||
|
<Spinner size="sm" />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={data}
|
||||||
|
alt={name}
|
||||||
|
className="w-8 h-8 flex-shrink-0"
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'w-8 h-8 flex-shrink-0'
|
||||||
|
}}
|
||||||
|
radius="sm"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
168
napcat.webui/src/components/file_manage/move_modal.tsx
Normal file
168
napcat.webui/src/components/file_manage/move_modal.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader
|
||||||
|
} from '@heroui/modal'
|
||||||
|
import { Spinner } from '@heroui/spinner'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import path from 'path-browserify'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { IoAdd, IoRemove } from 'react-icons/io5'
|
||||||
|
|
||||||
|
import FileManager from '@/controllers/file_manager'
|
||||||
|
|
||||||
|
interface MoveModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
moveTargetPath: string
|
||||||
|
selectionInfo: string
|
||||||
|
onClose: () => void
|
||||||
|
onMove: () => void
|
||||||
|
onSelect: (dir: string) => void // 新增回调
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 DirectoryTree 改为递归组件
|
||||||
|
// 新增 selectedPath 属性,用于标识当前选中的目录
|
||||||
|
function DirectoryTree({
|
||||||
|
basePath,
|
||||||
|
onSelect,
|
||||||
|
selectedPath
|
||||||
|
}: {
|
||||||
|
basePath: string
|
||||||
|
onSelect: (dir: string) => void
|
||||||
|
selectedPath?: string
|
||||||
|
}) {
|
||||||
|
const [dirs, setDirs] = useState<string[]>([])
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
// 新增loading状态
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const fetchDirectories = async () => {
|
||||||
|
try {
|
||||||
|
// 直接使用 basePath 调用接口,移除 process.platform 判断
|
||||||
|
const list = await FileManager.listDirectories(basePath)
|
||||||
|
setDirs(list.map((item) => item.name))
|
||||||
|
} catch (error) {
|
||||||
|
// ...error handling...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggle = async () => {
|
||||||
|
if (!expanded) {
|
||||||
|
setExpanded(true)
|
||||||
|
setLoading(true)
|
||||||
|
await fetchDirectories()
|
||||||
|
setLoading(false)
|
||||||
|
} else {
|
||||||
|
setExpanded(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onSelect(basePath)
|
||||||
|
handleToggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算显示的名称
|
||||||
|
const getDisplayName = () => {
|
||||||
|
if (basePath === '/') return '/'
|
||||||
|
if (/^[A-Z]:$/i.test(basePath)) return basePath
|
||||||
|
return path.basename(basePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 Button 的 variant 逻辑
|
||||||
|
const isSeleted = selectedPath === basePath
|
||||||
|
const variant = isSeleted
|
||||||
|
? 'solid'
|
||||||
|
: selectedPath && path.dirname(selectedPath) === basePath
|
||||||
|
? 'flat'
|
||||||
|
: 'light'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ml-4">
|
||||||
|
<Button
|
||||||
|
onPress={handleClick}
|
||||||
|
className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
variant={variant}
|
||||||
|
startContent={
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'rounded-md',
|
||||||
|
isSeleted ? 'bg-primary-600' : 'bg-primary-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{expanded ? <IoRemove /> : <IoAdd />}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getDisplayName()}
|
||||||
|
</Button>
|
||||||
|
{expanded && (
|
||||||
|
<div>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex py-1 px-8">
|
||||||
|
<Spinner size="sm" color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
dirs.map((dirName) => {
|
||||||
|
const childPath =
|
||||||
|
basePath === '/' && /^[A-Z]:$/i.test(dirName)
|
||||||
|
? dirName
|
||||||
|
: path.join(basePath, dirName)
|
||||||
|
return (
|
||||||
|
<DirectoryTree
|
||||||
|
key={childPath}
|
||||||
|
basePath={childPath}
|
||||||
|
onSelect={onSelect}
|
||||||
|
selectedPath={selectedPath}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MoveModal({
|
||||||
|
isOpen,
|
||||||
|
moveTargetPath,
|
||||||
|
selectionInfo,
|
||||||
|
onClose,
|
||||||
|
onMove,
|
||||||
|
onSelect
|
||||||
|
}: MoveModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>选择目标目录</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="rounded-md p-2 border border-default-300 overflow-auto max-h-60">
|
||||||
|
<DirectoryTree
|
||||||
|
basePath="/"
|
||||||
|
onSelect={onSelect}
|
||||||
|
selectedPath={moveTargetPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-default-500 mt-2">
|
||||||
|
当前选择:{moveTargetPath || '未选择'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-default-500">移动项:{selectionInfo}</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" variant="flat" onPress={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onPress={onMove}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
44
napcat.webui/src/components/file_manage/rename_modal.tsx
Normal file
44
napcat.webui/src/components/file_manage/rename_modal.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import { Input } from '@heroui/input'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader
|
||||||
|
} from '@heroui/modal'
|
||||||
|
|
||||||
|
interface RenameModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
newFileName: string
|
||||||
|
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
onClose: () => void
|
||||||
|
onRename: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RenameModal({
|
||||||
|
isOpen,
|
||||||
|
newFileName,
|
||||||
|
onNameChange,
|
||||||
|
onClose,
|
||||||
|
onRename
|
||||||
|
}: RenameModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>重命名</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Input label="新名称" value={newFileName} onChange={onNameChange} />
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" variant="flat" onPress={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onPress={onRename}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
@@ -33,10 +33,10 @@ export default function Hitokoto() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
{loading && <PageLoading />}
|
{loading && <PageLoading />}
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="text-danger-400">一言加载失败:{error.message}</div>
|
<div className="text-primary-400">一言加载失败:{error.message}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="font-noto-serif">{data?.hitokoto}</div>
|
<div>{data?.hitokoto}</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
—— <span className="text-default-400">{data?.from}</span>{' '}
|
—— <span className="text-default-400">{data?.from}</span>{' '}
|
||||||
{data?.from_who}
|
{data?.from_who}
|
||||||
@@ -52,7 +52,7 @@ export default function Hitokoto() {
|
|||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
>
|
>
|
||||||
<IoRefresh />
|
<IoRefresh />
|
||||||
|
146
napcat.webui/src/components/hover_titled_card.tsx
Normal file
146
napcat.webui/src/components/hover_titled_card.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { motion, useMotionValue, useSpring } from 'motion/react'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
const springValues = {
|
||||||
|
damping: 30,
|
||||||
|
stiffness: 100,
|
||||||
|
mass: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HoverTiltedCardProps {
|
||||||
|
imageSrc: string
|
||||||
|
altText?: string
|
||||||
|
captionText?: string
|
||||||
|
containerHeight?: string
|
||||||
|
containerWidth?: string
|
||||||
|
imageHeight?: string
|
||||||
|
imageWidth?: string
|
||||||
|
scaleOnHover?: number
|
||||||
|
rotateAmplitude?: number
|
||||||
|
showTooltip?: boolean
|
||||||
|
overlayContent?: React.ReactNode
|
||||||
|
displayOverlayContent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HoverTiltedCard({
|
||||||
|
imageSrc,
|
||||||
|
altText = 'NapCat',
|
||||||
|
captionText = 'NapCat',
|
||||||
|
containerHeight = '200px',
|
||||||
|
containerWidth = '100%',
|
||||||
|
imageHeight = '200px',
|
||||||
|
imageWidth = '200px',
|
||||||
|
scaleOnHover = 1.1,
|
||||||
|
rotateAmplitude = 14,
|
||||||
|
showTooltip = false,
|
||||||
|
overlayContent = (
|
||||||
|
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-primary-600 text-default-100 bg-opacity-80">
|
||||||
|
NapCat
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
displayOverlayContent = true
|
||||||
|
}: HoverTiltedCardProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const x = useMotionValue(0)
|
||||||
|
const y = useMotionValue(0)
|
||||||
|
const rotateX = useSpring(useMotionValue(0), springValues)
|
||||||
|
const rotateY = useSpring(useMotionValue(0), springValues)
|
||||||
|
const scale = useSpring(1, springValues)
|
||||||
|
const opacity = useSpring(0)
|
||||||
|
const rotateFigcaption = useSpring(0, {
|
||||||
|
stiffness: 350,
|
||||||
|
damping: 30,
|
||||||
|
mass: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const [lastY, setLastY] = useState(0)
|
||||||
|
|
||||||
|
function handleMouse(e: React.MouseEvent) {
|
||||||
|
if (!ref.current) return
|
||||||
|
|
||||||
|
const rect = ref.current.getBoundingClientRect()
|
||||||
|
const offsetX = e.clientX - rect.left - rect.width / 2
|
||||||
|
const offsetY = e.clientY - rect.top - rect.height / 2
|
||||||
|
|
||||||
|
const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude
|
||||||
|
const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude
|
||||||
|
|
||||||
|
rotateX.set(rotationX)
|
||||||
|
rotateY.set(rotationY)
|
||||||
|
|
||||||
|
x.set(e.clientX - rect.left)
|
||||||
|
y.set(e.clientY - rect.top)
|
||||||
|
|
||||||
|
const velocityY = offsetY - lastY
|
||||||
|
rotateFigcaption.set(-velocityY * 0.6)
|
||||||
|
setLastY(offsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseEnter() {
|
||||||
|
scale.set(scaleOnHover)
|
||||||
|
opacity.set(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
opacity.set(0)
|
||||||
|
scale.set(1)
|
||||||
|
rotateX.set(0)
|
||||||
|
rotateY.set(0)
|
||||||
|
rotateFigcaption.set(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<figure
|
||||||
|
ref={ref}
|
||||||
|
className="relative w-full h-full [perspective:800px] flex flex-col items-center justify-center"
|
||||||
|
style={{
|
||||||
|
height: containerHeight,
|
||||||
|
width: containerWidth
|
||||||
|
}}
|
||||||
|
onMouseMove={handleMouse}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="relative [transform-style:preserve-3d]"
|
||||||
|
style={{
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
rotateX,
|
||||||
|
rotateY,
|
||||||
|
scale
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.img
|
||||||
|
src={imageSrc}
|
||||||
|
alt={altText}
|
||||||
|
className="absolute top-0 left-0 object-cover rounded-md will-change-transform [transform:translateZ(0)] pointer-events-none select-none"
|
||||||
|
style={{
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{displayOverlayContent && overlayContent && (
|
||||||
|
<motion.div className="absolute top-0 left-0 right-0 z-10 flex justify-center will-change-transform [transform:translateZ(30px)]">
|
||||||
|
{overlayContent}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{showTooltip && (
|
||||||
|
<motion.figcaption
|
||||||
|
className="pointer-events-none absolute left-0 top-0 rounded-md bg-white px-2 py-1 text-sm text-default-900 opacity-0 z-10 hidden sm:block"
|
||||||
|
style={{
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
opacity,
|
||||||
|
rotate: rotateFigcaption
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{captionText}
|
||||||
|
</motion.figcaption>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
)
|
||||||
|
}
|
@@ -1152,7 +1152,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
|||||||
begin="0ms"
|
begin="0ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1197,7 +1197,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
|||||||
begin="800ms"
|
begin="800ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1247,7 +1247,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
|||||||
begin="1600ms"
|
begin="1600ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1297,7 +1297,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
|||||||
begin="2400ms"
|
begin="2400ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1344,7 +1344,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
|||||||
begin="3200ms"
|
begin="3200ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1399,7 +1399,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="0ms"
|
begin="0ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1446,7 +1446,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="600ms"
|
begin="600ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1496,7 +1496,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="1200ms"
|
begin="1200ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1543,7 +1543,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="1800ms"
|
begin="1800ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1590,7 +1590,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="2400ms"
|
begin="2400ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1637,7 +1637,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="3000ms"
|
begin="3000ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1684,7 +1684,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="3600ms"
|
begin="3600ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1731,7 +1731,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
begin="4200ms"
|
begin="4200ms"
|
||||||
></animate>
|
></animate>
|
||||||
<animate
|
<animate
|
||||||
attributeName="fill-opacity"
|
attributeName="fillOpacity"
|
||||||
to="1"
|
to="1"
|
||||||
dur="800ms"
|
dur="800ms"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
@@ -1744,3 +1744,224 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
|||||||
</svg>
|
</svg>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const FileIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="_x36_"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||||
|
<g
|
||||||
|
id="SVGRepo_tracerCarrier"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#D4B476' }}
|
||||||
|
d="M441.853,393.794H70.147C31.566,393.794,0,362.228,0,323.647V106.969 c0-38.581,31.566-70.147,70.147-70.147h371.706c38.581,0,70.147,31.566,70.147,70.147v216.678 C512,362.228,480.434,393.794,441.853,393.794z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#D4B476' }}
|
||||||
|
d="M199.884,249.574H70.147C31.566,249.574,0,218.008,0,179.427V70.147C0,31.566,31.566,0,70.147,0 h129.737c38.581,0,70.147,31.566,70.147,70.147v109.28C270.031,218.008,238.465,249.574,199.884,249.574z"
|
||||||
|
></path>
|
||||||
|
<polygon
|
||||||
|
style={{ fill: '#F0EFEF' }}
|
||||||
|
points="485.439,329.388 87.357,347.774 78.653,130.095 476.734,111.709 "
|
||||||
|
></polygon>
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
id="Adobe_OpacityMaskFilter"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
x="34.381"
|
||||||
|
y="60.216"
|
||||||
|
width="416.68"
|
||||||
|
height="259.557"
|
||||||
|
>
|
||||||
|
<feFlood
|
||||||
|
style={{
|
||||||
|
floodColor: 'white',
|
||||||
|
floodOpacity: 1
|
||||||
|
}}
|
||||||
|
result="back"
|
||||||
|
></feFlood>
|
||||||
|
<feBlend in="SourceGraphic" in2="back" mode="normal"></feBlend>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<mask
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="34.381"
|
||||||
|
y="60.216"
|
||||||
|
width="416.68"
|
||||||
|
height="259.557"
|
||||||
|
id="SVGID_1_"
|
||||||
|
>
|
||||||
|
<g style={{ filter: 'url(#Adobe_OpacityMaskFilter)' }}>
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
id="Adobe_OpacityMaskFilter_1_"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
x="34.381"
|
||||||
|
y="60.216"
|
||||||
|
width="416.68"
|
||||||
|
height="259.557"
|
||||||
|
>
|
||||||
|
<feFlood
|
||||||
|
style={{ floodColor: 'white', floodOpacity: 1 }}
|
||||||
|
result="back"
|
||||||
|
></feFlood>
|
||||||
|
<feBlend in="SourceGraphic" in2="back" mode="normal"></feBlend>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<mask
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="34.381"
|
||||||
|
y="60.216"
|
||||||
|
width="416.68"
|
||||||
|
height="259.557"
|
||||||
|
id="SVGID_1_"
|
||||||
|
>
|
||||||
|
<g style={{ filter: 'url(#Adobe_OpacityMaskFilter_1_)' }}> </g>
|
||||||
|
</mask>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_2_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="34.3814"
|
||||||
|
y1="189.9944"
|
||||||
|
x2="451.061"
|
||||||
|
y2="189.9944"
|
||||||
|
>
|
||||||
|
<stop offset="0.57" style={{ stopColor: '#F6F6F6' }}></stop>
|
||||||
|
<stop offset="0.6039" style={{ stopColor: '#F6F6F6' }}></stop>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon
|
||||||
|
style={{ mask: 'url(#SVGID_1_)', fill: 'url(#SVGID_2_)' }}
|
||||||
|
points="451.061,277.073 54.598,319.773 34.381,102.916 430.845,60.216 "
|
||||||
|
></polygon>
|
||||||
|
</g>
|
||||||
|
</mask>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_3_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="34.3814"
|
||||||
|
y1="189.9944"
|
||||||
|
x2="451.061"
|
||||||
|
y2="189.9944"
|
||||||
|
>
|
||||||
|
<stop offset="0.57" style={{ stopColor: '#FFFFFF' }}></stop>
|
||||||
|
<stop offset="0.6039" style={{ stopColor: '#F0F0F0' }}></stop>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon
|
||||||
|
style={{ fill: 'url(#SVGID_3_)' }}
|
||||||
|
points="451.061,277.073 54.598,319.773 34.381,102.916 430.845,60.216 "
|
||||||
|
></polygon>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#69A092' }}
|
||||||
|
d="M441.853,417.32H70.147C31.566,417.32,0,385.754,0,347.173V168.515h512v178.658 C512,385.754,480.434,417.32,441.853,417.32z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#D4B476' }}
|
||||||
|
d="M441.853,429.594H70.147C31.566,429.594,0,398.028,0,359.447V189.995h512v169.453 C512,398.028,480.434,429.594,441.853,429.594z"
|
||||||
|
></path>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#CBBC89' }}
|
||||||
|
d="M41.051,330.321h28.918h7.581c0.686,0,1.357,0,2.012,0c0.655,0,1.171,0.126,1.545,0.375 c0.499,0.312,0.795,0.764,0.889,1.357c0.094,0.594,0.141,1.296,0.141,2.106c0,0.312,0.014,0.608,0.047,0.888 c0.031,0.281-0.016,0.547-0.14,0.796c-0.25,0.998-0.796,1.543-1.638,1.638c-0.843,0.094-1.888,0.14-3.136,0.14h-9.733H53.778 c-0.998,0-2.058-0.015-3.182-0.047c-1.122-0.03-1.965,0.173-2.526,0.608c-0.563,0.375-0.858,1.03-0.89,1.965 c-0.032,0.937-0.047,1.873-0.047,2.808v9.265c0,0.5-0.015,1.123-0.046,1.872c-0.033,0.749,0.014,1.373,0.139,1.871v0.748 c0.125,0.375,0.235,0.735,0.328,1.077c0.093,0.344,0.295,0.608,0.608,0.796c0.562,0.374,1.436,0.547,2.621,0.514 c1.184-0.03,2.214-0.047,3.088-0.047h16.845c0.561,0,1.171-0.014,1.825-0.046c0.655-0.031,1.31-0.031,1.965,0 c0.655,0.032,1.248,0.109,1.779,0.234c0.529,0.126,0.889,0.344,1.076,0.655c0.311,0.375,0.467,0.843,0.467,1.404 c0,0.56,0,1.186,0,1.871c0,0.375,0,0.719,0,1.03c0,0.313-0.062,0.594-0.186,0.842c-0.25,0.625-0.595,0.969-1.03,1.03 c-0.25,0.125-0.485,0.186-0.702,0.186c-0.219,0-0.483,0.033-0.796,0.094c-0.25,0.063-0.514,0.079-0.796,0.047 c-0.281-0.03-0.546-0.047-0.794-0.047h-3.277H54.433c-0.999,0-2.168-0.014-3.51-0.047c-1.342-0.03-2.292,0.172-2.853,0.609 c-0.314,0.25-0.547,0.64-0.702,1.169c-0.156,0.531-0.25,1.14-0.281,1.825c-0.033,0.687-0.033,1.389,0,2.106 c0.03,0.717,0.046,1.357,0.046,1.918v16.096c0,1.062,0.032,2.217,0.094,3.463c0.062,1.249-0.156,2.185-0.655,2.807 c-0.186,0.25-0.469,0.391-0.842,0.422c-0.374,0.032-0.749,0.109-1.123,0.234h-1.779c-0.875,0-1.684-0.03-2.433-0.093 c-0.749-0.062-1.279-0.375-1.59-0.937c-0.25-0.374-0.375-0.888-0.375-1.543c0-0.656,0-1.326,0-2.013v-7.488v-39.119v-10.107 c0-0.686-0.016-1.45-0.047-2.292c-0.032-0.842,0.107-1.512,0.422-2.012c0.249-0.374,0.715-0.685,1.404-0.935 c0.124-0.062,0.264-0.077,0.42-0.047C40.785,330.4,40.925,330.383,41.051,330.321z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#CBBC89' }}
|
||||||
|
d="M96.895,330.508c0.436,0,0.935-0.014,1.496-0.046c0.563-0.032,1.108-0.032,1.639,0 c0.529,0.032,1.013,0.109,1.45,0.234c0.437,0.125,0.749,0.312,0.937,0.561c0.374,0.438,0.576,1.046,0.608,1.825 c0.03,0.78,0.047,1.576,0.047,2.386v9.079v36.124v10.949c0,0.811,0,1.669,0,2.574c0,0.905-0.156,1.606-0.468,2.105 c-0.25,0.314-0.547,0.5-0.889,0.563c-0.344,0.062-0.765,0.156-1.264,0.28h-1.777c-0.874,0-1.684-0.034-2.433-0.094 c-0.749-0.061-1.28-0.374-1.592-0.935c-0.25-0.374-0.375-0.89-0.375-1.545c0-0.655,0-1.325,0-2.012v-7.487V345.95v-10.107 c0-0.686-0.015-1.451-0.045-2.292c-0.034-0.843,0.108-1.514,0.42-2.013c0.249-0.436,0.748-0.748,1.497-0.936 c0.125-0.061,0.25-0.077,0.375-0.047C96.645,330.587,96.769,330.571,96.895,330.508z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#CBBC89' }}
|
||||||
|
d="M126.093,330.321c0.436,0,0.935-0.015,1.498-0.047c0.561-0.03,1.107-0.03,1.638,0 c0.529,0.032,1.012,0.109,1.451,0.234c0.436,0.125,0.748,0.313,0.936,0.562c0.374,0.5,0.56,1.155,0.56,1.965 c0,0.811,0,1.654,0,2.528v9.826v30.042v8.609c0,0.687,0,1.436,0,2.245c0,0.813,0.094,1.468,0.281,1.967 c0.188,0.436,0.529,0.811,1.03,1.122c0.313,0.125,0.717,0.203,1.217,0.235c0.499,0.032,0.998,0.046,1.498,0.046h4.772h16.845 h5.24c0.436,0,0.89-0.014,1.359-0.046c0.467-0.032,0.888,0.016,1.263,0.139c0.81,0.25,1.34,0.594,1.591,1.03 c0.062,0.188,0.124,0.5,0.188,0.937c0.061,0.436,0.107,0.889,0.139,1.357c0.03,0.467,0.015,0.935-0.047,1.402 c-0.062,0.469-0.126,0.829-0.187,1.077c-0.126,0.437-0.406,0.782-0.843,1.029c-0.313,0.25-0.765,0.375-1.357,0.375 c-0.593,0-1.169,0-1.731,0h-6.177H133.58h-6.083c-0.874,0-1.623-0.047-2.246-0.139c-0.624-0.094-1.092-0.39-1.404-0.89 c-0.25-0.374-0.373-0.857-0.373-1.451c0-0.591,0-1.168,0-1.729v-6.74v-25.549v-20.308v-5.802c0-0.561,0.014-1.122,0.045-1.684 c0.032-0.561,0.141-0.998,0.328-1.31c0.249-0.374,0.717-0.685,1.404-0.935c0.124-0.062,0.265-0.077,0.422-0.047 C125.827,330.4,125.968,330.383,126.093,330.321z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#CBBC89' }}
|
||||||
|
d="M187.883,330.321h30.602h8.049c0.686,0,1.372-0.015,2.059-0.047 c0.685-0.03,1.248,0.109,1.684,0.422c0.188,0.125,0.343,0.281,0.468,0.467c0.124,0.188,0.249,0.375,0.374,0.563 c0.125,0.436,0.187,1.138,0.187,2.105c0,0.967-0.062,1.701-0.187,2.199c-0.25,0.874-0.78,1.357-1.592,1.451 c-0.811,0.094-1.777,0.14-2.9,0.14h-9.359h-15.722c-0.998,0-2.169-0.015-3.51-0.047c-1.343-0.03-2.325,0.139-2.949,0.514 c-0.624,0.374-0.951,1.03-0.982,1.966c-0.032,0.936-0.048,1.904-0.048,2.901v9.546c0,0.688-0.015,1.498-0.045,2.433 c-0.034,0.937,0.014,1.686,0.139,2.246c0.188,0.749,0.562,1.249,1.123,1.498c0.249,0.125,0.686,0.25,1.31,0.373h0.935 c0.249,0.063,0.515,0.08,0.796,0.047c0.281-0.03,0.577-0.047,0.89-0.047h3.181h18.343c0.561,0,1.184-0.014,1.872-0.046 c0.685-0.031,1.387-0.031,2.104,0c0.717,0.032,1.372,0.093,1.967,0.187c0.591,0.094,1.044,0.233,1.357,0.421 c0.186,0.126,0.341,0.407,0.467,0.842c0.125,0.438,0.218,0.905,0.282,1.404c0.061,0.5,0.077,1.016,0.045,1.545 c-0.031,0.531-0.109,0.983-0.233,1.357c-0.126,0.561-0.406,0.967-0.842,1.217c-0.127,0.126-0.297,0.203-0.515,0.235 c-0.22,0.032-0.422,0.079-0.608,0.139h-0.563c-0.312,0.063-0.623,0.079-0.935,0.047c-0.313-0.03-0.624-0.047-0.935-0.047h-3.463 h-19.747c-0.747,0-1.482-0.014-2.199-0.047c-0.719-0.03-1.39,0-2.012,0.094c-0.624,0.094-1.171,0.267-1.639,0.515 c-0.467,0.25-0.794,0.687-0.982,1.31c-0.126,0.375-0.173,0.811-0.139,1.31c0.03,0.5,0.045,0.999,0.045,1.498v5.239v8.892 c0,0.873,0.032,1.669,0.094,2.386c0.062,0.718,0.312,1.233,0.749,1.544c0.435,0.313,0.935,0.483,1.497,0.515 c0.561,0.032,1.185,0.046,1.871,0.046h5.99h17.314h5.334c0.499,0,0.998-0.014,1.497-0.046c0.498-0.032,0.936,0.016,1.31,0.139 c0.749,0.25,1.248,0.594,1.498,1.03c0.123,0.188,0.217,0.5,0.281,0.937c0.061,0.436,0.108,0.889,0.14,1.357 c0.03,0.467,0.015,0.935-0.047,1.402c-0.062,0.469-0.126,0.829-0.187,1.077c-0.25,0.5-0.531,0.842-0.842,1.029 c-0.375,0.25-0.858,0.375-1.45,0.375c-0.594,0-1.171,0-1.731,0h-6.552h-24.894h-6.551c-0.874,0-1.623-0.047-2.245-0.139 c-0.624-0.094-1.093-0.39-1.404-0.89c-0.25-0.374-0.374-0.857-0.374-1.451c0-0.591,0-1.168,0-1.729v-6.551v-25.082v-20.964 v-5.802c0-0.561,0-1.122,0-1.684c0-0.561,0.124-0.998,0.374-1.31c0.249-0.435,0.716-0.749,1.404-0.935 c0.125-0.062,0.248-0.077,0.375-0.047C187.632,330.4,187.756,330.383,187.883,330.321z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#98806E' }}
|
||||||
|
d="M41.051,330.321h28.918h7.581c0.686,0,1.357,0,2.012,0c0.655,0,1.171,0.126,1.545,0.375 c0.499,0.312,0.795,0.764,0.889,1.357c0.094,0.594,0.141,1.296,0.141,2.106c0,0.312,0.014,0.608,0.047,0.888 c0.031,0.281-0.016,0.547-0.14,0.796c-0.25,0.998-0.796,1.543-1.638,1.638c-0.843,0.094-1.888,0.14-3.136,0.14h-9.733H53.778 c-0.998,0-2.058-0.015-3.182-0.047c-1.122-0.03-1.965,0.173-2.526,0.608c-0.563,0.375-0.858,1.03-0.89,1.965 c-0.032,0.937-0.047,1.873-0.047,2.808v9.265c0,0.5-0.015,1.123-0.046,1.872c-0.033,0.749,0.014,1.373,0.139,1.871v0.748 c0.125,0.375,0.235,0.735,0.328,1.077c0.093,0.344,0.295,0.608,0.608,0.796c0.562,0.374,1.436,0.547,2.621,0.514 c1.184-0.03,2.214-0.047,3.088-0.047h16.845c0.561,0,1.171-0.014,1.825-0.046c0.655-0.031,1.31-0.031,1.965,0 c0.655,0.032,1.248,0.109,1.779,0.234c0.529,0.126,0.889,0.344,1.076,0.655c0.311,0.375,0.467,0.843,0.467,1.404 c0,0.56,0,1.186,0,1.871c0,0.375,0,0.719,0,1.03c0,0.313-0.062,0.594-0.186,0.842c-0.25,0.625-0.595,0.969-1.03,1.03 c-0.25,0.125-0.485,0.186-0.702,0.186c-0.219,0-0.483,0.033-0.796,0.094c-0.25,0.063-0.514,0.079-0.796,0.047 c-0.281-0.03-0.546-0.047-0.794-0.047h-3.277H54.433c-0.999,0-2.168-0.014-3.51-0.047c-1.342-0.03-2.292,0.172-2.853,0.609 c-0.314,0.25-0.547,0.64-0.702,1.169c-0.156,0.531-0.25,1.14-0.281,1.825c-0.033,0.687-0.033,1.389,0,2.106 c0.03,0.717,0.046,1.357,0.046,1.918v16.096c0,1.062,0.032,2.217,0.094,3.463c0.062,1.249-0.156,2.185-0.655,2.807 c-0.186,0.25-0.469,0.391-0.842,0.422c-0.374,0.032-0.749,0.109-1.123,0.234h-1.779c-0.875,0-1.684-0.03-2.433-0.093 c-0.749-0.062-1.279-0.375-1.59-0.937c-0.25-0.374-0.375-0.888-0.375-1.543c0-0.656,0-1.326,0-2.013v-7.488v-39.119v-10.107 c0-0.686-0.016-1.45-0.047-2.292c-0.032-0.842,0.107-1.512,0.422-2.012c0.249-0.374,0.715-0.685,1.404-0.935 c0.124-0.062,0.264-0.077,0.42-0.047C40.785,330.4,40.925,330.383,41.051,330.321z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#98806E' }}
|
||||||
|
d="M96.895,330.508c0.436,0,0.935-0.014,1.496-0.046c0.563-0.032,1.108-0.032,1.639,0 c0.529,0.032,1.013,0.109,1.45,0.234c0.437,0.125,0.749,0.312,0.937,0.561c0.374,0.438,0.576,1.046,0.608,1.825 c0.03,0.78,0.047,1.576,0.047,2.386v9.079v36.124v10.949c0,0.811,0,1.669,0,2.574c0,0.905-0.156,1.606-0.468,2.105 c-0.25,0.314-0.547,0.5-0.889,0.563c-0.344,0.062-0.765,0.156-1.264,0.28h-1.777c-0.874,0-1.684-0.034-2.433-0.094 c-0.749-0.061-1.28-0.374-1.592-0.935c-0.25-0.374-0.375-0.89-0.375-1.545c0-0.655,0-1.325,0-2.012v-7.487V345.95v-10.107 c0-0.686-0.015-1.451-0.045-2.292c-0.034-0.843,0.108-1.514,0.42-2.013c0.249-0.436,0.748-0.748,1.497-0.936 c0.125-0.061,0.25-0.077,0.375-0.047C96.645,330.587,96.769,330.571,96.895,330.508z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#98806E' }}
|
||||||
|
d="M126.093,330.321c0.436,0,0.935-0.015,1.498-0.047c0.561-0.03,1.107-0.03,1.638,0 c0.529,0.032,1.012,0.109,1.451,0.234c0.436,0.125,0.748,0.313,0.936,0.562c0.374,0.5,0.56,1.155,0.56,1.965 c0,0.811,0,1.654,0,2.528v9.826v30.042v8.609c0,0.687,0,1.436,0,2.245c0,0.813,0.094,1.468,0.281,1.967 c0.188,0.436,0.529,0.811,1.03,1.122c0.313,0.125,0.717,0.203,1.217,0.235c0.499,0.032,0.998,0.046,1.498,0.046h4.772h16.845 h5.24c0.436,0,0.89-0.014,1.359-0.046c0.467-0.032,0.888,0.016,1.263,0.139c0.81,0.25,1.34,0.594,1.591,1.03 c0.062,0.188,0.124,0.5,0.188,0.937c0.061,0.436,0.107,0.889,0.139,1.357c0.03,0.467,0.015,0.935-0.047,1.402 c-0.062,0.469-0.126,0.829-0.187,1.077c-0.126,0.437-0.406,0.782-0.843,1.029c-0.313,0.25-0.765,0.375-1.357,0.375 c-0.593,0-1.169,0-1.731,0h-6.177H133.58h-6.083c-0.874,0-1.623-0.047-2.246-0.139c-0.624-0.094-1.092-0.39-1.404-0.89 c-0.25-0.374-0.373-0.857-0.373-1.451c0-0.591,0-1.168,0-1.729v-6.74v-25.549v-20.308v-5.802c0-0.561,0.014-1.122,0.045-1.684 c0.032-0.561,0.141-0.998,0.328-1.31c0.249-0.374,0.717-0.685,1.404-0.935c0.124-0.062,0.265-0.077,0.422-0.047 C125.827,330.4,125.968,330.383,126.093,330.321z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
style={{ fill: '#98806E' }}
|
||||||
|
d="M187.883,330.321h30.602h8.049c0.686,0,1.372-0.015,2.059-0.047 c0.685-0.03,1.248,0.109,1.684,0.422c0.188,0.125,0.343,0.281,0.468,0.467c0.124,0.188,0.249,0.375,0.374,0.563 c0.125,0.436,0.187,1.138,0.187,2.105c0,0.967-0.062,1.701-0.187,2.199c-0.25,0.874-0.78,1.357-1.592,1.451 c-0.811,0.094-1.777,0.14-2.9,0.14h-9.359h-15.722c-0.998,0-2.169-0.015-3.51-0.047c-1.343-0.03-2.325,0.139-2.949,0.514 c-0.624,0.374-0.951,1.03-0.982,1.966c-0.032,0.936-0.048,1.904-0.048,2.901v9.546c0,0.688-0.015,1.498-0.045,2.433 c-0.034,0.937,0.014,1.686,0.139,2.246c0.188,0.749,0.562,1.249,1.123,1.498c0.249,0.125,0.686,0.25,1.31,0.373h0.935 c0.249,0.063,0.515,0.08,0.796,0.047c0.281-0.03,0.577-0.047,0.89-0.047h3.181h18.343c0.561,0,1.184-0.014,1.872-0.046 c0.685-0.031,1.387-0.031,2.104,0c0.717,0.032,1.372,0.093,1.967,0.187c0.591,0.094,1.044,0.233,1.357,0.421 c0.186,0.126,0.341,0.407,0.467,0.842c0.125,0.438,0.218,0.905,0.282,1.404c0.061,0.5,0.077,1.016,0.045,1.545 c-0.031,0.531-0.109,0.983-0.233,1.357c-0.126,0.561-0.406,0.967-0.842,1.217c-0.127,0.126-0.297,0.203-0.515,0.235 c-0.22,0.032-0.422,0.079-0.608,0.139h-0.563c-0.312,0.063-0.623,0.079-0.935,0.047c-0.313-0.03-0.624-0.047-0.935-0.047h-3.463 h-19.747c-0.747,0-1.482-0.014-2.199-0.047c-0.719-0.03-1.39,0-2.012,0.094c-0.624,0.094-1.171,0.267-1.639,0.515 c-0.467,0.25-0.794,0.687-0.982,1.31c-0.126,0.375-0.173,0.811-0.139,1.31c0.03,0.5,0.045,0.999,0.045,1.498v5.239v8.892 c0,0.873,0.032,1.669,0.094,2.386c0.062,0.718,0.312,1.233,0.749,1.544c0.435,0.313,0.935,0.483,1.497,0.515 c0.561,0.032,1.185,0.046,1.871,0.046h5.99h17.314h5.334c0.499,0,0.998-0.014,1.497-0.046c0.498-0.032,0.936,0.016,1.31,0.139 c0.749,0.25,1.248,0.594,1.498,1.03c0.123,0.188,0.217,0.5,0.281,0.937c0.061,0.436,0.108,0.889,0.14,1.357 c0.03,0.467,0.015,0.935-0.047,1.402c-0.062,0.469-0.126,0.829-0.187,1.077c-0.25,0.5-0.531,0.842-0.842,1.029 c-0.375,0.25-0.858,0.375-1.45,0.375c-0.594,0-1.171,0-1.731,0h-6.552h-24.894h-6.551c-0.874,0-1.623-0.047-2.245-0.139 c-0.624-0.094-1.093-0.39-1.404-0.89c-0.25-0.374-0.374-0.857-0.374-1.451c0-0.591,0-1.168,0-1.729v-6.551v-25.082v-20.964 v-5.802c0-0.561,0-1.122,0-1.684c0-0.561,0.124-0.998,0.374-1.31c0.249-0.435,0.716-0.749,1.404-0.935 c0.125-0.062,0.248-0.077,0.375-0.047C187.632,330.4,187.756,330.383,187.883,330.321z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<polygon
|
||||||
|
style={{ fill: '#BBAF98' }}
|
||||||
|
points="276.167,208.741 0,302.069 0,186.053 512,186.053 512,302.069 "
|
||||||
|
></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const LogIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||||
|
<g
|
||||||
|
id="SVGRepo_tracerCarrier"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<rect width="48" height="48" fill="white" fillOpacity="0.01"></rect>
|
||||||
|
<rect
|
||||||
|
x="13"
|
||||||
|
y="10"
|
||||||
|
width="28"
|
||||||
|
height="34"
|
||||||
|
fill="#2F88FF"
|
||||||
|
stroke="#000000"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
></rect>
|
||||||
|
<path
|
||||||
|
d="M35 10V4H8C7.44772 4 7 4.44772 7 5V38H13"
|
||||||
|
stroke="#000000"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M21 22H33"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M21 30H33"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
69
napcat.webui/src/components/input/file_input.tsx
Normal file
69
napcat.webui/src/components/input/file_input.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import { Input } from '@heroui/input'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export interface FileInputProps {
|
||||||
|
onChange: (file: File) => Promise<void> | void
|
||||||
|
onDelete?: () => Promise<void> | void
|
||||||
|
label?: string
|
||||||
|
accept?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileInput: React.FC<FileInputProps> = ({
|
||||||
|
onChange,
|
||||||
|
onDelete,
|
||||||
|
label,
|
||||||
|
accept
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
return (
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Input
|
||||||
|
isDisabled={isLoading}
|
||||||
|
ref={inputRef}
|
||||||
|
label={label}
|
||||||
|
type="file"
|
||||||
|
placeholder="选择文件"
|
||||||
|
accept={accept}
|
||||||
|
onChange={async (e) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
await onChange(file)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
if (inputRef.current) inputRef.current.value = ''
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isDisabled={isLoading}
|
||||||
|
onPress={async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
if (onDelete) await onDelete()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
if (inputRef.current) inputRef.current.value = ''
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileInput
|
@@ -43,7 +43,7 @@ const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => {
|
|||||||
onChange('')
|
onChange('')
|
||||||
if (inputRef.current) inputRef.current.value = ''
|
if (inputRef.current) inputRef.current.value = ''
|
||||||
}}
|
}}
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
|
@@ -16,13 +16,13 @@ const logLevelColor: {
|
|||||||
| 'secondary'
|
| 'secondary'
|
||||||
| 'success'
|
| 'success'
|
||||||
| 'warning'
|
| 'warning'
|
||||||
| 'danger'
|
| 'primary'
|
||||||
} = {
|
} = {
|
||||||
[LogLevel.DEBUG]: 'default',
|
[LogLevel.DEBUG]: 'default',
|
||||||
[LogLevel.INFO]: 'primary',
|
[LogLevel.INFO]: 'primary',
|
||||||
[LogLevel.WARN]: 'warning',
|
[LogLevel.WARN]: 'warning',
|
||||||
[LogLevel.ERROR]: 'danger',
|
[LogLevel.ERROR]: 'primary',
|
||||||
[LogLevel.FATAL]: 'danger'
|
[LogLevel.FATAL]: 'primary'
|
||||||
}
|
}
|
||||||
const LogLevelSelect = (props: LogLevelSelectProps) => {
|
const LogLevelSelect = (props: LogLevelSelectProps) => {
|
||||||
const { selectedKeys, onSelectionChange } = props
|
const { selectedKeys, onSelectionChange } = props
|
||||||
|
@@ -65,7 +65,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{showCancel && (
|
{showCancel && (
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onCancel?.()
|
onCancel?.()
|
||||||
@@ -76,7 +76,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onConfirm?.()
|
onConfirm?.()
|
||||||
nativeClose()
|
nativeClose()
|
||||||
|
@@ -28,7 +28,7 @@ import type {
|
|||||||
|
|
||||||
function displayData(data: number, loading: boolean, error?: Error) {
|
function displayData(data: number, loading: boolean, error?: Error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return <MdError className="text-danger-400" />
|
return <MdError className="text-primary-400" />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -175,7 +175,7 @@ export default function NapCatRepoInfo() {
|
|||||||
className="group h-auto py-3"
|
className="group h-auto py-3"
|
||||||
endContent={
|
endContent={
|
||||||
releaseError ? (
|
releaseError ? (
|
||||||
<MdError className="text-danger-400" />
|
<MdError className="text-primary-400" />
|
||||||
) : releaseLoading ? (
|
) : releaseLoading ? (
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
) : (
|
) : (
|
||||||
@@ -229,7 +229,7 @@ export default function NapCatRepoInfo() {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-danger/10 text-danger dark:text-danger-500">
|
<IconWrapper className="bg-primary/10 text-primary dark:text-primary-500">
|
||||||
<BookIcon />
|
<BookIcon />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
|
@@ -150,7 +150,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
isDisabled={formState.isSubmitting}
|
isDisabled={formState.isSubmitting}
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
|
@@ -20,7 +20,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
|||||||
enable: false,
|
enable: false,
|
||||||
name: '',
|
name: '',
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 3000,
|
port: 3001,
|
||||||
reportSelfMessage: false,
|
reportSelfMessage: false,
|
||||||
enableForcePushEvent: true,
|
enableForcePushEvent: true,
|
||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
|
@@ -91,7 +91,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="p-4 pt-14 rounded-lg shadow-md">
|
<section className="p-4 pt-14 rounded-lg shadow-md">
|
||||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-danger-400">
|
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400">
|
||||||
<PiCatDuotone />
|
<PiCatDuotone />
|
||||||
{data.description}
|
{data.description}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -125,7 +125,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onPress={sendRequest}
|
onPress={sendRequest}
|
||||||
color="danger"
|
color="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="full"
|
radius="full"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
@@ -138,7 +138,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
shadow="sm"
|
shadow="sm"
|
||||||
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible z-20"
|
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible z-20"
|
||||||
>
|
>
|
||||||
<CardHeader className="font-noto-serif font-bold text-lg gap-1 pb-0">
|
<CardHeader className="font-bold text-lg gap-1 pb-0">
|
||||||
<span className="mr-2">请求体</span>
|
<span className="mr-2">请求体</span>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
@@ -186,7 +186,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
className="my-4 relative bg-opacity-50 backdrop-blur-md"
|
className="my-4 relative bg-opacity-50 backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<PageLoading loading={isFetching} />
|
<PageLoading loading={isFetching} />
|
||||||
<CardHeader className="font-noto-serif font-bold text-lg gap-1 pb-0">
|
<CardHeader className="font-bold text-lg gap-1 pb-0">
|
||||||
<span className="mr-2">响应</span>
|
<span className="mr-2">响应</span>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
|
@@ -27,7 +27,7 @@ const SchemaType = ({
|
|||||||
name = '固定值'
|
name = '固定值'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
let chipColor: 'primary' | 'success' | 'danger' | 'warning' | 'secondary' =
|
let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
|
||||||
'primary'
|
'primary'
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'enum':
|
case 'enum':
|
||||||
@@ -37,7 +37,7 @@ const SchemaType = ({
|
|||||||
chipColor = 'secondary'
|
chipColor = 'secondary'
|
||||||
break
|
break
|
||||||
case 'array':
|
case 'array':
|
||||||
chipColor = 'danger'
|
chipColor = 'primary'
|
||||||
break
|
break
|
||||||
case 'object':
|
case 'object':
|
||||||
chipColor = 'success'
|
chipColor = 'success'
|
||||||
|
@@ -33,11 +33,11 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
|
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
|
||||||
<Input
|
<Input
|
||||||
className="sticky top-0 z-10 text-danger-600"
|
className="sticky top-0 z-10 text-primary-600"
|
||||||
classNames={{
|
classNames={{
|
||||||
inputWrapper:
|
inputWrapper:
|
||||||
'bg-opacity-30 bg-danger-50 backdrop-blur-sm border border-danger-300 mb-2',
|
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
||||||
input: 'bg-transparent !text-danger-400 !placeholder-danger-400'
|
input: 'bg-transparent !text-primary-400 !placeholder-primary-400'
|
||||||
}}
|
}}
|
||||||
radius="full"
|
radius="full"
|
||||||
placeholder="搜索 API"
|
placeholder="搜索 API"
|
||||||
@@ -51,7 +51,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
key={apiName}
|
key={apiName}
|
||||||
shadow="none"
|
shadow="none"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full border border-danger-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-danger-400',
|
'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400',
|
||||||
{
|
{
|
||||||
hidden: !(
|
hidden: !(
|
||||||
apiName.includes(searchValue) ||
|
apiName.includes(searchValue) ||
|
||||||
@@ -59,7 +59,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'!bg-opacity-40 border border-danger-400 bg-danger-50 text-danger-600':
|
'!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600':
|
||||||
apiName === selectedApi
|
apiName === selectedApi
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@@ -67,10 +67,10 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
|
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
|
||||||
>
|
>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<h2 className="font-ubuntu font-bold">{api.description}</h2>
|
<h2 className="font-bold">{api.description}</h2>
|
||||||
<div
|
<div
|
||||||
className={clsx('text-sm text-danger-200', {
|
className={clsx('text-sm text-primary-200', {
|
||||||
'!text-danger-400': apiName === selectedApi
|
'!text-primary-400': apiName === selectedApi
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{apiName}
|
{apiName}
|
||||||
|
@@ -109,7 +109,7 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
|||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
@@ -30,7 +30,7 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
|||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
radius="full"
|
radius="full"
|
||||||
className="text-medium"
|
className="text-medium"
|
||||||
|
@@ -43,7 +43,7 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||||
构造请求
|
构造请求
|
||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -75,11 +75,11 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<ChatInputModal />
|
<ChatInputModal />
|
||||||
|
|
||||||
<Button color="danger" variant="flat" onPress={onClose}>
|
<Button color="primary" variant="flat" onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="primary"
|
||||||
onPress={() => handleSendMessage(onClose)}
|
onPress={() => handleSendMessage(onClose)}
|
||||||
>
|
>
|
||||||
发送
|
发送
|
||||||
|
@@ -10,7 +10,7 @@ function StatusTag({
|
|||||||
color
|
color
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
color: 'success' | 'danger' | 'warning'
|
color: 'success' | 'primary' | 'warning'
|
||||||
}) {
|
}) {
|
||||||
const textClassName = `text-${color} text-sm`
|
const textClassName = `text-${color} text-sm`
|
||||||
const bgClassName = `bg-${color}`
|
const bgClassName = `bg-${color}`
|
||||||
@@ -27,7 +27,7 @@ export default function WSStatus({ state }: WSStatusProps) {
|
|||||||
return <StatusTag title="已连接" color="success" />
|
return <StatusTag title="已连接" color="success" />
|
||||||
}
|
}
|
||||||
if (state === ReadyState.CLOSED) {
|
if (state === ReadyState.CLOSED) {
|
||||||
return <StatusTag title="已关闭" color="danger" />
|
return <StatusTag title="已关闭" color="primary" />
|
||||||
}
|
}
|
||||||
if (state === ReadyState.CONNECTING) {
|
if (state === ReadyState.CONNECTING) {
|
||||||
return <StatusTag title="连接中" color="warning" />
|
return <StatusTag title="连接中" color="warning" />
|
||||||
|
@@ -16,23 +16,21 @@ export interface QQInfoCardProps {
|
|||||||
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="relative bg-danger-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-danger-300 dark:shadow-danger-50"
|
className="relative bg-primary-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-primary-300 dark:shadow-primary-50"
|
||||||
shadow="none"
|
shadow="none"
|
||||||
radius="lg"
|
radius="lg"
|
||||||
>
|
>
|
||||||
<PageLoading loading={loading} />
|
<PageLoading loading={loading} />
|
||||||
{error ? (
|
{error ? (
|
||||||
<CardBody className="items-center gap-1 justify-center">
|
<CardBody className="items-center gap-1 justify-center">
|
||||||
<div className="font-outfit flex-1 text-content1-foreground">
|
<div className="flex-1 text-content1-foreground">Error</div>
|
||||||
Error
|
|
||||||
</div>
|
|
||||||
<div className="whitespace-nowrap text-nowrap flex-shrink-0">
|
<div className="whitespace-nowrap text-nowrap flex-shrink-0">
|
||||||
{error.message}
|
{error.message}
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
) : (
|
) : (
|
||||||
<CardBody className="flex-row items-center gap-2 overflow-hidden relative">
|
<CardBody className="flex-row items-center gap-2 overflow-hidden relative">
|
||||||
<div className="absolute right-0 bottom-0 text-5xl text-danger-400">
|
<div className="absolute right-0 bottom-0 text-5xl text-primary-400">
|
||||||
<BsTencentQq />
|
<BsTencentQq />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex-shrink-0 z-10">
|
<div className="relative flex-shrink-0 z-10">
|
||||||
@@ -45,16 +43,14 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-danger-100 z-10',
|
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
|
||||||
data?.online ? 'bg-green-500' : 'bg-gray-500'
|
data?.online ? 'bg-green-500' : 'bg-gray-500'
|
||||||
)}
|
)}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-col justify-center">
|
<div className="flex-col justify-center">
|
||||||
<div className="font-outfit text-lg truncate">{data?.nick}</div>
|
<div className="text-lg truncate">{data?.nick}</div>
|
||||||
<div className="font-ubuntu text-danger-500 text-sm">
|
<div className="text-primary-500 text-sm">{data?.uin}</div>
|
||||||
{data?.uin}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
)}
|
)}
|
||||||
|
@@ -11,7 +11,7 @@ const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
|
|||||||
<div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
|
<div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
|
||||||
{!qrcode && (
|
{!qrcode && (
|
||||||
<div className="absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center">
|
<div className="absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center">
|
||||||
<Spinner color="danger" />
|
<Spinner color="primary" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<QRCodeSVG size={180} value={qrcode} />
|
<QRCodeSVG size={180} value={qrcode} />
|
||||||
|
265
napcat.webui/src/components/rotating_text.tsx
Normal file
265
napcat.webui/src/components/rotating_text.tsx
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import {
|
||||||
|
AnimatePresence,
|
||||||
|
HTMLMotionProps,
|
||||||
|
TargetAndTransition,
|
||||||
|
Transition,
|
||||||
|
motion
|
||||||
|
} from 'motion/react'
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
function cn(...classes: (string | undefined | null | boolean)[]): string {
|
||||||
|
return classes.filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RotatingTextRef {
|
||||||
|
next: () => void
|
||||||
|
previous: () => void
|
||||||
|
jumpTo: (index: number) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RotatingTextProps
|
||||||
|
extends Omit<
|
||||||
|
HTMLMotionProps<'span'>,
|
||||||
|
'children' | 'transition' | 'initial' | 'animate' | 'exit'
|
||||||
|
> {
|
||||||
|
texts: string[]
|
||||||
|
transition?: Transition
|
||||||
|
initial?: TargetAndTransition
|
||||||
|
animate?: TargetAndTransition
|
||||||
|
exit?: TargetAndTransition
|
||||||
|
animatePresenceMode?: 'sync' | 'wait'
|
||||||
|
animatePresenceInitial?: boolean
|
||||||
|
rotationInterval?: number
|
||||||
|
staggerDuration?: number
|
||||||
|
staggerFrom?: 'first' | 'last' | 'center' | 'random' | number
|
||||||
|
loop?: boolean
|
||||||
|
auto?: boolean
|
||||||
|
splitBy?: string
|
||||||
|
onNext?: (index: number) => void
|
||||||
|
mainClassName?: string
|
||||||
|
splitLevelClassName?: string
|
||||||
|
elementLevelClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
texts,
|
||||||
|
transition = { type: 'spring', damping: 25, stiffness: 300 },
|
||||||
|
initial = { y: '100%', opacity: 0 },
|
||||||
|
animate = { y: 0, opacity: 1 },
|
||||||
|
exit = { y: '-120%', opacity: 0 },
|
||||||
|
animatePresenceMode = 'wait',
|
||||||
|
animatePresenceInitial = false,
|
||||||
|
rotationInterval = 2000,
|
||||||
|
staggerDuration = 0,
|
||||||
|
staggerFrom = 'first',
|
||||||
|
loop = true,
|
||||||
|
auto = true,
|
||||||
|
splitBy = 'characters',
|
||||||
|
onNext,
|
||||||
|
mainClassName,
|
||||||
|
splitLevelClassName,
|
||||||
|
elementLevelClassName,
|
||||||
|
...rest
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [currentTextIndex, setCurrentTextIndex] = useState<number>(0)
|
||||||
|
|
||||||
|
const splitIntoCharacters = (text: string): string[] => {
|
||||||
|
return Array.from(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = useMemo(() => {
|
||||||
|
const currentText: string = texts[currentTextIndex]
|
||||||
|
if (splitBy === 'characters') {
|
||||||
|
const words = currentText.split(' ')
|
||||||
|
return words.map((word, i) => ({
|
||||||
|
characters: splitIntoCharacters(word),
|
||||||
|
needsSpace: i !== words.length - 1
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (splitBy === 'words') {
|
||||||
|
return currentText.split(' ').map((word, i, arr) => ({
|
||||||
|
characters: [word],
|
||||||
|
needsSpace: i !== arr.length - 1
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (splitBy === 'lines') {
|
||||||
|
return currentText.split('\n').map((line, i, arr) => ({
|
||||||
|
characters: [line],
|
||||||
|
needsSpace: i !== arr.length - 1
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentText.split(splitBy).map((part, i, arr) => ({
|
||||||
|
characters: [part],
|
||||||
|
needsSpace: i !== arr.length - 1
|
||||||
|
}))
|
||||||
|
}, [texts, currentTextIndex, splitBy])
|
||||||
|
|
||||||
|
const getStaggerDelay = useCallback(
|
||||||
|
(index: number, totalChars: number): number => {
|
||||||
|
const total = totalChars
|
||||||
|
if (staggerFrom === 'first') return index * staggerDuration
|
||||||
|
if (staggerFrom === 'last') return (total - 1 - index) * staggerDuration
|
||||||
|
if (staggerFrom === 'center') {
|
||||||
|
const center = Math.floor(total / 2)
|
||||||
|
return Math.abs(center - index) * staggerDuration
|
||||||
|
}
|
||||||
|
if (staggerFrom === 'random') {
|
||||||
|
const randomIndex = Math.floor(Math.random() * total)
|
||||||
|
return Math.abs(randomIndex - index) * staggerDuration
|
||||||
|
}
|
||||||
|
return Math.abs((staggerFrom as number) - index) * staggerDuration
|
||||||
|
},
|
||||||
|
[staggerFrom, staggerDuration]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleIndexChange = useCallback(
|
||||||
|
(newIndex: number) => {
|
||||||
|
setCurrentTextIndex(newIndex)
|
||||||
|
if (onNext) onNext(newIndex)
|
||||||
|
},
|
||||||
|
[onNext]
|
||||||
|
)
|
||||||
|
|
||||||
|
const next = useCallback(() => {
|
||||||
|
const nextIndex =
|
||||||
|
currentTextIndex === texts.length - 1
|
||||||
|
? loop
|
||||||
|
? 0
|
||||||
|
: currentTextIndex
|
||||||
|
: currentTextIndex + 1
|
||||||
|
if (nextIndex !== currentTextIndex) {
|
||||||
|
handleIndexChange(nextIndex)
|
||||||
|
}
|
||||||
|
}, [currentTextIndex, texts.length, loop, handleIndexChange])
|
||||||
|
|
||||||
|
const previous = useCallback(() => {
|
||||||
|
const prevIndex =
|
||||||
|
currentTextIndex === 0
|
||||||
|
? loop
|
||||||
|
? texts.length - 1
|
||||||
|
: currentTextIndex
|
||||||
|
: currentTextIndex - 1
|
||||||
|
if (prevIndex !== currentTextIndex) {
|
||||||
|
handleIndexChange(prevIndex)
|
||||||
|
}
|
||||||
|
}, [currentTextIndex, texts.length, loop, handleIndexChange])
|
||||||
|
|
||||||
|
const jumpTo = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const validIndex = Math.max(0, Math.min(index, texts.length - 1))
|
||||||
|
if (validIndex !== currentTextIndex) {
|
||||||
|
handleIndexChange(validIndex)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[texts.length, currentTextIndex, handleIndexChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
if (currentTextIndex !== 0) {
|
||||||
|
handleIndexChange(0)
|
||||||
|
}
|
||||||
|
}, [currentTextIndex, handleIndexChange])
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
next,
|
||||||
|
previous,
|
||||||
|
jumpTo,
|
||||||
|
reset
|
||||||
|
}),
|
||||||
|
[next, previous, jumpTo, reset]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auto) return
|
||||||
|
const intervalId = setInterval(next, rotationInterval)
|
||||||
|
return () => clearInterval(intervalId)
|
||||||
|
}, [next, rotationInterval, auto])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.span
|
||||||
|
className={cn(
|
||||||
|
'flex flex-wrap whitespace-pre-wrap relative',
|
||||||
|
mainClassName
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
layout
|
||||||
|
transition={transition}
|
||||||
|
>
|
||||||
|
<span className="sr-only">{texts[currentTextIndex]}</span>
|
||||||
|
<AnimatePresence
|
||||||
|
mode={animatePresenceMode}
|
||||||
|
initial={animatePresenceInitial}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
key={currentTextIndex}
|
||||||
|
className={cn(
|
||||||
|
splitBy === 'lines'
|
||||||
|
? 'flex flex-col w-full'
|
||||||
|
: 'flex flex-wrap whitespace-pre-wrap relative'
|
||||||
|
)}
|
||||||
|
layout
|
||||||
|
aria-hidden="true"
|
||||||
|
initial={initial as HTMLMotionProps<'div'>['initial']}
|
||||||
|
animate={animate as HTMLMotionProps<'div'>['animate']}
|
||||||
|
exit={exit as HTMLMotionProps<'div'>['exit']}
|
||||||
|
>
|
||||||
|
{elements.map((wordObj, wordIndex, array) => {
|
||||||
|
const previousCharsCount = array
|
||||||
|
.slice(0, wordIndex)
|
||||||
|
.reduce((sum, word) => sum + word.characters.length, 0)
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={wordIndex}
|
||||||
|
className={cn('inline-flex', splitLevelClassName)}
|
||||||
|
>
|
||||||
|
{wordObj.characters.map((char, charIndex) => (
|
||||||
|
<motion.span
|
||||||
|
key={charIndex}
|
||||||
|
initial={initial as HTMLMotionProps<'span'>['initial']}
|
||||||
|
animate={animate as HTMLMotionProps<'span'>['animate']}
|
||||||
|
exit={exit as HTMLMotionProps<'span'>['exit']}
|
||||||
|
transition={{
|
||||||
|
...transition,
|
||||||
|
delay: getStaggerDelay(
|
||||||
|
previousCharsCount + charIndex,
|
||||||
|
array.reduce(
|
||||||
|
(sum, word) => sum + word.characters.length,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
className={cn('inline-block', elementLevelClassName)}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
{wordObj.needsSpace && (
|
||||||
|
<span className="whitespace-pre"> </span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
RotatingText.displayName = 'RotatingText'
|
||||||
|
export default RotatingText
|
@@ -13,7 +13,6 @@ import { useTheme } from '@/hooks/use-theme'
|
|||||||
import logo from '@/assets/images/logo.png'
|
import logo from '@/assets/images/logo.png'
|
||||||
import type { MenuItem } from '@/config/site'
|
import type { MenuItem } from '@/config/site'
|
||||||
|
|
||||||
import { title } from '../primitives'
|
|
||||||
import Menus from './menus'
|
import Menus from './menus'
|
||||||
|
|
||||||
interface SideBarProps {
|
interface SideBarProps {
|
||||||
@@ -48,19 +47,15 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<motion.div className="w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right">
|
<motion.div className="w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right">
|
||||||
<div className="flex justify-center items-center mt-2 gap-2">
|
<div className="flex justify-center items-center my-2 gap-2">
|
||||||
<Image height={40} src={logo} className="mb-2" />
|
<Image radius="none" height={40} src={logo} className="mb-2" />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center hm-medium',
|
'flex items-center font-bold',
|
||||||
title({
|
'!text-2xl shiny-text'
|
||||||
shadow: true,
|
|
||||||
color: isDark ? 'violet' : 'pink'
|
|
||||||
}),
|
|
||||||
'!text-2xl'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
WebUI
|
NapCat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto flex flex-col flex-1 px-4">
|
<div className="overflow-y-auto flex flex-col flex-1 px-4">
|
||||||
@@ -68,7 +63,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
<div className="mt-auto mb-10 md:mb-0">
|
<div className="mt-auto mb-10 md:mb-0">
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
color="danger"
|
color="primary"
|
||||||
radius="full"
|
radius="full"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={toggleTheme}
|
onPress={toggleTheme}
|
||||||
@@ -80,7 +75,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-full mb-2"
|
className="w-full mb-2"
|
||||||
color="danger"
|
color="primary"
|
||||||
radius="full"
|
radius="full"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={onRevokeAuth}
|
onPress={onRevokeAuth}
|
||||||
|
@@ -55,15 +55,16 @@ const renderItems = (items: MenuItem[], children = false) => {
|
|||||||
isActive && 'bg-opacity-60',
|
isActive && 'bg-opacity-60',
|
||||||
b64img && 'backdrop-blur-md text-white'
|
b64img && 'backdrop-blur-md text-white'
|
||||||
)}
|
)}
|
||||||
color="danger"
|
color="primary"
|
||||||
endContent={
|
endContent={
|
||||||
canOpen ? (
|
canOpen ? (
|
||||||
// div实现箭头V效果
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'ml-auto relative w-3 h-3 transition-transform',
|
'ml-auto relative w-3 h-3 transition-transform',
|
||||||
open && 'transform rotate-180',
|
open && 'transform rotate-180',
|
||||||
isActive ? 'text-danger-500' : 'text-red-300 dark:text-white',
|
isActive
|
||||||
|
? 'text-primary-500'
|
||||||
|
: 'text-primary-200 dark:text-white',
|
||||||
'before:rounded-full',
|
'before:rounded-full',
|
||||||
'before:content-[""]',
|
'before:content-[""]',
|
||||||
'before:block',
|
'before:block',
|
||||||
@@ -95,8 +96,8 @@ const renderItems = (items: MenuItem[], children = false) => {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
|
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-danger-500 animate-spinner-ease-spin'
|
? 'bg-primary-500 animate-spinner-ease-spin'
|
||||||
: 'bg-red-300 dark:bg-white'
|
: 'bg-primary-200 dark:bg-white'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -4,6 +4,8 @@ import { Chip } from '@heroui/chip'
|
|||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner'
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip'
|
||||||
import { useRequest } from 'ahooks'
|
import { useRequest } from 'ahooks'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { BsStars } from 'react-icons/bs'
|
||||||
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
|
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
|
||||||
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
|
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
|
||||||
import { RiMacFill } from 'react-icons/ri'
|
import { RiMacFill } from 'react-icons/ri'
|
||||||
@@ -16,7 +18,6 @@ import { compareVersion } from '@/utils/version'
|
|||||||
import WebUIManager from '@/controllers/webui_manager'
|
import WebUIManager from '@/controllers/webui_manager'
|
||||||
import { GithubRelease } from '@/types/github'
|
import { GithubRelease } from '@/types/github'
|
||||||
|
|
||||||
import packageJson from '../../package.json'
|
|
||||||
import TailwindMarkdown from './tailwind_markdown'
|
import TailwindMarkdown from './tailwind_markdown'
|
||||||
|
|
||||||
export interface SystemInfoItemProps {
|
export interface SystemInfoItemProps {
|
||||||
@@ -33,10 +34,10 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
|
|||||||
endContent
|
endContent
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-danger-50 dark:shadow-danger-100 rounded text-danger-400">
|
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-100 dark:shadow-primary-100 rounded text-primary-400">
|
||||||
{icon}
|
{icon}
|
||||||
<div className="w-24">{title}</div>
|
<div className="w-24">{title}</div>
|
||||||
<div className="text-danger-200">{value}</div>
|
<div className="text-primary-200">{value}</div>
|
||||||
<div className="ml-auto">{endContent}</div>
|
<div className="ml-auto">{endContent}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -61,7 +62,7 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
|||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="shadow"
|
variant="shadow"
|
||||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -98,12 +99,48 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AISummaryComponent = () => {
|
||||||
|
const {
|
||||||
|
data: aiSummaryData,
|
||||||
|
loading: aiSummaryLoading,
|
||||||
|
error: aiSummaryError,
|
||||||
|
run: runAiSummary
|
||||||
|
} = useRequest(
|
||||||
|
(version) =>
|
||||||
|
request.get<ServerResponse<string | null>>(
|
||||||
|
`https://release.nc.152710.xyz/?version=${version}`,
|
||||||
|
{
|
||||||
|
timeout: 30000
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
manual: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
runAiSummary(currentVersion)
|
||||||
|
}, [currentVersion, runAiSummary])
|
||||||
|
|
||||||
|
if (aiSummaryLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center py-1">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (aiSummaryError) {
|
||||||
|
return <div className="text-center text-primary-500">AI 摘要获取失败</div>
|
||||||
|
}
|
||||||
|
return <span className="text-default-700">{aiSummaryData?.data.data}</span>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="有新版本可用">
|
<Tooltip content="有新版本可用">
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius="full"
|
||||||
color="danger"
|
color="primary"
|
||||||
variant="shadow"
|
variant="shadow"
|
||||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -121,6 +158,13 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
|||||||
<span>最新版本</span>
|
<span>最新版本</span>
|
||||||
<Chip color="primary">{latestVersion}</Chip>
|
<Chip color="primary">{latestVersion}</Chip>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-2 rounded-md bg-content2 text-sm">
|
||||||
|
<div className="text-primary-400 font-bold flex items-center gap-1 mb-1">
|
||||||
|
<BsStars />
|
||||||
|
<span>AI总结</span>
|
||||||
|
</div>
|
||||||
|
{<AISummaryComponent />}
|
||||||
|
</div>
|
||||||
<div className="text-sm space-y-2 !mt-4">
|
<div className="text-sm space-y-2 !mt-4">
|
||||||
{middleVersions.map((versionInfo) => (
|
{middleVersions.map((versionInfo) => (
|
||||||
<div
|
<div
|
||||||
@@ -190,19 +234,14 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
|||||||
error: qqVersionError
|
error: qqVersionError
|
||||||
} = useRequest(WebUIManager.getQQVersion)
|
} = useRequest(WebUIManager.getQQVersion)
|
||||||
return (
|
return (
|
||||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 overflow-visible flex-1">
|
<Card className="bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1">
|
||||||
<CardHeader className="pb-0 items-center gap-1 text-danger-500 font-extrabold">
|
<CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
|
||||||
<FaCircleInfo className="text-lg" />
|
<FaCircleInfo className="text-lg" />
|
||||||
<span>系统信息</span>
|
<span>系统信息</span>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="flex-1">
|
<CardBody className="flex-1">
|
||||||
<div className="flex flex-col justify-between h-full">
|
<div className="flex flex-col justify-between h-full">
|
||||||
<NapCatVersion />
|
<NapCatVersion />
|
||||||
<SystemInfoItem
|
|
||||||
title="WebUI 版本"
|
|
||||||
icon={<IoLogoChrome className="text-xl" />}
|
|
||||||
value={packageJson.version}
|
|
||||||
/>
|
|
||||||
<SystemInfoItem
|
<SystemInfoItem
|
||||||
title="QQ 版本"
|
title="QQ 版本"
|
||||||
icon={<FaQq className="text-lg" />}
|
icon={<FaQq className="text-lg" />}
|
||||||
@@ -216,6 +255,11 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<SystemInfoItem
|
||||||
|
title="WebUI 版本"
|
||||||
|
icon={<IoLogoChrome className="text-xl" />}
|
||||||
|
value="Next"
|
||||||
|
/>
|
||||||
<SystemInfoItem
|
<SystemInfoItem
|
||||||
title="系统版本"
|
title="系统版本"
|
||||||
icon={<RiMacFill className="text-xl" />}
|
icon={<RiMacFill className="text-xl" />}
|
||||||
|
@@ -24,7 +24,7 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'shadow-sm p-2 rounded-md text-sm bg-content1 bg-opacity-30',
|
'shadow-sm shadow-primary-100 p-2 rounded-md text-sm bg-content1 bg-opacity-30',
|
||||||
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
|
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 col-span-1 lg:col-span-2 relative overflow-hidden">
|
<Card className="bg-opacity-60 shadow-sm shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden">
|
||||||
<div className="absolute h-full right-0 top-0">
|
<div className="absolute h-full right-0 top-0">
|
||||||
<Image
|
<Image
|
||||||
src={bkg}
|
src={bkg}
|
||||||
@@ -69,7 +69,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
|||||||
</div>
|
</div>
|
||||||
<CardBody className="overflow-visible md:flex-row gap-4 items-center justify-stretch z-10">
|
<CardBody className="overflow-visible md:flex-row gap-4 items-center justify-stretch z-10">
|
||||||
<div className="flex-1 w-full md:max-w-96">
|
<div className="flex-1 w-full md:max-w-96">
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-danger-400">
|
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400">
|
||||||
<GiCpu className="text-xl" />
|
<GiCpu className="text-xl" />
|
||||||
<span>CPU</span>
|
<span>CPU</span>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -88,7 +88,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
|||||||
unit="%"
|
unit="%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-danger-400 mt-2">
|
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2">
|
||||||
<BiSolidMemoryCard className="text-xl" />
|
<BiSolidMemoryCard className="text-xl" />
|
||||||
<span>内存</span>
|
<span>内存</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
89
napcat.webui/src/components/tabs/index.tsx
Normal file
89
napcat.webui/src/components/tabs/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { type ReactNode, createContext, forwardRef, useContext } from 'react'
|
||||||
|
|
||||||
|
export interface TabsContextValue {
|
||||||
|
activeKey: string
|
||||||
|
onChange: (key: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContext = createContext<TabsContextValue>({
|
||||||
|
activeKey: '',
|
||||||
|
onChange: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface TabsProps {
|
||||||
|
activeKey: string
|
||||||
|
onChange: (key: string) => void
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tabs({ activeKey, onChange, children, className }: TabsProps) {
|
||||||
|
return (
|
||||||
|
<TabsContext.Provider value={{ activeKey, onChange }}>
|
||||||
|
<div className={clsx('flex flex-col gap-2', className)}>{children}</div>
|
||||||
|
</TabsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabListProps {
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabList({ children, className }: TabListProps) {
|
||||||
|
return (
|
||||||
|
<div className={clsx('flex items-center gap-1', className)}>{children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabProps extends React.ButtonHTMLAttributes<HTMLDivElement> {
|
||||||
|
value: string
|
||||||
|
className?: string
|
||||||
|
children: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||||
|
({ className, isSelected, value, ...props }, ref) => {
|
||||||
|
const { onChange } = useContext(TabsContext)
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
onChange(value)
|
||||||
|
props.onClick?.(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={isSelected}
|
||||||
|
onClick={handleClick}
|
||||||
|
className={clsx(
|
||||||
|
'px-2 py-1 flex items-center gap-1 text-sm font-medium border-b-2 transition-colors',
|
||||||
|
isSelected
|
||||||
|
? 'border-primary text-primary'
|
||||||
|
: 'border-transparent hover:border-default',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Tab.displayName = 'Tab'
|
||||||
|
|
||||||
|
export interface TabPanelProps {
|
||||||
|
value: string
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabPanel({ value, children, className }: TabPanelProps) {
|
||||||
|
const { activeKey } = useContext(TabsContext)
|
||||||
|
|
||||||
|
if (value !== activeKey) return null
|
||||||
|
|
||||||
|
return <div className={clsx('flex-1', className)}>{children}</div>
|
||||||
|
}
|
38
napcat.webui/src/components/tabs/sortable_tab.tsx
Normal file
38
napcat.webui/src/components/tabs/sortable_tab.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
|
||||||
|
import { Tab } from '@/components/tabs'
|
||||||
|
import type { TabProps } from '@/components/tabs'
|
||||||
|
|
||||||
|
interface SortableTabProps extends TabProps {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableTab({ id, ...props }: SortableTabProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({ id })
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
zIndex: isDragging ? 1 : 0,
|
||||||
|
position: 'relative' as const,
|
||||||
|
touchAction: 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@@ -19,7 +19,7 @@ const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
|
|||||||
p: ({ node, ...props }) => <p className="m-0" {...props} />,
|
p: ({ node, ...props }) => <p className="m-0" {...props} />,
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
className="text-blue-500 hover:underline"
|
className="text-primary-500 inline-block hover:underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -32,12 +32,12 @@ const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
|
|||||||
),
|
),
|
||||||
blockquote: ({ node, ...props }) => (
|
blockquote: ({ node, ...props }) => (
|
||||||
<blockquote
|
<blockquote
|
||||||
className="border-l-4 border-gray-300 pl-4 italic"
|
className="border-l-4 border-default-300 pl-4 italic"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
code: ({ node, ...props }) => (
|
code: ({ node, ...props }) => (
|
||||||
<code className="bg-gray-100 p-1 rounded" {...props} />
|
<code className="bg-default-100 p-1 rounded text-xs" {...props} />
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
56
napcat.webui/src/components/terminal/terminal-instance.tsx
Normal file
56
napcat.webui/src/components/terminal/terminal-instance.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import TerminalManager from '@/controllers/terminal_manager'
|
||||||
|
|
||||||
|
import XTerm, { XTermRef } from '../xterm'
|
||||||
|
|
||||||
|
interface TerminalInstanceProps {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
||||||
|
const termRef = useRef<XTermRef>(null)
|
||||||
|
const connected = useRef(false)
|
||||||
|
|
||||||
|
const handleData = (data: string) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data)
|
||||||
|
if (parsed.data) {
|
||||||
|
termRef.current?.write(parsed.data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
termRef.current?.write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (connected.current) {
|
||||||
|
TerminalManager.disconnectTerminal(id, handleData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
const handleInput = (data: string) => {
|
||||||
|
TerminalManager.sendInput(id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = (cols: number, rows: number) => {
|
||||||
|
if (!connected.current) {
|
||||||
|
connected.current = true
|
||||||
|
console.log('instance', rows, cols)
|
||||||
|
TerminalManager.connectTerminal(id, handleData, { rows, cols })
|
||||||
|
} else {
|
||||||
|
TerminalManager.sendResize(id, cols, rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XTerm
|
||||||
|
ref={termRef}
|
||||||
|
onInput={handleInput}
|
||||||
|
onResize={handleResize} // 使用 fitAddon 改变后触发的 resize 回调
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
12
napcat.webui/src/components/under_construction.tsx
Normal file
12
napcat.webui/src/components/under_construction.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default function UnderConstruction() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full pt-4">
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-4">
|
||||||
|
<div className="text-6xl font-bold text-gray-500">🚧</div>
|
||||||
|
<div className="text-2xl font-bold text-gray-500">
|
||||||
|
Under Construction
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
|
import { CanvasAddon } from '@xterm/addon-canvas'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||||
import { WebglAddon } from '@xterm/addon-webgl'
|
// import { WebglAddon } from '@xterm/addon-webgl'
|
||||||
import { Terminal } from '@xterm/xterm'
|
import { Terminal } from '@xterm/xterm'
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@@ -8,8 +9,6 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
|||||||
|
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme'
|
||||||
|
|
||||||
import { gradientText } from '@/utils/terminal'
|
|
||||||
|
|
||||||
export type XTermRef = {
|
export type XTermRef = {
|
||||||
write: (
|
write: (
|
||||||
...args: Parameters<Terminal['write']>
|
...args: Parameters<Terminal['write']>
|
||||||
@@ -20,52 +19,64 @@ export type XTermRef = {
|
|||||||
) => ReturnType<Terminal['writeln']>
|
) => ReturnType<Terminal['writeln']>
|
||||||
writelnAsync: (data: Parameters<Terminal['writeln']>[0]) => Promise<void>
|
writelnAsync: (data: Parameters<Terminal['writeln']>[0]) => Promise<void>
|
||||||
clear: () => void
|
clear: () => void
|
||||||
|
terminalRef: React.RefObject<Terminal | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
export interface XTermProps
|
||||||
(props, ref) => {
|
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput' | 'onResize'> {
|
||||||
|
onInput?: (data: string) => void
|
||||||
|
onKey?: (key: string, event: KeyboardEvent) => void
|
||||||
|
onResize?: (cols: number, rows: number) => void // 新增属性
|
||||||
|
}
|
||||||
|
|
||||||
|
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||||
const domRef = useRef<HTMLDivElement>(null)
|
const domRef = useRef<HTMLDivElement>(null)
|
||||||
const terminalRef = useRef<Terminal | null>(null)
|
const terminalRef = useRef<Terminal | null>(null)
|
||||||
const { className, ...rest } = props
|
const { className, onInput, onKey, onResize, ...rest } = props
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!domRef.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const terminal = new Terminal({
|
const terminal = new Terminal({
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
fontFamily: '"Fira Code", "Harmony", "Noto Serif SC", monospace',
|
fontFamily:
|
||||||
|
'"JetBrains Mono", "Aa偷吃可爱长大的", "Noto Serif SC", monospace',
|
||||||
cursorInactiveStyle: 'outline',
|
cursorInactiveStyle: 'outline',
|
||||||
drawBoldTextInBrightColors: false,
|
drawBoldTextInBrightColors: false,
|
||||||
letterSpacing: 0,
|
fontSize: 14,
|
||||||
lineHeight: 1.0
|
lineHeight: 1.2
|
||||||
})
|
})
|
||||||
terminalRef.current = terminal
|
terminalRef.current = terminal
|
||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon()
|
||||||
terminal.loadAddon(
|
terminal.loadAddon(
|
||||||
new WebLinksAddon((event, uri) => {
|
new WebLinksAddon((event, uri) => {
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
window.open(uri, '_blank')
|
window.open(uri, '_blank')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
terminal.loadAddon(fitAddon)
|
terminal.loadAddon(fitAddon)
|
||||||
terminal.loadAddon(new WebglAddon())
|
terminal.open(domRef.current!)
|
||||||
terminal.open(domRef.current)
|
|
||||||
|
|
||||||
terminal.writeln(
|
terminal.loadAddon(new CanvasAddon())
|
||||||
gradientText(
|
terminal.onData((data) => {
|
||||||
'Welcome to NapCat WebUI',
|
if (onInput) {
|
||||||
[255, 0, 0],
|
onInput(data)
|
||||||
[0, 255, 0],
|
}
|
||||||
true,
|
})
|
||||||
true,
|
|
||||||
true
|
terminal.onKey((event) => {
|
||||||
)
|
if (onKey) {
|
||||||
)
|
onKey(event.key, event.domEvent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
|
// 获取当前终端尺寸
|
||||||
|
const cols = terminal.cols
|
||||||
|
const rows = terminal.rows
|
||||||
|
if (onResize) {
|
||||||
|
onResize(cols, rows)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 字体加载完成后重新调整终端大小
|
// 字体加载完成后重新调整终端大小
|
||||||
@@ -85,21 +96,49 @@ const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminalRef.current) {
|
if (terminalRef.current) {
|
||||||
|
if (theme === 'dark') {
|
||||||
terminalRef.current.options.theme = {
|
terminalRef.current.options.theme = {
|
||||||
background: theme === 'dark' ? '#00000000' : '#ffffff00',
|
background: '#00000000',
|
||||||
foreground: theme === 'dark' ? '#fff' : '#000',
|
black: '#ffffff',
|
||||||
selectionBackground:
|
red: '#cd3131',
|
||||||
theme === 'dark'
|
green: '#0dbc79',
|
||||||
? 'rgba(179, 0, 0, 0.3)'
|
yellow: '#e5e510',
|
||||||
: 'rgba(255, 167, 167, 0.3)',
|
blue: '#2472c8',
|
||||||
cursor: theme === 'dark' ? '#fff' : '#000',
|
cyan: '#11a8cd',
|
||||||
cursorAccent: theme === 'dark' ? '#000' : '#fff',
|
white: '#e5e5e5',
|
||||||
black: theme === 'dark' ? '#fff' : '#000'
|
brightBlack: '#666666',
|
||||||
|
brightRed: '#f14c4c',
|
||||||
|
brightGreen: '#23d18b',
|
||||||
|
brightYellow: '#f5f543',
|
||||||
|
brightBlue: '#3b8eea',
|
||||||
|
brightCyan: '#29b8db',
|
||||||
|
brightWhite: '#e5e5e5',
|
||||||
|
foreground: '#cccccc',
|
||||||
|
selectionBackground: '#3a3d41',
|
||||||
|
cursor: '#ffffff'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
terminalRef.current.options.theme = {
|
||||||
|
background: '#ffffff00',
|
||||||
|
black: '#000000',
|
||||||
|
red: '#aa3731',
|
||||||
|
green: '#448c27',
|
||||||
|
yellow: '#cb9000',
|
||||||
|
blue: '#325cc0',
|
||||||
|
cyan: '#0083b2',
|
||||||
|
white: '#7f7f7f',
|
||||||
|
brightBlack: '#777777',
|
||||||
|
brightRed: '#f05050',
|
||||||
|
brightGreen: '#60cb00',
|
||||||
|
brightYellow: '#ffbc5d',
|
||||||
|
brightBlue: '#007acc',
|
||||||
|
brightCyan: '#00aacb',
|
||||||
|
brightWhite: '#b0b0b0',
|
||||||
|
foreground: '#000000',
|
||||||
|
selectionBackground: '#bfdbfe',
|
||||||
|
cursor: '#007acc'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
terminalRef.current.options.fontWeight =
|
|
||||||
theme === 'dark' ? 'normal' : '600'
|
|
||||||
terminalRef.current.options.fontWeightBold =
|
|
||||||
theme === 'dark' ? 'bold' : '900'
|
|
||||||
}
|
}
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
@@ -124,7 +163,8 @@ const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
|||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
terminalRef.current?.clear()
|
terminalRef.current?.clear()
|
||||||
}
|
},
|
||||||
|
terminalRef: terminalRef
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -147,7 +187,6 @@ const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
export default XTerm
|
export default XTerm
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
BugIcon2,
|
BugIcon2,
|
||||||
|
FileIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
|
LogIcon,
|
||||||
RouteIcon,
|
RouteIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
SignalTowerIcon,
|
SignalTowerIcon,
|
||||||
@@ -49,10 +51,10 @@ export const siteConfig = {
|
|||||||
href: '/config'
|
href: '/config'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '系统日志',
|
label: '猫猫日志',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className="w-5 h-5">
|
||||||
<TerminalIcon />
|
<LogIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/logs'
|
href: '/logs'
|
||||||
@@ -75,6 +77,24 @@ export const siteConfig = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '文件管理',
|
||||||
|
icon: (
|
||||||
|
<div className="w-5 h-5">
|
||||||
|
<FileIcon />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
href: '/file_manager'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '系统终端',
|
||||||
|
icon: (
|
||||||
|
<div className="w-5 h-5">
|
||||||
|
<TerminalIcon />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
href: '/terminal'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '关于我们',
|
label: '关于我们',
|
||||||
icon: (
|
icon: (
|
||||||
|
6
napcat.webui/src/const/themes.ts
Normal file
6
napcat.webui/src/const/themes.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import heroui from './themes/heroui'
|
||||||
|
import nc_pink from './themes/nc_pink'
|
||||||
|
|
||||||
|
const themes: ThemeInfo[] = [nc_pink, heroui]
|
||||||
|
|
||||||
|
export default themes
|
256
napcat.webui/src/const/themes/heroui.ts
Normal file
256
napcat.webui/src/const/themes/heroui.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
const theme: ThemeConfig = {
|
||||||
|
dark: {
|
||||||
|
'--heroui-background': '0 0% 0%',
|
||||||
|
'--heroui-foreground-50': '240 5.88% 10%',
|
||||||
|
'--heroui-foreground-100': '240 3.7% 15.88%',
|
||||||
|
'--heroui-foreground-200': '240 5.26% 26.08%',
|
||||||
|
'--heroui-foreground-300': '240 5.2% 33.92%',
|
||||||
|
'--heroui-foreground-400': '240 3.83% 46.08%',
|
||||||
|
'--heroui-foreground-500': '240 5.03% 64.9%',
|
||||||
|
'--heroui-foreground-600': '240 4.88% 83.92%',
|
||||||
|
'--heroui-foreground-700': '240 5.88% 90%',
|
||||||
|
'--heroui-foreground-800': '240 4.76% 95.88%',
|
||||||
|
'--heroui-foreground-900': '0 0% 98.04%',
|
||||||
|
'--heroui-foreground': '210 5.56% 92.94%',
|
||||||
|
'--heroui-focus': '212.01999999999998 100% 46.67%',
|
||||||
|
'--heroui-overlay': '0 0% 0%',
|
||||||
|
'--heroui-divider': '0 0% 100%',
|
||||||
|
'--heroui-divider-opacity': '0.15',
|
||||||
|
'--heroui-content1': '240 5.88% 10%',
|
||||||
|
'--heroui-content1-foreground': '0 0% 98.04%',
|
||||||
|
'--heroui-content2': '240 3.7% 15.88%',
|
||||||
|
'--heroui-content2-foreground': '240 4.76% 95.88%',
|
||||||
|
'--heroui-content3': '240 5.26% 26.08%',
|
||||||
|
'--heroui-content3-foreground': '240 5.88% 90%',
|
||||||
|
'--heroui-content4': '240 5.2% 33.92%',
|
||||||
|
'--heroui-content4-foreground': '240 4.88% 83.92%',
|
||||||
|
'--heroui-default-50': '240 5.88% 10%',
|
||||||
|
'--heroui-default-100': '240 3.7% 15.88%',
|
||||||
|
'--heroui-default-200': '240 5.26% 26.08%',
|
||||||
|
'--heroui-default-300': '240 5.2% 33.92%',
|
||||||
|
'--heroui-default-400': '240 3.83% 46.08%',
|
||||||
|
'--heroui-default-500': '240 5.03% 64.9%',
|
||||||
|
'--heroui-default-600': '240 4.88% 83.92%',
|
||||||
|
'--heroui-default-700': '240 5.88% 90%',
|
||||||
|
'--heroui-default-800': '240 4.76% 95.88%',
|
||||||
|
'--heroui-default-900': '0 0% 98.04%',
|
||||||
|
'--heroui-default-foreground': '0 0% 100%',
|
||||||
|
'--heroui-default': '240 5.26% 26.08%',
|
||||||
|
'--heroui-danger-50': '340 84.91% 10.39%',
|
||||||
|
'--heroui-danger-100': '339.33 86.54% 20.39%',
|
||||||
|
'--heroui-danger-200': '339.11 85.99% 30.78%',
|
||||||
|
'--heroui-danger-300': '339 86.54% 40.78%',
|
||||||
|
'--heroui-danger-400': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-danger-500': '339 90% 60.78%',
|
||||||
|
'--heroui-danger-600': '339.11 90.6% 70.78%',
|
||||||
|
'--heroui-danger-700': '339.33 90% 80.39%',
|
||||||
|
'--heroui-danger-800': '340 91.84% 90.39%',
|
||||||
|
'--heroui-danger-900': '339.13 92% 95.1%',
|
||||||
|
'--heroui-danger-foreground': '0 0% 100%',
|
||||||
|
'--heroui-danger': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-primary-50': '211.84 100% 9.61%',
|
||||||
|
'--heroui-primary-100': '211.84 100% 19.22%',
|
||||||
|
'--heroui-primary-200': '212.24 100% 28.82%',
|
||||||
|
'--heroui-primary-300': '212.14 100% 38.43%',
|
||||||
|
'--heroui-primary-400': '212.02 100% 46.67%',
|
||||||
|
'--heroui-primary-500': '212.14 92.45% 58.43%',
|
||||||
|
'--heroui-primary-600': '212.24 92.45% 68.82%',
|
||||||
|
'--heroui-primary-700': '211.84 92.45% 79.22%',
|
||||||
|
'--heroui-primary-800': '211.84 92.45% 89.61%',
|
||||||
|
'--heroui-primary-900': '212.5 92.31% 94.9%',
|
||||||
|
'--heroui-primary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-primary': '212.02 100% 46.67%',
|
||||||
|
'--heroui-secondary-50': '270 66.67% 9.41%',
|
||||||
|
'--heroui-secondary-100': '270 66.67% 18.82%',
|
||||||
|
'--heroui-secondary-200': '270 66.67% 28.24%',
|
||||||
|
'--heroui-secondary-300': '270 66.67% 37.65%',
|
||||||
|
'--heroui-secondary-400': '270 66.67% 47.06%',
|
||||||
|
'--heroui-secondary-500': '270 59.26% 57.65%',
|
||||||
|
'--heroui-secondary-600': '270 59.26% 68.24%',
|
||||||
|
'--heroui-secondary-700': '270 59.26% 78.82%',
|
||||||
|
'--heroui-secondary-800': '270 59.26% 89.41%',
|
||||||
|
'--heroui-secondary-900': '270 61.54% 94.9%',
|
||||||
|
'--heroui-secondary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-secondary': '270 59.26% 57.65%',
|
||||||
|
'--heroui-success-50': '145.71 77.78% 8.82%',
|
||||||
|
'--heroui-success-100': '146.2 79.78% 17.45%',
|
||||||
|
'--heroui-success-200': '145.79 79.26% 26.47%',
|
||||||
|
'--heroui-success-300': '146.01 79.89% 35.1%',
|
||||||
|
'--heroui-success-400': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-success-500': '146.01 62.45% 55.1%',
|
||||||
|
'--heroui-success-600': '145.79 62.57% 66.47%',
|
||||||
|
'--heroui-success-700': '146.2 61.74% 77.45%',
|
||||||
|
'--heroui-success-800': '145.71 61.4% 88.82%',
|
||||||
|
'--heroui-success-900': '146.67 64.29% 94.51%',
|
||||||
|
'--heroui-success-foreground': '0 0% 0%',
|
||||||
|
'--heroui-success': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-warning-50': '37.14 75% 10.98%',
|
||||||
|
'--heroui-warning-100': '37.14 75% 21.96%',
|
||||||
|
'--heroui-warning-200': '36.96 73.96% 33.14%',
|
||||||
|
'--heroui-warning-300': '37.01 74.22% 44.12%',
|
||||||
|
'--heroui-warning-400': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-warning-500': '37.01 91.26% 64.12%',
|
||||||
|
'--heroui-warning-600': '36.96 91.24% 73.14%',
|
||||||
|
'--heroui-warning-700': '37.14 91.3% 81.96%',
|
||||||
|
'--heroui-warning-800': '37.14 91.3% 90.98%',
|
||||||
|
'--heroui-warning-900': '54.55 91.67% 95.29%',
|
||||||
|
'--heroui-warning-foreground': '0 0% 0%',
|
||||||
|
'--heroui-warning': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-code-background': '240 5.56% 7.06%',
|
||||||
|
'--heroui-strong': '190.14 94.67% 44.12%',
|
||||||
|
'--heroui-code-mdx': '190.14 94.67% 44.12%',
|
||||||
|
'--heroui-divider-weight': '1px',
|
||||||
|
'--heroui-disabled-opacity': '.5',
|
||||||
|
'--heroui-font-size-tiny': '0.75rem',
|
||||||
|
'--heroui-font-size-small': '0.875rem',
|
||||||
|
'--heroui-font-size-medium': '1rem',
|
||||||
|
'--heroui-font-size-large': '1.125rem',
|
||||||
|
'--heroui-line-height-tiny': '1rem',
|
||||||
|
'--heroui-line-height-small': '1.25rem',
|
||||||
|
'--heroui-line-height-medium': '1.5rem',
|
||||||
|
'--heroui-line-height-large': '1.75rem',
|
||||||
|
'--heroui-radius-small': '8px',
|
||||||
|
'--heroui-radius-medium': '12px',
|
||||||
|
'--heroui-radius-large': '14px',
|
||||||
|
'--heroui-border-width-small': '1px',
|
||||||
|
'--heroui-border-width-medium': '2px',
|
||||||
|
'--heroui-border-width-large': '3px',
|
||||||
|
'--heroui-box-shadow-small':
|
||||||
|
'0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
|
||||||
|
'--heroui-box-shadow-medium':
|
||||||
|
'0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
|
||||||
|
'--heroui-box-shadow-large':
|
||||||
|
'0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
|
||||||
|
'--heroui-hover-opacity': '.9'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
'--heroui-background': '0 0% 100%',
|
||||||
|
'--heroui-foreground-50': '240 5.88% 95%',
|
||||||
|
'--heroui-foreground-100': '240 3.7% 90%',
|
||||||
|
'--heroui-foreground-200': '240 5.26% 80%',
|
||||||
|
'--heroui-foreground-300': '240 5.2% 70%',
|
||||||
|
'--heroui-foreground-400': '240 3.83% 60%',
|
||||||
|
'--heroui-foreground-500': '240 5.03% 50%',
|
||||||
|
'--heroui-foreground-600': '240 4.88% 40%',
|
||||||
|
'--heroui-foreground-700': '240 5.88% 30%',
|
||||||
|
'--heroui-foreground-800': '240 4.76% 20%',
|
||||||
|
'--heroui-foreground-900': '0 0% 10%',
|
||||||
|
'--heroui-foreground': '210 5.56% 7.06%',
|
||||||
|
'--heroui-focus': '212.01999999999998 100% 53.33%',
|
||||||
|
'--heroui-overlay': '0 0% 100%',
|
||||||
|
'--heroui-divider': '0 0% 0%',
|
||||||
|
'--heroui-divider-opacity': '0.85',
|
||||||
|
'--heroui-content1': '240 5.88% 95%',
|
||||||
|
'--heroui-content1-foreground': '0 0% 10%',
|
||||||
|
'--heroui-content2': '240 3.7% 90%',
|
||||||
|
'--heroui-content2-foreground': '240 4.76% 20%',
|
||||||
|
'--heroui-content3': '240 5.26% 80%',
|
||||||
|
'--heroui-content3-foreground': '240 5.88% 30%',
|
||||||
|
'--heroui-content4': '240 5.2% 70%',
|
||||||
|
'--heroui-content4-foreground': '240 4.88% 40%',
|
||||||
|
'--heroui-default-50': '240 5.88% 95%',
|
||||||
|
'--heroui-default-100': '240 3.7% 90%',
|
||||||
|
'--heroui-default-200': '240 5.26% 80%',
|
||||||
|
'--heroui-default-300': '240 5.2% 70%',
|
||||||
|
'--heroui-default-400': '240 3.83% 60%',
|
||||||
|
'--heroui-default-500': '240 5.03% 50%',
|
||||||
|
'--heroui-default-600': '240 4.88% 40%',
|
||||||
|
'--heroui-default-700': '240 5.88% 30%',
|
||||||
|
'--heroui-default-800': '240 4.76% 20%',
|
||||||
|
'--heroui-default-900': '0 0% 10%',
|
||||||
|
'--heroui-default-foreground': '0 0% 0%',
|
||||||
|
'--heroui-default': '240 5.26% 80%',
|
||||||
|
'--heroui-danger-50': '339.13 92% 95.1%',
|
||||||
|
'--heroui-danger-100': '340 91.84% 90.39%',
|
||||||
|
'--heroui-danger-200': '339.33 90% 80.39%',
|
||||||
|
'--heroui-danger-300': '339.11 90.6% 70.78%',
|
||||||
|
'--heroui-danger-400': '339 90% 60.78%',
|
||||||
|
'--heroui-danger-500': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-danger-600': '339 86.54% 40.78%',
|
||||||
|
'--heroui-danger-700': '339.11 85.99% 30.78%',
|
||||||
|
'--heroui-danger-800': '339.33 86.54% 20.39%',
|
||||||
|
'--heroui-danger-900': '340 84.91% 10.39%',
|
||||||
|
'--heroui-danger-foreground': '0 0% 100%',
|
||||||
|
'--heroui-danger': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-primary-50': '212.5 92.31% 94.9%',
|
||||||
|
'--heroui-primary-100': '211.84 92.45% 89.61%',
|
||||||
|
'--heroui-primary-200': '211.84 92.45% 79.22%',
|
||||||
|
'--heroui-primary-300': '212.24 92.45% 68.82%',
|
||||||
|
'--heroui-primary-400': '212.14 92.45% 58.43%',
|
||||||
|
'--heroui-primary-500': '212.02 100% 46.67%',
|
||||||
|
'--heroui-primary-600': '212.14 100% 38.43%',
|
||||||
|
'--heroui-primary-700': '212.24 100% 28.82%',
|
||||||
|
'--heroui-primary-800': '211.84 100% 19.22%',
|
||||||
|
'--heroui-primary-900': '211.84 100% 9.61%',
|
||||||
|
'--heroui-primary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-primary': '212.02 100% 46.67%',
|
||||||
|
'--heroui-secondary-50': '270 61.54% 94.9%',
|
||||||
|
'--heroui-secondary-100': '270 59.26% 89.41%',
|
||||||
|
'--heroui-secondary-200': '270 59.26% 78.82%',
|
||||||
|
'--heroui-secondary-300': '270 59.26% 68.24%',
|
||||||
|
'--heroui-secondary-400': '270 59.26% 57.65%',
|
||||||
|
'--heroui-secondary-500': '270 66.67% 47.06%',
|
||||||
|
'--heroui-secondary-600': '270 66.67% 37.65%',
|
||||||
|
'--heroui-secondary-700': '270 66.67% 28.24%',
|
||||||
|
'--heroui-secondary-800': '270 66.67% 18.82%',
|
||||||
|
'--heroui-secondary-900': '270 66.67% 9.41%',
|
||||||
|
'--heroui-secondary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-secondary': '270 66.67% 47.06%',
|
||||||
|
'--heroui-success-50': '146.67 64.29% 94.51%',
|
||||||
|
'--heroui-success-100': '145.71 61.4% 88.82%',
|
||||||
|
'--heroui-success-200': '146.2 61.74% 77.45%',
|
||||||
|
'--heroui-success-300': '145.79 62.57% 66.47%',
|
||||||
|
'--heroui-success-400': '146.01 62.45% 55.1%',
|
||||||
|
'--heroui-success-500': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-success-600': '146.01 79.89% 35.1%',
|
||||||
|
'--heroui-success-700': '145.79 79.26% 26.47%',
|
||||||
|
'--heroui-success-800': '146.2 79.78% 17.45%',
|
||||||
|
'--heroui-success-900': '145.71 77.78% 8.82%',
|
||||||
|
'--heroui-success-foreground': '0 0% 0%',
|
||||||
|
'--heroui-success': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-warning-50': '54.55 91.67% 95.29%',
|
||||||
|
'--heroui-warning-100': '37.14 91.3% 90.98%',
|
||||||
|
'--heroui-warning-200': '37.14 91.3% 81.96%',
|
||||||
|
'--heroui-warning-300': '36.96 91.24% 73.14%',
|
||||||
|
'--heroui-warning-400': '37.01 91.26% 64.12%',
|
||||||
|
'--heroui-warning-500': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-warning-600': '37.01 74.22% 44.12%',
|
||||||
|
'--heroui-warning-700': '36.96 73.96% 33.14%',
|
||||||
|
'--heroui-warning-800': '37.14 75% 21.96%',
|
||||||
|
'--heroui-warning-900': '37.14 75% 10.98%',
|
||||||
|
'--heroui-warning-foreground': '0 0% 0%',
|
||||||
|
'--heroui-warning': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-code-background': '221.25 17.39% 18.04%',
|
||||||
|
'--heroui-strong': '316.95 100% 65.29%',
|
||||||
|
'--heroui-code-mdx': '316.95 100% 65.29%',
|
||||||
|
'--heroui-divider-weight': '1px',
|
||||||
|
'--heroui-disabled-opacity': '.5',
|
||||||
|
'--heroui-font-size-tiny': '0.75rem',
|
||||||
|
'--heroui-font-size-small': '0.875rem',
|
||||||
|
'--heroui-font-size-medium': '1rem',
|
||||||
|
'--heroui-font-size-large': '1.125rem',
|
||||||
|
'--heroui-line-height-tiny': '1rem',
|
||||||
|
'--heroui-line-height-small': '1.25rem',
|
||||||
|
'--heroui-line-height-medium': '1.5rem',
|
||||||
|
'--heroui-line-height-large': '1.75rem',
|
||||||
|
'--heroui-radius-small': '8px',
|
||||||
|
'--heroui-radius-medium': '12px',
|
||||||
|
'--heroui-radius-large': '14px',
|
||||||
|
'--heroui-border-width-small': '1px',
|
||||||
|
'--heroui-border-width-medium': '2px',
|
||||||
|
'--heroui-border-width-large': '3px',
|
||||||
|
'--heroui-box-shadow-small':
|
||||||
|
'0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
|
||||||
|
'--heroui-box-shadow-medium':
|
||||||
|
'0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
|
||||||
|
'--heroui-box-shadow-large':
|
||||||
|
'0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
|
||||||
|
'--heroui-hover-opacity': '.8'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
theme,
|
||||||
|
author: 'HeroUI',
|
||||||
|
name: 'heroui',
|
||||||
|
description: 'HeroUI Default Theme'
|
||||||
|
} satisfies ThemeInfo
|
256
napcat.webui/src/const/themes/nc_pink.ts
Normal file
256
napcat.webui/src/const/themes/nc_pink.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
const theme: ThemeConfig = {
|
||||||
|
dark: {
|
||||||
|
'--heroui-background': '0 0% 0%',
|
||||||
|
'--heroui-foreground-50': '240 5.88% 10%',
|
||||||
|
'--heroui-foreground-100': '240 3.7% 15.88%',
|
||||||
|
'--heroui-foreground-200': '240 5.26% 26.08%',
|
||||||
|
'--heroui-foreground-300': '240 5.2% 33.92%',
|
||||||
|
'--heroui-foreground-400': '240 3.83% 46.08%',
|
||||||
|
'--heroui-foreground-500': '240 5.03% 64.9%',
|
||||||
|
'--heroui-foreground-600': '240 4.88% 83.92%',
|
||||||
|
'--heroui-foreground-700': '240 5.88% 90%',
|
||||||
|
'--heroui-foreground-800': '240 4.76% 95.88%',
|
||||||
|
'--heroui-foreground-900': '0 0% 98.04%',
|
||||||
|
'--heroui-foreground': '210 5.56% 92.94%',
|
||||||
|
'--heroui-focus': '212.01999999999998 100% 46.67%',
|
||||||
|
'--heroui-overlay': '0 0% 0%',
|
||||||
|
'--heroui-divider': '0 0% 100%',
|
||||||
|
'--heroui-divider-opacity': '0.15',
|
||||||
|
'--heroui-content1': '240 5.88% 10%',
|
||||||
|
'--heroui-content1-foreground': '0 0% 98.04%',
|
||||||
|
'--heroui-content2': '240 3.7% 15.88%',
|
||||||
|
'--heroui-content2-foreground': '240 4.76% 95.88%',
|
||||||
|
'--heroui-content3': '240 5.26% 26.08%',
|
||||||
|
'--heroui-content3-foreground': '240 5.88% 90%',
|
||||||
|
'--heroui-content4': '240 5.2% 33.92%',
|
||||||
|
'--heroui-content4-foreground': '240 4.88% 83.92%',
|
||||||
|
'--heroui-default-50': '240 5.88% 10%',
|
||||||
|
'--heroui-default-100': '240 3.7% 15.88%',
|
||||||
|
'--heroui-default-200': '240 5.26% 26.08%',
|
||||||
|
'--heroui-default-300': '240 5.2% 33.92%',
|
||||||
|
'--heroui-default-400': '240 3.83% 46.08%',
|
||||||
|
'--heroui-default-500': '240 5.03% 64.9%',
|
||||||
|
'--heroui-default-600': '240 4.88% 83.92%',
|
||||||
|
'--heroui-default-700': '240 5.88% 90%',
|
||||||
|
'--heroui-default-800': '240 4.76% 95.88%',
|
||||||
|
'--heroui-default-900': '0 0% 98.04%',
|
||||||
|
'--heroui-default-foreground': '0 0% 100%',
|
||||||
|
'--heroui-default': '240 5.26% 26.08%',
|
||||||
|
'--heroui-danger-50': '301.89 82.61% 22.55%',
|
||||||
|
'--heroui-danger-100': '308.18 76.39% 28.24%',
|
||||||
|
'--heroui-danger-200': '313.85 70.65% 36.08%',
|
||||||
|
'--heroui-danger-300': '319.73 65.64% 44.51%',
|
||||||
|
'--heroui-danger-400': '325.82 69.62% 53.53%',
|
||||||
|
'--heroui-danger-500': '331.82 75% 65.49%',
|
||||||
|
'--heroui-danger-600': '337.84 83.46% 73.92%',
|
||||||
|
'--heroui-danger-700': '343.42 90.48% 83.53%',
|
||||||
|
'--heroui-danger-800': '350.53 90.48% 91.76%',
|
||||||
|
'--heroui-danger-900': '324 90.91% 95.69%',
|
||||||
|
'--heroui-danger-foreground': '0 0% 100%',
|
||||||
|
'--heroui-danger': '325.82 69.62% 53.53%',
|
||||||
|
'--heroui-primary-50': '340 84.91% 10.39%',
|
||||||
|
'--heroui-primary-100': '339.33 86.54% 20.39%',
|
||||||
|
'--heroui-primary-200': '339.11 85.99% 30.78%',
|
||||||
|
'--heroui-primary-300': '339 86.54% 40.78%',
|
||||||
|
'--heroui-primary-400': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-primary-500': '339 90% 60.78%',
|
||||||
|
'--heroui-primary-600': '339.11 90.6% 70.78%',
|
||||||
|
'--heroui-primary-700': '339.33 90% 80.39%',
|
||||||
|
'--heroui-primary-800': '340 91.84% 90.39%',
|
||||||
|
'--heroui-primary-900': '339.13 92% 95.1%',
|
||||||
|
'--heroui-primary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-primary': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-secondary-50': '270 66.67% 9.41%',
|
||||||
|
'--heroui-secondary-100': '270 66.67% 18.82%',
|
||||||
|
'--heroui-secondary-200': '270 66.67% 28.24%',
|
||||||
|
'--heroui-secondary-300': '270 66.67% 37.65%',
|
||||||
|
'--heroui-secondary-400': '270 66.67% 47.06%',
|
||||||
|
'--heroui-secondary-500': '270 59.26% 57.65%',
|
||||||
|
'--heroui-secondary-600': '270 59.26% 68.24%',
|
||||||
|
'--heroui-secondary-700': '270 59.26% 78.82%',
|
||||||
|
'--heroui-secondary-800': '270 59.26% 89.41%',
|
||||||
|
'--heroui-secondary-900': '270 61.54% 94.9%',
|
||||||
|
'--heroui-secondary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-secondary': '270 59.26% 57.65%',
|
||||||
|
'--heroui-success-50': '145.71 77.78% 8.82%',
|
||||||
|
'--heroui-success-100': '146.2 79.78% 17.45%',
|
||||||
|
'--heroui-success-200': '145.79 79.26% 26.47%',
|
||||||
|
'--heroui-success-300': '146.01 79.89% 35.1%',
|
||||||
|
'--heroui-success-400': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-success-500': '146.01 62.45% 55.1%',
|
||||||
|
'--heroui-success-600': '145.79 62.57% 66.47%',
|
||||||
|
'--heroui-success-700': '146.2 61.74% 77.45%',
|
||||||
|
'--heroui-success-800': '145.71 61.4% 88.82%',
|
||||||
|
'--heroui-success-900': '146.67 64.29% 94.51%',
|
||||||
|
'--heroui-success-foreground': '0 0% 0%',
|
||||||
|
'--heroui-success': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-warning-50': '37.14 75% 10.98%',
|
||||||
|
'--heroui-warning-100': '37.14 75% 21.96%',
|
||||||
|
'--heroui-warning-200': '36.96 73.96% 33.14%',
|
||||||
|
'--heroui-warning-300': '37.01 74.22% 44.12%',
|
||||||
|
'--heroui-warning-400': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-warning-500': '37.01 91.26% 64.12%',
|
||||||
|
'--heroui-warning-600': '36.96 91.24% 73.14%',
|
||||||
|
'--heroui-warning-700': '37.14 91.3% 81.96%',
|
||||||
|
'--heroui-warning-800': '37.14 91.3% 90.98%',
|
||||||
|
'--heroui-warning-900': '54.55 91.67% 95.29%',
|
||||||
|
'--heroui-warning-foreground': '0 0% 0%',
|
||||||
|
'--heroui-warning': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-code-background': '240 5.56% 7.06%',
|
||||||
|
'--heroui-strong': '190.14 94.67% 44.12%',
|
||||||
|
'--heroui-code-mdx': '190.14 94.67% 44.12%',
|
||||||
|
'--heroui-divider-weight': '1px',
|
||||||
|
'--heroui-disabled-opacity': '.5',
|
||||||
|
'--heroui-font-size-tiny': '0.75rem',
|
||||||
|
'--heroui-font-size-small': '0.875rem',
|
||||||
|
'--heroui-font-size-medium': '1rem',
|
||||||
|
'--heroui-font-size-large': '1.125rem',
|
||||||
|
'--heroui-line-height-tiny': '1rem',
|
||||||
|
'--heroui-line-height-small': '1.25rem',
|
||||||
|
'--heroui-line-height-medium': '1.5rem',
|
||||||
|
'--heroui-line-height-large': '1.75rem',
|
||||||
|
'--heroui-radius-small': '8px',
|
||||||
|
'--heroui-radius-medium': '12px',
|
||||||
|
'--heroui-radius-large': '14px',
|
||||||
|
'--heroui-border-width-small': '1px',
|
||||||
|
'--heroui-border-width-medium': '2px',
|
||||||
|
'--heroui-border-width-large': '3px',
|
||||||
|
'--heroui-box-shadow-small':
|
||||||
|
'0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
|
||||||
|
'--heroui-box-shadow-medium':
|
||||||
|
'0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
|
||||||
|
'--heroui-box-shadow-large':
|
||||||
|
'0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
|
||||||
|
'--heroui-hover-opacity': '.9'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
'--heroui-background': '0 0% 100%',
|
||||||
|
'--heroui-foreground-50': '240 5.88% 95%',
|
||||||
|
'--heroui-foreground-100': '240 3.7% 90%',
|
||||||
|
'--heroui-foreground-200': '240 5.26% 80%',
|
||||||
|
'--heroui-foreground-300': '240 5.2% 70%',
|
||||||
|
'--heroui-foreground-400': '240 3.83% 60%',
|
||||||
|
'--heroui-foreground-500': '240 5.03% 50%',
|
||||||
|
'--heroui-foreground-600': '240 4.88% 40%',
|
||||||
|
'--heroui-foreground-700': '240 5.88% 30%',
|
||||||
|
'--heroui-foreground-800': '240 4.76% 20%',
|
||||||
|
'--heroui-foreground-900': '0 0% 10%',
|
||||||
|
'--heroui-foreground': '210 5.56% 7.06%',
|
||||||
|
'--heroui-focus': '212.01999999999998 100% 53.33%',
|
||||||
|
'--heroui-overlay': '0 0% 100%',
|
||||||
|
'--heroui-divider': '0 0% 0%',
|
||||||
|
'--heroui-divider-opacity': '0.85',
|
||||||
|
'--heroui-content1': '240 5.88% 95%',
|
||||||
|
'--heroui-content1-foreground': '0 0% 10%',
|
||||||
|
'--heroui-content2': '240 3.7% 90%',
|
||||||
|
'--heroui-content2-foreground': '240 4.76% 20%',
|
||||||
|
'--heroui-content3': '240 5.26% 80%',
|
||||||
|
'--heroui-content3-foreground': '240 5.88% 30%',
|
||||||
|
'--heroui-content4': '240 5.2% 70%',
|
||||||
|
'--heroui-content4-foreground': '240 4.88% 40%',
|
||||||
|
'--heroui-default-50': '240 5.88% 95%',
|
||||||
|
'--heroui-default-100': '240 3.7% 90%',
|
||||||
|
'--heroui-default-200': '240 5.26% 80%',
|
||||||
|
'--heroui-default-300': '240 5.2% 70%',
|
||||||
|
'--heroui-default-400': '240 3.83% 60%',
|
||||||
|
'--heroui-default-500': '240 5.03% 50%',
|
||||||
|
'--heroui-default-600': '240 4.88% 40%',
|
||||||
|
'--heroui-default-700': '240 5.88% 30%',
|
||||||
|
'--heroui-default-800': '240 4.76% 20%',
|
||||||
|
'--heroui-default-900': '0 0% 10%',
|
||||||
|
'--heroui-default-foreground': '0 0% 0%',
|
||||||
|
'--heroui-default': '240 5.26% 80%',
|
||||||
|
'--heroui-danger-50': '324 90.91% 95.69%',
|
||||||
|
'--heroui-danger-100': '350.53 90.48% 91.76%',
|
||||||
|
'--heroui-danger-200': '343.42 90.48% 83.53%',
|
||||||
|
'--heroui-danger-300': '337.84 83.46% 73.92%',
|
||||||
|
'--heroui-danger-400': '331.82 75% 65.49%',
|
||||||
|
'--heroui-danger-500': '325.82 69.62% 53.53%',
|
||||||
|
'--heroui-danger-600': '319.73 65.64% 44.51%',
|
||||||
|
'--heroui-danger-700': '313.85 70.65% 36.08%',
|
||||||
|
'--heroui-danger-800': '308.18 76.39% 28.24%',
|
||||||
|
'--heroui-danger-900': '301.89 82.61% 22.55%',
|
||||||
|
'--heroui-danger-foreground': '0 0% 100%',
|
||||||
|
'--heroui-danger': '325.82 69.62% 53.53%',
|
||||||
|
'--heroui-primary-50': '339.13 92% 95.1%',
|
||||||
|
'--heroui-primary-100': '340 91.84% 90.39%',
|
||||||
|
'--heroui-primary-200': '339.33 90% 80.39%',
|
||||||
|
'--heroui-primary-300': '339.11 90.6% 70.78%',
|
||||||
|
'--heroui-primary-400': '339 90% 60.78%',
|
||||||
|
'--heroui-primary-500': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-primary-600': '339 86.54% 40.78%',
|
||||||
|
'--heroui-primary-700': '339.11 85.99% 30.78%',
|
||||||
|
'--heroui-primary-800': '339.33 86.54% 20.39%',
|
||||||
|
'--heroui-primary-900': '340 84.91% 10.39%',
|
||||||
|
'--heroui-primary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-primary': '339.2 90.36% 51.18%',
|
||||||
|
'--heroui-secondary-50': '270 61.54% 94.9%',
|
||||||
|
'--heroui-secondary-100': '270 59.26% 89.41%',
|
||||||
|
'--heroui-secondary-200': '270 59.26% 78.82%',
|
||||||
|
'--heroui-secondary-300': '270 59.26% 68.24%',
|
||||||
|
'--heroui-secondary-400': '270 59.26% 57.65%',
|
||||||
|
'--heroui-secondary-500': '270 66.67% 47.06%',
|
||||||
|
'--heroui-secondary-600': '270 66.67% 37.65%',
|
||||||
|
'--heroui-secondary-700': '270 66.67% 28.24%',
|
||||||
|
'--heroui-secondary-800': '270 66.67% 18.82%',
|
||||||
|
'--heroui-secondary-900': '270 66.67% 9.41%',
|
||||||
|
'--heroui-secondary-foreground': '0 0% 100%',
|
||||||
|
'--heroui-secondary': '270 66.67% 47.06%',
|
||||||
|
'--heroui-success-50': '146.67 64.29% 94.51%',
|
||||||
|
'--heroui-success-100': '145.71 61.4% 88.82%',
|
||||||
|
'--heroui-success-200': '146.2 61.74% 77.45%',
|
||||||
|
'--heroui-success-300': '145.79 62.57% 66.47%',
|
||||||
|
'--heroui-success-400': '146.01 62.45% 55.1%',
|
||||||
|
'--heroui-success-500': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-success-600': '146.01 79.89% 35.1%',
|
||||||
|
'--heroui-success-700': '145.79 79.26% 26.47%',
|
||||||
|
'--heroui-success-800': '146.2 79.78% 17.45%',
|
||||||
|
'--heroui-success-900': '145.71 77.78% 8.82%',
|
||||||
|
'--heroui-success-foreground': '0 0% 0%',
|
||||||
|
'--heroui-success': '145.96 79.46% 43.92%',
|
||||||
|
'--heroui-warning-50': '54.55 91.67% 95.29%',
|
||||||
|
'--heroui-warning-100': '37.14 91.3% 90.98%',
|
||||||
|
'--heroui-warning-200': '37.14 91.3% 81.96%',
|
||||||
|
'--heroui-warning-300': '36.96 91.24% 73.14%',
|
||||||
|
'--heroui-warning-400': '37.01 91.26% 64.12%',
|
||||||
|
'--heroui-warning-500': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-warning-600': '37.01 74.22% 44.12%',
|
||||||
|
'--heroui-warning-700': '36.96 73.96% 33.14%',
|
||||||
|
'--heroui-warning-800': '37.14 75% 21.96%',
|
||||||
|
'--heroui-warning-900': '37.14 75% 10.98%',
|
||||||
|
'--heroui-warning-foreground': '0 0% 0%',
|
||||||
|
'--heroui-warning': '37.03 91.27% 55.1%',
|
||||||
|
'--heroui-code-background': '221.25 17.39% 18.04%',
|
||||||
|
'--heroui-strong': '316.95 100% 65.29%',
|
||||||
|
'--heroui-code-mdx': '316.95 100% 65.29%',
|
||||||
|
'--heroui-divider-weight': '1px',
|
||||||
|
'--heroui-disabled-opacity': '.5',
|
||||||
|
'--heroui-font-size-tiny': '0.75rem',
|
||||||
|
'--heroui-font-size-small': '0.875rem',
|
||||||
|
'--heroui-font-size-medium': '1rem',
|
||||||
|
'--heroui-font-size-large': '1.125rem',
|
||||||
|
'--heroui-line-height-tiny': '1rem',
|
||||||
|
'--heroui-line-height-small': '1.25rem',
|
||||||
|
'--heroui-line-height-medium': '1.5rem',
|
||||||
|
'--heroui-line-height-large': '1.75rem',
|
||||||
|
'--heroui-radius-small': '8px',
|
||||||
|
'--heroui-radius-medium': '12px',
|
||||||
|
'--heroui-radius-large': '14px',
|
||||||
|
'--heroui-border-width-small': '1px',
|
||||||
|
'--heroui-border-width-medium': '2px',
|
||||||
|
'--heroui-border-width-large': '3px',
|
||||||
|
'--heroui-box-shadow-small':
|
||||||
|
'0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
|
||||||
|
'--heroui-box-shadow-medium':
|
||||||
|
'0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
|
||||||
|
'--heroui-box-shadow-large':
|
||||||
|
'0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
|
||||||
|
'--heroui-hover-opacity': '.8'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
theme,
|
||||||
|
author: 'NapCat',
|
||||||
|
name: 'nc_pink',
|
||||||
|
description: 'NapCat Pink Theme'
|
||||||
|
} satisfies ThemeInfo
|
@@ -35,6 +35,7 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
|||||||
const [musicId, setMusicId] = useState<number>(0)
|
const [musicId, setMusicId] = useState<number>(0)
|
||||||
const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop)
|
const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop)
|
||||||
const music = musicList.find((music) => music.id === musicId)
|
const music = musicList.find((music) => music.id === musicId)
|
||||||
|
const [token] = useLocalStorage(key.token, '')
|
||||||
const onNext = () => {
|
const onNext = () => {
|
||||||
const nextID = getNextMusic(musicList, musicId, playMode)
|
const nextID = getNextMusic(musicList, musicId, playMode)
|
||||||
setMusicId(nextID)
|
setMusicId(nextID)
|
||||||
@@ -60,8 +61,8 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
|||||||
setMusicId(res[0].id)
|
setMusicId(res[0].id)
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMusicList(listId)
|
if (listId && token) fetchMusicList(listId)
|
||||||
}, [listId])
|
}, [listId, token])
|
||||||
return (
|
return (
|
||||||
<AudioContext.Provider
|
<AudioContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
221
napcat.webui/src/controllers/file_manager.ts
Normal file
221
napcat.webui/src/controllers/file_manager.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { serverRequest } from '@/utils/request'
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
name: string
|
||||||
|
isDirectory: boolean
|
||||||
|
size: number
|
||||||
|
mtime: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FileManager {
|
||||||
|
public static async listFiles(path: string = '/') {
|
||||||
|
const { data } = await serverRequest.get<ServerResponse<FileInfo[]>>(
|
||||||
|
`/File/list?path=${encodeURIComponent(path)}`
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:按目录获取
|
||||||
|
public static async listDirectories(path: string = '/') {
|
||||||
|
const { data } = await serverRequest.get<ServerResponse<FileInfo[]>>(
|
||||||
|
`/File/list?path=${encodeURIComponent(path)}&onlyDirectory=true`
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createDirectory(path: string): Promise<boolean> {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/mkdir',
|
||||||
|
{ path }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async delete(path: string) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/delete',
|
||||||
|
{ path }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async readFile(path: string) {
|
||||||
|
const { data } = await serverRequest.get<ServerResponse<string>>(
|
||||||
|
`/File/read?path=${encodeURIComponent(path)}`
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async writeFile(path: string, content: string) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/write',
|
||||||
|
{ path, content }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createFile(path: string): Promise<boolean> {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/create',
|
||||||
|
{ path }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async batchDelete(paths: string[]) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/batchDelete',
|
||||||
|
{ paths }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async rename(oldPath: string, newPath: string) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/rename',
|
||||||
|
{ oldPath, newPath }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async move(sourcePath: string, targetPath: string) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/move',
|
||||||
|
{ sourcePath, targetPath }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async batchMove(
|
||||||
|
items: { sourcePath: string; targetPath: string }[]
|
||||||
|
) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/batchMove',
|
||||||
|
{ items }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static download(path: string) {
|
||||||
|
const downloadUrl = `/File/download?path=${encodeURIComponent(path)}`
|
||||||
|
toast
|
||||||
|
.promise(
|
||||||
|
serverRequest
|
||||||
|
.post(downloadUrl, void 0, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
throw new Error('下载失败')
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: '正在下载文件...',
|
||||||
|
success: '下载成功',
|
||||||
|
error: '下载失败'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
let fileName = path.split('/').pop() || ''
|
||||||
|
if (path.split('.').length === 1) {
|
||||||
|
fileName += '.zip'
|
||||||
|
}
|
||||||
|
link.setAttribute('download', fileName)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async batchDownload(paths: string[]) {
|
||||||
|
const downloadUrl = `/File/batchDownload`
|
||||||
|
toast
|
||||||
|
.promise(
|
||||||
|
serverRequest
|
||||||
|
.post(
|
||||||
|
downloadUrl,
|
||||||
|
{ paths },
|
||||||
|
{
|
||||||
|
responseType: 'blob'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
throw new Error('下载失败')
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: '正在下载文件...',
|
||||||
|
success: '下载成功',
|
||||||
|
error: '下载失败'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
const fileName = 'files.zip'
|
||||||
|
link.setAttribute('download', fileName)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async downloadToURL(path: string) {
|
||||||
|
const downloadUrl = `/File/download?path=${encodeURIComponent(path)}`
|
||||||
|
const response = await serverRequest.post(downloadUrl, void 0, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
return window.URL.createObjectURL(new Blob([response.data]))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async upload(path: string, files: File[]) {
|
||||||
|
const formData = new FormData()
|
||||||
|
files.forEach((file) => {
|
||||||
|
formData.append('files', file)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
`/File/upload?path=${encodeURIComponent(path)}`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async uploadWebUIFont(file: File) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/font/upload/webui',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteWebUIFont() {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/font/delete/webui'
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
}
|
@@ -73,4 +73,17 @@ export default class QQManager {
|
|||||||
)
|
)
|
||||||
return data.data.data
|
return data.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getQuickLoginQQ() {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<string>>(
|
||||||
|
'/QQLogin/GetQuickLoginQQ'
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async setQuickLoginQQ(uin: string) {
|
||||||
|
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetQuickLoginQQ', {
|
||||||
|
uin
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
133
napcat.webui/src/controllers/terminal_manager.ts
Normal file
133
napcat.webui/src/controllers/terminal_manager.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { serverRequest } from '@/utils/request'
|
||||||
|
|
||||||
|
type TerminalCallback = (data: string) => void
|
||||||
|
|
||||||
|
interface TerminalConnection {
|
||||||
|
ws: WebSocket
|
||||||
|
callbacks: Set<TerminalCallback>
|
||||||
|
isConnected: boolean
|
||||||
|
buffer: string[] // 添加缓存数组
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminalSession {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminalInfo {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class TerminalManager {
|
||||||
|
private connections: Map<string, TerminalConnection> = new Map()
|
||||||
|
private readonly MAX_BUFFER_SIZE = 1000 // 限制缓存大小
|
||||||
|
|
||||||
|
async createTerminal(cols: number, rows: number): Promise<TerminalSession> {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<TerminalSession>>(
|
||||||
|
'/Log/terminal/create',
|
||||||
|
{ cols, rows }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeTerminal(id: string): Promise<void> {
|
||||||
|
await serverRequest.post(`/Log/terminal/${id}/close`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTerminalList(): Promise<TerminalInfo[]> {
|
||||||
|
const { data } =
|
||||||
|
await serverRequest.get<ServerResponse<TerminalInfo[]>>(
|
||||||
|
'/Log/terminal/list'
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
connectTerminal(
|
||||||
|
id: string,
|
||||||
|
callback: TerminalCallback,
|
||||||
|
config?: {
|
||||||
|
cols?: number
|
||||||
|
rows?: number
|
||||||
|
}
|
||||||
|
): WebSocket {
|
||||||
|
let conn = this.connections.get(id)
|
||||||
|
const { cols = 80, rows = 24 } = config || {}
|
||||||
|
if (!conn) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.protocol = url.protocol.replace('http', 'ws')
|
||||||
|
url.pathname = `/api/ws/terminal`
|
||||||
|
url.searchParams.set('id', id)
|
||||||
|
const token = JSON.parse(localStorage.getItem('token') || '')
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No token found')
|
||||||
|
}
|
||||||
|
url.searchParams.set('token', token)
|
||||||
|
const ws = new WebSocket(url.toString())
|
||||||
|
conn = {
|
||||||
|
ws,
|
||||||
|
callbacks: new Set([callback]),
|
||||||
|
isConnected: false,
|
||||||
|
buffer: [] // 初始化缓存
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = event.data
|
||||||
|
// 保存到缓存
|
||||||
|
conn?.buffer.push(data)
|
||||||
|
if ((conn?.buffer.length ?? 0) > this.MAX_BUFFER_SIZE) {
|
||||||
|
conn?.buffer.shift()
|
||||||
|
}
|
||||||
|
conn?.callbacks.forEach((cb) => cb(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
if (conn) conn.isConnected = true
|
||||||
|
this.sendResize(id, cols, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
if (conn) conn.isConnected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connections.set(id, conn)
|
||||||
|
} else {
|
||||||
|
conn.callbacks.add(callback)
|
||||||
|
// 恢复历史内容
|
||||||
|
conn.buffer.forEach((data) => callback(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn.ws
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectTerminal(id: string, callback: TerminalCallback) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (!conn) return
|
||||||
|
|
||||||
|
conn.callbacks.delete(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTerminal(id: string) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||||
|
conn.ws.close()
|
||||||
|
}
|
||||||
|
this.connections.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendInput(id: string, data: string) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||||
|
conn.ws.send(JSON.stringify({ type: 'input', data }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResize(id: string, cols: number, rows: number) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||||
|
conn.ws.send(JSON.stringify({ type: 'resize', cols, rows }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalManager = new TerminalManager()
|
||||||
|
|
||||||
|
export default terminalManager
|
@@ -24,6 +24,21 @@ export default class WebUIManager {
|
|||||||
return data.data.Credential
|
return data.data.Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async changePassword(oldToken: string, newToken: string) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/auth/update_token',
|
||||||
|
{ oldToken, newToken }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async checkUsingDefaultToken() {
|
||||||
|
const { data } = await serverRequest.get<ServerResponse<boolean>>(
|
||||||
|
'/auth/check_using_default_token'
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
public static async proxy<T>(url = '') {
|
public static async proxy<T>(url = '') {
|
||||||
const data = await serverRequest.get<ServerResponse<string>>(
|
const data = await serverRequest.get<ServerResponse<string>>(
|
||||||
'/base/proxy?url=' + encodeURIComponent(url)
|
'/base/proxy?url=' + encodeURIComponent(url)
|
||||||
@@ -44,6 +59,20 @@ export default class WebUIManager {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getThemeConfig() {
|
||||||
|
const { data } =
|
||||||
|
await serverRequest.get<ServerResponse<ThemeConfig>>('/base/Theme')
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async setThemeConfig(theme: ThemeConfig) {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/base/SetTheme',
|
||||||
|
{ theme }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
public static async getLogList() {
|
public static async getLogList() {
|
||||||
const { data } =
|
const { data } =
|
||||||
await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList')
|
await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList')
|
||||||
|
@@ -14,10 +14,12 @@ const useConfig = () => {
|
|||||||
key: T,
|
key: T,
|
||||||
value: OneBotConfig['network'][T][0]
|
value: OneBotConfig['network'][T][0]
|
||||||
) => {
|
) => {
|
||||||
if (
|
const allNetworkNames = Object.keys(config.network).reduce((acc, key) => {
|
||||||
value.name &&
|
const _key = key as keyof OneBotConfig['network']
|
||||||
config.network[key].some((item) => item.name === value.name)
|
return acc.concat(config.network[_key].map((item) => item.name))
|
||||||
) {
|
}, [] as string[])
|
||||||
|
|
||||||
|
if (value.name && allNetworkNames.includes(value.name)) {
|
||||||
throw new Error('已经存在相同的配置项名')
|
throw new Error('已经存在相同的配置项名')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
69
napcat.webui/src/hooks/use-preload-images.ts
Normal file
69
napcat.webui/src/hooks/use-preload-images.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
// 全局图片缓存
|
||||||
|
const imageCache = new Map<string, HTMLImageElement>()
|
||||||
|
|
||||||
|
export function usePreloadImages(urls: string[]) {
|
||||||
|
const [loadedUrls, setLoadedUrls] = useState<Record<string, boolean>>({})
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const isMounted = useRef(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isMounted.current = true
|
||||||
|
|
||||||
|
// 检查是否所有图片都已缓存
|
||||||
|
const allCached = urls.every((url) => imageCache.has(url))
|
||||||
|
if (allCached) {
|
||||||
|
setLoadedUrls(urls.reduce((acc, url) => ({ ...acc, [url]: true }), {}))
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
const loadedImages: Record<string, boolean> = {}
|
||||||
|
let pendingCount = urls.length
|
||||||
|
|
||||||
|
urls.forEach((url) => {
|
||||||
|
// 如果已经缓存,直接标记为已加载
|
||||||
|
if (imageCache.has(url)) {
|
||||||
|
loadedImages[url] = true
|
||||||
|
pendingCount--
|
||||||
|
if (pendingCount === 0) {
|
||||||
|
setLoadedUrls(loadedImages)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => {
|
||||||
|
if (!isMounted.current) return
|
||||||
|
loadedImages[url] = true
|
||||||
|
imageCache.set(url, img)
|
||||||
|
pendingCount--
|
||||||
|
|
||||||
|
if (pendingCount === 0) {
|
||||||
|
setLoadedUrls(loadedImages)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.onerror = () => {
|
||||||
|
if (!isMounted.current) return
|
||||||
|
loadedImages[url] = false
|
||||||
|
pendingCount--
|
||||||
|
|
||||||
|
if (pendingCount === 0) {
|
||||||
|
setLoadedUrls(loadedImages)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.src = url
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted.current = false
|
||||||
|
}
|
||||||
|
}, [urls])
|
||||||
|
|
||||||
|
return { loadedUrls, isLoading }
|
||||||
|
}
|
@@ -79,7 +79,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
}, [location.pathname])
|
}, [location.pathname])
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen relative flex bg-danger-50 dark:bg-black items-stretch"
|
className="h-screen relative flex bg-primary-50 dark:bg-black items-stretch"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${b64img})`,
|
backgroundImage: `url(${b64img})`,
|
||||||
backgroundSize: 'cover'
|
backgroundSize: 'cover'
|
||||||
@@ -98,10 +98,10 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'h-10 flex items-center hm-medium text-xl backdrop-blur-lg rounded-full',
|
'h-10 flex items-center font-bold text-xl backdrop-blur-lg rounded-full',
|
||||||
'dark:bg-background dark:shadow-danger-100',
|
'dark:bg-background dark:shadow-primary-100',
|
||||||
'bg-background !bg-opacity-50',
|
'bg-background !bg-opacity-50',
|
||||||
'shadow-sm shadow-danger-50',
|
'shadow-sm shadow-primary-50',
|
||||||
'z-30 m-2 mb-0 sticky top-2 left-0'
|
'z-30 m-2 mb-0 sticky top-2 left-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import 'react-photo-view/dist/react-photo-view.css'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
|
||||||
import App from '@/App.tsx'
|
import App from '@/App.tsx'
|
||||||
@@ -7,6 +8,7 @@ import '@/styles/globals.css'
|
|||||||
|
|
||||||
import key from './const/key'
|
import key from './const/key'
|
||||||
import WebUIManager from './controllers/webui_manager'
|
import WebUIManager from './controllers/webui_manager'
|
||||||
|
import { loadTheme } from './utils/theme'
|
||||||
|
|
||||||
WebUIManager.checkWebUiLogined()
|
WebUIManager.checkWebUiLogined()
|
||||||
|
|
||||||
@@ -21,6 +23,8 @@ if (theme && !theme.startsWith('"')) {
|
|||||||
localStorage.setItem(key.theme, JSON.stringify(theme))
|
localStorage.setItem(key.theme, JSON.stringify(theme))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadTheme()
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
// <React.StrictMode>
|
// <React.StrictMode>
|
||||||
<BrowserRouter basename="/webui/">
|
<BrowserRouter basename="/webui/">
|
||||||
|
@@ -1,91 +1,197 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Card, CardBody } from '@heroui/card'
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image'
|
||||||
|
import { Link } from '@heroui/link'
|
||||||
|
import { Skeleton } from '@heroui/skeleton'
|
||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner'
|
||||||
import { useRequest } from 'ahooks'
|
import { useRequest } from 'ahooks'
|
||||||
import clsx from 'clsx'
|
import { useMemo } from 'react'
|
||||||
|
import { BsTelegram, BsTencentQq } from 'react-icons/bs'
|
||||||
|
import { IoDocument } from 'react-icons/io5'
|
||||||
|
|
||||||
import { BietiaopIcon, WebUIIcon } from '@/components/icons'
|
import HoverTiltedCard from '@/components/hover_titled_card'
|
||||||
import NapCatRepoInfo from '@/components/napcat_repo_info'
|
import NapCatRepoInfo from '@/components/napcat_repo_info'
|
||||||
import { title } from '@/components/primitives'
|
import RotatingText from '@/components/rotating_text'
|
||||||
|
|
||||||
|
import { usePreloadImages } from '@/hooks/use-preload-images'
|
||||||
|
import { useTheme } from '@/hooks/use-theme'
|
||||||
|
|
||||||
import logo from '@/assets/images/logo.png'
|
import logo from '@/assets/images/logo.png'
|
||||||
import WebUIManager from '@/controllers/webui_manager'
|
import WebUIManager from '@/controllers/webui_manager'
|
||||||
|
|
||||||
import packageJson from '../../../package.json'
|
|
||||||
|
|
||||||
function VersionInfo() {
|
function VersionInfo() {
|
||||||
const { data, loading, error } = useRequest(WebUIManager.getPackageInfo)
|
const { data, loading, error } = useRequest(WebUIManager.getPackageInfo)
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 mb-5">
|
<div className="flex items-center gap-2 text-2xl font-bold">
|
||||||
<Chip
|
<div className="flex items-center gap-2">
|
||||||
startContent={
|
<div className="text-primary-500 drop-shadow-md">NapCat</div>
|
||||||
<Chip color="danger" size="sm" className="-ml-0.5 select-none">
|
|
||||||
WebUI
|
|
||||||
</Chip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{packageJson.version}
|
|
||||||
</Chip>
|
|
||||||
<Chip
|
|
||||||
startContent={
|
|
||||||
<Chip color="warning" size="sm" className="-ml-0.5 select-none">
|
|
||||||
NapCat
|
|
||||||
</Chip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{error ? (
|
{error ? (
|
||||||
error.message
|
error.message
|
||||||
) : loading ? (
|
) : loading ? (
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
) : (
|
) : (
|
||||||
data?.version
|
<RotatingText
|
||||||
|
texts={['WebUI', data?.version ?? '']}
|
||||||
|
mainClassName="overflow-hidden flex items-center bg-primary-500 px-2 rounded-lg text-default-50 shadow-md"
|
||||||
|
staggerFrom={'last'}
|
||||||
|
initial={{ y: '100%' }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
exit={{ y: '-120%' }}
|
||||||
|
staggerDuration={0.025}
|
||||||
|
splitLevelClassName="overflow-hidden"
|
||||||
|
transition={{ type: 'spring', damping: 30, stiffness: 400 }}
|
||||||
|
rotationInterval={2000}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Chip>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
|
const { isDark } = useTheme()
|
||||||
|
|
||||||
|
const imageUrls = useMemo(
|
||||||
|
() => [
|
||||||
|
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=light',
|
||||||
|
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=dark',
|
||||||
|
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=light',
|
||||||
|
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=dark'
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { loadedUrls, isLoading } = usePreloadImages(imageUrls)
|
||||||
|
|
||||||
|
const getImageUrl = useMemo(
|
||||||
|
() => (baseUrl: string) => {
|
||||||
|
const theme = isDark ? 'dark' : 'light'
|
||||||
|
const fullUrl = baseUrl.replace(
|
||||||
|
/color_scheme=(?:light|dark)/,
|
||||||
|
`color_scheme=${theme}`
|
||||||
|
)
|
||||||
|
return isLoading ? null : loadedUrls[fullUrl] ? fullUrl : null
|
||||||
|
},
|
||||||
|
[isDark, isLoading, loadedUrls]
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderImage = useMemo(
|
||||||
|
() => (baseUrl: string, alt: string) => {
|
||||||
|
const imageUrl = getImageUrl(baseUrl)
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
return <Skeleton className="h-16 rounded-lg" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
className="flex-1 pointer-events-none select-none rounded-none"
|
||||||
|
src={imageUrl}
|
||||||
|
alt={alt}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[getImageUrl]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>关于 NapCat WebUI</title>
|
<title>关于 NapCat WebUI</title>
|
||||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
<section className="max-w-7xl py-8 md:py-10 px-5 mx-auto space-y-10">
|
||||||
<div className="max-w-full w-[1000px] px-5 flex flex-col items-center">
|
<div className="w-full flex flex-col md:flex-row gap-4">
|
||||||
<div className="flex flex-col md:flex-row items-center">
|
<div className="flex flex-col md:flex-row items-center">
|
||||||
<Image
|
<HoverTiltedCard imageSrc={logo} overlayContent="" />
|
||||||
alt="logo"
|
|
||||||
className="flex-shrink-0 w-52 md:w-48 mr-2"
|
|
||||||
src={logo}
|
|
||||||
/>
|
|
||||||
<div className="flex -mt-9 md:mt-0">
|
|
||||||
<WebUIIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex opacity-60 items-center gap-2 mb-2 font-ubuntu">
|
|
||||||
Created By
|
|
||||||
<div className="flex scale-80 -ml-5 -mr-5">
|
|
||||||
<BietiaopIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1 flex flex-col gap-2 py-2">
|
||||||
<VersionInfo />
|
<VersionInfo />
|
||||||
<div className="mb-6 flex flex-col items-center gap-4">
|
<div className="space-y-1">
|
||||||
<p
|
<p className="font-bold text-primary-400">NapCat 是什么?</p>
|
||||||
className={clsx(
|
<p className="text-default-800">
|
||||||
title({
|
基于TypeScript构建的Bot框架,通过相应的启动器或者框架,主动调用QQ
|
||||||
color: 'cyan',
|
Node模块提供给客户端的接口,实现Bot的功能.
|
||||||
shadow: true
|
</p>
|
||||||
}),
|
<p className="font-bold text-primary-400">魔法版介绍</p>
|
||||||
'!text-3xl'
|
<p className="text-default-800">
|
||||||
)}
|
猫猫框架通过魔法的手段获得了 QQ 的发送消息、接收消息等接口。
|
||||||
>
|
为了方便使用,猫猫框架将通过一种名为 OneBot 的约定将你的 HTTP /
|
||||||
NapCat Contributors
|
WebSocket 请求按照规范读取,
|
||||||
|
再去调用猫猫框架所获得的QQ发送接口之类的接口。
|
||||||
</p>
|
</p>
|
||||||
<Image
|
|
||||||
className="w-[600px] max-w-full pointer-events-none select-none"
|
|
||||||
src="https://contrib.rocks/image?repo=bietiaop/NapCatQQ"
|
|
||||||
alt="Contributors"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 flex-wrap justify-around">
|
||||||
|
<Card
|
||||||
|
as={Link}
|
||||||
|
shadow="sm"
|
||||||
|
isPressable
|
||||||
|
isExternal
|
||||||
|
href="https://qm.qq.com/q/F9cgs1N3Mc"
|
||||||
|
>
|
||||||
|
<CardBody className="flex-row items-center gap-2">
|
||||||
|
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||||
|
<BsTencentQq size={16} />
|
||||||
|
</span>
|
||||||
|
<span>官方社群1</span>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
as={Link}
|
||||||
|
shadow="sm"
|
||||||
|
isPressable
|
||||||
|
isExternal
|
||||||
|
href="https://qm.qq.com/q/hSt0u9PVn"
|
||||||
|
>
|
||||||
|
<CardBody className="flex-row items-center gap-2">
|
||||||
|
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||||
|
<BsTencentQq size={16} />
|
||||||
|
</span>
|
||||||
|
<span>官方社群2</span>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
as={Link}
|
||||||
|
shadow="sm"
|
||||||
|
isPressable
|
||||||
|
isExternal
|
||||||
|
href="https://t.me/MelodicMoonlight"
|
||||||
|
>
|
||||||
|
<CardBody className="flex-row items-center gap-2">
|
||||||
|
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||||
|
<BsTelegram size={16} />
|
||||||
|
</span>
|
||||||
|
<span>Telegram</span>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
as={Link}
|
||||||
|
shadow="sm"
|
||||||
|
isPressable
|
||||||
|
isExternal
|
||||||
|
href="https://napcat.napneko.icu/"
|
||||||
|
>
|
||||||
|
<CardBody className="flex-row items-center gap-2">
|
||||||
|
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||||
|
<IoDocument size={16} />
|
||||||
|
</span>
|
||||||
|
<span>使用文档</span>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-4">
|
||||||
|
<div className="w-full flex flex-col gap-4">
|
||||||
|
{renderImage(
|
||||||
|
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=light',
|
||||||
|
'Contributors'
|
||||||
|
)}
|
||||||
|
{renderImage(
|
||||||
|
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=light',
|
||||||
|
'Activity Trends'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<NapCatRepoInfo />
|
<NapCatRepoInfo />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
81
napcat.webui/src/pages/dashboard/config/change_password.tsx
Normal file
81
napcat.webui/src/pages/dashboard/config/change_password.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Input } from '@heroui/input'
|
||||||
|
import { useLocalStorage } from '@uidotdev/usehooks'
|
||||||
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import key from '@/const/key'
|
||||||
|
|
||||||
|
import SaveButtons from '@/components/button/save_buttons'
|
||||||
|
|
||||||
|
import WebUIManager from '@/controllers/webui_manager'
|
||||||
|
|
||||||
|
const ChangePasswordCard = () => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit: handleWebuiSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
reset
|
||||||
|
} = useForm<{
|
||||||
|
oldToken: string
|
||||||
|
newToken: string
|
||||||
|
}>({
|
||||||
|
defaultValues: {
|
||||||
|
oldToken: '',
|
||||||
|
newToken: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [_, setToken] = useLocalStorage(key.token, '')
|
||||||
|
|
||||||
|
const onSubmit = handleWebuiSubmit(async (data) => {
|
||||||
|
try {
|
||||||
|
await WebUIManager.changePassword(data.oldToken, data.newToken)
|
||||||
|
toast.success('修改成功')
|
||||||
|
setToken('')
|
||||||
|
localStorage.removeItem(key.token)
|
||||||
|
navigate('/web_login')
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message
|
||||||
|
toast.error(`修改失败: ${msg}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>修改密码 - NapCat WebUI</title>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="oldToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
label="旧密码"
|
||||||
|
placeholder="请输入旧密码"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="newToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
label="新密码"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SaveButtons
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
reset={reset}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangePasswordCard
|
@@ -1,111 +1,48 @@
|
|||||||
|
import { Card, CardBody } from '@heroui/card'
|
||||||
import { Tab, Tabs } from '@heroui/tabs'
|
import { Tab, Tabs } from '@heroui/tabs'
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
import clsx from 'clsx'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import toast from 'react-hot-toast'
|
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
import key from '@/const/key'
|
import ChangePasswordCard from './change_password'
|
||||||
|
import LoginConfigCard from './login'
|
||||||
import PageLoading from '@/components/page_loading'
|
|
||||||
|
|
||||||
import useConfig from '@/hooks/use-config'
|
|
||||||
import useMusic from '@/hooks/use-music'
|
|
||||||
|
|
||||||
import OneBotConfigCard from './onebot'
|
import OneBotConfigCard from './onebot'
|
||||||
|
import ThemeConfigCard from './theme'
|
||||||
import WebUIConfigCard from './webui'
|
import WebUIConfigCard from './webui'
|
||||||
|
|
||||||
export default function ConfigPage() {
|
export interface ConfigPageProps {
|
||||||
const { config, saveConfigWithoutNetwork, refreshConfig } = useConfig()
|
children?: React.ReactNode
|
||||||
const [loading, setLoading] = useState(false)
|
size?: 'sm' | 'md' | 'lg'
|
||||||
const {
|
|
||||||
control: onebotControl,
|
|
||||||
handleSubmit: handleOnebotSubmit,
|
|
||||||
formState: { isSubmitting: isOnebotSubmitting },
|
|
||||||
setValue: setOnebotValue
|
|
||||||
} = useForm<IConfig['onebot']>({
|
|
||||||
defaultValues: {
|
|
||||||
musicSignUrl: '',
|
|
||||||
enableLocalFile2Url: false,
|
|
||||||
parseMultMsg: false
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
const ConfingPageItem: React.FC<ConfigPageProps> = ({
|
||||||
control: webuiControl,
|
children,
|
||||||
handleSubmit: handleWebuiSubmit,
|
size = 'md'
|
||||||
formState: { isSubmitting: isWebuiSubmitting },
|
}) => {
|
||||||
setValue: setWebuiValue
|
return (
|
||||||
} = useForm<IConfig['webui']>({
|
<Card className="bg-opacity-50 backdrop-blur-sm">
|
||||||
defaultValues: {
|
<CardBody className="items-center py-5">
|
||||||
background: '',
|
<div
|
||||||
musicListID: '',
|
className={clsx('max-w-full flex flex-col gap-2', {
|
||||||
customIcons: {}
|
'w-72': size === 'sm',
|
||||||
}
|
'w-96': size === 'md',
|
||||||
})
|
'w-[32rem]': size === 'lg'
|
||||||
|
})}
|
||||||
const isMediumUp = useMediaQuery({ minWidth: 768 })
|
>
|
||||||
const [b64img, setB64img] = useLocalStorage(key.backgroundImage, '')
|
{children}
|
||||||
const [customIcons, setCustomIcons] = useLocalStorage<Record<string, string>>(
|
</div>
|
||||||
key.customIcons,
|
</CardBody>
|
||||||
{}
|
</Card>
|
||||||
)
|
)
|
||||||
const { setListId, listId } = useMusic()
|
|
||||||
const resetOneBot = () => {
|
|
||||||
setOnebotValue('musicSignUrl', config.musicSignUrl)
|
|
||||||
setOnebotValue('enableLocalFile2Url', config.enableLocalFile2Url)
|
|
||||||
setOnebotValue('parseMultMsg', config.parseMultMsg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetWebUI = () => {
|
export default function ConfigPage() {
|
||||||
setWebuiValue('musicListID', listId)
|
const isMediumUp = useMediaQuery({ minWidth: 768 })
|
||||||
setWebuiValue('customIcons', customIcons)
|
const navigate = useNavigate()
|
||||||
setWebuiValue('background', b64img)
|
const search = useSearchParams({
|
||||||
}
|
tab: 'onebot'
|
||||||
|
})[0]
|
||||||
const onOneBotSubmit = handleOnebotSubmit((data) => {
|
const tab = search.get('tab') ?? 'onebot'
|
||||||
try {
|
|
||||||
saveConfigWithoutNetwork(data)
|
|
||||||
toast.success('保存成功')
|
|
||||||
} catch (error) {
|
|
||||||
const msg = (error as Error).message
|
|
||||||
toast.error(`保存失败: ${msg}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onWebuiSubmit = handleWebuiSubmit((data) => {
|
|
||||||
try {
|
|
||||||
setListId(data.musicListID)
|
|
||||||
setCustomIcons(data.customIcons)
|
|
||||||
setB64img(data.background)
|
|
||||||
toast.success('保存成功')
|
|
||||||
} catch (error) {
|
|
||||||
const msg = (error as Error).message
|
|
||||||
toast.error(`保存失败: ${msg}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onRefresh = async (shotTip = true) => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
await refreshConfig()
|
|
||||||
if (shotTip) toast.success('刷新成功')
|
|
||||||
} catch (error) {
|
|
||||||
const msg = (error as Error).message
|
|
||||||
toast.error(`刷新失败: ${msg}`)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resetOneBot()
|
|
||||||
resetWebUI()
|
|
||||||
}, [config])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onRefresh(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10">
|
<section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10">
|
||||||
@@ -114,6 +51,10 @@ export default function ConfigPage() {
|
|||||||
fullWidth
|
fullWidth
|
||||||
className="w-full"
|
className="w-full"
|
||||||
isVertical={isMediumUp}
|
isVertical={isMediumUp}
|
||||||
|
selectedKey={tab}
|
||||||
|
onSelectionChange={(key) => {
|
||||||
|
navigate(`/config?tab=${key}`)
|
||||||
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm',
|
tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm',
|
||||||
panel: 'w-full relative',
|
panel: 'w-full relative',
|
||||||
@@ -122,23 +63,30 @@ export default function ConfigPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab title="OneBot配置" key="onebot">
|
<Tab title="OneBot配置" key="onebot">
|
||||||
<PageLoading loading={loading} />
|
<ConfingPageItem>
|
||||||
<OneBotConfigCard
|
<OneBotConfigCard />
|
||||||
isSubmitting={isOnebotSubmitting}
|
</ConfingPageItem>
|
||||||
onRefresh={onRefresh}
|
|
||||||
onSubmit={onOneBotSubmit}
|
|
||||||
control={onebotControl}
|
|
||||||
reset={resetOneBot}
|
|
||||||
/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="WebUI配置" key="webui">
|
<Tab title="WebUI配置" key="webui">
|
||||||
<WebUIConfigCard
|
<ConfingPageItem>
|
||||||
isSubmitting={isWebuiSubmitting}
|
<WebUIConfigCard />
|
||||||
onRefresh={onRefresh}
|
</ConfingPageItem>
|
||||||
onSubmit={onWebuiSubmit}
|
</Tab>
|
||||||
control={webuiControl}
|
<Tab title="登录配置" key="login">
|
||||||
reset={resetWebUI}
|
<ConfingPageItem>
|
||||||
/>
|
<LoginConfigCard />
|
||||||
|
</ConfingPageItem>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="修改密码" key="token">
|
||||||
|
<ConfingPageItem>
|
||||||
|
<ChangePasswordCard />
|
||||||
|
</ConfingPageItem>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab title="主题配置" key="theme">
|
||||||
|
<ConfingPageItem size="lg">
|
||||||
|
<ThemeConfigCard />
|
||||||
|
</ConfingPageItem>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</section>
|
</section>
|
||||||
|
89
napcat.webui/src/pages/dashboard/config/login.tsx
Normal file
89
napcat.webui/src/pages/dashboard/config/login.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Input } from '@heroui/input'
|
||||||
|
import { useRequest } from 'ahooks'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import SaveButtons from '@/components/button/save_buttons'
|
||||||
|
import PageLoading from '@/components/page_loading'
|
||||||
|
|
||||||
|
import QQManager from '@/controllers/qq_manager'
|
||||||
|
|
||||||
|
const LoginConfigCard = () => {
|
||||||
|
const {
|
||||||
|
data: quickLoginData,
|
||||||
|
loading: quickLoginLoading,
|
||||||
|
error: quickLoginError,
|
||||||
|
refreshAsync: refreshQuickLogin
|
||||||
|
} = useRequest(QQManager.getQuickLoginQQ)
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit: handleOnebotSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
setValue: setOnebotValue
|
||||||
|
} = useForm<{
|
||||||
|
quickLoginQQ: string
|
||||||
|
}>({
|
||||||
|
defaultValues: {
|
||||||
|
quickLoginQQ: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setOnebotValue('quickLoginQQ', quickLoginData ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = handleOnebotSubmit(async (data) => {
|
||||||
|
try {
|
||||||
|
await QQManager.setQuickLoginQQ(data.quickLoginQQ)
|
||||||
|
toast.success('保存成功')
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message
|
||||||
|
toast.error(`保存失败: ${msg}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
try {
|
||||||
|
await refreshQuickLogin()
|
||||||
|
toast.success('刷新成功')
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message
|
||||||
|
toast.error(`刷新失败: ${msg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset()
|
||||||
|
}, [quickLoginData])
|
||||||
|
|
||||||
|
if (quickLoginLoading) return <PageLoading loading={true} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>OneBot配置 - NapCat WebUI</title>
|
||||||
|
<div className="flex-shrink-0 w-full">快速登录QQ</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="quickLoginQQ"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
label="快速登录QQ"
|
||||||
|
placeholder="请输入QQ号"
|
||||||
|
isDisabled={!!quickLoginError}
|
||||||
|
errorMessage={quickLoginError ? '获取快速登录QQ失败' : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SaveButtons
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
reset={reset}
|
||||||
|
isSubmitting={isSubmitting || quickLoginLoading}
|
||||||
|
refresh={onRefresh}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginConfigCard
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user