diff --git a/.editorconfig b/.editorconfig index c90a6350..52bb929d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,10 +15,10 @@ charset = utf-8 # 4 space indentation [*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}] indent_style = space -indent_size = 4 +indent_size = 2 [*.bat] 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. \ No newline at end of file +# You'll need to rely on your linter/formatter like ESLint or Prettier for that. diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 8c30a652..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 4, - "semi": true, - "singleQuote": true, - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 120, - "endOfLine": "auto" -} diff --git a/.vscode/settings.json b/.vscode/settings.json index de49d5e8..3860d57f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,15 +3,35 @@ "explorer.fileNesting.expand": false, "explorer.fileNesting.patterns": { ".env.universal": ".env.*", - "tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts", + "vite.config.ts": "vite*.ts", + "README.md": "CODE_OF_CONDUCT.md, RELEASES.md, CONTRIBUTING.md, CHANGELOG.md, SECURITY.md", + "tsconfig.json": "tsconfig.*.json, env.d.ts", "package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE" }, "css.customData": [ ".vscode/tailwindcss.json" ], - "editor.formatOnPaste": false, - "editor.formatOnSave": false, + "editor.detectIndentation": false, + "editor.tabSize": 2, + "editor.formatOnSave": true, + "editor.formatOnType": false, + "editor.formatOnPaste": true, + "editor.formatOnSaveMode": "file", "editor.codeActionsOnSave": { - "source.fixAll.eslint": "never" + "source.fixAll.eslint": "always" }, -} + "files.autoSave": "onFocusChange", + "javascript.preferences.quoteStyle": "single", + "typescript.preferences.quoteStyle": "single", + "javascript.format.semicolons": "insert", + "typescript.format.semicolons": "insert", + "javascript.format.insertSpaceBeforeFunctionParenthesis": true, + "typescript.format.insertSpaceBeforeFunctionParenthesis": true, + "typescript.format.insertSpaceAfterConstructor": true, + "javascript.format.insertSpaceAfterConstructor": true, + "typescript.preferences.importModuleSpecifier": "non-relative", + "typescript.preferences.importModuleSpecifierEnding": "minimal", + "javascript.preferences.importModuleSpecifier": "non-relative", + "javascript.preferences.importModuleSpecifierEnding": "minimal", + "typescript.disableAutomaticTypeAcquisition": true, +} \ No newline at end of file diff --git a/README.md b/README.md index 4852840d..0e2eb54d 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ # NapCat - - _Modern protocol-side framework implemented based on NTQQ._ > 云起兮风生,心向远方兮路未曾至. @@ -14,6 +12,7 @@ _Modern protocol-side framework implemented based on NTQQ._ --- ## New Feature + 在 v4.8.115+ 版本开始 1. NapCatQQ 支持 [Stream Api](https://napneko.github.io/develop/file) @@ -23,19 +22,21 @@ _Modern protocol-side framework implemented based on NTQQ._ - [2] 采用字符串可以解决扩展到int64的问题,同时也可以解决部分语言(如JavaScript)对大整数支持不佳的问题,增加极少成本。 ## Welcome -+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ. + +- NapCatQQ is a modern implementation of the Bot protocol based on NTQQ. - NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现 ## Feature -+ **Easy to Use** +- **Easy to Use** - 作为初学者能够轻松使用. -+ **Quick and Efficient** +- **Quick and Efficient** - 在低内存操作系统长时运行. -+ **Rich API Interface** +- **Rich API Interface** - 完整实现了大部分标准接口. -+ **Stable and Reliable** +- **Stable and Reliable** - 持续稳定的开发与维护. + ## Quick Start 可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本 @@ -43,6 +44,38 @@ _Modern protocol-side framework implemented based on NTQQ._ **首次使用**请务必查看如下文档看使用教程 > 项目非盈利,对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。 + +## Development Guide + +### 代码提交前检查 + +在提交代码前,**必须**执行以下命令进行代码检查: + +```bash +# 1. 代码格式化修复 +npm run lint:fix + +# 2. TypeScript 类型检查 +npm run tsc +``` + +#### 关于 TypeScript 类型检查 + +执行 `npm run tsc` 时,会出现 22 个已知的第三方库类型错误,**这是正常现象**: + +``` +Found 22 errors in 3 files. + +Errors Files + 3 node_modules/@homebridge/node-pty-prebuilt-multiarch/src/eventEmitter2.ts:42 + 2 node_modules/@homebridge/node-pty-prebuilt-multiarch/src/terminal.ts:158 + 17 node_modules/@napneko/nap-proto-core/NapProto.ts:94 +``` + +这些错误是由于启用了严格类型检查模式导致的第三方库内部类型问题,**不影响项目运行**。 + +⚠️ **注意**:除了上述 22 个已知错误外,不应该出现其他类型错误。如果有新的错误,请在提交前修复。 + ## Link | Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) | @@ -64,20 +97,22 @@ _Modern protocol-side framework implemented based on NTQQ._ ## Thanks -+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 +- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 -+ [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下 +- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下 -+ [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下 +- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下 -+ [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下 +- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下 -+ 不过最最重要的 还是需要感谢屏幕前的你哦~ +- 不过最最重要的 还是需要感谢屏幕前的你哦~ --- ## License + 本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点: + 1. 第三方库代码或修改部分遵循其原始开源许可. 2. 本项目获取部分项目授权而不受部分约束 2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE). diff --git a/eslint.config.mjs b/eslint.config.mjs index ae25a7a3..b413ff63 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,32 +1,52 @@ -import eslint from '@eslint/js'; -import tsEslintPlugin from '@typescript-eslint/eslint-plugin'; -import tsEslintParser from '@typescript-eslint/parser'; -import globals from "globals"; +import neostandard from 'neostandard'; -const customTsFlatConfig = [ - { - name: 'typescript-eslint/base', - languageOptions: { - parser: tsEslintParser, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - NodeJS: 'readonly', // 添加 NodeJS 全局变量 - }, - }, - files: ['**/*.{ts,tsx}'], - rules: { - ...tsEslintPlugin.configs.recommended.rules, - 'quotes': ['error', 'single'], // 使用单引号 - 'semi': ['error', 'always'], // 强制使用分号 - 'indent': ['error', 4], // 使用 4 空格缩进 - }, - plugins: { - '@typescript-eslint': tsEslintPlugin, - }, - ignores: ['src/webui/**'], // 忽略 src/webui/ 目录所有文件 - }, +/** 尾随逗号 */ +const commaDangle = val => { + if (val?.rules?.['@stylistic/comma-dangle']?.[0] === 'warn') { + const rule = val?.rules?.['@stylistic/comma-dangle']?.[1]; + Object.keys(rule).forEach(key => { + rule[key] = 'always-multiline'; + }); + val.rules['@stylistic/comma-dangle'][1] = rule; + } + + /** 三元表达式 */ + if (val?.rules?.['@stylistic/indent']) { + val.rules['@stylistic/indent'][2] = { + ...val.rules?.['@stylistic/indent']?.[2], + flatTernaryExpressions: true, + offsetTernaryExpressions: false, + }; + } + + /** 支持下划线 - 禁用 camelcase 规则 */ + if (val?.rules?.camelcase) { + val.rules.camelcase = 'off'; + } + + /** 未使用的变量强制报错 */ + if (val?.rules?.['@typescript-eslint/no-unused-vars']) { + val.rules['@typescript-eslint/no-unused-vars'] = ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }]; + } + + return val; +}; + +/** 忽略的文件 */ +const ignores = [ + 'node_modules', + '**/dist/**', + 'launcher', ]; -export default [eslint.configs.recommended, ...customTsFlatConfig]; \ No newline at end of file +const options = neostandard({ + ts: true, + ignores, + semi: true, // 强制使用分号 +}).map(commaDangle); + +export default options; diff --git a/napcat.webui/.prettierignore b/napcat.webui/.prettierignore deleted file mode 100644 index d44692da..00000000 --- a/napcat.webui/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -dist -*.md -*.html -yarn.lock -package-lock.json -node_modules -pnpm-lock.yaml \ No newline at end of file diff --git a/napcat.webui/.prettierrc b/napcat.webui/.prettierrc deleted file mode 100644 index f890346c..00000000 --- a/napcat.webui/.prettierrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, - "semi": false, - "trailingComma": "none", - "bracketSpacing": true, - "importOrder": [ - "", - "^@/const/(.*)$", - "^@/store/(.*)$", - "^@/components/(.*)$", - "^@/contexts/(.*)$", - "^@/hooks/(.*)$", - "^@/utils/(.*)$", - "^@/(.*)$", - "^[./]" - ], - "importOrderSeparation": true, - "importOrderSortSpecifiers": true, - "plugins": ["@trivago/prettier-plugin-sort-imports"] -} diff --git a/napcat.webui/eslint.config.mjs b/napcat.webui/eslint.config.mjs index 0d405ad7..ecbdd00f 100644 --- a/napcat.webui/eslint.config.mjs +++ b/napcat.webui/eslint.config.mjs @@ -1,91 +1,2 @@ -import eslint_js from '@eslint/js' -import tsEslintPlugin from '@typescript-eslint/eslint-plugin' -import tsEslintParser from '@typescript-eslint/parser' -import eslintConfigPrettier from 'eslint-config-prettier' -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' -import reactPlugin from 'eslint-plugin-react' -import reactHooksPlugin from 'eslint-plugin-react-hooks' -import globals from 'globals' - -const customTsFlatConfig = [ - { - name: 'typescript-eslint/base', - languageOptions: { - parser: tsEslintParser, - sourceType: 'module' - }, - files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], - rules: { - ...tsEslintPlugin.configs.recommended.rules - }, - plugins: { - '@typescript-eslint': tsEslintPlugin - } - } -] - -export default [ - eslint_js.configs.recommended, - - eslintPluginPrettierRecommended, - - ...customTsFlatConfig, - { - name: 'global config', - languageOptions: { - globals: { - ...globals.es2022, - ...globals.browser, - ...globals.node - }, - parserOptions: { - warnOnUnsupportedTypeScriptVersion: false - } - }, - rules: { - 'prettier/prettier': 'error', - 'no-unused-vars': 'off', - 'no-undef': 'off', - //关闭不能再promise中使用ansyc - 'no-async-promise-executor': 'off', - //关闭不能再常量中使用?? - 'no-constant-binary-expression': 'off', - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/no-unused-vars': 'off', - - //禁止失去精度的字面数字 - '@typescript-eslint/no-loss-of-precision': 'off', - //禁止使用any - '@typescript-eslint/no-explicit-any': 'error' - } - }, - { - ignores: ['**/node_modules', '**/dist', '**/output'] - }, - { - name: 'react-eslint', - files: ['src/*.{js,jsx,mjs,cjs,ts,tsx}'], - plugins: { - react: reactPlugin, - 'react-hooks': reactHooksPlugin - }, - languageOptions: { - ...reactPlugin.configs.recommended.languageOptions - }, - rules: { - ...reactPlugin.configs.recommended.rules, - - 'react/react-in-jsx-scope': 'off' - }, - settings: { - react: { - // 需要显示安装 react - version: 'detect' - } - } - }, - { - languageOptions: { globals: { ...globals.browser, ...globals.node } } - }, - eslintConfigPrettier -] +import eslintConfig from '../eslint.config.mjs'; +export default eslintConfig; diff --git a/napcat.webui/package.json b/napcat.webui/package.json index abe025ed..48ec4ab0 100644 --- a/napcat.webui/package.json +++ b/napcat.webui/package.json @@ -86,7 +86,6 @@ "zod": "^3.24.1" }, "devDependencies": { - "@eslint/js": "^9.19.0", "@react-types/shared": "^3.26.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/crypto-js": "^4.2.2", @@ -97,20 +96,14 @@ "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@types/react-window": "^1.8.8", - "@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.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.3", - "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-unused-imports": "^4.1.4", - "globals": "^15.14.0", "postcss": "^8.5.1", "prettier": "^3.4.2", "typescript": "^5.7.3", diff --git a/napcat.webui/src/App.tsx b/napcat.webui/src/App.tsx index 9db7243d..37a8f124 100644 --- a/napcat.webui/src/App.tsx +++ b/napcat.webui/src/App.tsx @@ -1,33 +1,33 @@ -import { Suspense, lazy, useEffect } from 'react' -import { Provider } from 'react-redux' -import { Route, Routes, useNavigate } from 'react-router-dom' +import { Suspense, lazy, useEffect } from 'react'; +import { Provider } from 'react-redux'; +import { Route, Routes, useNavigate } from 'react-router-dom'; -import PageBackground from '@/components/page_background' -import PageLoading from '@/components/page_loading' -import Toaster from '@/components/toaster' +import PageBackground from '@/components/page_background'; +import PageLoading from '@/components/page_loading'; +import Toaster from '@/components/toaster'; -import DialogProvider from '@/contexts/dialog' -import AudioProvider from '@/contexts/songs' +import DialogProvider from '@/contexts/dialog'; +import AudioProvider from '@/contexts/songs'; -import useAuth from '@/hooks/auth' +import useAuth from '@/hooks/auth'; -import store from '@/store' +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')) +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() { +function App () { return ( @@ -42,49 +42,49 @@ function App() { - ) + ); } -function AuthChecker({ children }: { children: React.ReactNode }) { - const { isAuth } = useAuth() - const navigate = useNavigate() +function AuthChecker ({ children }: { children: React.ReactNode }) { + const { isAuth } = useAuth(); + const navigate = useNavigate(); useEffect(() => { if (!isAuth) { - const search = new URLSearchParams(window.location.search) - const token = search.get('token') - let url = '/web_login' + const search = new URLSearchParams(window.location.search); + const token = search.get('token'); + let url = '/web_login'; if (token) { - url += `?token=${token}` + url += `?token=${token}`; } - navigate(url, { replace: true }) + navigate(url, { replace: true }); } - }, [isAuth, navigate]) + }, [isAuth, navigate]); - return <>{children} + return <>{children}; } -function AppRoutes() { +function AppRoutes () { return ( - }> + }> } /> - } /> - } /> - } /> - }> - } /> - } /> + } /> + } /> + } /> + }> + } /> + } /> - } /> - } /> - } /> + } /> + } /> + } /> - } /> - } /> + } /> + } /> - ) + ); } -export default App +export default App; diff --git a/napcat.webui/src/components/ColorPicker.tsx b/napcat.webui/src/components/ColorPicker.tsx index eb9f423a..481157bc 100644 --- a/napcat.webui/src/components/ColorPicker.tsx +++ b/napcat.webui/src/components/ColorPicker.tsx @@ -1,6 +1,6 @@ -import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' -import React from 'react' -import { ColorResult, SketchPicker } from 'react-color' +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; +import React from 'react'; +import { ColorResult, SketchPicker } from 'react-color'; // 假定 heroui 提供的 Popover组件 @@ -11,14 +11,14 @@ interface ColorPickerProps { const ColorPicker: React.FC = ({ color, onChange }) => { const handleChange = (colorResult: ColorResult) => { - onChange(colorResult) - } + onChange(colorResult); + }; return (
@@ -26,11 +26,11 @@ const ColorPicker: React.FC = ({ color, onChange }) => { - ) -} + ); +}; -export default ColorPicker +export default ColorPicker; diff --git a/napcat.webui/src/components/audio_player.tsx b/napcat.webui/src/components/audio_player.tsx index f88b31af..b77f4d6b 100644 --- a/napcat.webui/src/components/audio_player.tsx +++ b/napcat.webui/src/components/audio_player.tsx @@ -1,30 +1,30 @@ -import { Button } from '@heroui/button' -import { Card, CardBody, CardHeader } from '@heroui/card' -import { Image } from '@heroui/image' -import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' -import { Slider } from '@heroui/slider' -import { Tooltip } from '@heroui/tooltip' -import { useLocalStorage } from '@uidotdev/usehooks' -import clsx from 'clsx' -import { useEffect, useRef, useState } from 'react' +import { Button } from '@heroui/button'; +import { Card, CardBody, CardHeader } from '@heroui/card'; +import { Image } from '@heroui/image'; +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; +import { Slider } from '@heroui/slider'; +import { Tooltip } from '@heroui/tooltip'; +import { useLocalStorage } from '@uidotdev/usehooks'; +import clsx from 'clsx'; +import { useEffect, useRef, useState } from 'react'; import { BiSolidSkipNextCircle, - BiSolidSkipPreviousCircle -} from 'react-icons/bi' + BiSolidSkipPreviousCircle, +} from 'react-icons/bi'; import { FaPause, FaPlay, FaRegHandPointRight, FaRepeat, - FaShuffle -} from 'react-icons/fa6' -import { TbRepeatOnce } from 'react-icons/tb' -import { useMediaQuery } from 'react-responsive' + FaShuffle, +} from 'react-icons/fa6'; +import { TbRepeatOnce } from 'react-icons/tb'; +import { useMediaQuery } from 'react-responsive'; -import { PlayMode } from '@/const/enum' -import key from '@/const/key' +import { PlayMode } from '@/const/enum'; +import key from '@/const/key'; -import { VolumeHighIcon, VolumeLowIcon } from './icons' +import { VolumeHighIcon, VolumeLowIcon } from './icons'; export interface AudioPlayerProps extends React.AudioHTMLAttributes { @@ -39,7 +39,7 @@ export interface AudioPlayerProps mode?: PlayMode } -export default function AudioPlayer(props: AudioPlayerProps) { +export default function AudioPlayer (props: AudioPlayerProps) { const { src, pressNext, @@ -56,116 +56,116 @@ export default function AudioPlayer(props: AudioPlayerProps) { autoPlay, mode = PlayMode.Loop, ...rest - } = props + } = props; - const [currentTime, setCurrentTime] = useState(0) - const [duration, setDuration] = useState(0) - const [isPlaying, setIsPlaying] = useState(false) - const [volume, setVolume] = useState(100) + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [volume, setVolume] = useState(100); const [isCollapsed, setIsCollapsed] = useLocalStorage( key.isCollapsedMusicPlayer, false - ) - const audioRef = useRef(null) - const cardRef = useRef(null) - const startY = useRef(0) - const startX = useRef(0) - const [translateY, setTranslateY] = useState(0) - const [translateX, setTranslateX] = useState(0) - const isSmallScreen = useMediaQuery({ maxWidth: 767 }) - const isMediumUp = useMediaQuery({ minWidth: 768 }) - const shouldAdd = useRef(false) - const currentProgress = (currentTime / duration) * 100 + ); + const audioRef = useRef(null); + const cardRef = useRef(null); + const startY = useRef(0); + const startX = useRef(0); + const [translateY, setTranslateY] = useState(0); + const [translateX, setTranslateX] = useState(0); + const isSmallScreen = useMediaQuery({ maxWidth: 767 }); + const isMediumUp = useMediaQuery({ minWidth: 768 }); + const shouldAdd = useRef(false); + const currentProgress = (currentTime / duration) * 100; const [storageAutoPlay, setStorageAutoPlay] = useLocalStorage( key.autoPlay, true - ) + ); const handleTimeUpdate = (event: React.SyntheticEvent) => { - const audio = event.target as HTMLAudioElement - setCurrentTime(audio.currentTime) - onTimeUpdate?.(event) - } + const audio = event.target as HTMLAudioElement; + setCurrentTime(audio.currentTime); + onTimeUpdate?.(event); + }; const handleLoadedData = (event: React.SyntheticEvent) => { - const audio = event.target as HTMLAudioElement - setDuration(audio.duration) - onLoadedData?.(event) - } + const audio = event.target as HTMLAudioElement; + setDuration(audio.duration); + onLoadedData?.(event); + }; const handlePlay = (e: React.SyntheticEvent) => { - setIsPlaying(true) - setStorageAutoPlay(true) - onPlay?.(e) - } + setIsPlaying(true); + setStorageAutoPlay(true); + onPlay?.(e); + }; const handlePause = (e: React.SyntheticEvent) => { - setIsPlaying(false) - onPause?.(e) - } + setIsPlaying(false); + onPause?.(e); + }; const changeMode = () => { - const modes = [PlayMode.Loop, PlayMode.Random, PlayMode.Single] - const currentIndex = modes.findIndex((_mode) => _mode === mode) - const nextIndex = currentIndex + 1 - const nextMode = modes[nextIndex] || modes[0] - onChangeMode?.(nextMode) - } + const modes = [PlayMode.Loop, PlayMode.Random, PlayMode.Single]; + const currentIndex = modes.findIndex((_mode) => _mode === mode); + const nextIndex = currentIndex + 1; + const nextMode = modes[nextIndex] || modes[0]; + onChangeMode?.(nextMode); + }; const volumeChange = (value: number) => { - setVolume(value) - } + setVolume(value); + }; useEffect(() => { - const audio = audioRef.current + const audio = audioRef.current; if (audio) { - audio.volume = volume / 100 + audio.volume = volume / 100; } - }, [volume]) + }, [volume]); const handleTouchStart = (e: React.TouchEvent) => { - startY.current = e.touches[0].clientY - startX.current = e.touches[0].clientX - } + startY.current = e.touches[0].clientY; + startX.current = e.touches[0].clientX; + }; const handleTouchMove = (e: React.TouchEvent) => { - const deltaY = e.touches[0].clientY - startY.current - const deltaX = e.touches[0].clientX - startX.current - const container = cardRef.current - const header = cardRef.current?.querySelector('[data-header]') - const headerHeight = header?.clientHeight || 20 - const addHeight = (container?.clientHeight || headerHeight) - headerHeight - const _shouldAdd = isCollapsed && deltaY < 0 + const deltaY = e.touches[0].clientY - startY.current; + const deltaX = e.touches[0].clientX - startX.current; + const container = cardRef.current; + const header = cardRef.current?.querySelector('[data-header]'); + const headerHeight = header?.clientHeight || 20; + const addHeight = (container?.clientHeight || headerHeight) - headerHeight; + const _shouldAdd = isCollapsed && deltaY < 0; if (isSmallScreen) { - shouldAdd.current = _shouldAdd - setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY) + shouldAdd.current = _shouldAdd; + setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY); } else { - setTranslateX(deltaX) + setTranslateX(deltaX); } - } + }; const handleTouchEnd = () => { if (isSmallScreen) { - const container = cardRef.current - const header = cardRef.current?.querySelector('[data-header]') - const headerHeight = header?.clientHeight || 20 - const addHeight = (container?.clientHeight || headerHeight) - headerHeight - const _translateY = translateY - (shouldAdd.current ? addHeight : 0) + const container = cardRef.current; + const header = cardRef.current?.querySelector('[data-header]'); + const headerHeight = header?.clientHeight || 20; + const addHeight = (container?.clientHeight || headerHeight) - headerHeight; + const _translateY = translateY - (shouldAdd.current ? addHeight : 0); if (_translateY > 100) { - setIsCollapsed(true) + setIsCollapsed(true); } else if (_translateY < -100) { - setIsCollapsed(false) + setIsCollapsed(false); } - setTranslateY(0) + setTranslateY(0); } else { if (translateX > 100) { - setIsCollapsed(true) + setIsCollapsed(true); } else if (translateX < -100) { - setIsCollapsed(false) + setIsCollapsed(false); } - setTranslateX(0) + setTranslateX(0); } - } + }; const dragTranslate = isSmallScreen ? translateY @@ -173,16 +173,16 @@ export default function AudioPlayer(props: AudioPlayerProps) { : '' : translateX ? `translateX(${translateX}px)` - : '' + : ''; const collapsedTranslate = isCollapsed ? isSmallScreen ? 'translateY(90%)' : 'translateX(96%)' - : '' + : ''; - const translateStyle = dragTranslate || collapsedTranslate + const translateStyle = dragTranslate || collapsedTranslate; - if (!src) return null + if (!src) return null; return (
- ) + ); } diff --git a/napcat.webui/src/components/button/add_button.tsx b/napcat.webui/src/components/button/add_button.tsx index 571766e7..3461cc5e 100644 --- a/napcat.webui/src/components/button/add_button.tsx +++ b/napcat.webui/src/components/button/add_button.tsx @@ -1,87 +1,87 @@ -import { Button } from '@heroui/button' +import { Button } from '@heroui/button'; import { Dropdown, DropdownItem, DropdownMenu, - DropdownTrigger -} from '@heroui/dropdown' -import { Tooltip } from '@heroui/tooltip' -import { FaRegCircleQuestion } from 'react-icons/fa6' -import { IoAddCircleOutline } from 'react-icons/io5' + DropdownTrigger, +} from '@heroui/dropdown'; +import { Tooltip } from '@heroui/tooltip'; +import { FaRegCircleQuestion } from 'react-icons/fa6'; +import { IoAddCircleOutline } from 'react-icons/io5'; import { HTTPClientIcon, HTTPServerIcon, PCIcon, PlusIcon, - WebsocketIcon -} from '../icons' + WebsocketIcon, +} from '../icons'; export interface AddButtonProps { onOpen: (key: keyof OneBotConfig['network']) => void } const AddButton: React.FC = (props) => { - const { onOpen } = props + const { onOpen } = props; return ( { - onOpen(key as keyof OneBotConfig['network']) + onOpen(key as keyof OneBotConfig['network']); }} > -
-
+
+
-
新建网络配置
+
新建网络配置
+
} > -
+
HTTP服务器 @@ -89,27 +89,27 @@ const AddButton: React.FC = (props) => {
+
} > -
+
HTTP SSE服务器 @@ -117,27 +117,27 @@ const AddButton: React.FC = (props) => {
+
} > -
+
HTTP客户端 @@ -145,27 +145,27 @@ const AddButton: React.FC = (props) => {
+
} > -
+
Websocket服务器 @@ -173,27 +173,27 @@ const AddButton: React.FC = (props) => {
+
} > -
+
Websocket客户端 @@ -202,7 +202,7 @@ const AddButton: React.FC = (props) => { - ) -} + ); +}; -export default AddButton +export default AddButton; diff --git a/napcat.webui/src/components/button/save_buttons.tsx b/napcat.webui/src/components/button/save_buttons.tsx index d943677a..ba00eaa5 100644 --- a/napcat.webui/src/components/button/save_buttons.tsx +++ b/napcat.webui/src/components/button/save_buttons.tsx @@ -1,7 +1,7 @@ -import { Button } from '@heroui/button' -import clsx from 'clsx' -import toast from 'react-hot-toast' -import { IoMdRefresh } from 'react-icons/io' +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 @@ -16,7 +16,7 @@ const SaveButtons: React.FC = ({ reset, isSubmitting, refresh, - className + className, }) => (
= ({ className )} > -
+
-) +); -export default SaveButtons +export default SaveButtons; diff --git a/napcat.webui/src/components/chat_input/components/audio_insert.tsx b/napcat.webui/src/components/chat_input/components/audio_insert.tsx index d94ba2cf..7f8058df 100644 --- a/napcat.webui/src/components/chat_input/components/audio_insert.tsx +++ b/napcat.webui/src/components/chat_input/components/audio_insert.tsx @@ -1,170 +1,170 @@ -import { Button } from '@heroui/button' -import { Input } from '@heroui/input' -import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' -import { Tooltip } from '@heroui/tooltip' -import clsx from 'clsx' -import { useEffect, useRef, useState } from 'react' -import toast from 'react-hot-toast' -import { FaMicrophone } from 'react-icons/fa6' -import { IoMic } from 'react-icons/io5' -import { MdEdit, MdUpload } from 'react-icons/md' +import { Button } from '@heroui/button'; +import { Input } from '@heroui/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; +import { Tooltip } from '@heroui/tooltip'; +import clsx from 'clsx'; +import { useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import { FaMicrophone } from 'react-icons/fa6'; +import { IoMic } from 'react-icons/io5'; +import { MdEdit, MdUpload } from 'react-icons/md'; -import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' +import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'; -import { isURI } from '@/utils/url' +import { isURI } from '@/utils/url'; -import type { OB11Segment } from '@/types/onebot' +import type { OB11Segment } from '@/types/onebot'; const AudioInsert = () => { - const [audioUrl, setAudioUrl] = useState('') - const audioInputRef = useRef(null) - const showStructuredMessage = useShowStructuredMessage() + const [audioUrl, setAudioUrl] = useState(''); + const audioInputRef = useRef(null); + const showStructuredMessage = useShowStructuredMessage(); const showAudioSegment = (file: string) => { const messages: OB11Segment[] = [ { type: 'record', data: { - file: file - } - } - ] - showStructuredMessage(messages) - } + file, + }, + }, + ]; + showStructuredMessage(messages); + }; - const [isRecording, setIsRecording] = useState(false) - const mediaRecorderRef = useRef(null) - const audioChunksRef = useRef([]) - const [audioPreview, setAudioPreview] = useState(null) - const [showPreview, setShowPreview] = useState(false) - const streamRef = useRef(null) - const [recordingTime, setRecordingTime] = useState(0) - const recordingIntervalRef = useRef(null) + const [isRecording, setIsRecording] = useState(false); + const mediaRecorderRef = useRef(null); + const audioChunksRef = useRef([]); + const [audioPreview, setAudioPreview] = useState(null); + const [showPreview, setShowPreview] = useState(false); + const streamRef = useRef(null); + const [recordingTime, setRecordingTime] = useState(0); + const recordingIntervalRef = useRef(null); useEffect(() => { if (isRecording) { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { - streamRef.current = stream - const recorder = new MediaRecorder(stream) - mediaRecorderRef.current = recorder - recorder.start() + streamRef.current = stream; + const recorder = new MediaRecorder(stream); + mediaRecorderRef.current = recorder; + recorder.start(); recorder.ondataavailable = (event) => { if (event.data.size > 0) { - audioChunksRef.current.push(event.data) + audioChunksRef.current.push(event.data); } - } + }; recorder.onstop = () => { if (audioChunksRef.current.length > 0) { const audioBlob = new Blob(audioChunksRef.current, { - type: 'audio/wav' - }) - const reader = new FileReader() - reader.readAsDataURL(audioBlob) + type: 'audio/wav', + }); + const reader = new FileReader(); + reader.readAsDataURL(audioBlob); reader.onloadend = () => { - const base64Audio = reader.result as string - setAudioPreview(base64Audio) - setShowPreview(true) - } - audioChunksRef.current = [] + const base64Audio = reader.result as string; + setAudioPreview(base64Audio); + setShowPreview(true); + }; + audioChunksRef.current = []; } - stream.getTracks().forEach((track) => track.stop()) - } - }) + stream.getTracks().forEach((track) => track.stop()); + }; + }); recordingIntervalRef.current = setInterval(() => { - setRecordingTime((prevTime) => prevTime + 1) - }, 1000) + setRecordingTime((prevTime) => prevTime + 1); + }, 1000); } else { - mediaRecorderRef.current?.stop() + mediaRecorderRef.current?.stop(); if (recordingIntervalRef.current) { - clearInterval(recordingIntervalRef.current) - recordingIntervalRef.current = null + clearInterval(recordingIntervalRef.current); + recordingIntervalRef.current = null; } } - }, [isRecording]) + }, [isRecording]); const startRecording = () => { - setAudioPreview(null) - setShowPreview(false) - setRecordingTime(0) - setIsRecording(true) - } + setAudioPreview(null); + setShowPreview(false); + setRecordingTime(0); + setIsRecording(true); + }; const stopRecording = () => { - setIsRecording(false) - } + setIsRecording(false); + }; const handleShowPreview = () => { if (audioPreview) { - showAudioSegment(audioPreview) + showAudioSegment(audioPreview); } - } + }; const formatTime = (time: number) => { - const minutes = Math.floor(time / 60) - const seconds = time % 60 - return `${minutes}:${seconds.toString().padStart(2, '0')}` - } + const minutes = Math.floor(time / 60); + const seconds = time % 60; + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; return ( <> - -
+ +
-
- - + + - -
- + +
+
- + setAudioUrl(e.target.value)} - placeholder="请输入音频地址" + placeholder='请输入音频地址' />
- -
+ +
{showPreview && audioPreview && (
{(isRecording || audioPreview) && ( -
+
{ ? 'animate-pulse bg-primary-400' : 'bg-success-400' )} - > + /> 录制时长: {formatTime(recordingTime)}
)} @@ -228,27 +228,27 @@ const AudioInsert = () => { { - const file = e.target.files?.[0] + const file = e.target.files?.[0]; if (!file) { - return + return; } - const reader = new FileReader() - reader.readAsDataURL(file) + const reader = new FileReader(); + reader.readAsDataURL(file); reader.onload = (event) => { - const dataURL = event.target?.result - showAudioSegment(dataURL as string) - e.target.value = '' - } + const dataURL = event.target?.result; + showAudioSegment(dataURL as string); + e.target.value = ''; + }; }} /> - ) -} + ); +}; -export default AudioInsert +export default AudioInsert; diff --git a/napcat.webui/src/components/chat_input/components/dice_insert.tsx b/napcat.webui/src/components/chat_input/components/dice_insert.tsx index 23417558..57728d54 100644 --- a/napcat.webui/src/components/chat_input/components/dice_insert.tsx +++ b/napcat.webui/src/components/chat_input/components/dice_insert.tsx @@ -1,31 +1,31 @@ -import { Button } from '@heroui/button' -import { Tooltip } from '@heroui/tooltip' -import { BsDice3Fill } from 'react-icons/bs' +import { Button } from '@heroui/button'; +import { Tooltip } from '@heroui/tooltip'; +import { BsDice3Fill } from 'react-icons/bs'; -import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' +import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'; const DiceInsert = () => { - const showStructuredMessage = useShowStructuredMessage() + const showStructuredMessage = useShowStructuredMessage(); return ( - + - ) -} + ); +}; -export default DiceInsert +export default DiceInsert; diff --git a/napcat.webui/src/components/chat_input/components/emoji_picker.tsx b/napcat.webui/src/components/chat_input/components/emoji_picker.tsx index 5496bf07..c6a47db1 100644 --- a/napcat.webui/src/components/chat_input/components/emoji_picker.tsx +++ b/napcat.webui/src/components/chat_input/components/emoji_picker.tsx @@ -1,20 +1,20 @@ -import { Button } from '@heroui/button' -import { Image } from '@heroui/image' -import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' -import { Tooltip } from '@heroui/tooltip' -import { data, getUrl } from 'qface' -import { useEffect, useRef, useState } from 'react' -import { MdEmojiEmotions } from 'react-icons/md' +import { Button } from '@heroui/button'; +import { Image } from '@heroui/image'; +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; +import { Tooltip } from '@heroui/tooltip'; +import { data, getUrl } from 'qface'; +import { useEffect, useRef, useState } from 'react'; +import { MdEmojiEmotions } from 'react-icons/md'; -import { EmojiValue } from '../formats/emoji_blot' +import { EmojiValue } from '../formats/emoji_blot'; const emojis = data.map((item) => { return { alt: item.QDes, src: getUrl(item.QSid), - id: item.QSid - } as EmojiValue -}) + id: item.QSid, + } as EmojiValue; +}); export interface EmojiPickerProps { onInsertEmoji: (emoji: EmojiValue) => void @@ -22,62 +22,62 @@ export interface EmojiPickerProps { } const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => { - const [visibleEmojis, setVisibleEmojis] = useState([]) - const [isPopoverOpen, setIsPopoverOpen] = useState(false) - const containerRef = useRef(null) + const [visibleEmojis, setVisibleEmojis] = useState([]); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const containerRef = useRef(null); useEffect(() => { if (isPopoverOpen) { - setVisibleEmojis([]) // Reset visible emojis - requestAnimationFrame(() => loadEmojis()) // Start loading emojis + setVisibleEmojis([]); // Reset visible emojis + requestAnimationFrame(() => loadEmojis()); // Start loading emojis } - }, [isPopoverOpen]) + }, [isPopoverOpen]); const loadEmojis = (index = 0, batchSize = 10) => { if (index < emojis.length) { setVisibleEmojis((prev) => [ ...prev, - ...emojis.slice(index, index + batchSize) - ]) - requestAnimationFrame(() => loadEmojis(index + batchSize, batchSize)) + ...emojis.slice(index, index + batchSize), + ]); + requestAnimationFrame(() => loadEmojis(index + batchSize, batchSize)); } - } + }; return (
{ - onOpenChange(v) - setIsPopoverOpen(v) + onOpenChange(v); + setIsPopoverOpen(v); }} > - -
+ +
-
- + {visibleEmojis.map((emoji) => ( ))}
- ) -} + ); +}; -export default EmojiPicker +export default EmojiPicker; diff --git a/napcat.webui/src/components/chat_input/components/file_insert.tsx b/napcat.webui/src/components/chat_input/components/file_insert.tsx index ff46305f..d7d583ec 100644 --- a/napcat.webui/src/components/chat_input/components/file_insert.tsx +++ b/napcat.webui/src/components/chat_input/components/file_insert.tsx @@ -1,95 +1,95 @@ -import { Button } from '@heroui/button' -import { Input } from '@heroui/input' -import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' -import { Tooltip } from '@heroui/tooltip' -import { useRef, useState } from 'react' -import toast from 'react-hot-toast' -import { FaFolder } from 'react-icons/fa6' -import { LuFilePlus2 } from 'react-icons/lu' -import { MdEdit, MdUpload } from 'react-icons/md' +import { Button } from '@heroui/button'; +import { Input } from '@heroui/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; +import { Tooltip } from '@heroui/tooltip'; +import { useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import { FaFolder } from 'react-icons/fa6'; +import { LuFilePlus2 } from 'react-icons/lu'; +import { MdEdit, MdUpload } from 'react-icons/md'; -import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' +import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'; -import { isURI } from '@/utils/url' +import { isURI } from '@/utils/url'; -import type { OB11Segment } from '@/types/onebot' +import type { OB11Segment } from '@/types/onebot'; const FileInsert = () => { - const [fileUrl, setFileUrl] = useState('') - const fileInputRef = useRef(null) - const showStructuredMessage = useShowStructuredMessage() + const [fileUrl, setFileUrl] = useState(''); + const fileInputRef = useRef(null); + const showStructuredMessage = useShowStructuredMessage(); const showFileSegment = (file: string) => { const messages: OB11Segment[] = [ { type: 'file', data: { - file: file - } - } - ] - showStructuredMessage(messages) - } + file, + }, + }, + ]; + showStructuredMessage(messages); + }; return ( <> - -
+ +
-
- - + + - -
- + +
+
- + setFileUrl(e.target.value)} - placeholder="请输入文件地址" + placeholder='请输入文件地址' />
- - + + - -
- + +
+
- + setImgUrl(e.target.value)} - placeholder="请输入图片地址" + placeholder='请输入图片地址' />
- + { - if (key !== null) setMode(key) + if (key !== null) setMode(key); }} > - + setMusicId(e.target.value)} - placeholder="请输入音乐ID" - label="音乐ID" + placeholder='请输入音乐ID' + label='音乐ID' />
( { - return !isURI(v) ? '请输入正确的音乐URL' : null + return !isURI(v) ? '请输入正确的音乐URL' : null; }} - size="sm" - placeholder="请输入音乐URL" - label="音乐URL" + size='sm' + placeholder='请输入音乐URL' + label='音乐URL' /> )} /> ( { - return !isURI(v) ? '请输入正确的音频URL' : null + return !isURI(v) ? '请输入正确的音频URL' : null; }} - size="sm" - placeholder="请输入音频URL" - label="音频URL" + size='sm' + placeholder='请输入音频URL' + label='音频URL' /> )} /> ( )} /> ( )} /> ( )} />
- ) -} + ); +}; -export default MusicInsert +export default MusicInsert; diff --git a/napcat.webui/src/components/chat_input/components/reply_insert.tsx b/napcat.webui/src/components/chat_input/components/reply_insert.tsx index b626551b..f45d9a5b 100644 --- a/napcat.webui/src/components/chat_input/components/reply_insert.tsx +++ b/napcat.webui/src/components/chat_input/components/reply_insert.tsx @@ -1,50 +1,50 @@ -import { Button } from '@heroui/button' -import { Input } from '@heroui/input' -import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' -import { Tooltip } from '@heroui/tooltip' -import { useState } from 'react' -import { BsChatQuoteFill } from 'react-icons/bs' -import { MdAdd } from 'react-icons/md' +import { Button } from '@heroui/button'; +import { Input } from '@heroui/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; +import { Tooltip } from '@heroui/tooltip'; +import { useState } from 'react'; +import { BsChatQuoteFill } from 'react-icons/bs'; +import { MdAdd } from 'react-icons/md'; export interface ReplyInsertProps { insertReply: (messageId: string) => void } const ReplyInsert = ({ insertReply }: ReplyInsertProps) => { - const [replyId, setReplyId] = useState('') + const [replyId, setReplyId] = useState(''); return ( <> - -
+ +
-
- + { - const value = e.target.value - const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/ + const value = e.target.value; + const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/; if (isNumberReg.test(value)) { - setReplyId(value) + setReplyId(value); } }} /> - ) -} + ); +}; -export default RPSInsert +export default RPSInsert; diff --git a/napcat.webui/src/components/chat_input/components/show_structed_message.tsx b/napcat.webui/src/components/chat_input/components/show_structed_message.tsx index b4153dc5..6dd63425 100644 --- a/napcat.webui/src/components/chat_input/components/show_structed_message.tsx +++ b/napcat.webui/src/components/chat_input/components/show_structed_message.tsx @@ -1,6 +1,6 @@ -import { Snippet } from '@heroui/snippet' +import { Snippet } from '@heroui/snippet'; -import { OB11Segment } from '@/types/onebot' +import { OB11Segment } from '@/types/onebot'; export interface ShowStructedMessageProps { messages: OB11Segment[] @@ -11,22 +11,22 @@ const ShowStructedMessage = ({ messages }: ShowStructedMessageProps) => { {JSON.stringify(messages, null, 2) .split('\n') .map((line, i) => ( - + {line} ))} - ) -} + ); +}; -export default ShowStructedMessage +export default ShowStructedMessage; diff --git a/napcat.webui/src/components/chat_input/components/video_insert.tsx b/napcat.webui/src/components/chat_input/components/video_insert.tsx index 044e68c8..9d58fb9c 100644 --- a/napcat.webui/src/components/chat_input/components/video_insert.tsx +++ b/napcat.webui/src/components/chat_input/components/video_insert.tsx @@ -1,95 +1,95 @@ -import { Button } from '@heroui/button' -import { Input } from '@heroui/input' -import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' -import { Tooltip } from '@heroui/tooltip' -import { useRef, useState } from 'react' -import toast from 'react-hot-toast' -import { IoVideocam } from 'react-icons/io5' -import { MdEdit, MdUpload } from 'react-icons/md' -import { TbVideoPlus } from 'react-icons/tb' +import { Button } from '@heroui/button'; +import { Input } from '@heroui/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; +import { Tooltip } from '@heroui/tooltip'; +import { useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import { IoVideocam } from 'react-icons/io5'; +import { MdEdit, MdUpload } from 'react-icons/md'; +import { TbVideoPlus } from 'react-icons/tb'; -import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' +import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'; -import { isURI } from '@/utils/url' +import { isURI } from '@/utils/url'; -import type { OB11Segment } from '@/types/onebot' +import type { OB11Segment } from '@/types/onebot'; const VideoInsert = () => { - const [videoUrl, setVideoUrl] = useState('') - const videoInputRef = useRef(null) - const showStructuredMessage = useShowStructuredMessage() + const [videoUrl, setVideoUrl] = useState(''); + const videoInputRef = useRef(null); + const showStructuredMessage = useShowStructuredMessage(); const showVideoSegment = (file: string) => { const messages: OB11Segment[] = [ { type: 'video', data: { - file: file - } - } - ] - showStructuredMessage(messages) - } + file, + }, + }, + ]; + showStructuredMessage(messages); + }; return ( <> - -
+ +
-
- - + + - -
- + +
+
- + setVideoUrl(e.target.value)} - placeholder="请输入视频地址" + placeholder='请输入视频地址' />
- ) -} + ); +}; -export default ChatInput +export default ChatInput; diff --git a/napcat.webui/src/components/chat_input/modal.tsx b/napcat.webui/src/components/chat_input/modal.tsx index 1aa686f1..841c9258 100644 --- a/napcat.webui/src/components/chat_input/modal.tsx +++ b/napcat.webui/src/components/chat_input/modal.tsx @@ -1,42 +1,42 @@ -import { Button } from '@heroui/button' +import { Button } from '@heroui/button'; import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, - useDisclosure -} from '@heroui/modal' + useDisclosure, +} from '@heroui/modal'; -import ChatInput from '.' +import ChatInput from '.'; -export default function ChatInputModal() { - const { isOpen, onOpen, onOpenChange } = useDisclosure() +export default function ChatInputModal () { + const { isOpen, onOpen, onOpenChange } = useDisclosure(); return ( <> - {(onClose) => ( <> - + 构造消息 - -
+ +
- @@ -45,5 +45,5 @@ export default function ChatInputModal() { - ) + ); } diff --git a/napcat.webui/src/components/code_editor.tsx b/napcat.webui/src/components/code_editor.tsx index 43af156f..f7d034d3 100644 --- a/napcat.webui/src/components/code_editor.tsx +++ b/napcat.webui/src/components/code_editor.tsx @@ -1,46 +1,46 @@ -import Editor, { OnMount } from '@monaco-editor/react' -import { loader } from '@monaco-editor/react' -import React from 'react' +import Editor, { OnMount, loader } from '@monaco-editor/react'; -import { useTheme } from '@/hooks/use-theme' +import React from 'react'; -import monaco from '@/monaco' +import { useTheme } from '@/hooks/use-theme'; + +import monaco from '@/monaco'; loader.config({ monaco, paths: { - vs: '/webui/monaco-editor/min/vs' - } -}) + vs: '/webui/monaco-editor/min/vs', + }, +}); loader.config({ 'vs/nls': { - availableLanguages: { '*': 'zh-cn' } - } -}) + availableLanguages: { '*': 'zh-cn' }, + }, +}); export interface CodeEditorProps extends React.ComponentProps { test?: string } -export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor +export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor; const CodeEditor = React.forwardRef( (props, ref) => { - const { isDark } = useTheme() + const { isDark } = useTheme(); const handleEditorDidMount: OnMount = (editor, monaco) => { if (ref) { if (typeof ref === 'function') { - ref(editor) + ref(editor); } else { - ;(ref as React.RefObject).current = editor + (ref as React.RefObject).current = editor; } } if (props.onMount) { - props.onMount(editor, monaco) + props.onMount(editor, monaco); } - } + }; return ( ( onMount={handleEditorDidMount} theme={isDark ? 'vs-dark' : 'light'} /> - ) + ); } -) +); -export default CodeEditor +export default CodeEditor; diff --git a/napcat.webui/src/components/display_card/common_card.tsx b/napcat.webui/src/components/display_card/common_card.tsx index 74e29720..b920198d 100644 --- a/napcat.webui/src/components/display_card/common_card.tsx +++ b/napcat.webui/src/components/display_card/common_card.tsx @@ -1,13 +1,13 @@ -import { Button, ButtonGroup } from '@heroui/button' -import { Switch } from '@heroui/switch' -import { useState } from 'react' -import { CgDebug } from 'react-icons/cg' -import { FiEdit3 } from 'react-icons/fi' -import { MdDeleteForever } from 'react-icons/md' +import { Button, ButtonGroup } from '@heroui/button'; +import { Switch } from '@heroui/switch'; +import { useState } from 'react'; +import { CgDebug } from 'react-icons/cg'; +import { FiEdit3 } from 'react-icons/fi'; +import { MdDeleteForever } from 'react-icons/md'; -import DisplayCardContainer from './container' +import DisplayCardContainer from './container'; -type NetworkType = OneBotConfig['network'] +type NetworkType = OneBotConfig['network']; export type NetworkDisplayCardFields = Array<{ label: string @@ -15,7 +15,7 @@ export type NetworkDisplayCardFields = Array<{ render?: ( value: NetworkType[T][0][keyof NetworkType[T][0]] ) => React.ReactNode -}> +}>; export interface NetworkDisplayCardProps { data: NetworkType[T][0] @@ -36,25 +36,25 @@ const NetworkDisplayCard = ({ onEdit, onEnable, onDelete, - onEnableDebug + onEnableDebug, }: NetworkDisplayCardProps) => { - const { name, enable, debug } = data - const [editing, setEditing] = useState(false) + const { name, enable, debug } = data; + const [editing, setEditing] = useState(false); const handleEnable = () => { - setEditing(true) - onEnable().finally(() => setEditing(false)) - } + setEditing(true); + onEnable().finally(() => setEditing(false)); + }; const handleDelete = () => { - setEditing(true) - onDelete().finally(() => setEditing(false)) - } + setEditing(true); + onDelete().finally(() => setEditing(false)); + }; const handleEnableDebug = () => { - setEditing(true) - onEnableDebug().finally(() => setEditing(false)) - } + setEditing(true); + onEnableDebug().finally(() => setEditing(false)); + }; return ( ({
- ) + ); } -export default errorFallbackRender +export default errorFallbackRender; diff --git a/napcat.webui/src/components/file_icon.tsx b/napcat.webui/src/components/file_icon.tsx index 9327abe9..c955d921 100644 --- a/napcat.webui/src/components/file_icon.tsx +++ b/napcat.webui/src/components/file_icon.tsx @@ -11,8 +11,8 @@ import { FaFileVideo, FaFileWord, FaFileZipper, - FaFolderClosed -} from 'react-icons/fa6' + FaFolderClosed, +} from 'react-icons/fa6'; export interface FileIconProps { name?: string @@ -20,12 +20,12 @@ export interface FileIconProps { } const FileIcon = (props: FileIconProps) => { - const { name, isDirectory = false } = props + const { name, isDirectory = false } = props; if (isDirectory) { - return + return ; } - const ext = name?.split('.').pop() || '' + const ext = name?.split('.').pop() || ''; if (ext) { switch (ext.toLowerCase()) { case 'jpg': @@ -50,20 +50,20 @@ const FileIcon = (props: FileIconProps) => { case 'fig': case 'xd': case 'svgz': - return + return ; case 'pdf': - return + return ; case 'doc': case 'docx': - return + return ; case 'xls': case 'xlsx': - return + return ; case 'csv': - return + return ; case 'ppt': case 'pptx': - return + return ; case 'zip': case 'rar': case '7z': @@ -79,18 +79,18 @@ const FileIcon = (props: FileIconProps) => { case 'taz': case 'tz': case 'tzo': - return + return ; case 'txt': - return + return ; case 'mp3': case 'wav': case 'flac': - return + return ; case 'mp4': case 'avi': case 'mov': case 'wmv': - return + return ; case 'html': case 'css': case 'js': @@ -154,13 +154,13 @@ const FileIcon = (props: FileIconProps) => { case 'userosscache': case 'sln.docstates': case 'dll': - return + return ; default: - return + return ; } } - return -} + return ; +}; -export default FileIcon +export default FileIcon; diff --git a/napcat.webui/src/components/file_manage/create_file_modal.tsx b/napcat.webui/src/components/file_manage/create_file_modal.tsx index 51b3a14c..d959a044 100644 --- a/napcat.webui/src/components/file_manage/create_file_modal.tsx +++ b/napcat.webui/src/components/file_manage/create_file_modal.tsx @@ -1,12 +1,12 @@ -import { Button, ButtonGroup } from '@heroui/button' -import { Input } from '@heroui/input' +import { Button, ButtonGroup } from '@heroui/button'; +import { Input } from '@heroui/input'; import { Modal, ModalBody, ModalContent, ModalFooter, - ModalHeader -} from '@heroui/modal' + ModalHeader, +} from '@heroui/modal'; interface CreateFileModalProps { isOpen: boolean @@ -18,22 +18,22 @@ interface CreateFileModalProps { onCreate: () => void } -export default function CreateFileModal({ +export default function CreateFileModal ({ isOpen, fileType, newFileName, onTypeChange, onNameChange, onClose, - onCreate + onCreate, }: CreateFileModalProps) { return ( 新建 -
- +
+ - +
- - - ) + ); } diff --git a/napcat.webui/src/components/file_manage/file_edit_modal.tsx b/napcat.webui/src/components/file_manage/file_edit_modal.tsx index cc906e63..309d8f78 100644 --- a/napcat.webui/src/components/file_manage/file_edit_modal.tsx +++ b/napcat.webui/src/components/file_manage/file_edit_modal.tsx @@ -1,14 +1,14 @@ -import { Button } from '@heroui/button' -import { Code } from '@heroui/code' +import { Button } from '@heroui/button'; +import { Code } from '@heroui/code'; import { Modal, ModalBody, ModalContent, ModalFooter, - ModalHeader -} from '@heroui/modal' + ModalHeader, +} from '@heroui/modal'; -import CodeEditor from '@/components/code_editor' +import CodeEditor from '@/components/code_editor'; interface FileEditModalProps { isOpen: boolean @@ -18,61 +18,61 @@ interface FileEditModalProps { onContentChange: (newContent?: string) => void } -export default function FileEditModal({ +export default function FileEditModal ({ isOpen, file, onClose, onSave, - onContentChange + 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' - } + 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 ( - + - + 编辑文件 - {file?.path} + {file?.path} - -
+ +
- - - ) + ); } diff --git a/napcat.webui/src/components/file_manage/file_preview_modal.tsx b/napcat.webui/src/components/file_manage/file_preview_modal.tsx index e416eb48..c559875f 100644 --- a/napcat.webui/src/components/file_manage/file_preview_modal.tsx +++ b/napcat.webui/src/components/file_manage/file_preview_modal.tsx @@ -1,17 +1,17 @@ -import { Button } from '@heroui/button' +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' + 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' +import FileManager from '@/controllers/file_manager'; interface FilePreviewModalProps { isOpen: boolean @@ -19,74 +19,74 @@ interface FilePreviewModalProps { onClose: () => void } -export const videoExts = ['.mp4', '.webm'] -export const audioExts = ['.mp3', '.wav'] +export const videoExts = ['.mp4', '.webm']; +export const audioExts = ['.mp3', '.wav']; -export const supportedPreviewExts = [...videoExts, ...audioExts] +export const supportedPreviewExts = [...videoExts, ...audioExts]; -export default function FilePreviewModal({ +export default function FilePreviewModal ({ isOpen, filePath, - onClose + onClose, }: FilePreviewModalProps) { - const ext = path.extname(filePath).toLowerCase() + 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() + const ext = path.extname(filePath).toLowerCase(); if (!filePath || !supportedPreviewExts.includes(ext)) { - return + return; } - run() - } + run(); + }, } - ) + ); useEffect(() => { if (filePath) { - run() + run(); } - }, [filePath]) + }, [filePath]); - let contentElement = null + let contentElement = null; if (!supportedPreviewExts.includes(ext)) { - contentElement =
暂不支持预览此文件类型
+ contentElement =
暂不支持预览此文件类型
; } else if (error) { - contentElement =
读取文件失败
+ contentElement =
读取文件失败
; } else if (loading || !data) { contentElement = ( -
+
- ) + ); } else if (videoExts.includes(ext)) { - contentElement =