mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
206 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2d921c4577 | ||
![]() |
9accff7323 | ||
![]() |
88b1ee8c31 | ||
![]() |
3ac618bb4e | ||
![]() |
0051df3741 | ||
![]() |
7eb4e010b0 | ||
![]() |
33cc23ada3 | ||
![]() |
e5aee372e3 | ||
![]() |
6b6ce4a761 | ||
![]() |
8c4ea7f8f2 | ||
![]() |
c8b268b806 | ||
![]() |
cf5e0e0f14 | ||
![]() |
7b79f9cc17 | ||
![]() |
708d599966 | ||
![]() |
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 | ||
![]() |
7e1fe31085 | ||
![]() |
c3cba8ba4e | ||
![]() |
ba619986c9 | ||
![]() |
dcef3f3c3b | ||
![]() |
823faa2790 | ||
![]() |
ef4248d2a3 | ||
![]() |
3917cb0dc9 | ||
![]() |
520cec0eaa | ||
![]() |
e7655e0ff6 | ||
![]() |
350ced55c0 | ||
![]() |
2ca6d0a00e | ||
![]() |
844abad0d0 | ||
![]() |
d278e9d8bc | ||
![]() |
6e261f30c2 | ||
![]() |
84f0e43369 | ||
![]() |
3223a06983 | ||
![]() |
1b874a0264 | ||
![]() |
26525a0ff9 | ||
![]() |
49234ea5c7 | ||
![]() |
c474158a09 | ||
![]() |
813a541e7f | ||
![]() |
efd489bfd4 |
@@ -12,7 +12,7 @@ insert_final_newline = true
|
||||
# Set default charset
|
||||
charset = utf-8
|
||||
|
||||
# 2 space indentation
|
||||
# 4 space indentation
|
||||
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -21,4 +21,4 @@ indent_size = 4
|
||||
charset = latin1
|
||||
|
||||
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
|
||||
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.
|
||||
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# Develop
|
||||
node_modules/
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
out/
|
||||
dist/
|
||||
/src/core.lib/common/
|
||||
@@ -13,4 +14,4 @@ devconfig/*
|
||||
# Build
|
||||
*.db
|
||||
checkVersion.sh
|
||||
bun.lockb
|
||||
bun.lockb
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -5,5 +5,6 @@
|
||||
".env.universal": ".env.*",
|
||||
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts",
|
||||
"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 _import from "eslint-plugin-import";
|
||||
import { fixupPluginRules } from "@eslint/compat";
|
||||
import eslint from '@eslint/js';
|
||||
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import tsEslintParser from '@typescript-eslint/parser';
|
||||
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),
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"],
|
||||
},
|
||||
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
const customTsFlatConfig = [
|
||||
{
|
||||
name: 'typescript-eslint/base',
|
||||
languageOptions: {
|
||||
parser: tsEslintParser,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
NodeJS: 'readonly', // 添加 NodeJS 全局变量
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
indent: ["error", 4],
|
||||
semi: ["error", "always"],
|
||||
"no-unused-vars": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
},
|
||||
}, {
|
||||
files: ["**/.eslintrc.{js,cjs}"],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
...tsEslintPlugin.configs.recommended.rules,
|
||||
'quotes': ['error', 'single'], // 使用单引号
|
||||
'semi': ['error', 'always'], // 强制使用分号
|
||||
'indent': ['error', 4], // 使用 4 空格缩进
|
||||
},
|
||||
ecmaVersion: 5,
|
||||
sourceType: "commonjs",
|
||||
plugins: {
|
||||
'@typescript-eslint': tsEslintPlugin,
|
||||
},
|
||||
ignores: ['src/webui/**'], // 忽略 src/webui/ 目录所有文件
|
||||
},
|
||||
}];
|
||||
];
|
||||
|
||||
export default [eslint.configs.recommended, ...customTsFlatConfig];
|
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.4.6",
|
||||
"version": "4.5.23",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -4,14 +4,16 @@
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host=0.0.0.0",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint -c eslint.config.mjs ./src/**/**/*.{ts,tsx} --fix",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.4.0",
|
||||
"@monaco-editor/react": "4.7.0-rc.0",
|
||||
"@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/breadcrumbs": "2.2.7",
|
||||
"@heroui/button": "2.2.10",
|
||||
@@ -28,76 +30,88 @@
|
||||
"@heroui/listbox": "2.3.10",
|
||||
"@heroui/modal": "2.2.8",
|
||||
"@heroui/navbar": "2.2.9",
|
||||
"@heroui/pagination": "^2.2.9",
|
||||
"@heroui/popover": "2.3.10",
|
||||
"@heroui/select": "2.4.10",
|
||||
"@heroui/skeleton": "^2.2.6",
|
||||
"@heroui/slider": "2.4.8",
|
||||
"@heroui/snippet": "2.2.11",
|
||||
"@heroui/spinner": "2.2.7",
|
||||
"@heroui/switch": "2.2.9",
|
||||
"@heroui/system": "2.4.7",
|
||||
"@heroui/table": "^2.2.9",
|
||||
"@heroui/tabs": "2.2.8",
|
||||
"@heroui/theme": "2.4.6",
|
||||
"@heroui/tooltip": "2.2.8",
|
||||
"@react-aria/visually-hidden": "3.8.18",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@monaco-editor/loader": "^1.4.0",
|
||||
"@monaco-editor/react": "4.7.0-rc.0",
|
||||
"@react-aria/visually-hidden": "^3.8.19",
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "2.1.1",
|
||||
"clsx": "^2.1.1",
|
||||
"echarts": "^5.5.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"framer-motion": "^11.15.0",
|
||||
"framer-motion": "^12.0.6",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"motion": "^11.15.0",
|
||||
"motion": "^12.0.6",
|
||||
"path-browserify": "^1.0.1",
|
||||
"qface": "^1.4.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"quill": "^2.0.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "^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-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-photo-view": "^1.2.7",
|
||||
"react-redux": "^9.2.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-window": "^1.8.11",
|
||||
"tailwind-variants": "0.3.0",
|
||||
"tailwindcss": "3.4.17",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-variants": "^0.3.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@eslint/js": "^9.19.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/fabric": "^5.3.9",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.18.1",
|
||||
"@typescript-eslint/parser": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"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-hooks": "^5.1.0",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "8.4.49",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "5.7.2",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"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 IndexPage = lazy(() => import('@/pages/index'))
|
||||
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() {
|
||||
return (
|
||||
@@ -58,9 +68,21 @@ function AuthChecker({ children }: { children: React.ReactNode }) {
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<IndexPage />} path="/*" />
|
||||
<Route element={<QQLoginPage />} path="/qq_login" />
|
||||
<Route element={<WebLoginPage />} path="/web_login" />
|
||||
<Route path="/" element={<IndexPage />}>
|
||||
<Route index element={<DashboardIndexPage />} />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
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
|
@@ -187,7 +187,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'fixed right-0 bottom-0 z-[9999] w-full md:w-96',
|
||||
'fixed right-0 bottom-0 z-[52] w-full md:w-96',
|
||||
!translateX && !translateY && 'transition-transform',
|
||||
isCollapsed && 'md:hover:!translate-x-80'
|
||||
)}
|
||||
@@ -231,7 +231,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
||||
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
|
||||
)}
|
||||
variant="solid"
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
|
@@ -33,7 +33,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
||||
>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
startContent={<IoAddCircleOutline className="text-2xl" />}
|
||||
>
|
||||
新建
|
||||
|
@@ -1,21 +1,29 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import clsx from 'clsx'
|
||||
import toast from 'react-hot-toast'
|
||||
import { IoMdRefresh } from 'react-icons/io'
|
||||
|
||||
export interface SaveButtonsProps {
|
||||
onSubmit: () => void
|
||||
reset: () => void
|
||||
refresh: () => void
|
||||
refresh?: () => void
|
||||
isSubmitting: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SaveButtons: React.FC<SaveButtonsProps> = ({
|
||||
onSubmit,
|
||||
reset,
|
||||
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">
|
||||
<Button
|
||||
color="default"
|
||||
@@ -33,15 +41,17 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="secondary"
|
||||
radius="full"
|
||||
variant="flat"
|
||||
onPress={() => refresh()}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
</Button>
|
||||
{refresh && (
|
||||
<Button
|
||||
isIconOnly
|
||||
color="secondary"
|
||||
radius="full"
|
||||
variant="flat"
|
||||
onPress={() => refresh()}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@@ -110,7 +110,7 @@ const AudioInsert = () => {
|
||||
<Tooltip content="发送音频">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoMic className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -120,7 +120,7 @@ const AudioInsert = () => {
|
||||
<Tooltip content="上传音频">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -137,7 +137,7 @@ const AudioInsert = () => {
|
||||
<PopoverTrigger tooltip="输入音频地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -154,7 +154,7 @@ const AudioInsert = () => {
|
||||
placeholder="请输入音频地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
@@ -177,7 +177,7 @@ const AudioInsert = () => {
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -190,7 +190,7 @@ const AudioInsert = () => {
|
||||
<PopoverContent className="flex-col gap-2 p-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color={isRecording ? 'danger' : 'danger'}
|
||||
color={isRecording ? 'primary' : 'primary'}
|
||||
variant="flat"
|
||||
onPress={isRecording ? stopRecording : startRecording}
|
||||
>
|
||||
@@ -198,7 +198,7 @@ const AudioInsert = () => {
|
||||
</Button>
|
||||
{showPreview && audioPreview && (
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={handleShowPreview}
|
||||
>
|
||||
@@ -212,7 +212,7 @@ const AudioInsert = () => {
|
||||
className={clsx(
|
||||
'w-4 h-4 rounded-full',
|
||||
isRecording
|
||||
? 'animate-pulse bg-danger-400'
|
||||
? 'animate-pulse bg-primary-400'
|
||||
: 'bg-success-400'
|
||||
)}
|
||||
></span>
|
||||
|
@@ -10,7 +10,7 @@ const DiceInsert = () => {
|
||||
return (
|
||||
<Tooltip content="发送骰子">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -55,7 +55,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
||||
<Tooltip content="插入表情">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<MdEmojiEmotions className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -65,7 +65,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
||||
{visibleEmojis.map((emoji) => (
|
||||
<Button
|
||||
key={emoji.id}
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -35,7 +35,7 @@ const FileInsert = () => {
|
||||
<Tooltip content="发送文件">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<FaFolder className="text-lg" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -45,7 +45,7 @@ const FileInsert = () => {
|
||||
<Tooltip content="上传文件">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -62,7 +62,7 @@ const FileInsert = () => {
|
||||
<PopoverTrigger tooltip="输入文件地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -79,7 +79,7 @@ const FileInsert = () => {
|
||||
placeholder="请输入文件地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -23,7 +23,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<Tooltip content="插入图片">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<MdImage className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -33,7 +33,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<Tooltip content="上传图片">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -50,7 +50,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<PopoverTrigger tooltip="输入图片地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -67,7 +67,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
placeholder="请输入图片地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -80,7 +80,7 @@ const MusicInsert = () => {
|
||||
<Tooltip content="发送音乐">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoMusicalNotes className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -132,7 +132,7 @@ const MusicInsert = () => {
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
onPress={() => {
|
||||
@@ -236,7 +236,7 @@ const MusicInsert = () => {
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
type="submit"
|
||||
|
@@ -19,7 +19,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
||||
<Tooltip content="回复消息">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<BsChatQuoteFill className="text-lg" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -38,7 +38,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -10,7 +10,7 @@ const RPSInsert = () => {
|
||||
return (
|
||||
<Tooltip content="发送猜拳">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -35,7 +35,7 @@ const VideoInsert = () => {
|
||||
<Tooltip content="发送视频">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoVideocam className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -45,7 +45,7 @@ const VideoInsert = () => {
|
||||
<Tooltip content="上传视频">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -62,7 +62,7 @@ const VideoInsert = () => {
|
||||
<PopoverTrigger tooltip="输入视频地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -79,7 +79,7 @@ const VideoInsert = () => {
|
||||
placeholder="请输入视频地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -190,7 +190,7 @@ const ChatInput = () => {
|
||||
<DiceInsert />
|
||||
<RPSInsert />
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
const messages = getChatMessage()
|
||||
showStructuredMessage(messages)
|
||||
|
@@ -15,7 +15,7 @@ export default function ChatInputModal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||
构造聊天消息
|
||||
</Button>
|
||||
<Modal
|
||||
@@ -36,7 +36,7 @@ export default function ChatInputModal() {
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" onPress={onClose} variant="flat">
|
||||
<Button color="primary" onPress={onClose} variant="flat">
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -19,12 +19,6 @@ loader.config({
|
||||
}
|
||||
})
|
||||
|
||||
loader.config({
|
||||
'vs/nls': {
|
||||
availableLanguages: { '*': 'zh-cn' }
|
||||
}
|
||||
})
|
||||
|
||||
export interface CodeEditorProps extends React.ComponentProps<typeof Editor> {
|
||||
test?: string
|
||||
}
|
||||
|
@@ -78,7 +78,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
||||
{debug ? '关闭调试' : '开启调试'}
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
startContent={<MdDeleteForever />}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
|
@@ -19,7 +19,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||
className={clsx(
|
||||
'bg-opacity-60 shadow-sm md:rounded-3xl',
|
||||
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'
|
||||
)}
|
||||
shadow="sm"
|
||||
@@ -27,7 +27,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||
<CardBody className="items-center md:gap-1 p-1 md:p-2">
|
||||
<div
|
||||
className={clsx(
|
||||
'font-outfit flex-1',
|
||||
'flex-1',
|
||||
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
||||
title({
|
||||
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">
|
||||
{loading && <PageLoading />}
|
||||
{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">
|
||||
—— <span className="text-default-400">{data?.from}</span>{' '}
|
||||
{data?.from_who}
|
||||
@@ -52,7 +52,7 @@ export default function Hitokoto() {
|
||||
isLoading={loading}
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
@@ -1125,7 +1125,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1152,7 +1152,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="0ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1173,7 +1173,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1197,7 +1197,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="800ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1220,7 +1220,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1247,7 +1247,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="1600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1270,7 +1270,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1297,7 +1297,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="2400ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1320,7 +1320,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1344,7 +1344,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="3200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1372,7 +1372,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1399,7 +1399,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="0ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1422,7 +1422,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1446,7 +1446,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1469,7 +1469,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1496,7 +1496,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="1200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1519,7 +1519,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1543,7 +1543,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="1800ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1566,7 +1566,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1590,7 +1590,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="2400ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1613,7 +1613,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1637,7 +1637,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="3000ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1660,7 +1660,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1684,7 +1684,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="3600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1707,7 +1707,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
>
|
||||
<g
|
||||
id="svgGroup"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke="#000"
|
||||
fill="transparent"
|
||||
style={{
|
||||
@@ -1731,7 +1731,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="4200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1744,3 +1744,224 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
</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('')
|
||||
if (inputRef.current) inputRef.current.value = ''
|
||||
}}
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
>
|
||||
|
@@ -16,13 +16,13 @@ const logLevelColor: {
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'primary'
|
||||
} = {
|
||||
[LogLevel.DEBUG]: 'default',
|
||||
[LogLevel.INFO]: 'primary',
|
||||
[LogLevel.WARN]: 'warning',
|
||||
[LogLevel.ERROR]: 'danger',
|
||||
[LogLevel.FATAL]: 'danger'
|
||||
[LogLevel.ERROR]: 'primary',
|
||||
[LogLevel.FATAL]: 'primary'
|
||||
}
|
||||
const LogLevelSelect = (props: LogLevelSelectProps) => {
|
||||
const { selectedKeys, onSelectionChange } = props
|
||||
|
@@ -47,7 +47,6 @@ const RealTimeLogs = () => {
|
||||
}
|
||||
return logLevel.has(log.level)
|
||||
})
|
||||
.slice(-100)
|
||||
.map((log) => colorizeLogLevelWithTag(log.message, log.level))
|
||||
.join('\r\n')
|
||||
Xterm.current?.clear()
|
||||
@@ -65,7 +64,6 @@ const RealTimeLogs = () => {
|
||||
useEffect(() => {
|
||||
const subscribeLogs = () => {
|
||||
try {
|
||||
console.log('subscribeLogs')
|
||||
const source = WebUIManager.getRealTimeLogs((data) => {
|
||||
setDataArr((prev) => {
|
||||
const newData = [...prev, ...data]
|
||||
|
@@ -13,12 +13,15 @@ export interface ModalProps {
|
||||
content: React.ReactNode
|
||||
title?: React.ReactNode
|
||||
size?: React.ComponentProps<typeof NextUIModal>['size']
|
||||
scrollBehavior?: React.ComponentProps<typeof NextUIModal>['scrollBehavior']
|
||||
onClose?: () => void
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
backdrop?: 'opaque' | 'blur' | 'transparent'
|
||||
showCancel?: boolean
|
||||
dismissible?: boolean
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
@@ -26,12 +29,14 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
backdrop = 'blur',
|
||||
title,
|
||||
content,
|
||||
size = 'md',
|
||||
showCancel = true,
|
||||
dismissible,
|
||||
confirmText = '确定',
|
||||
cancelText = '取消',
|
||||
onClose,
|
||||
onConfirm,
|
||||
onCancel
|
||||
onCancel,
|
||||
...rest
|
||||
} = props
|
||||
const { onClose: onNativeClose } = useDisclosure()
|
||||
|
||||
@@ -44,11 +49,11 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
onClose?.()
|
||||
onNativeClose()
|
||||
}}
|
||||
size={size}
|
||||
classNames={{
|
||||
backdrop: 'z-[99999999]',
|
||||
wrapper: 'z-[999999999]'
|
||||
backdrop: 'z-[99]',
|
||||
wrapper: 'z-[99]'
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<ModalContent>
|
||||
{(nativeClose) => (
|
||||
@@ -56,18 +61,18 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
{title && (
|
||||
<ModalHeader className="flex flex-col gap-1">{title}</ModalHeader>
|
||||
)}
|
||||
<ModalBody>{content}</ModalBody>
|
||||
<ModalBody className="break-all">{content}</ModalBody>
|
||||
<ModalFooter>
|
||||
{showCancel && (
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
onCancel?.()
|
||||
nativeClose()
|
||||
}}
|
||||
>
|
||||
取消
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -77,7 +82,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
nativeClose()
|
||||
}}
|
||||
>
|
||||
确定
|
||||
{confirmText}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
|
@@ -28,7 +28,7 @@ import type {
|
||||
|
||||
function displayData(data: number, loading: boolean, error?: Error) {
|
||||
if (error) {
|
||||
return <MdError className="text-danger-400" />
|
||||
return <MdError className="text-primary-400" />
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -175,7 +175,7 @@ export default function NapCatRepoInfo() {
|
||||
className="group h-auto py-3"
|
||||
endContent={
|
||||
releaseError ? (
|
||||
<MdError className="text-danger-400" />
|
||||
<MdError className="text-primary-400" />
|
||||
) : releaseLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
@@ -229,7 +229,7 @@ export default function NapCatRepoInfo() {
|
||||
</span>
|
||||
}
|
||||
startContent={
|
||||
<IconWrapper className="bg-danger/10 text-danger dark:text-danger-500">
|
||||
<IconWrapper className="bg-primary/10 text-primary dark:text-primary-500">
|
||||
<BookIcon />
|
||||
</IconWrapper>
|
||||
}
|
||||
|
@@ -150,7 +150,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
isDisabled={formState.isSubmitting}
|
||||
variant="light"
|
||||
onPress={onClose}
|
||||
|
@@ -20,7 +20,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
||||
enable: false,
|
||||
name: '',
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
port: 3001,
|
||||
reportSelfMessage: false,
|
||||
enableForcePushEvent: true,
|
||||
messagePostFormat: 'array',
|
||||
|
@@ -2,12 +2,14 @@ import { Button } from '@heroui/button'
|
||||
import { Card, CardBody, CardHeader } from '@heroui/card'
|
||||
import { Input } from '@heroui/input'
|
||||
import { Snippet } from '@heroui/snippet'
|
||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
||||
import { motion } from 'motion/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { IoLink, IoSend } from 'react-icons/io5'
|
||||
import { PiCatDuotone } from 'react-icons/pi'
|
||||
|
||||
import key from '@/const/key'
|
||||
import { OneBotHttpApiContent, OneBotHttpApiPath } from '@/const/ob_api'
|
||||
|
||||
import ChatInputModal from '@/components/chat_input/modal'
|
||||
@@ -27,9 +29,10 @@ export interface OneBotApiDebugProps {
|
||||
|
||||
const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
const { path, data } = props
|
||||
const url = new URL(window.location.origin).href
|
||||
const defaultHttpUrl = url.replace(':6099', ':3000')
|
||||
const [httpConfig, setHttpConfig] = useState({
|
||||
const currentURL = new URL(window.location.origin)
|
||||
currentURL.port = '3000'
|
||||
const defaultHttpUrl = currentURL.href
|
||||
const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, {
|
||||
url: defaultHttpUrl,
|
||||
token: ''
|
||||
})
|
||||
@@ -38,6 +41,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false)
|
||||
const [isResponseOpen, setIsResponseOpen] = useState(false)
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const responseRef = useRef<HTMLDivElement>(null)
|
||||
const parsedRequest = parse(data.request)
|
||||
const parsedResponse = parse(data.response)
|
||||
|
||||
@@ -47,8 +51,10 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
const r = toast.loading('正在发送请求...')
|
||||
try {
|
||||
const parsedRequestBody = JSON.parse(requestBody)
|
||||
const requestURL = new URL(httpConfig.url)
|
||||
requestURL.pathname = path
|
||||
request
|
||||
.post(httpConfig.url + path, parsedRequestBody, {
|
||||
.post(requestURL.href, parsedRequestBody, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${httpConfig.token}`
|
||||
},
|
||||
@@ -64,6 +70,11 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
setIsResponseOpen(true)
|
||||
responseRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
})
|
||||
toast.dismiss(r)
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -79,8 +90,8 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
}, [path])
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-4 rounded-lg shadow-md">
|
||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-danger-400 ">
|
||||
<section className="p-4 pt-14 rounded-lg shadow-md">
|
||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400">
|
||||
<PiCatDuotone />
|
||||
{data.description}
|
||||
</h1>
|
||||
@@ -88,6 +99,9 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
<Snippet
|
||||
className="bg-default-50 bg-opacity-50 backdrop-blur-md"
|
||||
symbol={<IoLink size={18} className="inline-block mr-1" />}
|
||||
tooltipProps={{
|
||||
content: '点击复制地址'
|
||||
}}
|
||||
>
|
||||
{path}
|
||||
</Snippet>
|
||||
@@ -111,7 +125,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
/>
|
||||
<Button
|
||||
onPress={sendRequest}
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="lg"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
@@ -120,8 +134,11 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
<IoSend />
|
||||
</Button>
|
||||
</div>
|
||||
<Card shadow="sm" className="my-4 bg-opacity-50 backdrop-blur-md">
|
||||
<CardHeader className="font-noto-serif font-bold text-lg gap-1 pb-0">
|
||||
<Card
|
||||
shadow="sm"
|
||||
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible z-20"
|
||||
>
|
||||
<CardHeader className="font-bold text-lg gap-1 pb-0">
|
||||
<span className="mr-2">请求体</span>
|
||||
<Button
|
||||
color="warning"
|
||||
@@ -135,7 +152,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<motion.div
|
||||
className="overflow-hidden"
|
||||
ref={responseRef}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{
|
||||
opacity: isCodeEditorOpen ? 1 : 0,
|
||||
@@ -169,7 +186,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
className="my-4 relative bg-opacity-50 backdrop-blur-md"
|
||||
>
|
||||
<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>
|
||||
<Button
|
||||
color="warning"
|
||||
@@ -218,7 +235,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
<h2 className="text-xl font-semibold mt-4 mb-2">响应体结构</h2>
|
||||
<DisplayStruct schema={parsedResponse} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -27,7 +27,7 @@ const SchemaType = ({
|
||||
name = '固定值'
|
||||
break
|
||||
}
|
||||
let chipColor: 'primary' | 'success' | 'danger' | 'warning' | 'secondary' =
|
||||
let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
|
||||
'primary'
|
||||
switch (type) {
|
||||
case 'enum':
|
||||
@@ -37,7 +37,7 @@ const SchemaType = ({
|
||||
chipColor = 'secondary'
|
||||
break
|
||||
case 'array':
|
||||
chipColor = 'danger'
|
||||
chipColor = 'primary'
|
||||
break
|
||||
case 'object':
|
||||
chipColor = 'success'
|
||||
|
@@ -19,9 +19,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
return (
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'flex-shrink-0 absolute md:!top-0 md:bottom-0 left-0 !overflow-hidden md:relative md:w-auto z-20',
|
||||
openSideBar &&
|
||||
'bottom-8 z-10 bg-background bg-opacity-20 backdrop-blur-md top-14'
|
||||
'h-[calc(100vh-3.5rem)] left-0 !overflow-hidden md:w-auto z-20 top-[3.3rem] md:top-[3rem] absolute md:sticky md:float-start',
|
||||
openSideBar && 'bg-background bg-opacity-20 backdrop-blur-md'
|
||||
)}
|
||||
initial={{ width: 0 }}
|
||||
transition={{
|
||||
@@ -32,13 +31,13 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
animate={{ width: openSideBar ? '16rem' : '0rem' }}
|
||||
style={{ overflowY: openSideBar ? 'auto' : 'hidden' }}
|
||||
>
|
||||
<div className="w-64 h-full overflow-y-auto px-2 float-right">
|
||||
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
|
||||
<Input
|
||||
className="sticky top-0 z-10 text-danger-600"
|
||||
className="sticky top-0 z-10 text-primary-600"
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
'bg-opacity-30 bg-danger-50 backdrop-blur-sm border border-danger-300 mb-2',
|
||||
input: 'bg-transparent !text-danger-400 !placeholder-danger-400'
|
||||
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
||||
input: 'bg-transparent !text-primary-400 !placeholder-primary-400'
|
||||
}}
|
||||
radius="full"
|
||||
placeholder="搜索 API"
|
||||
@@ -52,7 +51,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
key={apiName}
|
||||
shadow="none"
|
||||
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: !(
|
||||
apiName.includes(searchValue) ||
|
||||
@@ -60,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
|
||||
}
|
||||
)}
|
||||
@@ -68,10 +67,10 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
|
||||
>
|
||||
<CardBody>
|
||||
<h2 className="font-ubuntu font-bold">{api.description}</h2>
|
||||
<h2 className="font-bold">{api.description}</h2>
|
||||
<div
|
||||
className={clsx('text-sm text-danger-200', {
|
||||
'!text-danger-400': apiName === selectedApi
|
||||
className={clsx('text-sm text-primary-200', {
|
||||
'!text-primary-400': apiName === selectedApi
|
||||
})}
|
||||
>
|
||||
{apiName}
|
||||
|
@@ -109,7 +109,7 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -30,7 +30,7 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
className="text-medium"
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
import { useCallback, useRef } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import ChatInputModal from '@/components/chat_input/modal'
|
||||
import CodeEditor from '@/components/code_editor'
|
||||
import type { CodeEditorRef } from '@/components/code_editor'
|
||||
|
||||
@@ -42,7 +43,7 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||
构造请求
|
||||
</Button>
|
||||
<Modal
|
||||
@@ -72,11 +73,13 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<ChatInputModal />
|
||||
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => handleSendMessage(onClose)}
|
||||
>
|
||||
发送
|
||||
|
@@ -10,7 +10,7 @@ function StatusTag({
|
||||
color
|
||||
}: {
|
||||
title: string
|
||||
color: 'success' | 'danger' | 'warning'
|
||||
color: 'success' | 'primary' | 'warning'
|
||||
}) {
|
||||
const textClassName = `text-${color} text-sm`
|
||||
const bgClassName = `bg-${color}`
|
||||
@@ -27,7 +27,7 @@ export default function WSStatus({ state }: WSStatusProps) {
|
||||
return <StatusTag title="已连接" color="success" />
|
||||
}
|
||||
if (state === ReadyState.CLOSED) {
|
||||
return <StatusTag title="已关闭" color="danger" />
|
||||
return <StatusTag title="已关闭" color="primary" />
|
||||
}
|
||||
if (state === ReadyState.CONNECTING) {
|
||||
return <StatusTag title="连接中" color="warning" />
|
||||
|
@@ -16,23 +16,21 @@ export interface QQInfoCardProps {
|
||||
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
return (
|
||||
<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"
|
||||
radius="lg"
|
||||
>
|
||||
<PageLoading loading={loading} />
|
||||
{error ? (
|
||||
<CardBody className="items-center gap-1 justify-center">
|
||||
<div className="font-outfit flex-1 text-content1-foreground">
|
||||
Error
|
||||
</div>
|
||||
<div className="flex-1 text-content1-foreground">Error</div>
|
||||
<div className="whitespace-nowrap text-nowrap flex-shrink-0">
|
||||
{error.message}
|
||||
</div>
|
||||
</CardBody>
|
||||
) : (
|
||||
<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 />
|
||||
</div>
|
||||
<div className="relative flex-shrink-0 z-10">
|
||||
@@ -45,16 +43,14 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
/>
|
||||
<div
|
||||
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'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex-col justify-center">
|
||||
<div className="font-outfit text-lg truncate">{data?.nick}</div>
|
||||
<div className="font-ubuntu text-danger-500 text-sm">
|
||||
{data?.uin}
|
||||
</div>
|
||||
<div className="text-lg truncate">{data?.nick}</div>
|
||||
<div className="text-primary-500 text-sm">{data?.uin}</div>
|
||||
</div>
|
||||
</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">
|
||||
{!qrcode && (
|
||||
<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>
|
||||
)}
|
||||
<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 type { MenuItem } from '@/config/site'
|
||||
|
||||
import { title } from '../primitives'
|
||||
import Menus from './menus'
|
||||
|
||||
interface SideBarProps {
|
||||
@@ -48,19 +47,15 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
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">
|
||||
<div className="flex justify-center items-center mt-2 gap-2">
|
||||
<Image height={40} src={logo} className="mb-2" />
|
||||
<div className="flex justify-center items-center my-2 gap-2">
|
||||
<Image radius="none" height={40} src={logo} className="mb-2" />
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center hm-medium',
|
||||
title({
|
||||
shadow: true,
|
||||
color: isDark ? 'violet' : 'pink'
|
||||
}),
|
||||
'!text-2xl'
|
||||
'flex items-center font-bold',
|
||||
'!text-2xl shiny-text'
|
||||
)}
|
||||
>
|
||||
WebUI
|
||||
NapCat
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Button
|
||||
className="w-full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="full"
|
||||
variant="light"
|
||||
onPress={toggleTheme}
|
||||
@@ -80,7 +75,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full mb-2"
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="full"
|
||||
variant="light"
|
||||
onPress={onRevokeAuth}
|
||||
|
@@ -55,15 +55,16 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
isActive && 'bg-opacity-60',
|
||||
b64img && 'backdrop-blur-md text-white'
|
||||
)}
|
||||
color="danger"
|
||||
color="primary"
|
||||
endContent={
|
||||
canOpen ? (
|
||||
// div实现箭头V效果
|
||||
<div
|
||||
className={clsx(
|
||||
'ml-auto relative w-3 h-3 transition-transform',
|
||||
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:content-[""]',
|
||||
'before:block',
|
||||
@@ -95,8 +96,8 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
className={clsx(
|
||||
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
|
||||
isActive
|
||||
? 'bg-danger-500 animate-spinner-ease-spin'
|
||||
: 'bg-red-300 dark:bg-white'
|
||||
? 'bg-primary-500 animate-spinner-ease-spin'
|
||||
: 'bg-primary-200 dark:bg-white'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
@@ -1,76 +1,247 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Card, CardBody, CardHeader } from '@heroui/card'
|
||||
import { Chip } from '@heroui/chip'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { Tooltip } from '@heroui/tooltip'
|
||||
import { useRequest } from 'ahooks'
|
||||
import { FaCircleInfo } from 'react-icons/fa6'
|
||||
import { FaQq } from 'react-icons/fa6'
|
||||
import { useEffect } from 'react'
|
||||
import { BsStars } from 'react-icons/bs'
|
||||
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
|
||||
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
|
||||
import { RiMacFill } from 'react-icons/ri'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
import useDialog from '@/hooks/use-dialog'
|
||||
|
||||
import packageJson from '../../package.json'
|
||||
import { request } from '@/utils/request'
|
||||
import { compareVersion } from '@/utils/version'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
import { GithubRelease } from '@/types/github'
|
||||
|
||||
import TailwindMarkdown from './tailwind_markdown'
|
||||
|
||||
export interface SystemInfoItemProps {
|
||||
title: string
|
||||
icon?: React.ReactNode
|
||||
value?: React.ReactNode
|
||||
endContent?: React.ReactNode
|
||||
}
|
||||
|
||||
const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
|
||||
title,
|
||||
value = '--',
|
||||
icon
|
||||
icon,
|
||||
endContent
|
||||
}) => {
|
||||
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}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export interface NewVersionTipProps {
|
||||
currentVersion?: string
|
||||
}
|
||||
|
||||
const NewVersionTip = (props: NewVersionTipProps) => {
|
||||
const { currentVersion } = props
|
||||
const dialog = useDialog()
|
||||
const { data: releaseData, error } = useRequest(() =>
|
||||
request.get<GithubRelease[]>(
|
||||
'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
|
||||
)
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Tooltip content="检查新版本失败">
|
||||
<Button
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="primary"
|
||||
variant="shadow"
|
||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||
onPress={() => {
|
||||
dialog.alert({
|
||||
title: '检查新版本失败',
|
||||
content: error.message
|
||||
})
|
||||
}}
|
||||
>
|
||||
<FaInfo />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const latestVersion = releaseData?.data?.[0]?.tag_name
|
||||
|
||||
if (!latestVersion || !currentVersion) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (compareVersion(latestVersion, currentVersion) <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const middleVersions: GithubRelease[] = []
|
||||
|
||||
for (let i = 0; i < releaseData.data.length; i++) {
|
||||
const versionInfo = releaseData.data[i]
|
||||
if (compareVersion(versionInfo.tag_name, currentVersion) > 0) {
|
||||
middleVersions.push(versionInfo)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Tooltip content="有新版本可用">
|
||||
<Button
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="primary"
|
||||
variant="shadow"
|
||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||
onPress={() => {
|
||||
dialog.confirm({
|
||||
title: '有新版本可用',
|
||||
content: (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm space-x-2">
|
||||
<span>当前版本</span>
|
||||
<Chip color="primary" variant="flat">
|
||||
v{currentVersion}
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="text-sm space-x-2">
|
||||
<span>最新版本</span>
|
||||
<Chip color="primary">{latestVersion}</Chip>
|
||||
</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">
|
||||
{middleVersions.map((versionInfo) => (
|
||||
<div
|
||||
key={versionInfo.tag_name}
|
||||
className="p-4 bg-content1 rounded-md shadow-small"
|
||||
>
|
||||
<TailwindMarkdown content={versionInfo.body} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
scrollBehavior: 'inside',
|
||||
size: '3xl',
|
||||
confirmText: '前往下载',
|
||||
onConfirm() {
|
||||
window.open(
|
||||
'https://github.com/NapNeko/NapCatQQ/releases',
|
||||
'_blank',
|
||||
'noopener'
|
||||
)
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<FaInfo />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const NapCatVersion = () => {
|
||||
const {
|
||||
data: packageData,
|
||||
loading: packageLoading,
|
||||
error: packageError
|
||||
} = useRequest(WebUIManager.getPackageInfo)
|
||||
|
||||
const currentVersion = packageData?.version
|
||||
|
||||
return (
|
||||
<SystemInfoItem
|
||||
title="NapCat 版本"
|
||||
icon={<IoLogoOctocat className="text-xl" />}
|
||||
value={
|
||||
packageError ? (
|
||||
`错误:${packageError.message}`
|
||||
) : packageLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
currentVersion
|
||||
)
|
||||
}
|
||||
endContent={<NewVersionTip currentVersion={currentVersion} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SystemInfoProps {
|
||||
archInfo?: string
|
||||
}
|
||||
const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
||||
const { archInfo } = props
|
||||
const {
|
||||
data: packageData,
|
||||
loading: packageLoading,
|
||||
error: packageError
|
||||
} = useRequest(WebUIManager.getPackageInfo)
|
||||
const {
|
||||
data: qqVersionData,
|
||||
loading: qqVersionLoading,
|
||||
error: qqVersionError
|
||||
} = useRequest(WebUIManager.getQQVersion)
|
||||
return (
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 overflow-visible flex-1">
|
||||
<CardHeader className="pb-0 items-center gap-1 text-danger-500 font-extrabold">
|
||||
<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-primary-500 font-extrabold">
|
||||
<FaCircleInfo className="text-lg" />
|
||||
<span>系统信息</span>
|
||||
</CardHeader>
|
||||
<CardBody className="flex-1">
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<SystemInfoItem
|
||||
title="NapCat 版本"
|
||||
icon={<IoLogoOctocat className="text-xl" />}
|
||||
value={
|
||||
packageError ? (
|
||||
`错误:${packageError.message}`
|
||||
) : packageLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
packageData?.version
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SystemInfoItem
|
||||
title="WebUI 版本"
|
||||
icon={<IoLogoChrome className="text-xl" />}
|
||||
value={packageJson.version}
|
||||
/>
|
||||
<NapCatVersion />
|
||||
<SystemInfoItem
|
||||
title="QQ 版本"
|
||||
icon={<FaQq className="text-lg" />}
|
||||
@@ -84,6 +255,11 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SystemInfoItem
|
||||
title="WebUI 版本"
|
||||
icon={<IoLogoChrome className="text-xl" />}
|
||||
value="Next"
|
||||
/>
|
||||
<SystemInfoItem
|
||||
title="系统版本"
|
||||
icon={<RiMacFill className="text-xl" />}
|
||||
|
@@ -24,7 +24,7 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
||||
return (
|
||||
<div
|
||||
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'
|
||||
)}
|
||||
>
|
||||
@@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
}
|
||||
|
||||
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">
|
||||
<Image
|
||||
src={bkg}
|
||||
@@ -69,7 +69,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
</div>
|
||||
<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">
|
||||
<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" />
|
||||
<span>CPU</span>
|
||||
</h2>
|
||||
@@ -88,7 +88,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
unit="%"
|
||||
/>
|
||||
</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" />
|
||||
<span>内存</span>
|
||||
</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}
|
||||
/>
|
||||
)
|
||||
}
|
49
napcat.webui/src/components/tailwind_markdown.tsx
Normal file
49
napcat.webui/src/components/tailwind_markdown.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
|
||||
return (
|
||||
<Markdown
|
||||
className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ node, ...props }) => (
|
||||
<h1 className="text-2xl font-bold" {...props} />
|
||||
),
|
||||
h2: ({ node, ...props }) => (
|
||||
<h2 className="text-xl font-bold" {...props} />
|
||||
),
|
||||
h3: ({ node, ...props }) => (
|
||||
<h3 className="text-lg font-bold" {...props} />
|
||||
),
|
||||
p: ({ node, ...props }) => <p className="m-0" {...props} />,
|
||||
a: ({ node, ...props }) => (
|
||||
<a
|
||||
className="text-primary-500 inline-block hover:underline"
|
||||
target="_blank"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul className="list-disc list-inside" {...props} />
|
||||
),
|
||||
ol: ({ node, ...props }) => (
|
||||
<ol className="list-decimal list-inside" {...props} />
|
||||
),
|
||||
blockquote: ({ node, ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-default-300 pl-4 italic"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ node, ...props }) => (
|
||||
<code className="bg-default-100 p-1 rounded text-xs" {...props} />
|
||||
)
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Markdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default TailwindMarkdown
|
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 { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import { WebglAddon } from '@xterm/addon-webgl'
|
||||
// import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import clsx from 'clsx'
|
||||
@@ -8,8 +9,6 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
||||
|
||||
import { useTheme } from '@/hooks/use-theme'
|
||||
|
||||
import { gradientText } from '@/utils/terminal'
|
||||
|
||||
export type XTermRef = {
|
||||
write: (
|
||||
...args: Parameters<Terminal['write']>
|
||||
@@ -20,118 +19,174 @@ export type XTermRef = {
|
||||
) => ReturnType<Terminal['writeln']>
|
||||
writelnAsync: (data: Parameters<Terminal['writeln']>[0]) => Promise<void>
|
||||
clear: () => void
|
||||
terminalRef: React.RefObject<Terminal | null>
|
||||
}
|
||||
const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
||||
(props, ref) => {
|
||||
const domRef = useRef<HTMLDivElement>(null)
|
||||
const terminalRef = useRef<Terminal | null>(null)
|
||||
const { className, ...rest } = props
|
||||
const { theme } = useTheme()
|
||||
useEffect(() => {
|
||||
if (!domRef.current) {
|
||||
return
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
allowTransparency: true,
|
||||
fontFamily: '"Fira Code", "Harmony", "Noto Serif SC", monospace'
|
||||
|
||||
export interface XTermProps
|
||||
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 terminalRef = useRef<Terminal | null>(null)
|
||||
const { className, onInput, onKey, onResize, ...rest } = props
|
||||
const { theme } = useTheme()
|
||||
useEffect(() => {
|
||||
const terminal = new Terminal({
|
||||
allowTransparency: true,
|
||||
fontFamily:
|
||||
'"JetBrains Mono", "Aa偷吃可爱长大的", "Noto Serif SC", monospace',
|
||||
cursorInactiveStyle: 'outline',
|
||||
drawBoldTextInBrightColors: false,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.2
|
||||
})
|
||||
terminalRef.current = terminal
|
||||
const fitAddon = new FitAddon()
|
||||
terminal.loadAddon(
|
||||
new WebLinksAddon((event, uri) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(uri, '_blank')
|
||||
}
|
||||
})
|
||||
terminalRef.current = terminal
|
||||
const fitAddon = new FitAddon()
|
||||
terminal.loadAddon(new WebLinksAddon())
|
||||
terminal.loadAddon(fitAddon)
|
||||
terminal.loadAddon(new WebglAddon())
|
||||
terminal.open(domRef.current)
|
||||
)
|
||||
terminal.loadAddon(fitAddon)
|
||||
terminal.open(domRef.current!)
|
||||
|
||||
terminal.loadAddon(new CanvasAddon())
|
||||
terminal.onData((data) => {
|
||||
if (onInput) {
|
||||
onInput(data)
|
||||
}
|
||||
})
|
||||
|
||||
terminal.onKey((event) => {
|
||||
if (onKey) {
|
||||
onKey(event.key, event.domEvent)
|
||||
}
|
||||
})
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit()
|
||||
// 获取当前终端尺寸
|
||||
const cols = terminal.cols
|
||||
const rows = terminal.rows
|
||||
if (onResize) {
|
||||
onResize(cols, rows)
|
||||
}
|
||||
})
|
||||
|
||||
// 字体加载完成后重新调整终端大小
|
||||
document.fonts.ready.then(() => {
|
||||
fitAddon.fit()
|
||||
|
||||
terminal.writeln(
|
||||
gradientText(
|
||||
'Welcome to NapCat WebUI',
|
||||
[255, 0, 0],
|
||||
[0, 255, 0],
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
)
|
||||
resizeObserver.observe(domRef.current!)
|
||||
})
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit()
|
||||
})
|
||||
resizeObserver.observe(domRef.current)
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
setTimeout(() => {
|
||||
terminal.dispose()
|
||||
}, 0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleFontLoad = () => {
|
||||
terminal.refresh(0, terminal.rows - 1)
|
||||
}
|
||||
document.fonts.addEventListener('loadingdone', handleFontLoad)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
document.fonts.removeEventListener('loadingdone', handleFontLoad)
|
||||
setTimeout(() => {
|
||||
terminal.dispose()
|
||||
}, 0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalRef.current) {
|
||||
useEffect(() => {
|
||||
if (terminalRef.current) {
|
||||
if (theme === 'dark') {
|
||||
terminalRef.current.options.theme = {
|
||||
background:
|
||||
theme === 'dark' ? 'rgba(0, 0, 0, 0)' : 'rgba(255, 255, 255, 0)',
|
||||
foreground: theme === 'dark' ? '#fff' : '#000',
|
||||
selectionBackground: theme === 'dark' ? '#666' : '#ddd',
|
||||
cursor: theme === 'dark' ? '#fff' : '#000',
|
||||
cursorAccent: theme === 'dark' ? '#000' : '#fff',
|
||||
black: theme === 'dark' ? '#fff' : '#000'
|
||||
background: '#00000000',
|
||||
black: '#ffffff',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
blue: '#2472c8',
|
||||
cyan: '#11a8cd',
|
||||
white: '#e5e5e5',
|
||||
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'
|
||||
}
|
||||
}
|
||||
}, [theme])
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
write: (...args) => {
|
||||
return terminalRef.current?.write(...args)
|
||||
},
|
||||
writeAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.write(data, resolve)
|
||||
})
|
||||
},
|
||||
writeln: (...args) => {
|
||||
return terminalRef.current?.writeln(...args)
|
||||
},
|
||||
writelnAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.writeln(data, resolve)
|
||||
})
|
||||
},
|
||||
clear: () => {
|
||||
terminalRef.current?.clear()
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
write: (...args) => {
|
||||
return terminalRef.current?.write(...args)
|
||||
},
|
||||
writeAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.write(data, resolve)
|
||||
})
|
||||
},
|
||||
writeln: (...args) => {
|
||||
return terminalRef.current?.writeln(...args)
|
||||
},
|
||||
writelnAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.writeln(data, resolve)
|
||||
})
|
||||
},
|
||||
clear: () => {
|
||||
terminalRef.current?.clear()
|
||||
},
|
||||
terminalRef: terminalRef
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'p-2 rounded-md shadow-sm border border-default-200 w-full h-full overflow-hidden bg-opacity-50 backdrop-blur-sm',
|
||||
theme === 'dark' ? 'bg-black' : 'bg-white',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'p-2 rounded-md shadow-sm border border-default-200 w-full h-full overflow-hidden bg-opacity-50 backdrop-blur-sm',
|
||||
theme === 'dark' ? 'bg-black' : 'bg-white',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
ref={domRef}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
ref={domRef}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default XTerm
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
BugIcon2,
|
||||
FileIcon,
|
||||
InfoIcon,
|
||||
LogIcon,
|
||||
RouteIcon,
|
||||
SettingsIcon,
|
||||
SignalTowerIcon,
|
||||
@@ -49,10 +51,10 @@ export const siteConfig = {
|
||||
href: '/config'
|
||||
},
|
||||
{
|
||||
label: '系统日志',
|
||||
label: '猫猫日志',
|
||||
icon: (
|
||||
<div className="w-5 h-5">
|
||||
<TerminalIcon />
|
||||
<LogIcon />
|
||||
</div>
|
||||
),
|
||||
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: '关于我们',
|
||||
icon: (
|
||||
|
@@ -7,7 +7,9 @@ enum key {
|
||||
autoPlay = 'auto-play',
|
||||
customIcons = 'custom-icons',
|
||||
isCollapsedMusicPlayer = 'is-collapsed-music-player',
|
||||
sideBarOpen = 'side-bar-open'
|
||||
sideBarOpen = 'side-bar-open',
|
||||
httpDebugConfig = 'http-debug-config',
|
||||
wsDebugConfig = 'ws-debug-config'
|
||||
}
|
||||
|
||||
export default key
|
||||
|
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
|
@@ -1,22 +1,15 @@
|
||||
// Dialog Context
|
||||
import React from 'react'
|
||||
|
||||
import type { ModalProps } from '@/components/modal'
|
||||
import Modal from '@/components/modal'
|
||||
import type { ModalProps } from '@/components/modal'
|
||||
|
||||
export interface AlertProps {
|
||||
title?: React.ReactNode
|
||||
content: React.ReactNode
|
||||
size?: ModalProps['size']
|
||||
dismissible?: boolean
|
||||
export interface AlertProps
|
||||
extends Omit<ModalProps, 'onCancel' | 'showCancel' | 'cancelText'> {
|
||||
onConfirm?: () => void
|
||||
}
|
||||
|
||||
export interface ConfirmProps {
|
||||
title?: React.ReactNode
|
||||
content: React.ReactNode
|
||||
size?: ModalProps['size']
|
||||
dismissible?: boolean
|
||||
export interface ConfirmProps extends ModalProps {
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
@@ -42,7 +35,7 @@ export const DialogContext = React.createContext<DialogContextProps>({
|
||||
const DialogProvider: React.FC<DialogProviderProps> = ({ children }) => {
|
||||
const [dialogs, setDialogs] = React.useState<ModalItem[]>([])
|
||||
const alert = (config: AlertProps) => {
|
||||
const { title, content, dismissible, onConfirm, size = 'md' } = config
|
||||
const { onConfirm, size = 'md', ...rest } = config
|
||||
|
||||
setDialogs((before) => {
|
||||
const id = before[before.length - 1]?.id + 1 || 0
|
||||
@@ -51,32 +44,23 @@ const DialogProvider: React.FC<DialogProviderProps> = ({ children }) => {
|
||||
...before,
|
||||
{
|
||||
id,
|
||||
content,
|
||||
size,
|
||||
title,
|
||||
backdrop: 'blur',
|
||||
showCancel: false,
|
||||
dismissible: dismissible,
|
||||
onConfirm: () => {
|
||||
onConfirm?.()
|
||||
setTimeout(() => {
|
||||
setDialogs((before) => before.filter((item) => item.id !== id))
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
...rest
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const confirm = (config: ConfirmProps) => {
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
dismissible,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
size = 'md'
|
||||
} = config
|
||||
const { onConfirm, onCancel, size = 'md', ...rest } = config
|
||||
|
||||
setDialogs((before) => {
|
||||
const id = before[before.length - 1]?.id + 1 || 0
|
||||
@@ -85,12 +69,9 @@ const DialogProvider: React.FC<DialogProviderProps> = ({ children }) => {
|
||||
...before,
|
||||
{
|
||||
id,
|
||||
content,
|
||||
size,
|
||||
title,
|
||||
backdrop: 'blur',
|
||||
showCancel: true,
|
||||
dismissible: dismissible,
|
||||
onConfirm: () => {
|
||||
onConfirm?.()
|
||||
setTimeout(() => {
|
||||
@@ -102,7 +83,8 @@ const DialogProvider: React.FC<DialogProviderProps> = ({ children }) => {
|
||||
setTimeout(() => {
|
||||
setDialogs((before) => before.filter((item) => item.id !== id))
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
...rest
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@@ -35,6 +35,7 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
||||
const [musicId, setMusicId] = useState<number>(0)
|
||||
const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop)
|
||||
const music = musicList.find((music) => music.id === musicId)
|
||||
const [token] = useLocalStorage(key.token, '')
|
||||
const onNext = () => {
|
||||
const nextID = getNextMusic(musicList, musicId, playMode)
|
||||
setMusicId(nextID)
|
||||
@@ -60,8 +61,8 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
||||
setMusicId(res[0].id)
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchMusicList(listId)
|
||||
}, [listId])
|
||||
if (listId && token) fetchMusicList(listId)
|
||||
}, [listId, token])
|
||||
return (
|
||||
<AudioContext.Provider
|
||||
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
|
||||
}
|
||||
|
||||
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,29 @@ export default class WebUIManager {
|
||||
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 = '') {
|
||||
const data = await serverRequest.get<ServerResponse<string>>(
|
||||
'/base/proxy?url=' + encodeURIComponent(url)
|
||||
)
|
||||
data.data.data = JSON.parse(data.data.data)
|
||||
return data.data as ServerResponse<T>
|
||||
}
|
||||
|
||||
public static async getPackageInfo() {
|
||||
const { data } =
|
||||
await serverRequest.get<ServerResponse<PackageInfo>>('/base/PackageInfo')
|
||||
@@ -36,6 +59,20 @@ export default class WebUIManager {
|
||||
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() {
|
||||
const { data } =
|
||||
await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList')
|
||||
|
@@ -14,10 +14,12 @@ const useConfig = () => {
|
||||
key: T,
|
||||
value: OneBotConfig['network'][T][0]
|
||||
) => {
|
||||
if (
|
||||
value.name &&
|
||||
config.network[key].some((item) => item.name === value.name)
|
||||
) {
|
||||
const allNetworkNames = Object.keys(config.network).reduce((acc, key) => {
|
||||
const _key = key as keyof OneBotConfig['network']
|
||||
return acc.concat(config.network[_key].map((item) => item.name))
|
||||
}, [] as string[])
|
||||
|
||||
if (value.name && allNetworkNames.includes(value.name)) {
|
||||
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 }
|
||||
}
|
@@ -2,7 +2,7 @@ import { BreadcrumbItem, Breadcrumbs } from '@heroui/breadcrumbs'
|
||||
import { Button } from '@heroui/button'
|
||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
||||
import clsx from 'clsx'
|
||||
import { motion } from 'motion/react'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { MdMenu, MdMenuOpen } from 'react-icons/md'
|
||||
@@ -79,7 +79,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
}, [location.pathname])
|
||||
return (
|
||||
<div
|
||||
className="h-screen relative flex bg-danger-50 dark:bg-black"
|
||||
className="h-screen relative flex bg-primary-50 dark:bg-black items-stretch"
|
||||
style={{
|
||||
backgroundImage: `url(${b64img})`,
|
||||
backgroundSize: 'cover'
|
||||
@@ -89,13 +89,22 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={clsx(
|
||||
'overflow-y-auto relative flex-1 rounded-md m-1 bg-content1 pb-10 md:pb-0',
|
||||
'overflow-y-auto flex-1 rounded-md m-1 bg-content1 pb-10 md:pb-0',
|
||||
openSideBar ? 'ml-0' : 'ml-1',
|
||||
!b64img && 'shadow-inner',
|
||||
b64img && '!bg-opacity-50 backdrop-blur-none dark:bg-background'
|
||||
b64img && '!bg-opacity-50 backdrop-blur-none dark:bg-background',
|
||||
'overflow-x-hidden'
|
||||
)}
|
||||
>
|
||||
<div className="h-10 flex items-center hm-medium text-xl sticky top-2 left-0 backdrop-blur-lg z-20 shadow-sm bg-background dark:bg-background shadow-danger-50 dark:shadow-danger-100 m-2 rounded-full !bg-opacity-50">
|
||||
<div
|
||||
className={clsx(
|
||||
'h-10 flex items-center font-bold text-xl backdrop-blur-lg rounded-full',
|
||||
'dark:bg-background dark:shadow-primary-100',
|
||||
'bg-background !bg-opacity-50',
|
||||
'shadow-sm shadow-primary-50',
|
||||
'z-30 m-2 mb-0 sticky top-2 left-0'
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'mr-1 ease-in-out ml-0 md:relative',
|
||||
@@ -117,7 +126,19 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
</motion.div>
|
||||
<Breadcrumbs isDisabled size="lg">
|
||||
{title?.map((item, index) => (
|
||||
<BreadcrumbItem key={index}>{item}</BreadcrumbItem>
|
||||
<BreadcrumbItem key={index}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={item}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{item}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import 'react-photo-view/dist/react-photo-view.css'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
import App from '@/App.tsx'
|
||||
@@ -7,6 +8,7 @@ import '@/styles/globals.css'
|
||||
|
||||
import key from './const/key'
|
||||
import WebUIManager from './controllers/webui_manager'
|
||||
import { loadTheme } from './utils/theme'
|
||||
|
||||
WebUIManager.checkWebUiLogined()
|
||||
|
||||
@@ -21,6 +23,8 @@ if (theme && !theme.startsWith('"')) {
|
||||
localStorage.setItem(key.theme, JSON.stringify(theme))
|
||||
}
|
||||
|
||||
loadTheme()
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
// <React.StrictMode>
|
||||
<BrowserRouter basename="/webui/">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user