Compare commits

...
643 Commits
Author SHA1 Message Date
手瓜一十雪 91df57d932 fix: async error
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-16 11:55:00 +08:00
Mlikiowa e27d4c4302 release: v4.7.1 2025-03-16 03:40:48 +00:00
手瓜一十雪 55847f6e10 fix: quick login 延迟问题
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-16 11:39:38 +08:00
手瓜一十雪 b39d8bae27 fix: login timer / add:不规范的promise 2025-03-16 10:42:15 +08:00
手瓜一十雪 b0cf23f775 doc: security 2025-03-16 09:36:27 +08:00
手瓜一十雪 c641246056 doc: code of conduct 2025-03-16 09:33:06 +08:00
Mlikiowa 1e5bc9bbea release: v4.7.0 2025-03-16 01:13:17 +00:00
手瓜一十雪 99b504b5f6 fix: #880
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-16 09:12:52 +08:00
Mlikiowa 1146454fec release: v4.6.9 2025-03-15 10:58:09 +00:00
手瓜一十雪 805e014a75 fix: #877
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-15 18:54:51 +08:00
Mlikiowa d3acd1efc1 release: v4.6.8 2025-03-14 10:13:29 +00:00
手瓜一十雪 9fcd218a5a fix: #873
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-14 18:12:58 +08:00
手瓜一十雪 d6a0830cfe fix: #875 2025-03-14 18:07:03 +08:00
手瓜一十雪 40a63b9c66 fix: #870 2025-03-14 17:53:03 +08:00
手瓜一十雪 eeb19a04cc fix: packet异常 2025-03-14 17:39:37 +08:00
Mlikiowa 91e457eb03 release: v4.6.7 2025-03-09 08:31:07 +00:00
手瓜一十雪 78d1919d7f feat: 32896
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-09 16:30:43 +08:00
手瓜一十雪 8393acf173 Merge pull request #856 from HDTianRu/main
feat: 额外返回原msgSeq条目
2025-03-09 10:09:41 +08:00
手瓜一十雪 bca152a047 feat: readme 翻新 2025-03-09 10:08:49 +08:00
HDTianRu 6a15908a93 feat: 额外返回原msgSeq条目 2025-03-08 16:36:17 +08:00
bietiaop c626bbab74 fix: #854 2025-03-07 10:25:38 +08:00
Mlikiowa c5c7dcc6f2 release: v4.6.6 2025-03-06 10:51:30 +00:00
手瓜一十雪 03dafe727e fix: win
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-06 18:51:05 +08:00
Mlikiowa 744921c45e release: v4.6.5 2025-03-06 10:09:45 +00:00
手瓜一十雪 abc4a4dcba feat: 32793
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-06 18:09:14 +08:00
Mlikiowa 7e0da2f929 release: v4.6.4 2025-03-05 13:15:11 +00:00
手瓜一十雪 a3b70d0f1f fix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-05 21:14:52 +08:00
Mlikiowa d291724f06 release: v4.6.3 2025-03-03 09:17:03 +00:00
手瓜一十雪 122a9ca2cc feat: o3拦截
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-03 17:16:36 +08:00
手瓜一十雪 48aaddd32b feat:rkey 2025-03-03 12:28:55 +08:00
手瓜一十雪 47401af856 feat: searchMsgWithKeywords 2025-03-02 16:07:27 +08:00
Mlikiowa 709adfd812 release: v4.6.2 2025-03-02 07:11:16 +00:00
手瓜一十雪 038d0c5412 fix: #785
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-03-02 14:55:47 +08:00
手瓜一十雪 6bb4362ed4 feat: 32721 2025-03-02 14:36:11 +08:00
手瓜一十雪 e617f9452d fix: #841 2025-03-02 14:32:21 +08:00
手瓜一十雪 6d8bb49a37 fix: #837 2025-03-02 14:27:09 +08:00
手瓜一十雪 4f6073ee86 fix: 837 2025-03-02 14:26:28 +08:00
手瓜一十雪 2e7176304b fix: #843 2025-03-02 14:24:51 +08:00
Mlikiowa e36cf11004 release: v4.6.1 2025-02-27 08:35:02 +00:00
手瓜一十雪 0e49e17f68 feat: 32690
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-27 16:34:09 +08:00
手瓜一十雪 524de45f6b Merge pull request #827 from NapNeko/dependabot/npm_and_yarn/globals-16.0.0
chore(deps-dev): bump globals from 15.15.0 to 16.0.0
2025-02-27 16:18:17 +08:00
手瓜一十雪 85741a4b60 feat: 32690 2025-02-27 16:14:39 +08:00
dependabot[bot] f9ccb8c978 chore(deps-dev): bump globals from 15.15.0 to 16.0.0
Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 16.0.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v16.0.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 09:06:38 +00:00
手瓜一十雪 ea3d069e49 feat: vsc build dev体验增强 2025-02-23 17:54:19 +08:00
Mlikiowa 3e6024f183 release: v4.6.0 2025-02-23 09:31:55 +00:00
Mlikiowa 337871693a release: v4.5.24 2025-02-23 09:31:19 +00:00
手瓜一十雪 2d921c4577 feat: sisi的妙妙rkey
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-23 17:30:01 +08:00
手瓜一十雪 9accff7323 fix: ts warning 2025-02-23 17:28:30 +08:00
手瓜一十雪 88b1ee8c31 docs: todo #819 2025-02-23 17:17:52 +08:00
手瓜一十雪 3ac618bb4e fix: #822 2025-02-23 17:01:00 +08:00
手瓜一十雪 0051df3741 fix: #824 2025-02-23 16:57:55 +08:00
手瓜一十雪 7eb4e010b0 Merge pull request #823 from NapNeko/refactor-worker
refactor: 即刻起逐出piscina
2025-02-23 14:31:33 +08:00
手瓜一十雪 33cc23ada3 refactor: 即刻起逐出piscina 2025-02-23 14:29:26 +08:00
手瓜一十雪 e5aee372e3 fix: 调整依赖 2025-02-23 13:40:47 +08:00
手瓜一十雪 6b6ce4a761 fix: 依赖迁移到dev 2025-02-22 12:59:37 +08:00
手瓜一十雪 8c4ea7f8f2 fix: 异常代码 2025-02-22 11:57:48 +08:00
手瓜一十雪 c8b268b806 fix: #791 2025-02-22 11:50:54 +08:00
手瓜一十雪 cf5e0e0f14 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-18 17:08:26 +08:00
手瓜一十雪 7b79f9cc17 fix: 日志显示 2025-02-18 17:08:24 +08:00
Mlikiowa 708d599966 release: v4.5.23 2025-02-18 08:56:25 +00:00
手瓜一十雪 1ecd5b78e6 feat: 文件移除path字段增强部分能力
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-18 16:55:43 +08:00
手瓜一十雪 fca2e3c51a style: remove debug 2025-02-18 16:52:30 +08:00
手瓜一十雪 95ea761b2d feat: get_private_file_url 2025-02-18 16:51:51 +08:00
手瓜一十雪 6b3bfa1ee9 fix #810 2025-02-18 13:24:37 +08:00
bietiaop df3e302a9d fix: #802 2025-02-14 21:26:16 +08:00
pk5ls20 c88a68c9a8 fix: typo x2 2025-02-14 20:52:31 +08:00
Mlikiowa 92d01b9cdd release: v4.5.22 2025-02-14 10:36:03 +00:00
手瓜一十雪 fe04fa5986 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-14 17:41:40 +08:00
手瓜一十雪 c382f541b4 fix: 优化文件处理错误信息并简化下载逻辑 2025-02-14 17:41:25 +08:00
手瓜一十雪 f420527207 Update msg.ts 2025-02-14 17:41:03 +08:00
手瓜一十雪 e0c83ebf79 fix: #793 2025-02-14 17:15:19 +08:00
手瓜一十雪 c7fb18fc08 feat: 补全一些type 2025-02-14 15:39:06 +08:00
手瓜一十雪 2db8ab937d feat: GetUnidirectionalFriendList router 2025-02-14 15:06:36 +08:00
手瓜一十雪 819f5dd8e5 fix: #785 2025-02-14 14:50:00 +08:00
手瓜一十雪 d4a8ed735e fix: #789 2025-02-14 14:48:36 +08:00
手瓜一十雪 f07e3bb4d5 fix: type 2025-02-14 14:44:10 +08:00
手瓜一十雪 fa5ef0c221 fix: #797 2025-02-14 14:41:16 +08:00
手瓜一十雪 da7499ec0b Merge pull request #790 from NapNeko/dependabot/npm_and_yarn/esbuild-0.25.0
chore(deps-dev): bump esbuild from 0.24.0 to 0.25.0
2025-02-14 13:51:47 +08:00
Mlikiowa d2f4327e44 release: v4.5.21 2025-02-12 18:57:14 +00:00
pk5ls20 2eba640180 fix: typo
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-13 02:56:07 +08:00
dependabot[bot] 29ae55f340 chore(deps-dev): bump esbuild from 0.24.0 to 0.25.0
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 08:16:17 +00:00
Mlikiowa 3d2bca3f9f release: v4.5.20 2025-02-09 05:05:52 +00:00
手瓜一十雪 7fd8c0c822 style:lint
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-09 13:00:54 +08:00
手瓜一十雪 a9e9c81505 refactor: data recv 2025-02-09 13:00:17 +08:00
手瓜一十雪 e8cc68bdea style:lint 2025-02-09 12:53:42 +08:00
手瓜一十雪 9e51a661a4 fix: #761 2025-02-09 12:53:10 +08:00
bietiaop a167aaf55f style: 修改首页卡片色适配主题 2025-02-09 12:28:57 +08:00
bietiaop a54ecbcaa0 style: 修改侧边栏标题色适配主题 2025-02-09 12:21:34 +08:00
bietiaop 788462cdfa fix: 修复heroui primary色 2025-02-09 12:13:43 +08:00
bietiaop 45c5965b99 style: 增加heroui主题色 2025-02-09 12:11:27 +08:00
bietiaop ce7614de46 fix: 缺少default 2025-02-09 12:00:02 +08:00
bietiaop 9f78e1ce1e feat: 预定义主题 2025-02-09 11:58:46 +08:00
pk5ls20 2c7b0625e8 chore: format 2025-02-09 01:35:37 +08:00
pk5ls20 c3a5da9be1 feat: #768 2025-02-09 01:33:56 +08:00
bietiaop ca796e1920 feat: 设置快速登录QQ & 自定义webui主题色
feat: 设置快速登录QQ & 自定义webui主题色
2025-02-09 00:54:27 +08:00
bietiaop 7ce04cf781 final 2025-02-09 00:47:00 +08:00
bietiaop 024a3eb760 fix 2025-02-09 00:18:14 +08:00
bietiaop 1702f429b4 fix 2025-02-09 00:17:49 +08:00
bietiaop 96d79cf495 fix 2025-02-08 23:45:33 +08:00
bietiaop a6a11a7026 fix 2025-02-08 23:38:30 +08:00
bietiaop 970a49e2a5 fix: 猪咪 2025-02-08 23:05:48 +08:00
bietiaop 2e013ed4f5 fix 2025-02-08 22:43:53 +08:00
bietiaop f8c396b1fe feat(webui): 快速登录config 2025-02-08 21:16:49 +08:00
手瓜一十雪 b54870cb60 fix 2025-02-08 21:03:59 +08:00
bietiaop 84318acb18 feat(webui): theme 2025-02-08 21:01:29 +08:00
手瓜一十雪 a11a042b93 docs: update 2025-02-08 20:22:51 +08:00
Mlikiowa 8a8aa8f62c release: v4.5.18 2025-02-08 09:43:06 +00:00
手瓜一十雪 93f78f4db5 feat: #780
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-08 17:34:31 +08:00
手瓜一十雪 404bfdd5e6 fix: #783 2025-02-08 17:00:11 +08:00
Mlikiowa e4577dc2f1 release: v4.5.17 2025-02-07 12:40:47 +00:00
pk5ls20 5c932e5a27 fix: native rkey
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-07 19:20:35 +08:00
Mlikiowa 4bd63c6267 release: v4.5.16 2025-02-07 10:02:35 +00:00
bietiaop aabe24f903 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-07 18:00:31 +08:00
bietiaop 69cebd7fbc feat: 提示修改默认密码 2025-02-07 18:00:22 +08:00
Mlikiowa 8da371176a release: v4.5.15 2025-02-07 09:52:51 +00:00
手瓜一十雪 dd08adf1d1 fix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-07 17:43:08 +08:00
手瓜一十雪 2f67bef139 fix: #775 2025-02-07 17:25:48 +08:00
手瓜一十雪 8968c51cdc fix: 砍掉mac pty 沙盒权限不足 2025-02-07 17:11:10 +08:00
手瓜一十雪 f2fdcc9289 feat: webui体验优化 2025-02-07 13:56:48 +08:00
手瓜一十雪 aa3a575cbe feat: 优化初始化步骤 2025-02-07 13:26:48 +08:00
bietiaop 11816d038d fix: #776 2025-02-06 20:10:11 +08:00
Mlikiowa 6a990edb38 release: v4.5.14 2025-02-06 09:17:22 +00:00
手瓜一十雪 fa12865924 fix: error
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-06 17:10:30 +08:00
Mlikiowa ecdd717742 release: v4.5.12 2025-02-06 08:23:07 +00:00
bietiaop 6851334af9 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-06 15:29:04 +08:00
bietiaop 9051b29565 feat: 字体修改#771 2025-02-06 15:28:42 +08:00
手瓜一十雪 95c7d3dfbd fix: remove __dirname 2025-02-06 15:28:24 +08:00
手瓜一十雪 bc1148c00a fix: require_dlopen 2025-02-06 15:25:47 +08:00
Mlikiowa d4556d9299 release: v4.5.11 2025-02-06 03:13:17 +00:00
pk5ls20 5d389a2359 fix: fake forwardMsg construct
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-06 01:09:23 +08:00
Mlikiowa 305116874b release: v4.5.10 2025-02-05 11:49:14 +00:00
bietiaop b08a29897f fix: #769
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-05 19:45:30 +08:00
Mlikiowa b59c1d9122 release: v4.5.9 2025-02-05 11:14:25 +00:00
手瓜一十雪 adb9cea701 Merge pull request #765 from NapNeko/fix/multi-forward-protocol-fetch
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
fix: #721
2025-02-05 19:08:08 +08:00
Mlikiowa 5e148d2e82 release: v4.5.8 2025-02-05 11:02:28 +00:00
手瓜一十雪 a0d780558e fix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-05 19:01:14 +08:00
Mlikiowa ad56065a4e release: v4.5.7 2025-02-05 07:10:27 +00:00
手瓜一十雪 f5dee80b6e Merge branch 'main' of https://github.com/NapNeko/NapCatQQ
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-05 15:09:27 +08:00
手瓜一十雪 9cc75881b8 fix: arm64 2025-02-05 14:51:12 +08:00
bietiaop 593fb13b61 style: 语义化样式 2025-02-05 10:38:12 +08:00
pk5ls20 fca90592d6 try fix: #755 2025-02-05 08:29:37 +08:00
pk5ls20 d6848e2855 fix: #721 2025-02-05 08:07:58 +08:00
bietiaop 7539a4129f fix: 获取歌单 2025-02-04 22:14:23 +08:00
bietiaop 5402574266 feat: AI更新总结 2025-02-04 22:03:37 +08:00
Mlikiowa 853175aa1a release: v4.5.6 2025-02-04 13:24:46 +00:00
手瓜一十雪 feb84809ec fix: #761
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-04 21:22:36 +08:00
bietiaop a812c568e4 fix: 文件预览 2025-02-04 21:12:13 +08:00
bietiaop 11db25e355 fix: 文件预览 2025-02-04 21:08:28 +08:00
手瓜一十雪 ecd2fba629 fix: #762 2025-02-04 20:42:13 +08:00
Mlikiowa a6763cf5a1 release: v4.5.5 2025-02-04 11:50:37 +00:00
手瓜一十雪 c9e91a9b94 fix: defalut config
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-04 19:49:56 +08:00
Mlikiowa 43fb62c5bd release: v4.5.4 2025-02-04 11:35:51 +00:00
手瓜一十雪 cb8727d487 fix: reload and parse msg
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-04 19:34:51 +08:00
Mlikiowa a94e03e2fd release: v4.5.3 2025-02-04 10:16:07 +00:00
手瓜一十雪 425c3c6432 fix: 避免重复reload
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-04 18:14:13 +08:00
手瓜一十雪 89b9610016 fix: 避免read异常 2025-02-04 18:13:42 +08:00
手瓜一十雪 62fe88f868 Merge pull request #760 from NapNeko/config-refactor
refactor
2025-02-04 18:09:57 +08:00
手瓜一十雪 11a7f5fade refactor 2025-02-04 18:09:30 +08:00
bietiaop fbde997f7c style: 调整样式 2025-02-04 17:58:38 +08:00
bietiaop 26734a35ef fix: 文件下载 2025-02-04 15:31:10 +08:00
Mlikiowa 715c4ac534 release: v4.5.2 2025-02-04 06:52:11 +00:00
bietiaop bd4b0885a1 fix: 预览
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-04 14:47:38 +08:00
手瓜一十雪 e3c7af3d91 fix: 解决nonebot可能卡死问题 2025-02-04 14:42:14 +08:00
手瓜一十雪 a7ee21bfd8 fix: #757 2025-02-04 14:34:55 +08:00
手瓜一十雪 d0f51d92ac feat: tailwind css 2025-02-04 13:52:53 +08:00
手瓜一十雪 e6dc148ea2 fix: diy status问题 2025-02-04 13:44:35 +08:00
手瓜一十雪 514ab6637f feat: 全局字体优化 2025-02-04 13:37:11 +08:00
bietiaop 377794abe8 style: font & terminal
style: font & terminal
2025-02-04 13:09:00 +08:00
bietiaop 0f3251f35b fix: 字体、终端样式 2025-02-04 12:59:51 +08:00
手瓜一十雪 8002dc5bc5 fix 2025-02-04 00:16:59 +08:00
手瓜一十雪 c75a13dcf4 fix 2025-02-04 00:14:15 +08:00
Mlikiowa 91d153bb9d release: v4.5.1 2025-02-03 12:48:00 +00:00
bietiaop b32f9fa397 feat: 文件下载/上传
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-03 19:56:33 +08:00
Mlikiowa 80593730ae release: v4.4.20 2025-02-03 08:36:59 +00:00
手瓜一十雪 090d54a78d fix: typo
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-03 16:36:25 +08:00
Mlikiowa b7d1fb181c release: v4.4.19 2025-02-03 08:34:24 +00:00
手瓜一十雪 6e56693ca7 feat: 支持set_diy_online_status
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-03 16:33:31 +08:00
Mlikiowa 7403db9b20 release: v4.4.18 2025-02-03 07:05:56 +00:00
手瓜一十雪 9d167cd883 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-03 15:05:16 +08:00
手瓜一十雪 197eec40ad fix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-03 15:05:12 +08:00
Mlikiowa 07819a6618 release: v4.4.17 2025-02-03 06:50:58 +00:00
手瓜一十雪 b72156866d fix: broken pipe
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-02-03 14:50:11 +08:00
手瓜一十雪 59a7d12a8c style: lint 2025-02-03 14:29:38 +08:00
手瓜一十雪 179351b23a Merge pull request #754 from NapNeko/qrcode-refactor
fix: thumb残留
2025-02-03 14:11:22 +08:00
手瓜一十雪 790809e8e5 fix: thumb残留 2025-02-03 14:09:51 +08:00
手瓜一十雪 1414a8a8c9 Merge pull request #753 from NapNeko/qrcode-refactor
refactor: qrcode to ts
2025-02-03 14:02:48 +08:00
bietiaop 9ab41734a5 fix: piscina src 2025-02-03 13:33:18 +08:00
手瓜一十雪 03cace2ea1 优化依赖 2025-02-03 13:07:05 +08:00
手瓜一十雪 c7371ab869 refactor: qrcode to ts 2025-02-03 12:51:50 +08:00
手瓜一十雪 b32d4b618c fix: 必须清理并回收 2025-02-03 11:40:58 +08:00
手瓜一十雪 3a27f37686 fix: pcm清理 2025-02-03 11:37:39 +08:00
手瓜一十雪 fe2d21979d Merge pull request #749 from NapNeko/type-force
style: Type force
2025-02-03 11:13:55 +08:00
手瓜一十雪 48b1f3d4f0 Merge branch 'main' into type-force 2025-02-03 11:13:46 +08:00
手瓜一十雪 93ed589ac7 Merge pull request #745 from NapNeko/dev-terminal
feat: 文件管理 & 系统终端
2025-02-03 11:13:14 +08:00
手瓜一十雪 96de9e2c16 style: 简化loader 避免全局error 2025-02-03 10:40:33 +08:00
手瓜一十雪 b25f9d3bec feat: 摇树生成&多平台统一改造 2025-02-03 10:33:10 +08:00
手瓜一十雪 15854c605b style: 强类型大法 2025-02-02 23:22:21 +08:00
手瓜一十雪 ac193cc94a style: lint 2025-02-02 20:17:28 +08:00
手瓜一十雪 d626f872e6 feat: 类型规范 2025-02-02 20:16:11 +08:00
bietiaop 3eb66fa34a fix: 关闭终端启动QQ 2025-02-02 15:34:53 +08:00
bietiaop 0fdd0175b7 fix: 重复关闭 2025-02-02 15:09:38 +08:00
pk5ls20 dec9b477e0 fix: #747 2025-02-02 14:51:36 +08:00
bietiaop a0a4b0dd1d fix: 频率限制 2025-02-02 14:47:34 +08:00
bietiaop 8dc6da56a7 fix: node-pty 2025-02-02 14:33:39 +08:00
bietiaop b4e07aacfe chore(terminal): 使用prebuild 2025-02-02 12:59:00 +08:00
bietiaop 19b47f0f42 fix: id生成使用uuid 2025-02-02 12:00:55 +08:00
bietiaop f9ef3d63c7 fix: id生成使用uuid 2025-02-02 11:57:28 +08:00
bietiaop 2b574d33b5 fix: 更换重命名图标防止误解 2025-02-02 11:46:04 +08:00
bietiaop 6039e9bb46 feat: 文件管理 2025-02-02 11:37:58 +08:00
手瓜一十雪 adfd4b043f docs: fix 2025-02-02 11:02:54 +08:00
bietiaop 719189be55 feat: file manager 2025-02-01 22:47:51 +08:00
bietiaop ef9907f4b6 style: 优化样式 2025-02-01 20:51:45 +08:00
bietiaop 16b7447df1 style: 优化样式 2025-02-01 20:47:54 +08:00
bietiaop 4157746478 feat: 系统终端 2025-02-01 20:35:01 +08:00
bietiaop 5120786708 Merge branch 'dev-terminal' of https://github.com/NapNeko/NapCatQQ into dev-terminal 2025-02-01 13:44:18 +08:00
bietiaop 0176fa75ef dev: terminal 2025-02-01 13:41:20 +08:00
bietiaop e6968f2d80 fix(webui): name重复问题 2025-02-01 11:44:30 +08:00
bietiaop c0dd8a53e8 chore(dep): 更新依赖,移除overrides(strtok3已经修复) 2025-01-31 19:05:40 +08:00
bietiaop 3cb3135235 feat: 登录状态机 2025-01-31 18:48:46 +08:00
bietiaop 28182cac64 chore(dep): 更新webui依赖 2025-01-31 12:07:57 +08:00
bietiaop 73b80d2482 release: v4.4.16
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-30 10:42:46 +08:00
bietiaop f22eb22409 release: v4.4.16 2025-01-30 10:12:35 +08:00
bietiaop 4a95b17a47 feat(webui): 修改token 2025-01-29 22:08:45 +08:00
bietiaop f4a71159fd fix(dep): 尝试解决peek-readable依赖问题 2025-01-29 21:34:59 +08:00
bietiaop c0431e3dc2 feat(webui_api): update token 2025-01-29 20:40:23 +08:00
Mlikiowa 7f87cee282 release: v4.4.15 2025-01-27 11:53:31 +00:00
手瓜一十雪 c24c704439 fix: clone object
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-27 19:53:03 +08:00
Mlikiowa 232e5d55b8 release: v4.4.14 2025-01-27 11:31:15 +00:00
手瓜一十雪 da24ae7e1c fix: 空json5
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-27 19:30:45 +08:00
Mlikiowa 8fc13f8a8f release: v4.4.13 2025-01-27 11:26:17 +00:00
手瓜一十雪 7e1fe31085 fix: http支持json5
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-27 19:25:51 +08:00
Mlikiowa c3cba8ba4e release: v4.4.12 2025-01-27 10:44:43 +00:00
手瓜一十雪 ba619986c9 fix: #739
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-27 18:42:26 +08:00
bietiaop dcef3f3c3b fix: 终端字符宽度&微调样式&路由切换动画 2025-01-27 15:58:27 +08:00
bietiaop 823faa2790 fix: 质量检查 2025-01-26 21:58:04 +08:00
bietiaop ef4248d2a3 fix: 依赖缺失 2025-01-26 21:52:35 +08:00
bietiaop 3917cb0dc9 feat: webui检查更新&修复日志字体渲染 2025-01-26 21:48:45 +08:00
Mlikiowa 520cec0eaa release: v4.4.11 2025-01-26 13:03:34 +00:00
手瓜一十雪 e7655e0ff6 Merge pull request #737 from NapNeko/ffmpeg
feat: no spawn ffmpeg
2025-01-26 21:02:57 +08:00
手瓜一十雪 350ced55c0 fix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-26 21:00:47 +08:00
手瓜一十雪 2ca6d0a00e feat: 移除测试代码 2025-01-26 20:44:27 +08:00
手瓜一十雪 844abad0d0 fix: 彻底完成迁移 2025-01-26 20:42:05 +08:00
手瓜一十雪 d278e9d8bc feat: ffmpeg 2025-01-26 16:26:21 +08:00
手瓜一十雪 6e261f30c2 fix: typo 2025-01-26 13:29:47 +08:00
bietiaop 84f0e43369 feat: debug 配置记忆 2025-01-26 10:44:31 +08:00
Mlikiowa 3223a06983 release: v4.4.10 2025-01-25 12:08:25 +00:00
手瓜一十雪 1b874a0264 fix: defaultHttpUrl
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-25 20:00:24 +08:00
Mlikiowa 26525a0ff9 release: v4.4.9 2025-01-25 10:59:08 +00:00
手瓜一十雪 49234ea5c7 fix: music proxy
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2025-01-25 18:34:11 +08:00
Mlikiowa c474158a09 release: v4.4.8 2025-01-25 05:40:09 +00:00
bietiaop 813a541e7f fix: config首次加载 2025-01-25 13:36:56 +08:00
Mlikiowa efd489bfd4 release: v4.4.7 2025-01-25 04:56:25 +00:00
手瓜一十雪 9c88fcb610 style: lint 2025-01-25 12:56:02 +08:00
手瓜一十雪 c1cac8de19 fix: #736 2025-01-25 12:54:39 +08:00
手瓜一十雪 b03fa54e63 fix: fonts 2025-01-25 12:40:01 +08:00
bietiaop 9785731f25 fix: 字体路径 2025-01-25 10:50:47 +08:00
手瓜一十雪 566afde18b fix 2025-01-25 10:41:15 +08:00
手瓜一十雪 bae8dabc5c fix: 兼容JSON配置 异常检查 2025-01-25 10:24:39 +08:00
手瓜一十雪 0a9dfea20a fix 2025-01-25 01:21:06 +08:00
Mlikiowa 50498aa52d release: v4.4.6 2025-01-24 16:41:07 +00:00
手瓜一十雪 cc99fa8346 fix: renderer 2025-01-25 00:37:33 +08:00
bietiaop 500c10ea7a fix: 移除部分字体 2025-01-24 22:44:18 +08:00
bietiaop 71a62caf8f fix: sse配置缺失 2025-01-24 22:36:40 +08:00
bietiaop fe9c565ad4 fix: 字体缺失 2025-01-24 22:23:45 +08:00
手瓜一十雪 f5f915dc91 fix: 移除nc_openwebui_ex_react 2025-01-24 22:09:41 +08:00
手瓜一十雪 eba17fd9b4 Merge pull request #734 from bietiaop/main
feat: 新版webui
2025-01-24 21:56:32 +08:00
bietiaop 86f6caa714 fix: 移除统计&添加环境变量 2025-01-24 21:44:29 +08:00
手瓜一十雪 8ec5a4d071 Merge branch 'main' into pr/734 2025-01-24 21:42:49 +08:00
手瓜一十雪 eae49667ef fix: GetProfileLike 2025-01-24 21:39:53 +08:00
bietiaop 9a87b5ec1a Merge branch 'NapNeko:main' into main 2025-01-24 21:13:57 +08:00
bietiaop ee1291e42c feat: 新版webui 2025-01-24 21:13:44 +08:00
手瓜一十雪 15aa1fd0b8 fix: NTVoteInfo type 2025-01-24 21:03:15 +08:00
手瓜一十雪 e069e0e8aa re: 移除 FetchUserProfileLike 2025-01-24 20:42:19 +08:00
手瓜一十雪 57e72c197f fix: #733 2025-01-24 20:39:52 +08:00
手瓜一十雪 1d0d25eea2 fix: re at
6b041becb0
2025-01-24 18:55:10 +08:00
手瓜一十雪 1a6194b38c fix: Schema miss 2025-01-22 15:36:49 +08:00
手瓜一十雪 22057083ce fix 2025-01-22 15:35:21 +08:00
Wesley F. Young 6b041becb0 fix: convert signed atUin into unsigned int 2025-01-22 15:31:56 +08:00
Mlikiowa 96d4a91ee9 release: v4.4.4 2025-01-22 07:13:53 +00:00
手瓜一十雪 3069900202 fix: fallback 2025-01-22 15:07:44 +08:00
pk5ls20 c46fb0f48a fix: 6.9.63 -> 6.9.65 [skip ci] 2025-01-22 14:50:48 +08:00
pk5ls20 07cd8f883e feat: 31363 2025-01-22 14:47:48 +08:00
手瓜一十雪 cfdb9d64ad fix: #722 2025-01-22 14:45:02 +08:00
Mlikiowa b73e3aa3b7 release: v4.4.3 2025-01-22 01:35:33 +00:00
手瓜一十雪 cd315b0e71 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-01-22 09:33:23 +08:00
手瓜一十雪 4d4d79e66f fix #727 2025-01-22 09:33:19 +08:00
手瓜一十雪 395ce97a78 Merge pull request #726 from Caulic/main
fix: 无法修改网络配置表单值
2025-01-22 09:16:53 +08:00
Caulic e44e8423d0 fix: 无法修改网络配置表单值 2025-01-22 01:16:35 +08:00
手瓜一十雪 fa13a56697 Merge pull request #725 from clansty/feat/face_resultId
update face list
2025-01-21 22:45:25 +08:00
Clansty 6383164aec update face list 2025-01-21 22:42:37 +08:00
Mlikiowa d9adfad1c0 release: v4.4.2 2025-01-21 14:15:28 +00:00
手瓜一十雪 901828f5a6 Merge pull request #724 from clansty/feat/face_resultId
为新的接龙表情提供 resultId 和 chainCount 返回
2025-01-21 22:14:18 +08:00
Clansty 2a4b0cbc09 force resultId to string 2025-01-21 22:10:52 +08:00
Clansty c5434efd56 为新的接龙表情提供 resultId 和 chainCount 返回 2025-01-21 22:02:35 +08:00
手瓜一十雪 b73f283095 Merge pull request #717 from NapNeko/dependabot/npm_and_yarn/file-type-20.0.0
chore(deps-dev): bump file-type from 19.6.0 to 20.0.0
2025-01-21 21:55:45 +08:00
Mlikiowa 24ef54f01c release: v4.4.1 2025-01-21 13:46:31 +00:00
手瓜一十雪 bff3b85337 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-01-21 21:46:08 +08:00
手瓜一十雪 811d9a7237 fix 2025-01-21 21:46:00 +08:00
Mlikiowa a764cb8dc2 release: v4.4.0 2025-01-21 13:45:37 +00:00
手瓜一十雪 9204b9b286 fix: #723 2025-01-21 21:43:11 +08:00
手瓜一十雪 da94faa9bb fix 2025-01-21 20:41:13 +08:00
手瓜一十雪 4b53e9a895 fix: 进一步重构 2025-01-21 20:40:52 +08:00
Mlikiowa f5db96187b release: v4.3.9 2025-01-20 09:35:55 +00:00
手瓜一十雪 857b191b03 feat: sse完全体 2025-01-20 17:35:31 +08:00
Mlikiowa 09014d1ab5 release: v4.3.8 2025-01-20 09:18:32 +00:00
手瓜一十雪 7557b71869 fix: httpSseServers DefaultConfig 2025-01-20 17:02:52 +08:00
dependabot[bot] 9c4751794f chore(deps-dev): bump file-type from 19.6.0 to 20.0.0
Bumps [file-type](https://github.com/sindresorhus/file-type) from 19.6.0 to 20.0.0.
- [Release notes](https://github.com/sindresorhus/file-type/releases)
- [Commits](https://github.com/sindresorhus/file-type/compare/v19.6.0...v20.0.0)

---
updated-dependencies:
- dependency-name: file-type
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 08:59:49 +00:00
Mlikiowa d07187bd5d release: v4.3.7 2025-01-20 08:48:13 +00:00
手瓜一十雪 2c6a6ba440 fix: type 2025-01-20 16:47:06 +08:00
手瓜一十雪 4592bf7817 fix: nickname可能为null 2025-01-20 16:06:34 +08:00
Mlikiowa afd6d450a0 release: v4.3.6 2025-01-20 06:51:29 +00:00
手瓜一十雪 b134849dcf feat: 支持文件名发送&兼容单空格问题 2025-01-20 14:51:08 +08:00
手瓜一十雪 e7d0f6d6da feat: 更合适的记录与rkey限制 2025-01-19 15:55:56 +08:00
手瓜一十雪 16a29b0127 feat: file 2025-01-19 15:47:09 +08:00
pk5ls20 1f5596ef16 Merge pull request #715 from FfmpegZZZ/main
chore:移除失效链接
2025-01-19 15:19:01 +08:00
Ffmpeg bef05432d0 Update README.md 2025-01-19 15:10:18 +08:00
手瓜一十雪 67533d7743 docs: 已重写部分实现 2025-01-13 20:37:43 +08:00
Mlikiowa 0cc86c6348 release: v4.3.5 2025-01-13 12:36:23 +00:00
手瓜一十雪 607dd68620 refactor: 标准化与提高缓存策略 2025-01-13 20:35:52 +08:00
手瓜一十雪 7c8cbc0799 style: lint 2025-01-13 20:30:57 +08:00
手瓜一十雪 ec0c2e8c33 refactor: 大部分文件处理部分 2025-01-13 20:30:08 +08:00
手瓜一十雪 7f3dbe0552 fix: groupfile file_size 2025-01-13 19:38:29 +08:00
手瓜一十雪 0e9044e0c8 drop: umami 2025-01-13 19:32:34 +08:00
手瓜一十雪 3171640193 Merge pull request #701 from Stapxs/main
HTTP SSE 消息上报模式
2025-01-13 19:26:56 +08:00
手瓜一十雪 a56cee3485 fix: #682 2025-01-13 19:25:08 +08:00
pk5ls20 c8ee371982 feat: packet ocr 2025-01-12 10:13:12 +08:00
pk5ls20 5778daeb60 refactor: packet 2025-01-11 16:23:02 +08:00
手瓜一十雪 f51f3b9861 fix 2025-01-11 14:29:44 +08:00
Mlikiowa 44dd1a0b02 release: v4.3.4 2025-01-11 04:16:54 +00:00
pk5ls20 61a00ffcbf feat: 31245 2025-01-11 12:10:46 +08:00
pk5ls20 4b0a0f0a32 feat: #702 2025-01-10 15:23:40 +08:00
stapxs a3088fb8bc 质量保障 2025-01-09 13:42:55 +08:00
stapxs 88fd1f9eb1 http sse 消息上报模式 2025-01-09 13:22:46 +08:00
手瓜一十雪 15156bac1e fix: log 2025-01-07 20:49:49 +08:00
Mlikiowa a898d2e7be release: v4.3.3 2025-01-07 11:31:56 +00:00
手瓜一十雪 95b003802c fix: win 31245 2025-01-07 19:31:35 +08:00
Mlikiowa 95c9eae4ed release: v4.3.2 2025-01-07 11:30:35 +00:00
手瓜一十雪 e3814403e4 fix: #682 2025-01-07 19:28:46 +08:00
手瓜一十雪 3d16d52dd8 fix: #696 fallback 2025-01-07 18:51:04 +08:00
Mlikiowa 1ae47fffb4 release: v4.2.68 2025-01-06 13:08:43 +00:00
手瓜一十雪 4e7096b9e2 feat: offset win 31219 2025-01-06 21:08:18 +08:00
Mlikiowa 8cc9b7f6a7 release: v4.2.67 2025-01-05 02:50:38 +00:00
手瓜一十雪 fb45c1020e revert: 再也不用umami了 2025-01-05 10:49:49 +08:00
Mlikiowa e9db4ae8f4 release: v4.2.66 2025-01-04 11:52:38 +00:00
手瓜一十雪 c46ec32bd6 fix: 简化代码 2025-01-04 13:12:04 +08:00
手瓜一十雪 c58a26ed99 fix 2025-01-04 13:06:37 +08:00
手瓜一十雪 a66f5e4971 fix 2025-01-04 13:03:29 +08:00
pk5ls20 574c8c6089 fix: SendGroupAiRecord
- smtx
2025-01-04 12:48:53 +08:00
手瓜一十雪 67afd95910 feat: getUserDetailInfoV2 2025-01-03 21:38:57 +08:00
手瓜一十雪 f7d0cb0be7 fix 2025-01-03 21:07:26 +08:00
手瓜一十雪 be9b68a0b1 feat: CancelableTask&Fallback 2025-01-03 20:46:51 +08:00
Mlikiowa 4637414af2 release: v4.2.65 2025-01-03 05:14:05 +00:00
手瓜一十雪 4bd92a72bd fix: ua agent 2025-01-03 13:09:43 +08:00
手瓜一十雪 a3be26f3e4 fix: error 2025-01-03 12:44:55 +08:00
Mlikiowa 675c906cbf release: v4.2.64 2024-12-31 14:42:17 +00:00
手瓜一十雪 6be6023236 Merge pull request #679 from wu-yafeng/main
fix #678
2024-12-31 22:41:34 +08:00
WuYafeng 42cee0d018 在没有写入权限时记录warn日志 2024-12-31 22:38:01 +08:00
WuYafeng 041f725748 Update config.ts 2024-12-31 22:25:05 +08:00
WuYafeng 0594d61631 Update config.ts 2024-12-31 22:20:34 +08:00
Mlikiowa 15cae6b765 release: v4.2.63 2024-12-31 13:52:37 +00:00
pk5ls20 b984116c35 fix: #677 2024-12-31 21:34:45 +08:00
Mlikiowa 13bda6e3f4 release: v4.2.62 2024-12-31 08:35:55 +00:00
手瓜一十雪 c0d18549d1 fix 2024-12-31 15:09:08 +08:00
手瓜一十雪 3caff72fce fix: cache trace 2024-12-31 13:10:58 +08:00
Mlikiowa 1313e9c3f4 release: v4.2.61 2024-12-30 15:59:33 +00:00
手瓜一十雪 0848d5a39e feat: clouflare 2024-12-30 23:59:09 +08:00
手瓜一十雪 7660646059 fix 2024-12-30 22:16:09 +08:00
Mlikiowa bcd90fc744 release: v4.2.60 2024-12-30 12:48:11 +00:00
手瓜一十雪 638fc22d62 fix:error 2024-12-30 20:47:50 +08:00
手瓜一十雪 c87d365b88 fix 2024-12-30 20:42:23 +08:00
Mlikiowa aee9602f25 release: v4.2.59 2024-12-30 12:29:33 +00:00
手瓜一十雪 976fbd0220 fix: 修复精确统计 2024-12-30 20:27:28 +08:00
手瓜一十雪 afd955d06f fix: error 2024-12-30 20:14:51 +08:00
手瓜一十雪 4d548da66b fix: console-log 2024-12-30 19:56:06 +08:00
手瓜一十雪 41b70f53d1 fix: LoginErrorCode 2024-12-30 19:29:14 +08:00
Wesley F. Young a47a618bcd make author NapNeko in manifest.json
毕竟我已经脱离一线开发挺久了(
2024-12-30 18:40:57 +08:00
Mlikiowa 62170a30af release: v4.2.58 2024-12-30 09:56:02 +00:00
手瓜一十雪 780c5ac23c fix 2024-12-30 17:55:40 +08:00
手瓜一十雪 9fba519a5a fix 2024-12-30 17:55:13 +08:00
Mlikiowa 3cd0e7d26b release: v4.2.57 2024-12-30 09:54:40 +00:00
手瓜一十雪 a8fd6af994 Merge pull request #676 from NapNeko/dependabot/npm_and_yarn/commander-13.0.0
chore(deps-dev): bump commander from 12.1.0 to 13.0.0
2024-12-30 17:54:03 +08:00
手瓜一十雪 4000b89644 fix 2024-12-30 17:53:05 +08:00
Mlikiowa 9c00bbc0b7 release: v4.2.56 2024-12-30 08:55:36 +00:00
手瓜一十雪 a2989d3b38 fix:workname 2024-12-30 16:54:40 +08:00
Mlikiowa fc731b60d5 release: v4.2.54 2024-12-30 08:47:57 +00:00
手瓜一十雪 193980dd4a fix: trace 2024-12-30 16:47:33 +08:00
dependabot[bot] 35427b0768 chore(deps-dev): bump commander from 12.1.0 to 13.0.0
Bumps [commander](https://github.com/tj/commander.js) from 12.1.0 to 13.0.0.
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tj/commander.js/compare/v12.1.0...v13.0.0)

---
updated-dependencies:
- dependency-name: commander
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 08:36:27 +00:00
Mlikiowa 73ea130e40 release: v4.2.53 2024-12-30 08:34:23 +00:00
手瓜一十雪 5667e6aaee fix 2024-12-30 16:34:01 +08:00
Mlikiowa fbd626131d release: v4.2.52 2024-12-30 08:17:53 +00:00
手瓜一十雪 7b82444338 fix 2024-12-30 16:17:29 +08:00
Mlikiowa 8108b9f565 release: v4.2.51 2024-12-30 08:15:48 +00:00
手瓜一十雪 c6ddd00cd9 Merge pull request #675 from NapNeko/test
fix: 重写远程遥测
2024-12-30 16:14:48 +08:00
手瓜一十雪 20c0c00fa0 fixfix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-30 16:14:32 +08:00
手瓜一十雪 1f90364ba6 fix: 重写远程遥测 2024-12-30 16:10:38 +08:00
手瓜一十雪 49ea4d31a5 fix 2024-12-30 13:23:33 +08:00
Mlikiowa dc35f1456a release: v4.2.50 2024-12-30 04:33:08 +00:00
手瓜一十雪 0ebeb90804 fix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-30 12:31:43 +08:00
手瓜一十雪 3ef5436c98 Merge pull request #673 from NapNeko/fixseq
fix: #670
2024-12-30 12:28:29 +08:00
手瓜一十雪 de7996d789 refactor: 进群入群 2024-12-30 12:27:42 +08:00
手瓜一十雪 ac52d9bae2 fix: 算了能用就行 2024-12-30 11:59:16 +08:00
手瓜一十雪 cb02df3b76 Merge pull request #672 from clansty/feat/download_file-file-proto
feat: download_file 支持 file://
2024-12-29 22:45:27 +08:00
手瓜一十雪 5fc5a6f1a6 fix: #670 2024-12-29 22:44:25 +08:00
手瓜一十雪 726a0d0394 fix 2024-12-29 22:30:33 +08:00
Clansty 6edf5345a3 chore: remove unused import 2024-12-29 22:27:41 +08:00
Clansty 242bbfdb14 chore: uriToLocalFile 2024-12-29 22:03:04 +08:00
Clansty 89e7712676 feat: download_file 支持 file:// 2024-12-29 21:41:44 +08:00
手瓜一十雪 9525786929 fix 2024-12-29 16:20:41 +08:00
Mlikiowa 72088e41a8 release: v4.2.46 2024-12-29 07:47:38 +00:00
手瓜一十雪 a3ed9ff2ef Merge branch 'main' of https://github.com/NapNeko/NapCatQQ
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-29 15:47:18 +08:00
手瓜一十雪 ff16dc73ec fix: typo 2024-12-29 15:47:16 +08:00
Mlikiowa 2da4ef5f0f release: v4.2.45 2024-12-29 07:46:08 +00:00
手瓜一十雪 eaf481799d feat: 追踪退出
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-29 15:44:37 +08:00
手瓜一十雪 1f72863aba fix: 追踪登录失败 2024-12-29 15:25:51 +08:00
Mlikiowa 6b353fd8d8 release: v4.2.44 2024-12-29 06:45:43 +00:00
手瓜一十雪 56cde4ad79 fix: #669
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-29 14:44:47 +08:00
手瓜一十雪 3b86d3c632 feat: umami 统计追踪 2024-12-29 14:39:17 +08:00
Mlikiowa 4ac7a25afb release: v4.2.43 2024-12-29 04:04:55 +00:00
手瓜一十雪 8248011a12 fix: #667
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-29 11:54:09 +08:00
手瓜一十雪 5f454456d2 fix: #655 #663 2024-12-29 11:47:09 +08:00
手瓜一十雪 e99a619c23 Merge pull request #665 from clansty/feat/group_name
fix: wrong notice_type sub_type
2024-12-28 21:42:14 +08:00
Clansty 1fc791bb68 fix: wrong notice_type sub_type 2024-12-28 21:40:32 +08:00
手瓜一十雪 f1d83f7c16 Merge pull request #664 from clansty/feat/group_name
feat: 群名称变更事件
2024-12-28 21:05:50 +08:00
Clansty 527bb72bcf feat: 群名称变更事件 2024-12-28 20:54:41 +08:00
手瓜一十雪 d78409fd07 Merge pull request #662 from Shua-github/main
新增send_poke
2024-12-27 15:16:11 +08:00
手瓜一十雪 d5e7e8944f feat: send_poke 2024-12-27 15:14:57 +08:00
Shua-github fb405a5c1c all_poke替换成send_poke 2024-12-27 13:15:10 +08:00
Shua-github a9e471deca 新增all_poke 2024-12-27 00:24:35 +08:00
Mlikiowa 9cd15ae337 release: v4.2.42 2024-12-26 12:37:35 +00:00
手瓜一十雪 8ed4cc4b0a feat: send_packet
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-26 20:36:53 +08:00
Mlikiowa a62de441cf release: v4.2.41 2024-12-26 05:31:09 +00:00
手瓜一十雪 02a8999410 Merge pull request #652 from JerryZRF/main
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
feat: add `get_clientkey`
2024-12-25 12:51:44 +08:00
手瓜一十雪 59c7979d69 readme: new 2024-12-25 12:28:53 +08:00
手瓜一十雪 bb7b28cd8f feat: 调整logo 2024-12-25 12:24:03 +08:00
手瓜一十雪 056497b98a Merge pull request #657 from FfmpegZZZ/main
chore:修改文档链接
2024-12-24 20:22:59 +08:00
手瓜一十雪 ac2fb032c4 Merge branch 'main' into main 2024-12-24 20:22:40 +08:00
Ffmpeg c933bdd5d9 chore:修改链接
## 我是猪咪

文档链接打错了
2024-12-24 20:07:11 +08:00
Ffmpeg 89c71a58fa 添加文档地址 (#656) 2024-12-24 19:45:34 +08:00
Ffmpeg 27ba85b4ff 添加文档地址 2024-12-24 19:41:35 +08:00
手瓜一十雪 79a75fed8e feat: 30899 2024-12-24 15:38:53 +08:00
Mlikiowa ee7a76b29f release: v4.2.40 2024-12-24 07:29:39 +00:00
手瓜一十雪 c53bdc3ce0 feat: 30899
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-24 15:19:44 +08:00
Mlikiowa f36e328751 release: v4.2.39 2024-12-22 13:32:51 +00:00
pk5ls20 871b5688c2 feat: webui api (/QQVersion & /GetSysStatusRealTime)
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-22 21:31:14 +08:00
JerryZRF b96076b297 fix: incorrect import 2024-12-22 13:28:20 +08:00
pk5ls20 d4488e40cf feat: better system status helper
- cpu usage diff
2024-12-22 03:56:24 +08:00
pk5ls20 7e61497243 chore: remove console logs 2024-12-22 02:57:00 +08:00
pk5ls20 e71ccdd12a feat: system status helper
- remove duplicate os import
2024-12-22 02:55:49 +08:00
pk5ls20 202129d491 feat: system status helper
- remove pidusage
2024-12-22 02:24:10 +08:00
JerryZRF a1700dd503 fix: incorrect import 2024-12-22 01:33:42 +08:00
JerryZRF 2954776539 feat: add get_clientkey 2024-12-21 20:43:15 +08:00
手瓜一十雪 fb1f122ef7 feat: 9.9.17-30851 2024-12-21 14:29:41 +08:00
手瓜一十雪 96c63e4689 refactor: 移除不再使用的代码 2024-12-21 14:19:03 +08:00
手瓜一十雪 c94936d3dc refactor: #637 2024-12-21 14:09:57 +08:00
pk5ls20 8c22f11087 feat: system status helper 2024-12-21 13:11:10 +08:00
Mlikiowa 8a089c84a9 release: v4.2.38 2024-12-20 11:46:02 +00:00
手瓜一十雪 b631e6f8a2 fix: 更精确的筛选
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-20 19:44:57 +08:00
手瓜一十雪 b3b48b032c fix: 锁住esbuild版本 以缓解问题 2024-12-20 19:32:20 +08:00
手瓜一十雪 f3e8230eca fix: 暂时指定esbuild版本以缓解上游破坏
see https://github.com/evanw/esbuild/issues/4010
2024-12-20 19:30:30 +08:00
手瓜一十雪 cc9adf9d40 feat: 过滤空消息 2024-12-20 18:58:59 +08:00
手瓜一十雪 15a640d1dc fix: #637 2024-12-20 18:55:30 +08:00
凌莞~(=^▽^=) c25b9f86db 优化私聊戳一戳事件上报 (#643) 2024-12-18 21:27:44 +08:00
Mlikiowa ecfd033afb release: v4.2.37 2024-12-17 07:56:18 +00:00
手瓜一十雪 f3ed8c7dff fix #633
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-17 15:55:18 +08:00
Mlikiowa 6089046721 release: v4.2.36 2024-12-17 03:37:26 +00:00
手瓜一十雪 44ff92ad4b style: lint
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-17 09:25:10 +08:00
手瓜一十雪 892262eb85 Merge pull request #635 from NapNeko/ref/ob-network
refactor: adjust onebot network
2024-12-17 09:07:39 +08:00
pk5ls20 2d9cc4d198 fix: as design 2024-12-17 07:07:04 +08:00
pk5ls20 a0c479485d refactor: adjust onebot network 2024-12-17 05:26:27 +08:00
手瓜一十雪 5650f18e50 Merge pull request #634 from bietiaop/main
fix: handleQuickOperation error
2024-12-17 00:29:16 +08:00
bietiaop 553885d025 fix: handleQuickOperation 2024-12-17 00:27:56 +08:00
Mlikiowa 35de00c4af release: v4.2.35 2024-12-16 14:10:08 +00:00
手瓜一十雪 09583e5de5 fuck javascript
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-16 22:09:37 +08:00
Mlikiowa 38b0b7cd00 release: v4.2.34 2024-12-16 13:17:43 +00:00
手瓜一十雪 8b9c7b0c27 Merge pull request #632 from q8018414/patch-1
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
Update AboutUs.vue
2024-12-16 21:16:39 +08:00
手瓜一十雪 1005619bf3 Merge pull request #630 from NapNeko/dependabot/npm_and_yarn/rollup/plugin-typescript-12.1.2
chore(deps-dev): bump @rollup/plugin-typescript from 11.1.6 to 12.1.2
2024-12-16 21:16:01 +08:00
手瓜一十雪 3e09cff9cb Merge branch 'main' into dependabot/npm_and_yarn/rollup/plugin-typescript-12.1.2 2024-12-16 21:15:52 +08:00
手瓜一十雪 c24384e454 Merge pull request #629 from NapNeko/dependabot/npm_and_yarn/rollup/plugin-node-resolve-16.0.0
chore(deps-dev): bump @rollup/plugin-node-resolve from 15.3.1 to 16.0.0
2024-12-16 21:15:23 +08:00
手瓜一十雪 f87a543406 fix: #631 2024-12-16 21:14:14 +08:00
手瓜一十雪 f752136283 fix: #631 2024-12-16 21:06:51 +08:00
my_key 7e71622a44 Update AboutUs.vue
新增用于显示New NapCat的tag,便于区分当前版本和最新版本
2024-12-16 20:09:40 +08:00
dependabot[bot] da92afb379 chore(deps-dev): bump @rollup/plugin-typescript from 11.1.6 to 12.1.2
Bumps [@rollup/plugin-typescript](https://github.com/rollup/plugins/tree/HEAD/packages/typescript) from 11.1.6 to 12.1.2.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/typescript/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/typescript-v12.1.2/packages/typescript)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-typescript"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 08:44:42 +00:00
dependabot[bot] d3062de5f9 chore(deps-dev): bump @rollup/plugin-node-resolve from 15.3.1 to 16.0.0
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 15.3.1 to 16.0.0.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/commonjs-v16.0.0/packages/node-resolve)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-node-resolve"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 08:43:46 +00:00
Mlikiowa f1440b03a8 release: v4.2.33 2024-12-16 05:03:59 +00:00
手瓜一十雪 9a8b266cef Merge pull request #627 from bietiaop/main
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
feat: 查看登录QQ信息&获取快速登录列表详细信息&获取nc的包信息&优化了部分写法
2024-12-16 13:03:09 +08:00
手瓜一十雪 2a9bc57120 fix: #628 2024-12-16 13:00:07 +08:00
bietiaop 2ed83a0e30 feat: 查看登录QQ信息&获取快速登录列表详细信息&获取nc的包信息&优化了部分写法 2024-12-16 12:46:27 +08:00
Mlikiowa 116e8fd30a release: v4.2.32 2024-12-14 07:03:02 +00:00
手瓜一十雪 891f11173b fix
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-14 15:01:47 +08:00
手瓜一十雪 dfc7996c17 Merge branch 'maybe-feat/type' 2024-12-14 15:00:18 +08:00
手瓜一十雪 dc0561d34f refactor: OB11ActiveHttpAdapter 2024-12-14 14:26:23 +08:00
手瓜一十雪 4fb0845d79 fix: #619 2024-12-14 13:46:25 +08:00
pk5ls20 0e0d4837b8 feat & fix: GetMiniAppArk 2024-12-14 06:56:23 +08:00
pk5ls20 a6adde7966 feat: attempt to enhance type inference 2024-12-14 05:56:49 +08:00
手瓜一十雪 7b693132f9 fix 2024-12-13 23:27:24 +08:00
手瓜一十雪 3c3114b6ab fix: 支持类型推导 2024-12-13 23:23:58 +08:00
Mlikiowa 5cdbf58f59 release: v4.2.31 2024-12-13 14:04:13 +00:00
手瓜一十雪 6f0a4131a2 fix: 异常
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-13 22:03:51 +08:00
Mlikiowa aa520e2f5d release: v4.2.30 2024-12-13 11:28:32 +00:00
手瓜一十雪 2c3b7e9ee8 fix: fuck! tencent
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-13 19:28:04 +08:00
手瓜一十雪 b86a28092a fix: Login Check 2024-12-13 17:38:27 +08:00
手瓜一十雪 d59e5f2133 fix 2024-12-13 16:45:35 +08:00
手瓜一十雪 3fdd187102 fix: #610 2024-12-13 14:29:11 +08:00
Mlikiowa 3f085fd8ae release: v4.2.29 2024-12-13 04:47:13 +00:00
手瓜一十雪 a4fc131aec feat: 全平台30594
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-13 12:36:51 +08:00
手瓜一十雪 d7d446c3fc fix 2024-12-12 23:02:10 +08:00
手瓜一十雪 212666e603 fix 2024-12-12 18:41:46 +08:00
手瓜一十雪 b545c28340 feat: 30483全平台兼容 2024-12-12 16:57:50 +08:00
手瓜一十雪 72bc345515 feat: linux 3.2.15-30483 2024-12-12 16:53:31 +08:00
手瓜一十雪 cc5082a9e3 feat: mac 6.9.62-30483 2024-12-12 11:43:31 +08:00
手瓜一十雪 45782a6c6c chore: docs 2024-12-12 10:44:09 +08:00
手瓜一十雪 e86d646cce chore: 万里妹妹干坏事 2024-12-12 10:43:13 +08:00
手瓜一十雪 92cfc6b8c8 feat: 更换演示代码 2024-12-12 10:00:38 +08:00
手瓜一十雪 82289d9f1f chore: debug log remove 2024-12-12 09:57:52 +08:00
手瓜一十雪 4cdbdaaf4e feat: win 30483 2024-12-12 09:47:44 +08:00
手瓜一十雪 ecde2427da fix: path 2024-12-12 09:37:51 +08:00
手瓜一十雪 fed1ec5d83 Merge pull request #620 from NapNeko/plugin-support
Feat: 支持快速基于NapCat进行二次开发 不需要通过传统NetWork
2024-12-12 09:34:27 +08:00
手瓜一十雪 4fbd764ced fix 2024-12-12 09:34:13 +08:00
手瓜一十雪 5361079010 Merge pull request #616 from Ander-pixe/webui-new
fix:路由守卫
2024-12-12 09:33:23 +08:00
手瓜一十雪 002d135ef5 fix 2024-12-11 18:26:26 +08:00
手瓜一十雪 a39b0a4a78 feat: plugin 2024-12-11 17:07:21 +08:00
纸凤孤凰 eb5d68422f fix: 路由守卫 2024-12-10 16:59:57 +08:00
纸凤孤凰 3dc13e5c2e Merge remote-tracking branch 'origin/main' into webui-new 2024-12-10 15:29:27 +08:00
huankong233 16881f057a fix: solve the token error
fix: remove useless defineProps
fix: add the missing dependencies to package.json
fix: add ES2022 into tsconfig.json
2024-12-10 09:44:38 +08:00
手瓜一十雪 1cd7d0577f fix: error 2024-12-10 09:44:23 +08:00
Mlikiowa 3c872df97a release: v4.2.28 2024-12-08 14:37:51 +00:00
pk5ls20 218b7bd2a0 fix: #607
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-08 22:23:01 +08:00
Mlikiowa 4552d6970d release: v4.2.27 2024-12-06 03:40:03 +00:00
手瓜一十雪 4b319d15a7 refactor: GetGroupInfo
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-06 11:39:11 +08:00
Mlikiowa 0ae3a4172c release: v4.2.26 2024-12-06 02:40:53 +00:00
手瓜一十雪 bf0c12f1c4 fix: #605
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-06 10:39:49 +08:00
Mlikiowa cb5eeecb86 release: v4.2.25 2024-12-05 13:13:34 +00:00
手瓜一十雪 8d857cf2be Revert "refactor: CardChangedEvent"
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
This reverts commit 0e8ceeb6c9.
2024-12-05 21:13:03 +08:00
手瓜一十雪 6f232c465f Merge pull request #604 from Ander-pixe/webui-new
feat:实时日志、关于and部分样式优化
2024-12-05 21:10:08 +08:00
纸凤孤凰 032d444246 Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:27:08 +08:00
纸凤孤凰 49488dd3fb Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:25:14 +08:00
手瓜一十雪 9aec3865ff fix: historyLog 2024-12-05 20:22:55 +08:00
手瓜一十雪 b6b7f2051b fix 2024-12-05 20:02:24 +08:00
手瓜一十雪 46254a699a fix 2024-12-05 19:56:36 +08:00
手瓜一十雪 7b3c287137 fix: cors 2024-12-05 19:37:56 +08:00
手瓜一十雪 1a533742a5 fix: Log 2024-12-05 19:17:07 +08:00
手瓜一十雪 2027266852 fix 2024-12-05 18:56:55 +08:00
手瓜一十雪 946d8b1a7b fix: dependencies error 2024-12-05 18:49:45 +08:00
手瓜一十雪 6d2fb5de6f Merge branch 'main' into pr/604 2024-12-05 18:40:46 +08:00
pk5ls20 91c4a002dd fix: dependencies 2024-12-05 18:15:00 +08:00
纸凤孤凰 4d8112aae5 feat:实时日志、关于and部分样式优化 2024-12-05 15:31:42 +08:00
手瓜一十雪 bb53f245cf style: lint check 2024-12-05 14:50:27 +08:00
手瓜一十雪 9f31cdbf5b fix: 移除不使用api 2024-12-05 14:46:05 +08:00
手瓜一十雪 9a33039d73 fix: 暂时移除 QunAlbum 2024-12-05 14:45:12 +08:00
手瓜一十雪 7cf3be8333 refactor: predict time 2024-12-05 14:42:45 +08:00
Nanako 82afb88e53 Update README.md 2024-12-05 14:37:10 +08:00
Mlikiowa 4aa24b5d67 release: v4.2.24 2024-12-05 06:17:46 +00:00
手瓜一十雪 57112c21a2 refactor: flag handle&onebot标准化
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-05 14:17:09 +08:00
手瓜一十雪 0e8ceeb6c9 refactor: CardChangedEvent 2024-12-05 11:36:06 +08:00
手瓜一十雪 f52b8d1f04 feat: 30366 全平台通用性适配 2024-12-05 11:15:10 +08:00
手瓜一十雪 f374cc77ae chore: readme 2024-12-04 23:04:37 +08:00
手瓜一十雪 7c694e7fae chore: 跑路的规范/困难的实现 2024-12-04 23:03:46 +08:00
Mlikiowa 932ffc2673 release: v4.2.23 2024-12-04 14:18:02 +00:00
手瓜一十雪 3de5438139 fix: poke report
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-04 22:16:59 +08:00
手瓜一十雪 c4b5f34271 fix: 兜底 防止进入影响速度 2024-12-04 22:02:11 +08:00
手瓜一十雪 22d3ac33a2 Refactor: 更新群组通知处理逻辑,优化数据结构和异步处理 2024-12-04 21:59:53 +08:00
Mlikiowa 2e5dd6535a release: v4.2.22 2024-12-04 13:18:58 +00:00
手瓜一十雪 eac58a2a50 fix: 9.9.17-30366
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-04 21:04:24 +08:00
手瓜一十雪 e939ec0e52 fix: #597 2024-12-04 20:37:08 +08:00
Mlikiowa 5b17a14a2a release: v4.2.21 2024-12-04 11:49:26 +00:00
手瓜一十雪 8fb8c888f5 refactor: 移除未使用的uidCache和uinCache逻辑
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-04 19:48:59 +08:00
Mlikiowa 4a2884509e release: v4.2.20 2024-12-04 11:46:12 +00:00
手瓜一十雪 e295235a89 fix: #596
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-04 19:45:46 +08:00
手瓜一十雪 ef515a38d0 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-04 18:50:55 +08:00
手瓜一十雪 02cff040e3 refactor: 移除未使用的createUidFromTinyId和getStatusByUid方法 2024-12-04 18:50:51 +08:00
Mlikiowa bb0f65a52d release: v4.2.19 2024-12-04 10:42:32 +00:00
手瓜一十雪 d51d6a5cc1 refactor: getUidByUinV2/getUinByUidV2
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-04 18:41:28 +08:00
手瓜一十雪 eb99379a79 fix: 性能优化 2024-12-04 18:29:33 +08:00
Mlikiowa 388eb57d0d release: v4.2.18 2024-12-04 03:40:59 +00:00
手瓜一十雪 0b8131392a chore: 移出调试
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-04 11:40:36 +08:00
手瓜一十雪 229efbd006 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-04 11:39:09 +08:00
手瓜一十雪 a482fa3a8d refactor: 优化调整文件处理 2024-12-04 11:38:59 +08:00
Mlikiowa 6cf047af39 release: v4.2.17 2024-12-04 02:57:21 +00:00
手瓜一十雪 41748c0b3f refactor: 数据清理与刷新
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-04 10:55:56 +08:00
手瓜一十雪 1ce8be3c7e fix: 精简GroupApi实现 2024-12-04 10:47:21 +08:00
手瓜一十雪 32778acf57 refactor: NTQQGroupApi 2024-12-04 10:43:48 +08:00
Mlikiowa a3c71473ae release: v4.2.16 2024-12-03 14:15:16 +00:00
手瓜一十雪 aceece7e90 fix: code
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-03 22:12:57 +08:00
Mlikiowa 52efb4f9ef release: v4.2.15 2024-12-03 14:07:35 +00:00
手瓜一十雪 6b0d96fe8d fix: 移除复杂信息
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-03 22:07:11 +08:00
Mlikiowa ad052821b0 release: v4.2.14 2024-12-03 13:50:37 +00:00
手瓜一十雪 da7636e60c fix: #592
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-03 21:50:12 +08:00
Mlikiowa ef01dd0d77 release: v4.2.13 2024-12-03 13:43:37 +00:00
手瓜一十雪 03f7d4673f fix: code 2024-12-03 21:42:08 +08:00
手瓜一十雪 94e9c87978 fix: 优化处理 2024-12-03 21:42:08 +08:00
手瓜一十雪 501bbbe4df fix 2024-12-03 21:42:08 +08:00
手瓜一十雪 c9122a3fee fix: 临时的抽象方案 2024-12-03 21:42:08 +08:00
手瓜一十雪 8a289d014e fix: error 2024-12-03 21:42:08 +08:00
手瓜一十雪 ddadd38151 refactor: GroupAdminChange 2024-12-03 21:42:08 +08:00
手瓜一十雪 0b8d0e3cac feat: 迁移事件解析原理 2024-12-03 21:42:08 +08:00
手瓜一十雪 eeb27d38bc fix: 清理旧的脚本 2024-12-03 17:33:13 +08:00
Mlikiowa 491a79ec96 release: v4.2.12 2024-12-03 09:22:43 +00:00
手瓜一十雪 f429db61af fix: #594
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-03 17:16:58 +08:00
Mlikiowa 2881099602 release: v4.2.11 2024-12-02 14:08:36 +00:00
手瓜一十雪 672ae8decf fix: Once处理空格目录 中文目录
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-02 22:08:08 +08:00
手瓜一十雪 2abc7e541d fix: 空格目录启动问题 2024-12-02 21:23:13 +08:00
手瓜一十雪 45b1f369ac style: 异步实现 2024-12-02 11:44:37 +08:00
手瓜一十雪 3b5d2c8f6f style: 简化写法 2024-12-02 11:16:12 +08:00
Mlikiowa 5376e16c9f release: v4.2.10 2024-12-01 11:12:10 +00:00
bietiaop af052242fa feat:带等级的实时日志
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-12-01 15:19:23 +08:00
手瓜一十雪 85e0b71545 chore: daily -> weekly 2024-12-01 13:26:56 +08:00
手瓜一十雪 1206d1fcf6 chore: bug report 2024-12-01 13:25:36 +08:00
手瓜一十雪 f7534dc438 fix: 自动迁移 2024-12-01 13:20:20 +08:00
手瓜一十雪 97f317254e fix: MiniApp type check 2024-12-01 13:11:56 +08:00
手瓜一十雪 9eaf51e15f fix: nullable 2024-12-01 13:04:00 +08:00
手瓜一十雪 7221f4ac02 fix: type-check 2024-12-01 12:50:13 +08:00
手瓜一十雪 1bb6dce239 refactor: type-check (#586)
* refactor: type-check

* fix: default

* refactor: type-check
2024-12-01 12:41:51 +08:00
bietiaop d13db5e8eb feat: 实时日志 (#584)
* feat: 历史日志

* feat: 实时日志

* fix: EventEmitter实现事件监听
2024-12-01 09:31:47 +08:00
Mlikiowa 040b5535f3 release: v4.2.9 2024-11-30 06:12:44 +00:00
手瓜一十雪 b44e1618fb fix: quick error
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-11-30 14:12:23 +08:00
手瓜一十雪 1e13483bc3 fix: type 2024-11-30 13:32:21 +08:00
手瓜一十雪 f9519d3923 style: lint 2024-11-30 13:29:10 +08:00
手瓜一十雪 86cdfbb79b feat: 取消上报 pic_type 2024-11-30 12:11:33 +08:00
手瓜一十雪 a70585e854 feat: 处理失败的情况 2024-11-30 12:08:58 +08:00
Mlikiowa 040d0a8635 release: v4.2.8 2024-11-30 01:39:59 +00:00
手瓜一十雪 efa512ab21 fix: #580
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-11-30 09:34:03 +08:00
bietiaop 9b04aed8b3 feat: 历史日志 2024-11-30 09:30:13 +08:00
手瓜一十雪 7087eafe37 feat: Universal Package (#578)
* feat: 统一包支持

* feat: Universal
2024-11-29 15:11:35 +08:00
Mlikiowa c81c4af653 release: v4.2.7 2024-11-29 04:48:36 +00:00
手瓜一十雪 c05cc9dd02 feat: 迁移29927
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-11-29 12:48:03 +08:00
手瓜一十雪 1a0da00f2d refactor: webui log 移除公网输出 2024-11-29 12:41:52 +08:00
手瓜一十雪 31b0c1d3d7 refactor: react webui 2024-11-29 12:22:25 +08:00
手瓜一十雪 53c1d40bcf refactor: logger bind (#577) 2024-11-28 20:55:28 +08:00
手瓜一十雪 97cacb4383 refactor: framework的操作性 2024-11-28 20:00:24 +08:00
Mlikiowa e03905abaf release: v4.2.6 2024-11-28 07:28:33 +00:00
手瓜一十雪 06eba28b4c fux: #574
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-11-28 15:28:09 +08:00
手瓜一十雪 bbfeac46dd Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-28 15:27:26 +08:00
手瓜一十雪 2fe4da094a fix 2024-11-28 15:14:01 +08:00
Mlikiowa b454d8c0f9 release: v4.2.5 2024-11-28 07:07:10 +00:00
手瓜一十雪 1f9b5453cc fix: #573
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-11-28 15:06:47 +08:00
Mlikiowa 3261791e99 release: v4.2.4 2024-11-28 03:00:35 +00:00
手瓜一十雪 3bb12e3f45 fix: #572
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-11-28 10:56:57 +08:00
手瓜一十雪 1dc2f7e5a2 style: lint 2024-11-28 10:46:14 +08:00
手瓜一十雪 2531b08538 refactor: 提高解析兼容 2024-11-28 10:41:51 +08:00
手瓜一十雪 9fcfb5493c fix: #571 2024-11-28 10:27:04 +08:00
Mlikiowa 4576354c51 release: v4.2.3 2024-11-28 01:54:43 +00:00
手瓜一十雪 1dcf2ef0c6 fix: error handle
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
Build Release / check-version (push) Waiting to run
2024-11-28 09:53:50 +08:00
Mlikiowa 3642c65e8c release: v4.2.2 2024-11-27 12:39:03 +00:00
630 changed files with 35850 additions and 7537 deletions

No files matched your search

+2 -2
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
VITE_BUILD_TYPE = Production
VITE_BUILD_PLATFORM = Universal
+5 -6
View File
@@ -10,13 +10,12 @@ body:
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
- type: input
id: system-version
attributes:
label: 系统版本
description: 运行 QQNT 的系统版本
placeholder: Windows 10 Pro Workstation 22H2
placeholder: Windows 11 24H2
validations:
required: true
- type: input
@@ -24,7 +23,7 @@ body:
attributes:
label: QQNT 版本
description: 可在 QQNT 的「关于」的设置页中找到
placeholder: 9.9.7-21804
placeholder: 9.9.16-29927
validations:
required: true
- type: input
@@ -40,21 +39,21 @@ body:
attributes:
label: OneBot 客户端
description: 连接至 NapCat 的客户端版本信息
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
placeholder: Karin 1.0.0
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: 填写你认为的 NapCat 的不正常行为
description: 填写你认为的 NapCat 的异常行为
validations:
required: true
- type: textarea
id: how-reproduce
attributes:
label: 如何复现
description: 填写应当如何操作才能触发这个不正常行为
description: 填写应当如何操作才能触发这个异常行为
placeholder: |
1. xxx
2. xxx
+3 -8
View File
@@ -1,11 +1,6 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
+1 -2
View File
@@ -5,10 +5,9 @@ pnpm-lock.yaml
out/
dist/
/src/core.lib/common/
/localdebug/
devconfig/*
# Editor
.vscode/*
!.vscode/extensions.json
.idea/*
+115
View File
@@ -0,0 +1,115 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "dev:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "build:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "build:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "lint",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"lint"
]
},
{
"type": "node",
"request": "launch",
"name": "depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"depend"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:depend"
]
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
".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
View 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"
}
]
}
]
}
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
nanaeonn@outlook.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+40 -31
View File
@@ -1,53 +1,62 @@
<div align="center">
![Logo](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto)
# NapCat
![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
_Modern protocol-side framework implemented based on NTQQ._
> 云起兮风生,心向远方兮路未曾至.
</div>
---
## 欢迎回家
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 特性介绍
- [x] **安装简单**:就算是笨蛋也能使用
- [x] **性能友好**:就算是低内存也能使用
- [x] **接口丰富**:就算是没有也能使用
- [x] **稳定好用**:就算是被捉也能使用
## Welcome
+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 使用框架
## Feature
+ **Easy to Use**
- 作为初学者能够轻松使用.
+ **Quick and Efficient**
- 在低内存操作系统长时运行.
+ **Rich API Interface**
- 完整实现了大部分标准接口.
+ **Stable and Reliable**
- 持续稳定的开发与维护.
## Quick Start
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
**首次使用**请务必查看如下文档看使用教程
### 文档地址
## Link
[Cloudflare.Worker](https://doc.napneko.icu/)
| 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/) |
|:-:|:-:|:-:|:-:|
[Cloudflare.HKServer](https://napcat.napneko.icu/)
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://docs.napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) |
|:-:|:-:|:-:|:-:|
[Github.IO](https://napneko.github.io/)
| Contact | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/HaRcfrHpUk) | [![Telegram](https://img.shields.io/badge/Telegram-MelodicMoonlight-blue)](https://t.me/MelodicMoonlight) |
|:-:|:-:|:-:|:-:|
[Cloudflare.Pages](https://napneko.pages.dev/)
## Thanks
[Server.Other](https://napcat.cyou/)
+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
+ [LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目部分开发
## 回家旅途
[QQ Group](https://qm.qq.com/q/NWP25OeV0c)
## 感谢他们
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
不过最最重要的 还是需要感谢屏幕前的你哦~
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
---
## 特殊感谢
[LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目
## License
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
1. 第三方库代码或修改部分遵循其原始开源许可.
2. 本项目获取部分项目授权而不受部分约束
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
## 开源附加
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
+11
View File
@@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| > 4.0 | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
you should open an issue
+26 -64
View File
@@ -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];
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.
+1 -1
View File
@@ -19,7 +19,7 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
echo provided QQ path is invalid
pause
exit /b
)
+1 -1
View File
@@ -19,7 +19,7 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
echo provided QQ path is invalid
pause
exit /b
)
+2 -2
View File
@@ -27,8 +27,8 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
if not exist "%QQPath%" (
echo provided QQ path is invalid
pause
exit /b
)
+2 -2
View File
@@ -27,8 +27,8 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
if not exist "%QQPath%" (
echo provided QQ path is invalid
pause
exit /b
)
+23 -7
View File
@@ -1,10 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.16-29456",
"verHash": "dd395162",
"linuxVersion": "3.2.13-29456",
"linuxVerHash": "e379390a",
"type": "module",
"version": "9.9.18-32793",
"verHash": "d43f097e",
"linuxVersion": "3.2.16-32793",
"linuxVerHash": "ee4bd910",
"private": true,
"description": "QQ",
"productName": "QQ",
@@ -17,10 +16,27 @@
"bin": {
"qd": "externals/devtools/cli/index.js"
},
"appid": {
"win32": "537258389",
"darwin": "537258412",
"linux": "537258424"
},
"main": "./loadNapCat.js",
"buildVersion": "29456",
"peerDependenciesMeta": {
"*": {
"optional": true
}
},
"pnpm": {
"patchedDependencies": {
"@vue/runtime-dom@3.5.12": "patches/@vue__runtime-dom@3.5.12.patch",
"@swc/helpers@0.5.3": "patches/@swc__helpers@0.5.3.patch",
"vuex@4.1.0": "patches/vuex@4.1.0.patch"
}
},
"buildVersion": "32793",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",
"eleArch": "x64"
}
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 684 KiB

+3 -7
View File
@@ -4,16 +4,12 @@
"name": "NapCatQQ",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
"version": "4.2.1",
"version": "4.7.1",
"icon": "./logo.png",
"authors": [
{
"name": "MliKiowa",
"link": "https://github.com/MliKiowa"
},
{
"name": "Young",
"link": "https://github.com/Wesley-Young"
"name": "NapNeko",
"link": "https://github.com/NapNeko"
}
],
"repository": {
+1
View File
@@ -0,0 +1 @@
VITE_DEBUG_BACKEND_URL="http://127.0.0.1:6099"
+8
View File
@@ -22,3 +22,11 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# NPM LOCK files
package-lock.json
yarn.lock
pnpm-lock.yaml
dist.zip
+1
View File
@@ -0,0 +1 @@
public-hoist-pattern[]=*@heroui/*
+7
View File
@@ -0,0 +1,7 @@
dist
*.md
*.html
yarn.lock
package-lock.json
node_modules
pnpm-lock.yaml
+23
View File
@@ -0,0 +1,23 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"bracketSpacing": true,
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^@/const/(.*)$",
"^@/store/(.*)$",
"^@/components/(.*)$",
"^@/contexts/(.*)$",
"^@/hooks/(.*)$",
"^@/utils/(.*)$",
"^@/(.*)$",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar"]
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 bietiaop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+30 -3
View File
@@ -1,5 +1,32 @@
# Vue 3 + TypeScript + Vite
# NapCat WebUI
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## 功能
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
- WebUI登录
- QQ登录
- 网络配置
- OneBot/WebUI配置
- 日志查看(实时日志、历史日志)
- HTTP调试
- WS调试
- 在线音乐播放器,支持网易云音乐歌单(大屏在页面右下角,小屏在页面下方)
如果你有更多功能需求,欢迎在 issue 中提出。
# License
[MIT](LICENSE)
# Related Projects
- [NapCat](https://github.com/NapNeko/NapCatQQ/)
- [Karin](https://github.com/KarinJS/Karin/)
# Thanks to
- [Vercel](https://vercel.com/)
- [React](https://react.dev/)
- [HeroUI](https://nextui.org/)
- and more open-source projects
感谢群友“维拉”提供的在线音乐API。
+85 -46
View File
@@ -1,52 +1,91 @@
import globals from 'globals';
import ts from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import prettier from 'eslint-plugin-prettier/recommended';
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 [
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
eslint_js.configs.recommended,
eslintPluginPrettierRecommended,
...customTsFlatConfig,
{
name: 'global config',
languageOptions: {
globals: {
...globals.es2022,
...globals.browser,
...globals.node
},
parserOptions: {
warnOnUnsupportedTypeScriptVersion: false
}
},
...ts.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
},
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
},
...vue.configs['flat/base'],
{
files: ['*.vue', '**/*.vue'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
languageOptions: {
...reactPlugin.configs.recommended.languageOptions
},
{
rules: {
indent: ['error', 4],
semi: ['error', 'always'],
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'object-curly-spacing': ['error', 'always'],
'vue/v-for-delimiter-style': ['error', 'in'],
'vue/require-name-property': 'warn',
'vue/prefer-true-attribute-shorthand': 'warn',
'prefer-arrow-callback': 'warn',
},
rules: {
...reactPlugin.configs.recommended.rules,
'react/react-in-jsx-scope': 'off'
},
prettier,
{
rules: {
'prettier/prettier': 'warn',
},
},
];
settings: {
react: {
// 需要显示安装 react
version: 'detect'
}
}
},
{
languageOptions: { globals: { ...globals.browser, ...globals.node } }
},
eslintConfigPrettier
]
+22 -12
View File
@@ -1,13 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./logo_webui.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NapCat WebUI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NapCat WebUI</title>
<meta key="title" content="NapCat WebUI" property="og:title" />
<meta content="NapCat WebUI,基于React+tailwind+NextUI" property="og:description" />
<meta content="NapCat WebUI,基于React+tailwind+NextUI" name="description" />
<meta key="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
name="viewport" />
<link href="/favicon.ico" rel="icon" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+119 -24
View File
@@ -1,34 +1,129 @@
{
"name": "napcat.webui",
"name": "napcat-webui",
"private": true,
"version": "1.0.0",
"version": "0.0.6",
"type": "module",
"scripts": {
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
"webui:dev": "vite",
"webui:build": "vite build",
"webui:preview": "vite preview"
"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": {
"eslint-plugin-prettier": "^5.2.1",
"qrcode": "^1.5.4",
"tdesign-icons-vue-next": "^0.3.3",
"tdesign-vue-next": "^1.10.3",
"vue": "^3.5.13",
"vue-router": "^4.4.5"
"@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",
"@heroui/card": "2.2.10",
"@heroui/checkbox": "2.3.9",
"@heroui/chip": "2.2.7",
"@heroui/code": "2.2.7",
"@heroui/dropdown": "2.3.10",
"@heroui/form": "2.1.9",
"@heroui/image": "2.2.6",
"@heroui/input": "2.4.10",
"@heroui/kbd": "2.2.7",
"@heroui/link": "2.2.8",
"@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",
"@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/xterm": "^5.5.0",
"ahooks": "^3.8.4",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"echarts": "^5.5.1",
"event-source-polyfill": "^1.0.31",
"framer-motion": "^12.0.6",
"monaco-editor": "^0.52.2",
"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-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.4",
"react-use-websocket": "^4.11.1",
"react-window": "^1.8.11",
"remark-gfm": "^4.0.0",
"tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.17",
"zod": "^3.24.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-legacy": "^5.4.3",
"@vitejs/plugin-vue": "^5.1.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.31.0",
"globals": "^15.12.0",
"terser": "^5.36.0",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vue-tsc": "^2.1.8"
"@eslint/js": "^9.19.0",
"@react-types/shared": "^3.26.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/event-source-polyfill": "^1.0.5",
"@types/fabric": "^5.3.9",
"@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.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",
"vite": "^6.0.5",
"vite-plugin-static-copy": "^2.2.0",
"vite-tsconfig-paths": "^5.1.4"
},
"overrides": {
"ahooks": {
"react": "$react",
"react-dom": "$react-dom"
},
"react-window": {
"react": "$react",
"react-dom": "$react-dom"
}
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

+5
View File
@@ -0,0 +1,5 @@
{
"rewrites": [
{ "source": "/(.*)", "destination": "/" }
]
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+90
View File
@@ -0,0 +1,90 @@
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 DialogProvider from '@/contexts/dialog'
import AudioProvider from '@/contexts/songs'
import useAuth from '@/hooks/auth'
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 (
<DialogProvider>
<Provider store={store}>
<PageBackground />
<Toaster />
<AudioProvider>
<Suspense fallback={<PageLoading />}>
<AuthChecker>
<AppRoutes />
</AuthChecker>
</Suspense>
</AudioProvider>
</Provider>
</DialogProvider>
)
}
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'
if (token) {
url += `?token=${token}`
}
navigate(url, { replace: true })
}
}, [isAuth, navigate])
return <>{children}</>
}
function AppRoutes() {
return (
<Routes>
<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>
)
}
export default App
-112
View File
@@ -1,112 +0,0 @@
<template>
<div id="app" theme-mode="dark">
<router-view />
</div>
<div v-if="show">
<t-sticky-tool shape="round" placement="right-bottom" :offset="[-50, 10]" @click="changeTheme">
<t-sticky-item label="浅色" popup="切换浅色模式">
<template #icon><sunny-icon /></template>
</t-sticky-item>
<t-sticky-item label="深色" popup="切换深色模式">
<template #icon><mode-dark-icon /></template>
</t-sticky-item>
<t-sticky-item label="自动" popup="跟随系统">
<template #icon><control-platform-icon /></template>
</t-sticky-item>
</t-sticky-tool>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';
import { ControlPlatformIcon, ModeDarkIcon, SunnyIcon } from 'tdesign-icons-vue-next';
const smallScreen = window.matchMedia('(max-width: 768px)');
interface Item {
label: string;
popup: string;
}
interface Context {
item: Item;
}
enum ThemeMode {
Dark = 'dark',
Light = 'light',
Auto = 'auto',
}
const themeLabelMap: Record<string, ThemeMode> = {
"浅色": ThemeMode.Light,
"深色": ThemeMode.Dark,
"自动": ThemeMode.Auto,
};
const show = ref<boolean>(true);
const createSetThemeAttributeFunction = () => {
let mediaQueryForAutoTheme: MediaQueryList | null = null;
return (mode: ThemeMode | null) => {
const element = document.documentElement;
if (mode === ThemeMode.Dark) {
element.setAttribute('theme-mode', ThemeMode.Dark);
} else if (mode === ThemeMode.Light) {
element.removeAttribute('theme-mode');
} else if (mode === ThemeMode.Auto) {
mediaQueryForAutoTheme = window.matchMedia('(prefers-color-scheme: dark)');
const handleMediaChange = (e: MediaQueryListEvent) => {
if (e.matches) {
element.setAttribute('theme-mode', ThemeMode.Dark);
} else {
element.removeAttribute('theme-mode');
}
};
mediaQueryForAutoTheme.addEventListener('change', handleMediaChange);
const event = new Event('change');
Object.defineProperty(event, 'matches', {
value: mediaQueryForAutoTheme.matches,
writable: false,
});
mediaQueryForAutoTheme.dispatchEvent(event);
onBeforeUnmount(() => {
if (mediaQueryForAutoTheme) {
mediaQueryForAutoTheme.removeEventListener('change', handleMediaChange);
}
});
}
};
};
const setThemeAttribute = createSetThemeAttributeFunction();
const getStoredTheme = (): ThemeMode | null => {
return localStorage.getItem('theme') as ThemeMode | null;
};
const initTheme = () => {
const storedTheme = getStoredTheme();
if (storedTheme === null) {
setThemeAttribute(ThemeMode.Auto);
} else {
setThemeAttribute(storedTheme);
}
};
const changeTheme = (context: Context) => {
const themeLabel = themeLabelMap[context.item.label] as ThemeMode;
console.log(themeLabel);
setThemeAttribute(themeLabel);
localStorage.setItem('theme', themeLabel);
};
const haddingFbars = () => {
show.value = !smallScreen.matches;
if (smallScreen.matches) {
localStorage.setItem('theme', 'auto');
}
};
onMounted(() => {
initTheme();
haddingFbars();
window.addEventListener('resize', haddingFbars);
});
onUnmounted(() => {
window.removeEventListener('resize', haddingFbars);
});
</script>
<style scoped></style>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

-205
View File
@@ -1,205 +0,0 @@
import { OneBotConfig } from '../../../src/onebot/config/config';
export class QQLoginManager {
private retCredential: string;
private readonly apiPrefix: string;
//调试时http://127.0.0.1:6099/api 打包时 ../api
constructor(retCredential: string, apiPrefix: string = '../api') {
this.retCredential = retCredential;
this.apiPrefix = apiPrefix;
}
// TODO:
public async GetOB11Config(): Promise<OneBotConfig> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/GetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data as OneBotConfig;
}
}
} catch (error) {
console.error('Error getting OB11 config:', error);
}
return {} as OneBotConfig;
}
public async SetOB11Config(config: OneBotConfig): Promise<boolean> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ config: JSON.stringify(config) }),
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return true;
}
}
} catch (error) {
console.error('Error setting OB11 config:', error);
}
return false;
}
public async checkQQLoginStatus(): Promise<boolean> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.isLogin;
}
}
} catch (error) {
console.error('Error checking QQ login status:', error);
}
return false;
}
public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string; isLogin: string } | undefined> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data;
}
}
} catch (error) {
console.error('Error checking QQ login status:', error);
}
return undefined;
}
public async checkWebUiLogined(): Promise<boolean> {
try {
const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (LoginResponse.status == 200) {
const LoginResponseJson = await LoginResponse.json();
if (LoginResponseJson.code == 0) {
return true;
}
}
} catch (error) {
console.error('Error checking web UI login status:', error);
}
return false;
}
public async loginWithToken(token: string): Promise<string | null> {
try {
const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token }),
});
const loginResponseJson = await loginResponse.json();
const retCode = loginResponseJson.code;
if (retCode === 0) {
this.retCredential = loginResponseJson.data.Credential;
return this.retCredential;
}
} catch (error) {
console.error('Error logging in with token:', error);
}
return null;
}
public async getQQLoginQrcode(): Promise<string> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.qrcode || '';
}
}
} catch (error) {
console.error('Error getting QQ login QR code:', error);
}
return '';
}
public async getQQQuickLoginList(): Promise<string[]> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data || [];
}
}
} catch (error) {
console.error('Error getting QQ quick login list:', error);
}
return [];
}
public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ uin: uin }),
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return { result: true, errMsg: '' };
} else {
return { result: false, errMsg: QQLoginResponseJson.message };
}
}
} catch (error) {
console.error('Error setting quick login:', error);
}
return { result: false, errMsg: '接口异常' };
}
}
@@ -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
-58
View File
@@ -1,58 +0,0 @@
<template>
<t-layout class="dashboard-container">
<div ref="menuRef">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
</div>
<t-layout>
<router-view />
</t-layout>
</t-layout>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import SidebarMenu from './webui/Nav.vue';
import emitter from '@/ts/event-bus';
interface MenuItem {
value: string;
icon: string;
label: string;
route: string;
}
const menuItems = ref<MenuItem[]>([
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
{ value: 'item4', icon: 'setting', label: '其余配置', route: '/dashboard/other-config' },
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
]);
const menuRef = ref<HTMLDivElement | null>(null);
emitter.on('sendMenu', (event) => {
emitter.emit('sendWidth', menuRef.value?.offsetWidth);
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
});
onMounted(() => {
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
});
</script>
<style scoped>
.dashboard-container {
display: flex;
flex-direction: row;
height: 100vh;
width: 100%;
}
.sidebar-menu {
position: relative;
z-index: 2;
}
@media (max-width: 768px) {
.content {
padding: 10px;
}
}
</style>
-185
View File
@@ -1,185 +0,0 @@
<template>
<t-card class="layout">
<div class="login-container">
<h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods">
<t-tooltip content="快速登录">
<t-button
id="quick-login"
class="login-method"
:class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'"
>Quick Login</t-button
>
</t-tooltip>
<t-tooltip content="二维码登录">
<t-button
id="qrcode-login"
class="login-method"
:class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'"
>QR Code</t-button
>
</t-tooltip>
</div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select
id="quick-login-select"
v-model="selectedAccount"
placeholder="Select Account"
@change="selectAccount"
>
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select>
</div>
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
<canvas ref="qrcodeCanvas"></canvas>
</div>
</div>
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
</t-card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as QRCode from 'qrcode';
import { useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
const loginMethod = ref<'quick' | 'qrcode'>('quick');
const quickLoginList = ref<string[]>([]);
const selectedAccount = ref<string>('');
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
let heartBeatTimer: number | null = null;
let qrcodeUrl: string = '';
const selectAccount = async (accountName: string): Promise<void> => {
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
if (result) {
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' });
} else {
await MessagePlugin.error('登录失败,' + errMsg);
}
};
const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void => {
if (!canvas) {
console.error('Canvas element not found');
return;
}
QRCode.toCanvas(canvas, data, function (error: Error | null | undefined) {
if (error) {
console.error('Error generating QR Code:', error);
} else {
console.log('QR Code generated!');
}
});
};
const HeartBeat = async (): Promise<void> => {
const isLogined = await qqLoginManager.checkQQLoginStatusWithQrcode();
if (isLogined?.isLogin) {
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
// //判断是否已经调转
// if (router.currentRoute.value.path !== '/dashboard/basic-info') {
// return;
// }
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' });
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
qrcodeUrl = isLogined.qrcodeurl;
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
}
};
const InitPages = async (): Promise<void> => {
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
heartBeatTimer = window.setInterval(HeartBeat, 3000);
};
onMounted(() => {
InitPages();
});
</script>
<style scoped>
.layout {
height: 100vh;
}
.login-container {
padding: 20px;
border-radius: 5px;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 50px auto;
}
@media (max-width: 600px) {
.login-container {
width: 90%;
min-width: unset;
}
}
.login-methods {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.login-method {
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.login-method.active {
background-color: #e6f0ff;
color: #007bff;
}
.login-form,
.qrcode {
display: flex;
flex-direction: column;
gap: 15px;
}
.qrcode {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
text-align: center;
}
.sotheby-font {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
margin: 0;
font-size: 0.875rem;
color: #888;
position: fixed;
bottom: 20px;
left: 0;
right: 0;
}
</style>
-153
View File
@@ -1,153 +0,0 @@
<template>
<t-card class="layout">
<div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
<t-form-item name="password">
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
<template #prefix-icon>
<lock-on-icon />
</template>
</t-input>
</t-form-item>
<t-form-item>
<t-button theme="primary" type="submit" block>登录</t-button>
</t-form-item>
</t-form>
</div>
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
</t-card>
</template>
<script setup lang="ts">
import '../css/style.css';
import '../css/font.css';
import { reactive, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { LockOnIcon } from 'tdesign-icons-vue-next';
import { useRouter } from 'vue-router';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
interface FormData {
token: string;
}
const formData: FormData = reactive({
token: '',
});
const handleLoginSuccess = async (credential: string) => {
localStorage.setItem('auth', credential);
await checkLoginStatus();
};
const handleLoginFailure = (message: string) => {
MessagePlugin.error(message);
};
const checkLoginStatus = async () => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
return;
}
const loginManager = new QQLoginManager(storedCredential);
const isWenUiLoggedIn = await loginManager.checkWebUiLogined();
console.log('isWenUiLoggedIn', isWenUiLoggedIn);
if (!isWenUiLoggedIn) {
return;
}
const isQQLoggedIn = await loginManager.checkQQLoginStatus();
if (isQQLoggedIn) {
await router.push({ path: '/dashboard/basic-info' });
} else {
await router.push({ path: '/qqlogin' });
}
};
const loginWithToken = async (token: string) => {
const loginManager = new QQLoginManager('');
const credential = await loginManager.loginWithToken(token);
if (credential) {
await handleLoginSuccess(credential);
} else {
handleLoginFailure('登录失败,请检查Token');
}
};
onMounted(() => {
const url = new URL(window.location.href);
const token = url.searchParams.get('token');
if (token) {
loginWithToken(token);
}
checkLoginStatus();
});
const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
if (validateResult) {
await loginWithToken(formData.token);
} else {
handleLoginFailure('请填写Token');
}
};
</script>
<style scoped>
.layout {
height: 100vh;
}
.login-container {
padding: 20px;
border-radius: 5px;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 50px auto;
}
@media (max-width: 600px) {
.login-container {
width: 90%;
min-width: unset;
}
}
.tdesign-demo-block-column {
display: flex;
flex-direction: column;
row-gap: 16px;
}
.tdesign-demo-block-column-large {
display: flex;
flex-direction: column;
row-gap: 32px;
}
.tdesign-demo-block-row {
display: flex;
column-gap: 16px;
align-items: center;
}
.sotheby-font {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
margin: 0;
font-size: 0.875rem;
color: #888;
position: fixed;
bottom: 20px;
left: 0;
right: 0;
}
</style>
@@ -0,0 +1,425 @@
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'
import {
FaPause,
FaPlay,
FaRegHandPointRight,
FaRepeat,
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 { VolumeHighIcon, VolumeLowIcon } from './icons'
export interface AudioPlayerProps
extends React.AudioHTMLAttributes<HTMLAudioElement> {
src: string
title?: string
artist?: string
cover?: string
pressNext?: () => void
pressPrevious?: () => void
onPlayEnd?: () => void
onChangeMode?: (mode: PlayMode) => void
mode?: PlayMode
}
export default function AudioPlayer(props: AudioPlayerProps) {
const {
src,
pressNext,
pressPrevious,
cover = 'https://nextui.org/images/album-cover.png',
title = '未知',
artist = '未知',
onTimeUpdate,
onLoadedData,
onPlay,
onPause,
onPlayEnd,
onChangeMode,
autoPlay,
mode = PlayMode.Loop,
...rest
} = props
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<HTMLAudioElement>(null)
const cardRef = useRef<HTMLDivElement>(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<HTMLAudioElement>) => {
const audio = event.target as HTMLAudioElement
setCurrentTime(audio.currentTime)
onTimeUpdate?.(event)
}
const handleLoadedData = (event: React.SyntheticEvent<HTMLAudioElement>) => {
const audio = event.target as HTMLAudioElement
setDuration(audio.duration)
onLoadedData?.(event)
}
const handlePlay = (e: React.SyntheticEvent<HTMLAudioElement>) => {
setIsPlaying(true)
setStorageAutoPlay(true)
onPlay?.(e)
}
const handlePause = (e: React.SyntheticEvent<HTMLAudioElement>) => {
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 volumeChange = (value: number) => {
setVolume(value)
}
useEffect(() => {
const audio = audioRef.current
if (audio) {
audio.volume = volume / 100
}
}, [volume])
const handleTouchStart = (e: React.TouchEvent) => {
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
if (isSmallScreen) {
shouldAdd.current = _shouldAdd
setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY)
} else {
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)
if (_translateY > 100) {
setIsCollapsed(true)
} else if (_translateY < -100) {
setIsCollapsed(false)
}
setTranslateY(0)
} else {
if (translateX > 100) {
setIsCollapsed(true)
} else if (translateX < -100) {
setIsCollapsed(false)
}
setTranslateX(0)
}
}
const dragTranslate = isSmallScreen
? translateY
? `translateY(${translateY}px)`
: ''
: translateX
? `translateX(${translateX}px)`
: ''
const collapsedTranslate = isCollapsed
? isSmallScreen
? 'translateY(90%)'
: 'translateX(96%)'
: ''
const translateStyle = dragTranslate || collapsedTranslate
if (!src) return null
return (
<div
className={clsx(
'fixed right-0 bottom-0 z-[52] w-full md:w-96',
!translateX && !translateY && 'transition-transform',
isCollapsed && 'md:hover:!translate-x-80'
)}
style={{
transform: translateStyle
}}
>
<audio
src={src}
onLoadedData={handleLoadedData}
onTimeUpdate={handleTimeUpdate}
onPlay={handlePlay}
onPause={handlePause}
onEnded={onPlayEnd}
autoPlay={autoPlay ?? storageAutoPlay}
{...rest}
controls={false}
hidden
ref={audioRef}
/>
<Card
ref={cardRef}
className={clsx(
'border-none bg-background/60 dark:bg-default-300/50 w-full max-w-full transform transition-transform backdrop-blur-md duration-300 overflow-visible',
isSmallScreen ? 'rounded-t-3xl' : 'md:rounded-l-xl'
)}
classNames={{
body: 'p-0'
}}
shadow="sm"
radius="none"
>
{isMediumUp && (
<Button
isIconOnly
className={clsx(
'absolute data-[hover]:bg-foreground/10 text-lg z-50',
isCollapsed
? 'top-0 left-0 w-full h-full rounded-xl bg-opacity-0 hover:bg-opacity-30'
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
)}
variant="solid"
color="primary"
size="sm"
onPress={() => setIsCollapsed(!isCollapsed)}
>
<FaRegHandPointRight />
</Button>
)}
{isSmallScreen && (
<CardHeader
data-header
className="flex-row justify-center pt-4"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onClick={() => setIsCollapsed(!isCollapsed)}
>
<div className="w-24 h-2 rounded-full bg-content2-foreground shadow-sm"></div>
</CardHeader>
)}
<CardBody>
<div className="grid grid-cols-6 md:grid-cols-12 gap-6 md:gap-4 items-center justify-center overflow-hidden p-6 md:p-2 m-0">
<div className="relative col-span-6 md:col-span-4 flex justify-center">
<Image
alt="Album cover"
className="object-cover"
classNames={{
wrapper: 'w-36 aspect-square md:w-24 flex',
img: 'block w-full h-full'
}}
shadow="md"
src={cover}
width="100%"
/>
</div>
<div className="flex flex-col col-span-6 md:col-span-8">
<div className="flex flex-col gap-0">
<h1 className="font-medium truncate">{title}</h1>
<p className="text-xs text-foreground/80 truncate">{artist}</p>
</div>
<div className="flex flex-col">
<Slider
aria-label="Music progress"
classNames={{
track: 'bg-default-500/30 border-none',
thumb: 'w-2 h-2 after:w-1.5 after:h-1.5',
filler: 'rounded-full'
}}
color="foreground"
value={currentProgress || 0}
defaultValue={0}
size="sm"
onChange={(value) => {
value = Array.isArray(value) ? value[0] : value
const audio = audioRef.current
if (audio) {
audio.currentTime = (value / 100) * duration
}
}}
/>
<div className="flex justify-between h-3">
<p className="text-xs">
{Math.floor(currentTime / 60)}:
{Math.floor(currentTime % 60)
.toString()
.padStart(2, '0')}
</p>
<p className="text-xs text-foreground/50">
{Math.floor(duration / 60)}:
{Math.floor(duration % 60)
.toString()
.padStart(2, '0')}
</p>
</div>
</div>
<div className="flex w-full items-center justify-center">
<Tooltip
content={
mode === PlayMode.Loop
? '列表循环'
: mode === PlayMode.Random
? '随机播放'
: '单曲循环'
}
>
<Button
isIconOnly
className="data-[hover]:bg-foreground/10 text-lg md:text-medium"
radius="full"
variant="light"
size="md"
onPress={changeMode}
>
{mode === PlayMode.Loop && (
<FaRepeat className="text-foreground/80" />
)}
{mode === PlayMode.Random && (
<FaShuffle className="text-foreground/80" />
)}
{mode === PlayMode.Single && (
<TbRepeatOnce className="text-foreground/80 text-xl" />
)}
</Button>
</Tooltip>
<Tooltip content="上一首">
<Button
isIconOnly
className="data-[hover]:bg-foreground/10 text-2xl md:text-xl"
radius="full"
variant="light"
size="md"
onPress={pressPrevious}
>
<BiSolidSkipPreviousCircle />
</Button>
</Tooltip>
<Tooltip content={isPlaying ? '暂停' : '播放'}>
<Button
isIconOnly
className="data-[hover]:bg-foreground/10 text-3xl md:text-3xl"
radius="full"
variant="light"
size="lg"
onPress={() => {
if (isPlaying) {
audioRef.current?.pause()
setStorageAutoPlay(false)
} else {
audioRef.current?.play()
}
}}
>
{isPlaying ? <FaPause /> : <FaPlay className="ml-1" />}
</Button>
</Tooltip>
<Tooltip content="下一首">
<Button
isIconOnly
className="data-[hover]:bg-foreground/10 text-2xl md:text-xl"
radius="full"
variant="light"
size="md"
onPress={pressNext}
>
<BiSolidSkipNextCircle />
</Button>
</Tooltip>
<Popover
placement="top"
classNames={{
content: 'bg-opacity-30 backdrop-blur-md'
}}
>
<PopoverTrigger>
<Button
isIconOnly
className="data-[hover]:bg-foreground/10 text-xl md:text-xl"
radius="full"
variant="light"
size="md"
>
<VolumeHighIcon />
</Button>
</PopoverTrigger>
<PopoverContent>
<Slider
orientation="vertical"
showTooltip
aria-label="Volume"
className="h-40"
color="primary"
defaultValue={volume}
onChange={(value) => {
value = Array.isArray(value) ? value[0] : value
volumeChange(value)
}}
startContent={<VolumeHighIcon className="text-2xl" />}
size="sm"
endContent={<VolumeLowIcon className="text-2xl" />}
/>
</PopoverContent>
</Popover>
</div>
</div>
</div>
</CardBody>
</Card>
</div>
)
}
@@ -0,0 +1,208 @@
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'
import {
HTTPClientIcon,
HTTPServerIcon,
PCIcon,
PlusIcon,
WebsocketIcon
} from '../icons'
export interface AddButtonProps {
onOpen: (key: keyof OneBotConfig['network']) => void
}
const AddButton: React.FC<AddButtonProps> = (props) => {
const { onOpen } = props
return (
<Dropdown
classNames={{
content: 'bg-opacity-30 backdrop-blur-md'
}}
placement="right"
>
<DropdownTrigger>
<Button
color="primary"
startContent={<IoAddCircleOutline className="text-2xl" />}
>
新建
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Create Network Config"
color="primary"
variant="flat"
onAction={(key) => {
onOpen(key as keyof OneBotConfig['network'])
}}
>
<DropdownItem
key="title"
isReadOnly
className="cursor-default hover:!bg-transparent"
textValue="title"
>
<div className="flex items-center gap-2 justify-center">
<div className="w-5 h-5 -ml-3">
<PlusIcon />
</div>
<div className="text-primary-400">新建网络配置</div>
</div>
</DropdownItem>
<DropdownItem
key="httpServers"
textValue="httpServers"
startContent={
<div className="w-6 h-6">
<HTTPServerIcon />
</div>
}
>
<div className="flex gap-1 items-center">
HTTP服务器
<Tooltip
content="「由NapCat建立」一个HTTP服务器,你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址,你或者你的框架应该连接到这个地址。"
showArrow
className="max-w-64"
>
<Button
isIconOnly
radius="full"
size="sm"
variant="light"
className="w-4 h-4 min-w-0"
>
<FaRegCircleQuestion />
</Button>
</Tooltip>
</div>
</DropdownItem>
<DropdownItem
key="httpSseServers"
textValue="httpSseServers"
startContent={
<div className="w-6 h-6">
<HTTPServerIcon />
</div>
}
>
<div className="flex gap-1 items-center">
HTTP SSE服务器
<Tooltip
content="「由NapCat建立」一个HTTP SSE服务器,你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址,你或者你的框架应该连接到这个地址。"
showArrow
className="max-w-64"
>
<Button
isIconOnly
radius="full"
size="sm"
variant="light"
className="w-4 h-4 min-w-0"
>
<FaRegCircleQuestion />
</Button>
</Tooltip>
</div>
</DropdownItem>
<DropdownItem
key="httpClients"
textValue="httpClients"
startContent={
<div className="w-6 h-6">
<HTTPClientIcon />
</div>
}
>
<div className="flex gap-1 items-center">
HTTP客户端
<Tooltip
content="「由框架或者你自己建立」的一个用于「接收」NapCat向你发送请求的客户端,通常框架会提供一个HTTP地址。这个地址是你使用的框架提供的,NapCat会主动连接它。"
showArrow
className="max-w-64"
>
<Button
isIconOnly
radius="full"
size="sm"
variant="light"
className="w-4 h-4 min-w-0"
>
<FaRegCircleQuestion />
</Button>
</Tooltip>
</div>
</DropdownItem>
<DropdownItem
key="websocketServers"
textValue="websocketServers"
startContent={
<div className="w-6 h-6">
<WebsocketIcon />
</div>
}
>
<div className="flex gap-1 items-center">
Websocket服务器
<Tooltip
content="「由NapCat建立」一个WebSocket服务器,你的框架应该连接到此服务器。NapCat会根据你配置的IP和端口等建立一个WebSocket地址,你或者你的框架应该连接到这个地址。"
showArrow
className="max-w-64"
>
<Button
isIconOnly
radius="full"
size="sm"
variant="light"
className="w-4 h-4 min-w-0"
>
<FaRegCircleQuestion />
</Button>
</Tooltip>
</div>
</DropdownItem>
<DropdownItem
key="websocketClients"
textValue="websocketClients"
startContent={
<div className="w-6 h-6">
<PCIcon />
</div>
}
>
<div className="flex gap-1 items-center">
Websocket客户端
<Tooltip
content="「由框架或者你自己建立」的WebSocket,通常框架会「提供」一个ws地址,NapCat会主动连接它。"
showArrow
className="max-w-64"
>
<Button
isIconOnly
radius="full"
size="sm"
variant="light"
className="w-4 h-4 min-w-0"
>
<FaRegCircleQuestion />
</Button>
</Tooltip>
</div>
</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}
export default AddButton
@@ -0,0 +1,59 @@
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
isSubmitting: boolean
className?: string
}
const SaveButtons: React.FC<SaveButtonsProps> = ({
onSubmit,
reset,
isSubmitting,
refresh,
className
}) => (
<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"
onPress={() => {
reset()
toast.success('重置成功')
}}
>
取消更改
</Button>
<Button
color="primary"
isLoading={isSubmitting}
onPress={() => onSubmit()}
>
保存
</Button>
{refresh && (
<Button
isIconOnly
color="secondary"
radius="full"
variant="flat"
onPress={() => refresh()}
>
<IoMdRefresh size={24} />
</Button>
)}
</div>
</div>
)
export default SaveButtons
@@ -0,0 +1,254 @@
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 { isURI } from '@/utils/url'
import type { OB11Segment } from '@/types/onebot'
const AudioInsert = () => {
const [audioUrl, setAudioUrl] = useState<string>('')
const audioInputRef = useRef<HTMLInputElement>(null)
const showStructuredMessage = useShowStructuredMessage()
const showAudioSegment = (file: string) => {
const messages: OB11Segment[] = [
{
type: 'record',
data: {
file: file
}
}
]
showStructuredMessage(messages)
}
const [isRecording, setIsRecording] = useState(false)
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const [audioPreview, setAudioPreview] = useState<string | null>(null)
const [showPreview, setShowPreview] = useState(false)
const streamRef = useRef<MediaStream | null>(null)
const [recordingTime, setRecordingTime] = useState(0)
const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (isRecording) {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
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)
}
}
recorder.onstop = () => {
if (audioChunksRef.current.length > 0) {
const audioBlob = new Blob(audioChunksRef.current, {
type: 'audio/wav'
})
const reader = new FileReader()
reader.readAsDataURL(audioBlob)
reader.onloadend = () => {
const base64Audio = reader.result as string
setAudioPreview(base64Audio)
setShowPreview(true)
}
audioChunksRef.current = []
}
stream.getTracks().forEach((track) => track.stop())
}
})
recordingIntervalRef.current = setInterval(() => {
setRecordingTime((prevTime) => prevTime + 1)
}, 1000)
} else {
mediaRecorderRef.current?.stop()
if (recordingIntervalRef.current) {
clearInterval(recordingIntervalRef.current)
recordingIntervalRef.current = null
}
}
}, [isRecording])
const startRecording = () => {
setAudioPreview(null)
setShowPreview(false)
setRecordingTime(0)
setIsRecording(true)
}
const stopRecording = () => {
setIsRecording(false)
}
const handleShowPreview = () => {
if (audioPreview) {
showAudioSegment(audioPreview)
}
}
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60)
const seconds = time % 60
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
return (
<>
<Popover>
<Tooltip content="发送音频">
<div className="max-w-fit">
<PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full">
<IoMic className="text-xl" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-2 p-4">
<Tooltip content="上传音频">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
onPress={() => {
audioInputRef?.current?.click()
}}
>
<MdUpload />
</Button>
</Tooltip>
<Popover>
<Tooltip content="输入音频地址">
<div className="max-w-fit">
<PopoverTrigger tooltip="输入音频地址">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
>
<MdEdit />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-1 p-2">
<Input
value={audioUrl}
onChange={(e) => setAudioUrl(e.target.value)}
placeholder="请输入音频地址"
/>
<Button
color="primary"
variant="flat"
isIconOnly
radius="full"
onPress={() => {
if (!isURI(audioUrl)) {
toast.error('请输入正确的音频地址')
return
}
showAudioSegment(audioUrl)
setAudioUrl('')
}}
>
<FaMicrophone />
</Button>
</PopoverContent>
</Popover>
<Popover>
<Tooltip content="录制音频">
<div className="max-w-fit">
<PopoverTrigger>
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
>
<IoMic />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-col gap-2 p-4">
<div className="flex gap-2">
<Button
color={isRecording ? 'primary' : 'primary'}
variant="flat"
onPress={isRecording ? stopRecording : startRecording}
>
{isRecording ? '停止录制' : '开始录制'}
</Button>
{showPreview && audioPreview && (
<Button
color="primary"
variant="flat"
onPress={handleShowPreview}
>
查看消息
</Button>
)}
</div>
{(isRecording || audioPreview) && (
<div className="flex gap-1 items-center">
<span
className={clsx(
'w-4 h-4 rounded-full',
isRecording
? 'animate-pulse bg-primary-400'
: 'bg-success-400'
)}
></span>
<span>录制时长: {formatTime(recordingTime)}</span>
</div>
)}
{showPreview && audioPreview && (
<audio controls src={audioPreview} />
)}
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
<input
type="file"
ref={audioInputRef}
hidden
accept="audio/*"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (!file) {
return
}
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (event) => {
const dataURL = event.target?.result
showAudioSegment(dataURL as string)
e.target.value = ''
}
}}
/>
</>
)
}
export default AudioInsert
@@ -0,0 +1,31 @@
import { Button } from '@heroui/button'
import { Tooltip } from '@heroui/tooltip'
import { BsDice3Fill } from 'react-icons/bs'
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
const DiceInsert = () => {
const showStructuredMessage = useShowStructuredMessage()
return (
<Tooltip content="发送骰子">
<Button
color="primary"
variant="flat"
isIconOnly
radius="full"
onPress={() => {
showStructuredMessage([
{
type: 'dice'
}
])
}}
>
<BsDice3Fill className="text-lg" />
</Button>
</Tooltip>
)
}
export default DiceInsert
@@ -0,0 +1,83 @@
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'
const emojis = data.map((item) => {
return {
alt: item.QDes,
src: getUrl(item.QSid),
id: item.QSid
} as EmojiValue
})
export interface EmojiPickerProps {
onInsertEmoji: (emoji: EmojiValue) => void
onOpenChange: (open: boolean) => void
}
const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
const [visibleEmojis, setVisibleEmojis] = useState<EmojiValue[]>([])
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isPopoverOpen) {
setVisibleEmojis([]) // Reset visible emojis
requestAnimationFrame(() => loadEmojis()) // Start loading emojis
}
}, [isPopoverOpen])
const loadEmojis = (index = 0, batchSize = 10) => {
if (index < emojis.length) {
setVisibleEmojis((prev) => [
...prev,
...emojis.slice(index, index + batchSize)
])
requestAnimationFrame(() => loadEmojis(index + batchSize, batchSize))
}
}
return (
<div ref={containerRef}>
<Popover
portalContainer={containerRef.current!}
shouldCloseOnScroll={false}
placement="right-start"
onOpenChange={(v) => {
onOpenChange(v)
setIsPopoverOpen(v)
}}
>
<Tooltip content="插入表情">
<div className="max-w-fit">
<PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full">
<MdEmojiEmotions className="text-xl" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="grid grid-cols-8 gap-1 flex-wrap justify-start items-start overflow-y-auto max-w-full max-h-96 p-2">
{visibleEmojis.map((emoji) => (
<Button
key={emoji.id}
color="primary"
variant="flat"
isIconOnly
radius="full"
onPress={() => onInsertEmoji(emoji)}
>
<Image src={emoji.src} alt={emoji.alt} className="w-6 h-6" />
</Button>
))}
</PopoverContent>
</Popover>
</div>
)
}
export default EmojiPicker
@@ -0,0 +1,125 @@
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 { isURI } from '@/utils/url'
import type { OB11Segment } from '@/types/onebot'
const FileInsert = () => {
const [fileUrl, setFileUrl] = useState<string>('')
const fileInputRef = useRef<HTMLInputElement>(null)
const showStructuredMessage = useShowStructuredMessage()
const showFileSegment = (file: string) => {
const messages: OB11Segment[] = [
{
type: 'file',
data: {
file: file
}
}
]
showStructuredMessage(messages)
}
return (
<>
<Popover>
<Tooltip content="发送文件">
<div className="max-w-fit">
<PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full">
<FaFolder className="text-lg" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-2 p-4">
<Tooltip content="上传文件">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
onPress={() => {
fileInputRef?.current?.click()
}}
>
<MdUpload />
</Button>
</Tooltip>
<Popover>
<Tooltip content="输入文件地址">
<div className="max-w-fit">
<PopoverTrigger tooltip="输入文件地址">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
>
<MdEdit />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-1 p-2">
<Input
value={fileUrl}
onChange={(e) => setFileUrl(e.target.value)}
placeholder="请输入文件地址"
/>
<Button
color="primary"
variant="flat"
isIconOnly
radius="full"
onPress={() => {
if (!isURI(fileUrl)) {
toast.error('请输入正确的文件地址')
return
}
showFileSegment(fileUrl)
setFileUrl('')
}}
>
<LuFilePlus2 />
</Button>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
<input
type="file"
ref={fileInputRef}
hidden
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (!file) {
return
}
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (event) => {
const dataURL = event.target?.result
showFileSegment(dataURL as string)
e.target.value = ''
}
}}
/>
</>
)
}
export default FileInsert
@@ -0,0 +1,114 @@
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 { MdAddPhotoAlternate, MdEdit, MdImage, MdUpload } from 'react-icons/md'
import { isURI } from '@/utils/url'
export interface ImageInsertProps {
insertImage: (url: string) => void
onOpenChange: (open: boolean) => void
}
const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
const [imgUrl, setImgUrl] = useState<string>('')
const imageInputRef = useRef<HTMLInputElement>(null)
return (
<>
<Popover onOpenChange={onOpenChange}>
<Tooltip content="插入图片">
<div className="max-w-fit">
<PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full">
<MdImage className="text-xl" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-2 p-4">
<Tooltip content="上传图片">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
onPress={() => {
imageInputRef?.current?.click()
}}
>
<MdUpload />
</Button>
</Tooltip>
<Popover>
<Tooltip content="输入图片地址">
<div className="max-w-fit">
<PopoverTrigger tooltip="输入图片地址">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
>
<MdEdit />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-1 p-2">
<Input
value={imgUrl}
onChange={(e) => setImgUrl(e.target.value)}
placeholder="请输入图片地址"
/>
<Button
color="primary"
variant="flat"
isIconOnly
radius="full"
onPress={() => {
if (!isURI(imgUrl)) {
toast.error('请输入正确的图片地址')
return
}
insertImage(imgUrl)
setImgUrl('')
}}
>
<MdAddPhotoAlternate />
</Button>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
<input
type="file"
ref={imageInputRef}
hidden
accept="image/*"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (!file) {
return
}
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (event) => {
const dataURL = event.target?.result
insertImage(dataURL as string)
e.target.value = ''
}
}}
/>
</>
)
}
export default ImageInsert
@@ -0,0 +1,256 @@
import { Button } from '@heroui/button'
import { Form } from '@heroui/form'
import { Input } from '@heroui/input'
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
import { Select, SelectItem } from '@heroui/select'
import type { SharedSelection } from '@heroui/system'
import { Tab, Tabs } from '@heroui/tabs'
import { Tooltip } from '@heroui/tooltip'
import type { Key } from '@react-types/shared'
import { useRef, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import { IoMusicalNotes } from 'react-icons/io5'
import { TbMusicPlus } from 'react-icons/tb'
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
import { isURI } from '@/utils/url'
import type {
CustomMusicSegment,
MusicSegment,
OB11Segment
} from '@/types/onebot'
type MusicData = CustomMusicSegment['data'] | MusicSegment['data']
const MusicInsert = () => {
const [musicId, setMusicId] = useState<string>('')
const [musicType, setMusicType] = useState<SharedSelection>(new Set(['163']))
const [mode, setMode] = useState<Key>('default')
const containerRef = useRef<HTMLDivElement>(null)
const { control, handleSubmit, reset } = useForm<
Omit<CustomMusicSegment['data'], 'type'>
>({
defaultValues: {
url: '',
audio: '',
title: '',
image: '',
content: ''
}
})
const showStructuredMessage = useShowStructuredMessage()
const showMusicSegment = (data: MusicData) => {
const messages: OB11Segment[] = []
if (data.type === 'custom') {
messages.push({
type: 'music',
data: {
...data,
type: 'custom'
}
})
} else {
messages.push({
type: 'music',
data
})
}
showStructuredMessage(messages)
}
const onSubmit = (data: Omit<CustomMusicSegment['data'], 'type'>) => {
showMusicSegment({
type: 'custom',
...data
})
reset()
}
return (
<div ref={containerRef} className="overflow-visible">
<Popover
placement="right-start"
shouldCloseOnScroll={false}
portalContainer={containerRef.current!}
>
<Tooltip content="发送音乐">
<div className="max-w-fit">
<PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full">
<IoMusicalNotes className="text-xl" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="gap-2 p-4">
<Tabs
placement="top"
className="w-96"
fullWidth
selectedKey={mode}
onSelectionChange={setMode}
>
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
<Select
onClick={(e) => e.stopPropagation()}
aria-label="音乐平台"
selectedKeys={musicType}
label="音乐平台"
placeholder="请选择音乐平台"
items={[
{
name: 'QQ音乐',
id: 'qq'
},
{
name: '网易云音乐',
id: '163'
},
{
name: '虾米音乐',
id: 'xm'
}
]}
onSelectionChange={setMusicType}
>
{(item) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
</SelectItem>
)}
</Select>
<Input
value={musicId}
onChange={(e) => setMusicId(e.target.value)}
placeholder="请输入音乐ID"
label="音乐ID"
/>
<Button
fullWidth
size="lg"
color="primary"
variant="flat"
radius="full"
onPress={() => {
if (!musicId) {
toast.error('请输入音乐ID')
return
}
showMusicSegment({
type: Array.from(
musicType
)[0] as MusicSegment['data']['type'],
id: musicId
})
setMusicId('')
}}
startContent={<TbMusicPlus />}
>
创建{Array.from(musicType)[0] === '163' ? '网易云' : 'QQ'}音乐
</Button>
</Tab>
<Tab
title="自定义音乐"
key="custom"
className="flex flex-col gap-2"
>
<Form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-2"
validationBehavior="native"
>
<Controller
name="url"
control={control}
render={({ field }) => (
<Input
{...field}
isRequired
validate={(v) => {
return !isURI(v) ? '请输入正确的音乐URL' : null
}}
size="sm"
placeholder="请输入音乐URL"
label="音乐URL"
/>
)}
/>
<Controller
name="audio"
control={control}
render={({ field }) => (
<Input
{...field}
isRequired
validate={(v) => {
return !isURI(v) ? '请输入正确的音频URL' : null
}}
size="sm"
placeholder="请输入音频URL"
label="音频URL"
/>
)}
/>
<Controller
name="title"
control={control}
render={({ field }) => (
<Input
{...field}
isRequired
size="sm"
errorMessage="请输入音乐标题"
placeholder="请输入音乐标题"
label="音乐标题"
/>
)}
/>
<Controller
name="image"
control={control}
render={({ field }) => (
<Input
{...field}
size="sm"
placeholder="请输入封面图片URL"
label="封面图片URL"
/>
)}
/>
<Controller
name="content"
control={control}
render={({ field }) => (
<Input
{...field}
size="sm"
placeholder="请输入音乐描述"
label="音乐描述"
/>
)}
/>
<Button
fullWidth
size="lg"
color="primary"
variant="flat"
radius="full"
type="submit"
startContent={<TbMusicPlus />}
>
创建自定义音乐
</Button>
</Form>
</Tab>
</Tabs>
</PopoverContent>
</Popover>
</div>
)
}
export default MusicInsert
@@ -0,0 +1,58 @@
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<string>('')
return (
<>
<Popover>
<Tooltip content="回复消息">
<div className="max-w-fit">
<PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full">
<BsChatQuoteFill className="text-lg" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-2 p-4">
<Input
placeholder="输入消息 ID"
value={replyId}
onChange={(e) => {
const value = e.target.value
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/
if (isNumberReg.test(value)) {
setReplyId(value)
}
}}
/>
<Button
color="primary"
variant="flat"
radius="full"
isIconOnly
onPress={() => {
insertReply(replyId)
setReplyId('')
}}
>
<MdAdd />
</Button>
</PopoverContent>
</Popover>
</>
)
}
export default ReplyInsert
@@ -0,0 +1,31 @@
import { Button } from '@heroui/button'
import { Tooltip } from '@heroui/tooltip'
import { LiaHandScissors } from 'react-icons/lia'
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
const RPSInsert = () => {
const showStructuredMessage = useShowStructuredMessage()
return (
<Tooltip content="发送猜拳">
<Button
color="primary"
variant="flat"
isIconOnly
radius="full"
onPress={() => {
showStructuredMessage([
{
type: 'rps'
}
])
}}
>
<LiaHandScissors className="text-2xl" />
</Button>
</Tooltip>
)
}
export default RPSInsert
@@ -0,0 +1,32 @@
import { Snippet } from '@heroui/snippet'
import { OB11Segment } from '@/types/onebot'
export interface ShowStructedMessageProps {
messages: OB11Segment[]
}
const ShowStructedMessage = ({ messages }: ShowStructedMessageProps) => {
return (
<Snippet
hideSymbol
tooltipProps={{
content: '点击复制'
}}
classNames={{
copyButton: 'self-start sticky top-0 right-0'
}}
className="bg-content1 h-96 overflow-y-scroll items-start"
>
{JSON.stringify(messages, null, 2)
.split('\n')
.map((line, i) => (
<span key={i} className="whitespace-pre-wrap break-all">
{line}
</span>
))}
</Snippet>
)
}
export default ShowStructedMessage
@@ -0,0 +1,126 @@
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 { isURI } from '@/utils/url'
import type { OB11Segment } from '@/types/onebot'
const VideoInsert = () => {
const [videoUrl, setVideoUrl] = useState<string>('')
const videoInputRef = useRef<HTMLInputElement>(null)
const showStructuredMessage = useShowStructuredMessage()
const showVideoSegment = (file: string) => {
const messages: OB11Segment[] = [
{
type: 'video',
data: {
file: file
}
}
]
showStructuredMessage(messages)
}
return (
<>
<Popover>
<Tooltip content="发送视频">
<div className="max-w-fit">
<PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full">
<IoVideocam className="text-xl" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-2 p-4">
<Tooltip content="上传视频">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
onPress={() => {
videoInputRef?.current?.click()
}}
>
<MdUpload />
</Button>
</Tooltip>
<Popover>
<Tooltip content="输入视频地址">
<div className="max-w-fit">
<PopoverTrigger tooltip="输入视频地址">
<Button
className="text-lg"
color="primary"
isIconOnly
variant="flat"
radius="full"
>
<MdEdit />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
<PopoverContent className="flex-row gap-1 p-2">
<Input
value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)}
placeholder="请输入视频地址"
/>
<Button
color="primary"
variant="flat"
isIconOnly
radius="full"
onPress={() => {
if (!isURI(videoUrl)) {
toast.error('请输入正确的视频地址')
return
}
showVideoSegment(videoUrl)
setVideoUrl('')
}}
>
<TbVideoPlus />
</Button>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
<input
type="file"
ref={videoInputRef}
hidden
accept="video/*"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (!file) {
return
}
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (event) => {
const dataURL = event.target?.result
showVideoSegment(dataURL as string)
e.target.value = ''
}
}}
/>
</>
)
}
export default VideoInsert
@@ -0,0 +1,41 @@
import Quill from 'quill'
// eslint-disable-next-line
const Embed = Quill.import('blots/embed') as any
export interface EmojiValue {
alt: string
src: string
id: string
}
class EmojiBlot extends Embed {
static blotName: string = 'emoji'
static tagName: string = 'img'
static classNames: string[] = ['w-6', 'h-6']
static create(value: HTMLImageElement) {
const node = super.create(value)
node.setAttribute('alt', value.alt)
node.setAttribute('src', value.src)
node.setAttribute('data-id', value.id)
node.classList.add(...EmojiBlot.classNames)
return node
}
static formats(node: HTMLImageElement): EmojiValue {
return {
alt: node.getAttribute('alt') ?? '',
src: node.getAttribute('src') ?? '',
id: node.getAttribute('data-id') ?? ''
}
}
static value(node: HTMLImageElement): EmojiValue {
return {
alt: node.getAttribute('alt') ?? '',
src: node.getAttribute('src') ?? '',
id: node.getAttribute('data-id') ?? ''
}
}
}
export default EmojiBlot
@@ -0,0 +1,30 @@
import Quill from 'quill'
// eslint-disable-next-line
const Embed = Quill.import('blots/embed') as any
export interface ImageValue {
alt: string
src: string
}
class ImageBlot extends Embed {
static blotName = 'image'
static tagName = 'img'
static classNames: string[] = ['max-w-48', 'max-h-48', 'align-bottom']
static create(value: ImageValue) {
let node = super.create()
node.setAttribute('alt', value.alt)
node.setAttribute('src', value.src)
node.classList.add(...ImageBlot.classNames)
return node
}
static value(node: HTMLImageElement): ImageValue {
return {
alt: node.getAttribute('alt') ?? '',
src: node.getAttribute('src') ?? ''
}
}
}
export default ImageBlot
@@ -0,0 +1,43 @@
import Quill from 'quill'
// eslint-disable-next-line
const BlockEmbed = Quill.import('blots/block/embed') as any
export interface ReplyBlockValue {
messageId: string
}
class ReplyBlock extends BlockEmbed {
static blotName = 'reply'
static tagName = 'div'
static classNames = [
'p-2',
'select-none',
'bg-default-100',
'rounded-md',
'pointer-events-none'
]
static create(value: ReplyBlockValue) {
const node = super.create()
node.setAttribute('data-message-id', value.messageId)
node.setAttribute('contenteditable', 'false')
node.classList.add(...ReplyBlock.classNames)
const innerDom = document.createElement('div')
innerDom.classList.add('text-sm', 'text-default-500', 'relative')
const svgContainer = document.createElement('div')
svgContainer.classList.add('w-3', 'h-3', 'absolute', 'top-0', 'right-0')
const svg = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.9082 12.3714H20.5982C20.5182 17.0414 19.5982 17.8114 16.7282 19.5114C16.3982 19.7114 16.2882 20.1314 16.4882 20.4714C16.6882 20.8014 17.1082 20.9114 17.4482 20.7114C20.8282 18.7114 22.0082 17.4914 22.0082 11.6714V6.28141C22.0082 4.57141 20.6182 3.19141 18.9182 3.19141H15.9182C14.1582 3.19141 12.8282 4.52141 12.8282 6.28141V9.28141C12.8182 11.0414 14.1482 12.3714 15.9082 12.3714Z" fill="#292D32"></path> <path d="M5.09 12.3714H9.78C9.7 17.0414 8.78 17.8114 5.91 19.5114C5.58 19.7114 5.47 20.1314 5.67 20.4714C5.87 20.8014 6.29 20.9114 6.63 20.7114C10.01 18.7114 11.19 17.4914 11.19 11.6714V6.28141C11.19 4.57141 9.8 3.19141 8.1 3.19141H5.1C3.33 3.19141 2 4.52141 2 6.28141V9.28141C2 11.0414 3.33 12.3714 5.09 12.3714Z" fill="#292D32"></path> </g></svg>`
svgContainer.innerHTML = svg
innerDom.innerHTML = `消息ID:${value.messageId}`
innerDom.appendChild(svgContainer)
node.appendChild(innerDom)
return node
}
static value(node: HTMLElement): ReplyBlockValue {
return {
messageId: node.getAttribute('data-message-id') || ''
}
}
}
export default ReplyBlock
@@ -0,0 +1,207 @@
import { Button } from '@heroui/button'
import type { Range } from 'quill'
import 'quill/dist/quill.core.css'
import { useRef } from 'react'
import toast from 'react-hot-toast'
import { useCustomQuill } from '@/hooks/use_custom_quill'
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
import { quillToMessage } from '@/utils/onebot'
import type { OB11Segment } from '@/types/onebot'
import AudioInsert from './components/audio_insert'
import DiceInsert from './components/dice_insert'
import EmojiPicker from './components/emoji_picker'
import FileInsert from './components/file_insert'
import ImageInsert from './components/image_insert'
import MusicInsert from './components/music_insert'
import ReplyInsert from './components/reply_insert'
import RPSInsert from './components/rps_insert'
import VideoInsert from './components/video_insert'
import EmojiBlot from './formats/emoji_blot'
import type { EmojiValue } from './formats/emoji_blot'
import ImageBlot from './formats/image_blot'
import ReplyBlock from './formats/reply_blot'
const ChatInput = () => {
const memorizedRange = useRef<Range | null>(null)
const showStructuredMessage = useShowStructuredMessage()
const formats: string[] = ['image', 'emoji', 'reply']
const modules = {
toolbar: '#toolbar'
}
const { quillRef, quill, Quill } = useCustomQuill({
modules,
formats,
placeholder: '请输入消息'
})
if (Quill && !quill) {
Quill.register('formats/emoji', EmojiBlot)
Quill.register('formats/image', ImageBlot, true)
Quill.register('formats/reply', ReplyBlock)
}
if (quill) {
quill.on('selection-change', (range) => {
if (range) {
const editorContent = quill.getContents()
const firstOp = editorContent.ops[0]
if (
typeof firstOp?.insert !== 'string' &&
firstOp?.insert?.reply &&
range.index === 0 &&
range.length !== quill.getLength()
) {
quill.setSelection(1, Quill.sources.SILENT)
}
}
})
quill.on('text-change', () => {
const editorContent = quill.getContents()
const firstOp = editorContent.ops[0]
if (
firstOp &&
typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply &&
quill.getLength() === 1
) {
quill.insertText(1, '\n', Quill.sources.SILENT)
}
})
quill.on('editor-change', (eventName: string) => {
if (eventName === 'text-change') {
const editorContent = quill.getContents()
const firstOp = editorContent.ops[0]
if (
firstOp &&
typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply &&
quill.getLength() === 1
) {
quill.insertText(1, '\n', Quill.sources.SILENT)
}
}
})
quill.root.addEventListener('compositionstart', () => {
const editorContent = quill.getContents()
const firstOp = editorContent.ops[0]
if (
firstOp &&
typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply &&
quill.getLength() === 1
) {
quill.insertText(1, '\n', Quill.sources.SILENT)
}
})
}
const onOpenChange = (open: boolean) => {
if (open) {
const selection = quill?.getSelection()
if (selection) memorizedRange.current = selection
}
}
const insertImage = (url: string) => {
const selection = memorizedRange.current || quill?.getSelection()
quill?.deleteText(selection?.index || 0, selection?.length || 0)
quill?.insertEmbed(selection?.index || 0, 'image', {
src: url,
alt: '图片'
})
quill?.setSelection((selection?.index || 0) + 1, 0)
}
function insertReplyBlock(messageId: string) {
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/
if (!isNumberReg.test(messageId)) {
toast.error('请输入正确的消息ID')
return
}
const editorContent = quill?.getContents()
const firstOp = editorContent?.ops[0]
const currentSelection = quill?.getSelection()
if (
firstOp &&
typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply
) {
const delta = quill?.getContents()
if (delta) {
delta.ops[0] = {
insert: { reply: { messageId } }
}
quill?.setContents(delta, Quill.sources.USER)
}
} else {
quill?.insertEmbed(0, 'reply', { messageId }, Quill.sources.USER)
}
quill?.setSelection((currentSelection?.index || 0) + 1, 0)
quill?.blur()
}
const onInsertEmoji = (emoji: EmojiValue) => {
const selection = memorizedRange.current || quill?.getSelection()
quill?.deleteText(selection?.index || 0, selection?.length || 0)
quill?.insertEmbed(selection?.index || 0, 'emoji', {
alt: emoji.alt,
src: emoji.src,
id: emoji.id
})
quill?.setSelection((selection?.index || 0) + 1, 0)
}
const getChatMessage = () => {
const delta = quill?.getContents()
const ops =
delta?.ops?.filter((op) => {
return op.insert !== '\n'
}) ?? []
const messages: OB11Segment[] = ops.map((op) => {
return quillToMessage(op)
})
return messages
}
return (
<div>
<div
ref={quillRef}
className="border border-default-200 rounded-md !mb-2 !text-base !h-64"
/>
<div id="toolbar" className="!border-none flex gap-2">
<ImageInsert insertImage={insertImage} onOpenChange={onOpenChange} />
<EmojiPicker
onInsertEmoji={onInsertEmoji}
onOpenChange={onOpenChange}
/>
<ReplyInsert insertReply={insertReplyBlock} />
<FileInsert />
<AudioInsert />
<VideoInsert />
<MusicInsert />
<DiceInsert />
<RPSInsert />
<Button
color="primary"
onPress={() => {
const messages = getChatMessage()
showStructuredMessage(messages)
}}
className="ml-auto"
>
获取JSON格式
</Button>
</div>
</div>
)
}
export default ChatInput
@@ -0,0 +1,49 @@
import { Button } from '@heroui/button'
import {
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
useDisclosure
} from '@heroui/modal'
import ChatInput from '.'
export default function ChatInputModal() {
const { isOpen, onOpen, onOpenChange } = useDisclosure()
return (
<>
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
构造聊天消息
</Button>
<Modal
size="4xl"
scrollBehavior="inside"
isOpen={isOpen}
onOpenChange={onOpenChange}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
构造消息
</ModalHeader>
<ModalBody className="overflow-y-auto">
<div className="overflow-y-auto">
<ChatInput />
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" onPress={onClose} variant="flat">
关闭
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
)
}
@@ -0,0 +1,55 @@
import Editor, { OnMount } from '@monaco-editor/react'
import { loader } from '@monaco-editor/react'
import React from 'react'
import { useTheme } from '@/hooks/use-theme'
import monaco from '@/monaco'
loader.config({
monaco,
paths: {
vs: '/webui/monaco-editor/min/vs'
}
})
loader.config({
'vs/nls': {
availableLanguages: { '*': 'zh-cn' }
}
})
export interface CodeEditorProps extends React.ComponentProps<typeof Editor> {
test?: string
}
export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>(
(props, ref) => {
const { isDark } = useTheme()
const handleEditorDidMount: OnMount = (editor, monaco) => {
if (ref) {
if (typeof ref === 'function') {
ref(editor)
} else {
;(ref as React.RefObject<CodeEditorRef>).current = editor
}
}
if (props.onMount) {
props.onMount(editor, monaco)
}
}
return (
<Editor
{...props}
onMount={handleEditorDidMount}
theme={isDark ? 'vs-dark' : 'light'}
/>
)
}
)
export default CodeEditor
@@ -0,0 +1,120 @@
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'
type NetworkType = OneBotConfig['network']
export type NetworkDisplayCardFields<T extends keyof NetworkType> = Array<{
label: string
value: NetworkType[T][0][keyof NetworkType[T][0]]
render?: (
value: NetworkType[T][0][keyof NetworkType[T][0]]
) => React.ReactNode
}>
export interface NetworkDisplayCardProps<T extends keyof NetworkType> {
data: NetworkType[T][0]
showType?: boolean
typeLabel: string
fields: NetworkDisplayCardFields<T>
onEdit: () => void
onEnable: () => Promise<void>
onDelete: () => Promise<void>
onEnableDebug: () => Promise<void>
}
const NetworkDisplayCard = <T extends keyof NetworkType>({
data,
showType,
typeLabel,
fields,
onEdit,
onEnable,
onDelete,
onEnableDebug
}: NetworkDisplayCardProps<T>) => {
const { name, enable, debug } = data
const [editing, setEditing] = useState(false)
const handleEnable = () => {
setEditing(true)
onEnable().finally(() => setEditing(false))
}
const handleDelete = () => {
setEditing(true)
onDelete().finally(() => setEditing(false))
}
const handleEnableDebug = () => {
setEditing(true)
onEnableDebug().finally(() => setEditing(false))
}
return (
<DisplayCardContainer
action={
<ButtonGroup
fullWidth
isDisabled={editing}
radius="full"
size="sm"
variant="shadow"
>
<Button color="warning" startContent={<FiEdit3 />} onPress={onEdit}>
编辑
</Button>
<Button
color={debug ? 'success' : 'default'}
startContent={<CgDebug />}
onPress={handleEnableDebug}
>
{debug ? '关闭调试' : '开启调试'}
</Button>
<Button
color="primary"
startContent={<MdDeleteForever />}
onPress={handleDelete}
>
删除
</Button>
</ButtonGroup>
}
enableSwitch={
<Switch
isDisabled={editing}
isSelected={enable}
onChange={handleEnable}
/>
}
tag={showType && typeLabel}
title={name}
>
<div className="grid grid-cols-2 gap-1">
{fields.map((field, index) => (
<div
key={index}
className={`flex items-center gap-2 ${
field.label === 'URL' ? 'col-span-2' : ''
}`}
>
<span className="text-default-400">{field.label}</span>
{field.render ? (
field.render(field.value)
) : (
<span>{field.value}</span>
)}
</div>
))}
</div>
</DisplayCardContainer>
)
}
export default NetworkDisplayCard
@@ -0,0 +1,57 @@
import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card'
import clsx from 'clsx'
import { title } from '../primitives'
export interface ContainerProps {
title: string
tag?: React.ReactNode
action: React.ReactNode
enableSwitch: React.ReactNode
children: React.ReactNode
}
export interface DisplayCardProps {
showType?: boolean
onEdit: () => void
onEnable: () => Promise<void>
onDelete: () => Promise<void>
onEnableDebug: () => Promise<void>
}
const DisplayCardContainer: React.FC<ContainerProps> = ({
title: _title,
action,
tag,
enableSwitch,
children
}) => {
return (
<Card className="bg-opacity-50 backdrop-blur-sm">
<CardHeader className={'pb-0 flex items-center'}>
{tag && (
<div className="text-center text-default-400 mb-1 absolute top-0 left-1/2 -translate-x-1/2 text-sm pointer-events-none bg-warning-100 dark:bg-warning-50 px-2 rounded-b">
{tag}
</div>
)}
<h2
className={clsx(
title({
color: 'foreground',
size: 'xs',
shadow: true
}),
'truncate'
)}
>
{_title}
</h2>
<div className="ml-auto">{enableSwitch}</div>
</CardHeader>
<CardBody className="text-sm">{children}</CardBody>
<CardFooter>{action}</CardFooter>
</Card>
)
}
export default DisplayCardContainer
@@ -0,0 +1,47 @@
import { Chip } from '@heroui/chip'
import NetworkDisplayCard from './common_card'
import type { NetworkDisplayCardFields } from './common_card'
interface HTTPClientDisplayCardProps {
data: OneBotConfig['network']['httpClients'][0]
showType?: boolean
onEdit: () => void
onEnable: () => Promise<void>
onDelete: () => Promise<void>
onEnableDebug: () => Promise<void>
}
const HTTPClientDisplayCard: React.FC<HTTPClientDisplayCardProps> = (props) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
const { url, reportSelfMessage, messagePostFormat } = data
const fields: NetworkDisplayCardFields<'httpClients'> = [
{ label: 'URL', value: url },
{ label: '消息格式', value: messagePostFormat },
{
label: '上报自身消息',
value: reportSelfMessage,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '是' : '否'}
</Chip>
)
}
]
return (
<NetworkDisplayCard
data={data}
showType={showType}
typeLabel="HTTP客户端"
fields={fields}
onEdit={onEdit}
onEnable={onEnable}
onDelete={onDelete}
onEnableDebug={onEnableDebug}
/>
)
}
export default HTTPClientDisplayCard
@@ -0,0 +1,57 @@
import { Chip } from '@heroui/chip'
import NetworkDisplayCard from './common_card'
import type { NetworkDisplayCardFields } from './common_card'
interface HTTPServerDisplayCardProps {
data: OneBotConfig['network']['httpServers'][0]
showType?: boolean
onEdit: () => void
onEnable: () => Promise<void>
onDelete: () => Promise<void>
onEnableDebug: () => Promise<void>
}
const HTTPServerDisplayCard: React.FC<HTTPServerDisplayCardProps> = (props) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data
const fields: NetworkDisplayCardFields<'httpServers'> = [
{ label: '主机', value: host },
{ label: '端口', value: port },
{ label: '消息格式', value: messagePostFormat },
{
label: 'CORS',
value: enableCors,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '已启用' : '未启用'}
</Chip>
)
},
{
label: 'WS',
value: enableWebsocket,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '已启用' : '未启用'}
</Chip>
)
}
]
return (
<NetworkDisplayCard
data={data}
showType={showType}
typeLabel="HTTP服务器"
fields={fields}
onEdit={onEdit}
onEnable={onEnable}
onDelete={onDelete}
onEnableDebug={onEnableDebug}
/>
)
}
export default HTTPServerDisplayCard
@@ -0,0 +1,59 @@
import { Chip } from '@heroui/chip'
import NetworkDisplayCard from './common_card'
import type { NetworkDisplayCardFields } from './common_card'
interface HTTPSSEServerDisplayCardProps {
data: OneBotConfig['network']['httpSseServers'][0]
showType?: boolean
onEdit: () => void
onEnable: () => Promise<void>
onDelete: () => Promise<void>
onEnableDebug: () => Promise<void>
}
const HTTPSSEServerDisplayCard: React.FC<HTTPSSEServerDisplayCardProps> = (
props
) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data
const fields: NetworkDisplayCardFields<'httpServers'> = [
{ label: '主机', value: host },
{ label: '端口', value: port },
{ label: '消息格式', value: messagePostFormat },
{
label: 'CORS',
value: enableCors,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '已启用' : '未启用'}
</Chip>
)
},
{
label: 'WS',
value: enableWebsocket,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '已启用' : '未启用'}
</Chip>
)
}
]
return (
<NetworkDisplayCard
data={data}
showType={showType}
typeLabel="HTTP服务器"
fields={fields}
onEdit={onEdit}
onEnable={onEnable}
onDelete={onDelete}
onEnableDebug={onEnableDebug}
/>
)
}
export default HTTPSSEServerDisplayCard
@@ -0,0 +1,57 @@
import { Chip } from '@heroui/chip'
import NetworkDisplayCard from './common_card'
import type { NetworkDisplayCardFields } from './common_card'
interface WebsocketClientDisplayCardProps {
data: OneBotConfig['network']['websocketClients'][0]
showType?: boolean
onEdit: () => void
onEnable: () => Promise<void>
onDelete: () => Promise<void>
onEnableDebug: () => Promise<void>
}
const WebsocketClientDisplayCard: React.FC<WebsocketClientDisplayCardProps> = (
props
) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
const {
url,
heartInterval,
reconnectInterval,
messagePostFormat,
reportSelfMessage
} = data
const fields: NetworkDisplayCardFields<'websocketClients'> = [
{ label: 'URL', value: url },
{ label: '重连间隔', value: `${reconnectInterval}ms` },
{ label: '心跳间隔', value: `${heartInterval}ms` },
{ label: '消息格式', value: messagePostFormat },
{
label: '上报自身消息',
value: reportSelfMessage,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '是' : '否'}
</Chip>
)
}
]
return (
<NetworkDisplayCard
data={data}
showType={showType}
typeLabel="Websocket客户端"
fields={fields}
onEdit={onEdit}
onEnable={onEnable}
onDelete={onDelete}
onEnableDebug={onEnableDebug}
/>
)
}
export default WebsocketClientDisplayCard
@@ -0,0 +1,67 @@
import { Chip } from '@heroui/chip'
import NetworkDisplayCard from './common_card'
import type { NetworkDisplayCardFields } from './common_card'
interface WebsocketServerDisplayCardProps {
data: OneBotConfig['network']['websocketServers'][0]
showType?: boolean
onEdit: () => void
onEnable: () => Promise<void>
onDelete: () => Promise<void>
onEnableDebug: () => Promise<void>
}
const WebsocketServerDisplayCard: React.FC<WebsocketServerDisplayCardProps> = (
props
) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
const {
host,
port,
heartInterval,
messagePostFormat,
reportSelfMessage,
enableForcePushEvent
} = data
const fields: NetworkDisplayCardFields<'websocketServers'> = [
{ label: '主机', value: host },
{ label: '端口', value: port },
{ label: '心跳间隔', value: `${heartInterval}ms` },
{ label: '消息格式', value: messagePostFormat },
{
label: '上报自身消息',
value: reportSelfMessage,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '是' : '否'}
</Chip>
)
},
{
label: '强制推送事件',
value: enableForcePushEvent,
render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
{value ? '是' : '否'}
</Chip>
)
}
]
return (
<NetworkDisplayCard
data={data}
showType={showType}
typeLabel="Websocket服务器"
fields={fields}
onEdit={onEdit}
onEnable={onEnable}
onDelete={onDelete}
onEnableDebug={onEnableDebug}
/>
)
}
export default WebsocketServerDisplayCard
@@ -0,0 +1,58 @@
import { Card, CardBody } from '@heroui/card'
import clsx from 'clsx'
import { title } from '@/components/primitives'
export interface NetworkItemDisplayProps {
count: number
label: string
size?: 'sm' | 'md'
}
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
count,
label,
size = 'md'
}) => {
return (
<Card
className={clsx(
'bg-opacity-60 shadow-sm md:rounded-3xl',
size === 'md'
? '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"
>
<CardBody className="items-center md:gap-1 p-1 md:p-2">
<div
className={clsx(
'flex-1',
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
title({
color: size === 'md' ? 'pink' : 'yellow',
size
})
)}
>
{count}
</div>
<div
className={clsx(
'whitespace-nowrap text-nowrap flex-shrink-0',
size === 'md' ? 'text-sm md:text-base' : 'text-xs md:text-sm',
title({
color: size === 'md' ? 'pink' : 'yellow',
shadow: true,
size: 'xxs'
})
)}
>
{label}
</div>
</CardBody>
</Card>
)
}
export default NetworkItemDisplay
+109
View File
@@ -0,0 +1,109 @@
import { Card, CardProps } from '@heroui/card'
import clsx from 'clsx'
import React from 'react'
export interface HoverEffectCardProps extends CardProps {
children: React.ReactNode
maxXRotation?: number
maxYRotation?: number
lightClassName?: string
lightStyle?: React.CSSProperties
}
const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
const {
children,
maxXRotation = 5,
maxYRotation = 5,
className,
style,
lightClassName,
lightStyle
} = props
const cardRef = React.useRef<HTMLDivElement | null>(null)
const lightRef = React.useRef<HTMLDivElement | null>(null)
const [isShowLight, setIsShowLight] = React.useState(false)
const [pos, setPos] = React.useState({
left: 0,
top: 0
})
return (
<Card
{...props}
ref={cardRef}
className={clsx(
'relative overflow-hidden bg-opacity-50 backdrop-blur-lg',
className
)}
style={{
willChange: 'transform',
transform:
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)',
...style
}}
onMouseEnter={() => {
if (cardRef.current) {
cardRef.current.style.transition = 'transform 0.3s ease-out'
}
}}
onMouseLeave={() => {
setIsShowLight(false)
if (cardRef.current) {
cardRef.current.style.transition = 'transform 0.5s'
cardRef.current.style.transform =
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
}
}}
onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
if (cardRef.current) {
setIsShowLight(true)
const { x, y } = cardRef.current.getBoundingClientRect()
const { clientX, clientY } = e
const offsetX = clientX - x
const offsetY = clientY - y
const lightWidth = lightStyle?.width?.toString() || '100'
const lightHeight = lightStyle?.height?.toString() || '100'
const lightWidthNum = parseInt(lightWidth)
const lightHeightNum = parseInt(lightHeight)
const left = offsetX - lightWidthNum / 2
const top = offsetY - lightHeightNum / 2
setPos({
left,
top
})
cardRef.current.style.transition = 'transform 0.1s'
const rangeX = 400 / 2
const rangeY = 400 / 2
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
}
}}
>
<div
ref={lightRef}
className={clsx(
isShowLight ? 'opacity-100' : 'opacity-0',
'absolute rounded-full blur-[150px] filter transition-opacity duration-300 dark:bg-[#2850ff] bg-[#ff4132] w-[100px] h-[100px]',
lightClassName
)}
style={{
...pos
}}
/>
{children}
</Card>
)
}
export default HoverEffectCard
@@ -0,0 +1,30 @@
import { Button } from '@heroui/button'
import { Code } from '@heroui/code'
import { MdError } from 'react-icons/md'
export interface ErrorFallbackProps {
error: Error
resetErrorBoundary: () => void
}
function errorFallbackRender({
error,
resetErrorBoundary
}: ErrorFallbackProps) {
return (
<div className="pt-32 flex flex-col justify-center items-center">
<div className="flex items-center">
<MdError className="mr-2" color="red" size={30} />
<h1 className="text-2xl">出错了</h1>
</div>
<div className="my-6 flex flex-col justify-center items-center">
<p className="mb-2">错误信息</p>
<Code>{error.message}</Code>
</div>
<Button color="primary" size="md" onPress={resetErrorBoundary}>
重试
</Button>
</div>
)
}
export default errorFallbackRender
+166
View 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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -0,0 +1,19 @@
import clsx from 'clsx'
export interface IconWrapperProps {
children?: React.ReactNode
className?: string
}
const IconWrapper = ({ children, className }: IconWrapperProps) => (
<div
className={clsx(
className,
'flex items-center rounded-small justify-center w-7 h-7'
)}
>
{children}
</div>
)
export default IconWrapper
@@ -0,0 +1,10 @@
import { ChevronRightIcon } from '../icons'
const ItemCounter = ({ number }: { number: number }) => (
<div className="flex items-center gap-1 text-default-400">
<span className="text-small">{number}</span>
<ChevronRightIcon className="text-xl" />
</div>
)
export default ItemCounter
@@ -0,0 +1,40 @@
import { useEffect, useState } from 'react'
import { getReleaseTime } from '@/utils/time'
import type { GithubRelease as GithubReleaseType } from '@/types/github'
export interface GithubReleaseProps {
releaseData: GithubReleaseType
}
const GithubRelease: React.FC<GithubReleaseProps> = (props) => {
const { releaseData } = props
const [releaseTime, setReleaseTime] = useState<string | null>(null)
useEffect(() => {
if (releaseData) {
const timer = setInterval(() => {
const time = getReleaseTime(releaseData.published_at)
setReleaseTime(time)
}, 1000)
return () => clearInterval(timer)
}
}, [releaseData])
return (
<div className="flex flex-col gap-1">
<span>Releases</span>
<div className="px-2 py-1 rounded-small bg-default-100 bg-opacity-50 backdrop-blur-sm group-data-[hover=true]:bg-default-200">
<span className="text-tiny text-default-600">{releaseData.name}</span>
<div className="flex gap-2 text-tiny">
<span className="text-default-500">{releaseTime}</span>
<span className="text-success">Latest</span>
</div>
</div>
</div>
)
}
export default GithubRelease
+76
View File
@@ -0,0 +1,76 @@
import { Button } from '@heroui/button'
import { Tooltip } from '@heroui/tooltip'
import { useRequest } from 'ahooks'
import toast from 'react-hot-toast'
import { IoCopy, IoRefresh } from 'react-icons/io5'
import { request } from '@/utils/request'
import PageLoading from './page_loading'
export default function Hitokoto() {
const {
data: dataOri,
error,
loading,
run
} = useRequest(() => request.get<IHitokoto>('https://hitokoto.152710.xyz/'), {
pollingInterval: 10000,
throttleWait: 1000
})
const data = dataOri?.data
const onCopy = () => {
try {
const text = `${data?.hitokoto} —— ${data?.from} ${data?.from_who}`
navigator.clipboard.writeText(text)
toast.success('复制成功')
} catch (error) {
toast.error('复制失败, 请手动复制')
}
}
return (
<div>
<div className="relative">
{loading && <PageLoading />}
{error ? (
<div className="text-primary-400">一言加载失败:{error.message}</div>
) : (
<>
<div>{data?.hitokoto}</div>
<div className="text-right">
—— <span className="text-default-400">{data?.from}</span>{' '}
{data?.from_who}
</div>
</>
)}
</div>
<div className="flex gap-2">
<Tooltip content="刷新" placement="top">
<Button
onPress={run}
size="sm"
isLoading={loading}
isIconOnly
radius="full"
color="primary"
variant="flat"
>
<IoRefresh />
</Button>
</Tooltip>
<Tooltip content="复制" placement="top">
<Button
onPress={onCopy}
size="sm"
isIconOnly
radius="full"
color="success"
variant="flat"
>
<IoCopy />
</Button>
</Tooltip>
</div>
</div>
)
}
@@ -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>
)
}
File diff suppressed because it is too large. Load diff
@@ -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
Loaded 100 of 630 files, more files were not shown because too many files have changed in this diff. Show more