Compare commits

..

189 Commits

Author SHA1 Message Date
jxxghp
7c9c39fa0e 更新 package.json 2024-04-24 17:41:07 +08:00
jxxghp
3b800753ec Merge pull request #112 from hotlcc/develop-20240424-VDialog优化 2024-04-24 17:40:28 +08:00
jxxghp
647119052c Merge pull request #111 from dh336699/hotfix-history-scofield 2024-04-24 17:38:07 +08:00
Allen
e9ce6bbd4e 消息中心弹窗小屏时全屏 2024-04-24 17:34:37 +08:00
Allen
1fee27f78e 系统健康检查弹窗小屏时全屏 2024-04-24 17:34:16 +08:00
Allen
e7a334861d 规则测试弹窗小屏时全屏 2024-04-24 17:33:07 +08:00
Allen
267ae3436d 实时日志弹窗小屏时全屏 2024-04-24 17:32:48 +08:00
hao.dai
60ff9f1891 fix: 1.登录双重认证增加防抖 2.历史记录搜索框增加防抖 3.开启项目vscode配置文件 2024-04-24 17:29:54 +08:00
Allen
f83efd23df 网络测试弹窗小屏下全屏 2024-04-24 17:29:24 +08:00
Allen
db60f02745 名称测试弹窗小屏下全屏 2024-04-24 17:28:30 +08:00
Allen
3e109bd27c dashborad配置弹窗小屏下全屏 2024-04-24 17:25:50 +08:00
Allen
c4ccf6e3fa 订阅编辑弹窗小屏时全屏 2024-04-24 17:16:27 +08:00
Allen
fb1a246e4a 订阅历史弹窗小屏时全屏 2024-04-24 17:15:20 +08:00
Allen
a418b03c06 文件整理弹窗小屏时全屏 2024-04-24 17:13:32 +08:00
Allen
e9fee000ca 插件数据页面小屏下全屏 2024-04-24 17:09:26 +08:00
Allen
71c13e0653 插件配置弹窗小屏下全屏 2024-04-24 17:08:46 +08:00
Allen
32d7f933f8 站点编辑弹窗小屏下全屏 2024-04-24 17:07:15 +08:00
Allen
f28dd810ce 站点资源弹窗小屏下全屏 2024-04-24 17:05:46 +08:00
Allen
aaedd88ca7 站点更新弹窗小屏下全屏展示 2024-04-24 17:04:04 +08:00
Allen
00dee40917 站点更新弹窗添加关闭按钮 2024-04-24 16:49:50 +08:00
hao.dai
019248b605 Merge remote-tracking branch 'upstream/main'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2024-04-24 15:43:56 +08:00
hao.dai
826f37bcc4 fix: 账号设置warning问题 2024-04-24 15:43:11 +08:00
jxxghp
fa02a23e4c 更新 package.json 2024-04-24 10:19:02 +08:00
jxxghp
7143fb6f67 Merge pull request #110 from hotlcc/develop-20240423-低版safari时间兼容 2024-04-24 10:18:39 +08:00
Allen
e1524c26cd 统一处理低版本safari浏览器Date兼容性问题 2024-04-24 10:06:32 +08:00
jxxghp
72088dff2e release v1.8.3 2024-04-23 17:48:35 +08:00
jxxghp
8e6d3cf30e fix dashboard config 2024-04-23 10:25:43 +08:00
jxxghp
144992ccec Merge pull request #104 from hotlcc/develop-20240417-用户配置
dashboard配置支持保存入库
2024-04-23 10:00:36 +08:00
jxxghp
673e883ae6 Merge branch 'main' into develop-20240417-用户配置 2024-04-23 10:00:29 +08:00
Allen
f197ed7972 优化dashboard配置功能 2024-04-23 09:52:11 +08:00
jxxghp
ce642aceed release v2 2024-04-22 10:04:23 +08:00
jxxghp
d5411489c0 fix ui 2024-04-22 10:02:57 +08:00
jxxghp
26c66627f8 Merge pull request #108 from thsrite/main 2024-04-20 19:52:12 +08:00
thsrite
c654986042 fix 2024-04-20 19:22:55 +08:00
thsrite
c5b5c15f99 fix 2024-04-20 19:18:16 +08:00
thsrite
7727b0f1c3 fix 2024-04-20 19:11:44 +08:00
thsrite
3d551ac45b fix 订阅时编辑规则移到默认订阅规则页面 2024-04-20 18:48:02 +08:00
jxxghp
555a00b731 fix postercard 2024-04-19 23:08:41 +08:00
jxxghp
9f9091b23e 更新 package.json 2024-04-19 22:45:42 +08:00
jxxghp
14c343142f Merge pull request #107 from falling/main 2024-04-19 22:45:00 +08:00
falling
890920775a fix 安卓手机端hover事件被VCard的click事件覆盖问题 2024-04-19 22:00:15 +08:00
jxxghp
7b38d2d74f fix #105 2024-04-19 19:51:14 +08:00
jxxghp
e85c2870e2 更新 SubscribeHistoryDialog.vue 2024-04-19 17:08:41 +08:00
jxxghp
cfbc5802e4 fix VInfiniteScroll 2024-04-19 13:56:57 +08:00
jxxghp
40cdb820fb fix ui 2024-04-19 13:16:13 +08:00
jxxghp
f63beb776e fix 订阅历史记录 2024-04-19 08:24:57 +08:00
jxxghp
20f031b2e2 rename components 2024-04-18 22:59:00 +08:00
jxxghp
b0f28b7e7c fix 2024-04-18 22:33:03 +08:00
jxxghp
62bb6de80d feat:订阅历史 2024-04-18 21:00:35 +08:00
Allen
3db4d883af fixbug 2024-04-18 15:19:24 +08:00
Allen
8cb514d70e dashboard配置支持保存入库 2024-04-18 12:40:14 +08:00
jxxghp
2d7880351b release 2024-04-18 11:14:03 +08:00
jxxghp
e1ee3ef2db fix #1918 2024-04-18 11:13:36 +08:00
jxxghp
aff30c48a0 fix site stat 2024-04-18 08:12:46 +08:00
jxxghp
55eea50a6e test release 2024-04-17 23:02:37 +08:00
jxxghp
9ff212c94d feat: 插件页面支持slot 2024-04-17 22:55:45 +08:00
jxxghp
6350c7e9e6 feat:插件支持渲染弹窗关闭按钮 2024-04-17 21:20:31 +08:00
jxxghp
d097c1c17c fix ui 2024-04-17 19:31:50 +08:00
jxxghp
b9ee6b4039 fix ui 2024-04-17 15:30:40 +08:00
jxxghp
f1238a03b3 fix 2024-04-17 14:51:05 +08:00
jxxghp
e90cf3ee77 test release 2024-04-17 14:41:22 +08:00
jxxghp
468607c8e8 feat:站点状态显示 2024-04-17 14:38:40 +08:00
jxxghp
5bd9283177 Merge pull request #102 from dh336699/feature-issue-94 2024-04-17 12:44:17 +08:00
hao.dai
117b12348c fix: 低版本Safari浏览器不能正确显示订阅的更新日期 2024-04-17 12:38:34 +08:00
jxxghp
0d325b6eb8 fix ui 2024-04-17 08:16:11 +08:00
jxxghp
86d5903f32 更新 TransferHistoryView.vue 2024-04-16 18:31:32 +08:00
jxxghp
3b518d6f33 release 2024-04-16 11:34:37 +08:00
jxxghp
78f57e7d4b Merge pull request #101 from dh336699/feature-optimization-ranking 2024-04-16 11:33:17 +08:00
hao.dai
f710f1bfc0 fix: 修复ranking页面大批量warning问题 2024-04-16 11:24:07 +08:00
jxxghp
c5d4fc62e6 fix ui 2024-04-16 10:05:39 +08:00
jxxghp
60606d5eb9 fix poster card 2024-04-16 08:22:58 +08:00
jxxghp
8751236380 fix bug 2024-04-16 08:21:05 +08:00
jxxghp
2291ce3680 fix 仍有Bug 2024-04-15 21:23:56 +08:00
jxxghp
16ed589857 Merge pull request #100 from Aodi/main 2024-04-15 18:30:58 +08:00
aodi
b59254ca42 fix 编码斜杠禁用的反代无法加载图片 修改url编码 2024-04-15 18:09:44 +08:00
aodi
6e3f9b285d fix 编码斜杠禁用的反代无法加载图片 修改url编码 2024-04-15 18:08:58 +08:00
aodi
8bcff774fa fix 编码斜杠禁用的反代无法加载图片 修改url编码 2024-04-15 18:04:21 +08:00
aodi
9b04b12dec fix 编码斜杠禁用的反代无法加载图片
fix 编码斜杠禁用的反代无法加载图片 修改url编码

fix 编码斜杠禁用的反代无法加载图片 修改url编码
2024-04-15 18:02:28 +08:00
aodi
b22d81a9e9 fix 编码斜杠禁用的反代无法加载图片 修改url编码 2024-04-15 17:14:21 +08:00
aodi
6c80a3a8cd fix 编码斜杠禁用的反代无法加载图片 2024-04-15 16:27:30 +08:00
jxxghp
059d836653 Merge pull request #98 from hotlcc/develop-20240415-页面优化 2024-04-15 10:06:56 +08:00
Allen
2c3ecfeb6f 低版本浏览器at函数兼容性调整为应用级生效 2024-04-15 01:52:17 +00:00
Allen
b07e5eecc3 解决通知渠道不选时显示空白项的问题 2024-04-15 01:49:50 +00:00
jxxghp
1847bc90cf 更新 package.json 2024-04-15 06:46:27 +08:00
jxxghp
899aaae47c Merge pull request #97 from BlueflameLi/main 2024-04-15 06:44:58 +08:00
BlueflameLi
bcc05086a4 fix 历史记录切换每页条数时重复发送一次请求 2024-04-15 02:12:27 +08:00
BlueflameLi
d2cc547875 fix [错误报告]:历史记录默认一页50条但实际默认一页10条 #96 2024-04-15 02:05:31 +08:00
jxxghp
c6127f440e feat 更新时查看更新说明 2024-04-13 18:36:01 +08:00
jxxghp
c2849ad49f fix toast z-index 2024-04-13 18:16:28 +08:00
jxxghp
9c6ba294f9 fix ui 2024-04-13 09:20:59 +08:00
jxxghp
b2a8707e91 fix ui 2024-04-13 09:18:25 +08:00
jxxghp
193e3085a9 feat:下载状态标记 2024-04-13 09:01:18 +08:00
jxxghp
2401f38e9f fix ui 2024-04-12 21:31:24 +08:00
jxxghp
4ea65727a1 更新 package.json 2024-04-12 19:41:25 +08:00
jxxghp
3d6cfe260c fix ui 2024-04-12 18:26:00 +08:00
jxxghp
4c33a09c3c Merge pull request #95 from dh336699/feature-issue-1851 2024-04-12 15:21:04 +08:00
jxxghp
8d25743680 fix 2024-04-12 12:56:45 +08:00
hao.dai
4bc5b763a2 Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend into feature-issue-1851 2024-04-11 09:50:06 +08:00
jxxghp
00ea179c90 fix ui 2024-04-10 18:57:36 +08:00
hao.dai
a17d40d2d0 feat: 1.历史记录新增根据本地数据进行文件夹筛选 2.全量数据切换虚拟表格组件提升性能 2024-04-10 18:52:45 +08:00
jxxghp
62ddd703f1 feat:支持查看插件更新记录 2024-04-10 16:45:17 +08:00
jxxghp
77ab0ccae2 feat:搜索支持指定季 2024-04-10 14:46:36 +08:00
jxxghp
f377ac3fcc fix search 2024-04-09 13:20:54 +08:00
jxxghp
a81becd77b fix 2024-04-07 11:53:47 +08:00
jxxghp
a004f1c758 fix 2024-04-07 11:29:49 +08:00
jxxghp
b19b015986 fix PerfectScrollbar 2024-04-07 11:15:18 +08:00
jxxghp
3f0c1213ad upgrade packages 2024-04-07 10:54:12 +08:00
jxxghp
f3a781d857 fix:减少无效请求 2024-04-07 08:28:37 +08:00
jxxghp
a07a32f648 fix ui 2024-04-06 16:07:18 +08:00
jxxghp
1e1117b187 fix:优化文件管理性能 2024-04-06 09:25:46 +08:00
jxxghp
0e161b1735 fix filemanager ui 2024-04-06 09:04:02 +08:00
jxxghp
98cbb8dc29 fix ui 2024-04-05 23:50:48 +08:00
jxxghp
9c17d2d335 release 2024-04-05 23:33:54 +08:00
jxxghp
d4ea7f48c0 fix ui 2024-04-05 23:27:19 +08:00
jxxghp
3ecfe3ba94 Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend 2024-04-05 22:34:36 +08:00
jxxghp
a959594348 feat:插件搜索 2024-04-05 22:34:34 +08:00
jxxghp
faa1027c04 Merge pull request #93 from hotlcc/develop-低版本浏览器兼容性 2024-04-05 12:17:07 +08:00
Allen
6f68732ef9 低版本浏览器数组不支持at函数问题修复 2024-04-05 04:11:50 +00:00
jxxghp
3a4e936938 fix datatable 2024-04-03 11:55:37 +08:00
jxxghp
8b4b79fa10 fix name 2024-04-02 16:36:27 +08:00
jxxghp
ce0dda0455 fix ui 2024-04-02 13:17:40 +08:00
jxxghp
fd1ee398c4 update vuetify => 3.5.7 2024-04-02 11:37:25 +08:00
jxxghp
1488017bf2 fix #1797 2024-04-02 10:26:15 +08:00
jxxghp
367f4236ad fix #92 2024-04-02 08:26:58 +08:00
jxxghp
ec202f22e8 Merge pull request #92 from thsrite/main 2024-04-01 19:10:57 +08:00
thsrite
08081da29e fix 组件复用 2024-04-01 14:27:41 +08:00
thsrite
5511424bd6 feat 设置订阅默认规则 2024-04-01 13:30:51 +08:00
jxxghp
9f2c848413 fix hint 2024-03-31 19:23:11 +08:00
jxxghp
d5efe2b499 feat:增加设置项提示 2024-03-31 09:36:31 +08:00
jxxghp
9b989fc40f feat:插件配置保存动画 2024-03-31 08:24:02 +08:00
jxxghp
567e85d1f8 更新 package.json 2024-03-30 09:50:04 +08:00
jxxghp
af00036e7f fix login 2024-03-29 11:13:46 +08:00
jxxghp
e58e6e2a3e feat:OTP默认不显示 2024-03-29 10:59:26 +08:00
jxxghp
370039664f fix ui 2024-03-28 19:18:37 +08:00
jxxghp
d244363321 fix #91 otp ui 2024-03-28 19:04:15 +08:00
jxxghp
984d502259 Merge pull request #91 from z3shan33/main
feat #1763 用户可在个人设置中自行开启二次验证
2024-03-28 16:58:10 +08:00
zss
34b418af96 feat #1763 2024-03-28 16:35:20 +08:00
jxxghp
eee0c0c878 fix ui 2024-03-26 15:59:37 +08:00
jxxghp
952ef368ab fix plugin card 2024-03-26 12:37:52 +08:00
jxxghp
dfaa789f7c release 2024-03-25 18:32:14 +08:00
jxxghp
74dd549ffb plugin statistic 2024-03-25 18:31:58 +08:00
jxxghp
4d8f369ba0 fix icon 2024-03-21 21:31:23 +08:00
jxxghp
824e2d72c7 fix icon 2024-03-21 16:30:48 +08:00
jxxghp
143aa79797 fix defer 2024-03-19 12:53:23 +08:00
jxxghp
0e1120f407 fix 减少无效查询 2024-03-19 12:34:21 +08:00
jxxghp
c8dbb9672a fix 2024-03-19 11:47:00 +08:00
jxxghp
372b74776f fix bug 2024-03-18 23:42:24 +08:00
jxxghp
abcc3c6411 fix 2024-03-18 23:31:21 +08:00
jxxghp
ec4ab8762c add Bangumi 2024-03-18 19:03:13 +08:00
jxxghp
bc93de8ff2 fix SSE 2024-03-18 11:42:56 +08:00
jxxghp
b426f3c6f2 更新 PluginAppCard.vue 2024-03-16 22:04:51 +08:00
jxxghp
99d9bb29ce 更新 PluginAppCard.vue 2024-03-16 21:55:59 +08:00
jxxghp
5e109c666b fix plugin card 2024-03-16 21:39:05 +08:00
jxxghp
0bed216735 fix 消息控重 2024-03-16 21:07:09 +08:00
jxxghp
d55bb8d336 fix label 2024-03-16 19:44:10 +08:00
jxxghp
7c32b3edf0 Merge pull request #88 from lingjiameng/main 2024-03-16 18:51:56 +08:00
jxxghp
121cb7e442 v1.7.3 2024-03-16 18:28:32 +08:00
ljmeng
dec3e1ea92 更新注释 2024-03-16 18:26:11 +08:00
ljmeng
664b6610f3 支持本地CookieCloud服务器 2024-03-16 18:23:24 +08:00
jxxghp
44163f0fb2 fix bug 2024-03-16 17:20:49 +08:00
jxxghp
d43865fcad fix message ui 2024-03-16 17:16:10 +08:00
jxxghp
fed92f3853 fix message ui 2024-03-16 16:47:41 +08:00
jxxghp
823d2a816e fix 2024-03-16 08:40:44 +08:00
jxxghp
046c21edf6 add message view 2024-03-15 18:15:31 +08:00
jxxghp
8236d80b42 feat:优化插件升级使用体验 2024-03-12 21:31:56 +08:00
jxxghp
90e7eb1c79 Merge pull request #86 from WangEdward/main 2024-03-11 16:32:30 +08:00
WangEdward
ef09868af1 fix: display search_imdbid status 2024-03-11 16:30:24 +08:00
jxxghp
028981e3ae 更新 package.json 2024-03-10 21:07:17 +08:00
jxxghp
e8a6274cf6 Merge pull request #85 from honue/main 2024-03-10 17:16:54 +08:00
honue
ffd0265526 设置每周一为第一天,当天背景突显 2024-03-10 16:49:40 +08:00
jxxghp
13d7344bc0 fix bug 2024-03-09 20:53:37 +08:00
jxxghp
2ad36f92c5 feat:下载器多选 2024-03-09 18:52:48 +08:00
jxxghp
36b02f4423 release 2024-03-09 17:35:45 +08:00
jxxghp
01df990aa8 fix #1638 2024-03-09 17:05:44 +08:00
jxxghp
49b71fcf5d fix #1640 2024-03-09 16:49:48 +08:00
jxxghp
9e43d77ac4 feat:新增官种优先级规则 2024-03-09 09:15:03 +08:00
jxxghp
3ab9af720b Merge pull request #84 from WangEdward/main 2024-03-08 22:11:13 +08:00
WangEdward
abae304f87 chore: add half-increments for v-rating 2024-03-08 21:23:25 +08:00
jxxghp
659d8bff66 fix 下载及订阅用户匹配 2024-03-08 15:27:51 +08:00
jxxghp
1786e10101 fix:修改编译策略,避免一直loading 2024-03-08 14:18:30 +08:00
jxxghp
bda7f929e7 try fix loading 2024-03-08 13:31:38 +08:00
jxxghp
c309f80a94 更新 ModuleTestView.vue 2024-03-06 22:05:12 +08:00
jxxghp
97c987c561 fix ui 2024-03-06 21:52:20 +08:00
jxxghp
48949104e0 feat:目录检测 2024-03-06 21:41:06 +08:00
jxxghp
a38cc4fe34 add min_seeders 2024-03-06 20:15:23 +08:00
jxxghp
495dfbcb28 feat:add VoceChat 2024-03-06 15:55:01 +08:00
jxxghp
6e4dbd912b feat:健康检查 2024-03-06 13:27:17 +08:00
jxxghp
82904d956d thetvdb network test 2024-03-06 11:16:12 +08:00
jxxghp
ec7118b376 fix #82 后台登录成功但前端报错 2024-03-05 20:21:05 +08:00
103 changed files with 7495 additions and 5819 deletions

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '18' node-version: '20'
cache: 'yarn' cache: 'yarn'
- name: Download Icons - name: Download Icons

View File

@@ -21,14 +21,9 @@
} }
], ],
"rules": { "rules": {
"max-line-length": [
120,
{
"ignore": "comments"
}
],
"liberty/use-logical-spec": true, "liberty/use-logical-spec": true,
"selector-class-pattern": null, "selector-class-pattern": null,
"color-function-notation": null "color-function-notation": null
} },
} "fix": true
}

12
.vscode/settings.json vendored
View File

@@ -6,9 +6,6 @@
"[javascript]": { "[javascript]": {
"editor.formatOnSave": false "editor.formatOnSave": false
}, },
"[typescript]": {
"editor.formatOnSave": false
},
"[markdown]": { "[markdown]": {
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint" "editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
}, },
@@ -25,7 +22,7 @@
}, },
// Vue // Vue
"[vue]": { "[vue]": {
"editor.formatOnSave": false "editor.formatOnSave": true
}, },
// Extension: Volar // Extension: Volar
"volar.preview.port": 3000, "volar.preview.port": 3000,
@@ -34,6 +31,10 @@
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit" "source.fixAll.stylelint": "explicit"
}, },
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"eslint.alwaysShowStatus": true, "eslint.alwaysShowStatus": true,
"eslint.format.enable": true, "eslint.format.enable": true,
// Extension: Stylelint // Extension: Stylelint
@@ -53,6 +54,7 @@
"stylelint", "stylelint",
"touchless", "touchless",
"triggerer", "triggerer",
"unref",
"vuetify" "vuetify"
], ],
// Extension: Comment Anchors // Extension: Comment Anchors
@@ -104,4 +106,4 @@
] ]
}, },
"vue3snippets.enable-compile-vue-file-on-did-save-code": false "vue3snippets.enable-compile-vue-file-on-did-save-code": false
} }

View File

@@ -1,6 +1,6 @@
# MoviePilot-Frontend # MoviePilot-Frontend
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目。 [MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目NodeJS版本>= `v20.12.1`
## 推荐的IDE设置 ## 推荐的IDE设置

322
auto-imports.d.ts vendored
View File

@@ -1,6 +1,7 @@
/* eslint-disable */ /* eslint-disable */
/* prettier-ignore */ /* prettier-ignore */
// @ts-nocheck // @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
export {} export {}
declare global { declare global {
@@ -41,6 +42,7 @@ declare global {
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined'] const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy'] const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive'] const isReactive: typeof import('vue')['isReactive']
@@ -77,6 +79,7 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated'] const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide'] const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify'] const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive'] const reactive: typeof import('vue')['reactive']
@@ -105,6 +108,7 @@ declare global {
const toReactive: typeof import('@vueuse/core')['toReactive'] const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef'] const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs'] const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef'] const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -143,6 +147,7 @@ declare global {
const useCeil: typeof import('@vueuse/math')['useCeil'] const useCeil: typeof import('@vueuse/math')['useCeil']
const useClamp: typeof import('@vueuse/math')['useClamp'] const useClamp: typeof import('@vueuse/math')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard'] const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned'] const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
@@ -308,11 +313,13 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
} }
// for vue template auto import // for vue template auto import
import { UnwrapRef } from 'vue' import { UnwrapRef } from 'vue'
declare module 'vue' { declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties { interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']> readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']> readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
@@ -351,6 +358,7 @@ declare module 'vue' {
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']> readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']> readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@@ -387,6 +395,7 @@ declare module 'vue' {
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']> readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']> readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -415,6 +424,7 @@ declare module 'vue' {
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']> readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']> readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']> readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']> readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -453,6 +463,316 @@ declare module 'vue' {
readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']> readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']> readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']> readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFloor: UnwrapRef<typeof import('@vueuse/math')['useFloor']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMath: UnwrapRef<typeof import('@vueuse/math')['useMath']>
readonly useMax: UnwrapRef<typeof import('@vueuse/math')['useMax']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePrecision: UnwrapRef<typeof import('@vueuse/math')['usePrecision']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStore: UnwrapRef<typeof import('vuex')['useStore']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useTrunc: UnwrapRef<typeof import('@vueuse/math')['useTrunc']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}
declare module '@vue/runtime-core' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createLogger: UnwrapRef<typeof import('vuex')['createLogger']>
readonly createNamespacedHelpers: UnwrapRef<typeof import('vuex')['createNamespacedHelpers']>
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createStore: UnwrapRef<typeof import('vuex')['createStore']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly logicAnd: UnwrapRef<typeof import('@vueuse/math')['logicAnd']>
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('vuex')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('vuex')['mapGetters']>
readonly mapMutations: UnwrapRef<typeof import('vuex')['mapMutations']>
readonly mapState: UnwrapRef<typeof import('vuex')['mapState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAverage: UnwrapRef<typeof import('@vueuse/math')['useAverage']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']> readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']> readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>

6
components.d.ts vendored
View File

@@ -3,18 +3,18 @@
// @ts-nocheck // @ts-nocheck
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {} export {}
declare module '@vue/runtime-core' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default'] DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default'] ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default'] ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default'] MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default'] ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
} }
} }

View File

@@ -1,214 +1,162 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
<title>MoviePilot</title>
<meta name="Robots" content="noindex,nofollow,noarchive" />
<meta name="referrer" content="origin" />
<link rel="icon" type="image/png" href="/logo.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
<meta name="description" content="MoviePilot" />
<meta name="format-detection" content="telephone=no" />
<meta name="referrer" content="never" />
<meta name="msapplication-TileColor" content="#7D34FD" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<link rel="stylesheet" type="text/css" href="/loader.css" />
</head>
<body> <head>
<div id="loading-bg"> <meta http-equiv="pragma" content="no-cache">
<div class="loading-logo"> <meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<!-- Logo --> <meta http-equiv="expires" content="0">
<svg <meta charset="UTF-8" />
width="100px" <link rel="icon" href="/favicon.ico" />
height="100px" <meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
viewBox="0 0 192 192" <title>MoviePilot</title>
version="1.1" <meta name="Robots" content="noindex,nofollow,noarchive" />
xmlns="http://www.w3.org/2000/svg" <meta name="referrer" content="origin" />
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2" <link rel="icon" type="image/png" href="/logo.png" />
> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<g transform="matrix(1,0,0,1,-2606,-236)"> <link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)"> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<rect x="0" y="0" width="192" height="192" style="fill: none" /> <link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)"> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
<meta name="description" content="MoviePilot" />
<meta name="format-detection" content="telephone=no" />
<meta name="referrer" content="never" />
<meta name="msapplication-TileColor" content="#7D34FD" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<link rel="stylesheet" type="text/css" href="/loader.css" />
</head>
<body>
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
<svg width="100px" height="100px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
<g transform="matrix(1,0,0,1,-2606,-236)">
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
<rect x="0" y="0" width="192" height="192" style="fill: none" />
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
<path
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
style="fill: url(#_Linear1)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
style="fill: url(#_Linear2)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
style="fill: rgb(141, 81, 249)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
style="fill: url(#_Linear3)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
style="fill: rgb(165, 118, 255)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
style="fill: url(#_Linear4)" />
</g>
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
<path
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
style="fill: rgb(141, 81, 249)" />
</g>
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
<path
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
style="fill: rgb(104, 0, 197)" />
<clipPath id="_clip5">
<path <path
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z" d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
style="fill: url(#_Linear1)" </clipPath>
/> <g clip-path="url(#_clip5)">
</g> <g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
style="fill: url(#_Linear2)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
style="fill: rgb(141, 81, 249)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
style="fill: url(#_Linear3)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
style="fill: rgb(165, 118, 255)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
style="fill: url(#_Linear4)"
/>
</g>
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
<path
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
style="fill: rgb(141, 81, 249)"
/>
</g>
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
<path
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
style="fill: rgb(104, 0, 197)"
/>
<clipPath id="_clip5">
<path <path
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
/> style="fill: url(#_Linear6)" />
</clipPath> </g>
<g clip-path="url(#_clip5)"> <g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)"> <path
<path d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z" style="fill: rgb(141, 81, 249)" />
style="fill: url(#_Linear6)" </g>
/> <g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
</g> <path
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)"> d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
<path style="fill: url(#_Radial7)" />
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
style="fill: rgb(141, 81, 249)"
/>
</g>
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
<path
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
style="fill: url(#_Radial7)"
/>
</g>
</g> </g>
</g> </g>
</g> </g>
</g> </g>
<defs> </g>
<linearGradient <defs>
id="_Linear1" <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
x1="0" gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
y1="0" <stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
x2="1" <stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
y2="0" </linearGradient>
gradientUnits="userSpaceOnUse" <linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)" gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
> <stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" /> <stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" /> </linearGradient>
</linearGradient> <linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
<linearGradient gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
id="_Linear2" <stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
x1="0" <stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
y1="0" </linearGradient>
x2="1" <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
y2="0" gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
gradientUnits="userSpaceOnUse" <stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)" <stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
> </linearGradient>
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" /> <linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" /> gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
</linearGradient> <stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
<linearGradient <stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
id="_Linear3" <stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
x1="0" </linearGradient>
y1="0" <radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
x2="1" gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
y2="0" <stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
gradientUnits="userSpaceOnUse" <stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)" </radialGradient>
> </defs>
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" /> </svg>
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
</linearGradient>
<linearGradient
id="_Linear4"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
>
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
</linearGradient>
<linearGradient
id="_Linear6"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
>
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
</linearGradient>
<radialGradient
id="_Radial7"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
>
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
</radialGradient>
</defs>
</svg>
</div>
<div class="loading">
<div class="effect-1 effects"></div>
<div class="effect-2 effects"></div>
<div class="effect-3 effects"></div>
</div>
</div> </div>
<div id="app"></div> <div class="loading">
<script type="module" src="/src/main.ts"></script> <div class="effect-1 effects"></div>
<script> <div class="effect-2 effects"></div>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF' <div class="effect-3 effects"></div>
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD' </div>
</div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor) if (loaderColor)
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor) if (primaryColor)
</script> document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
</body> </script>
</html> </body>
</html>

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "1.7.0", "version": "1.8.3-2",
"private": true, "private": true,
"bin": "dist/service.js", "bin": "dist/service.js",
"scripts": { "scripts": {
@@ -21,45 +21,47 @@
"dependencies": { "dependencies": {
"@casl/ability": "^6.2.0", "@casl/ability": "^6.2.0",
"@casl/vue": "^2.2.0", "@casl/vue": "^2.2.0",
"@floating-ui/dom": "1.2.8", "@floating-ui/dom": "1.6.3",
"@iconify/utils": "^2.1.22",
"@vueuse/core": "^10.1.2", "@vueuse/core": "^10.1.2",
"@vueuse/math": "^10.1.2", "@vueuse/math": "^10.1.2",
"ace-builds": "^1.32.6", "ace-builds": "^1.32.6",
"apexcharts-clevision": "^3.28.5", "apexcharts-clevision": "^3.28.5",
"axios": "1.4.0", "axios": "1.6.8",
"axios-mock-adapter": "^1.21.4", "axios-mock-adapter": "^1.21.4",
"chart.js": "^4.1.2", "chart.js": "^4.1.2",
"colorthief": "^2.4.0", "colorthief": "^2.4.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-http-proxy": "^2.0.0", "express-http-proxy": "^2.0.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^4.0.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"postcss-purgecss": "^5.0.0", "postcss-purgecss": "^5.0.0",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"pull-refresh-vue3": "^0.3.1", "pull-refresh-vue3": "^0.3.1",
"qrcode.vue": "^3.4.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"sass": "^1.59.3", "sass": "^1.59.3",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"unplugin-vue-define-options": "^1.3.5", "unplugin-vue-define-options": "^1.3.5",
"vite-plugin-pwa": "^0.16.4", "vite-plugin-pwa": "^0.19.8",
"vue": "^3.3.2", "vue": "^3.3.2",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
"vue-flatpickr-component": "11.0.3", "vue-flatpickr-component": "11.0.5",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-prism-component": "^2.0.0", "vue-prism-component": "^2.0.0",
"vue-router": "^4.2.0", "vue-router": "^4.2.0",
"vue-toast-notification": "^3", "vue-toast-notification": "^3",
"vue3-ace-editor": "^2.2.4", "vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.4.1", "vue3-apexcharts": "^1.4.1",
"vue3-perfect-scrollbar": "^1.6.0", "vue3-perfect-scrollbar": "^2.0.0",
"vuetify": "3.3.5", "vuetify": "3.5.14",
"vuetify-use-dialog": "^0.6.0", "vuetify-use-dialog": "^0.6.0",
"vuex": "^4.1.0", "vuex": "^4.1.0",
"vuex-persistedstate": "^4.1.0", "vuex-persistedstate": "^4.1.0",
"webfontloader": "^1.6.28" "webfontloader": "^1.6.28"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config-vue": "^0.38.6", "@antfu/eslint-config-vue": "^0.43.1",
"@fullcalendar/core": "^6.1.8", "@fullcalendar/core": "^6.1.8",
"@fullcalendar/daygrid": "^6.1.8", "@fullcalendar/daygrid": "^6.1.8",
"@fullcalendar/interaction": "^6.1.7", "@fullcalendar/interaction": "^6.1.7",
@@ -67,43 +69,45 @@
"@fullcalendar/timegrid": "^6.1.7", "@fullcalendar/timegrid": "^6.1.7",
"@fullcalendar/vue3": "^6.1.8", "@fullcalendar/vue3": "^6.1.8",
"@iconify-json/mdi": "^1.1.52", "@iconify-json/mdi": "^1.1.52",
"@iconify/tools": "^2.2.0", "@iconify/tools": "^4.0.4",
"@iconify/vue": "4.1.1", "@iconify/vue": "4.1.1",
"@intlify/unplugin-vue-i18n": "^0.10.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@types/lodash": "^4.14.197", "@types/lodash": "^4.14.197",
"@types/node": "^20.1.4", "@types/node": "^20.1.4",
"@types/webfontloader": "^1.6.34", "@types/webfontloader": "^1.6.34",
"@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^5.59.5", "@typescript-eslint/parser": "^7.5.0",
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"eslint": "^8.40.0", "dayjs": "^1.11.10",
"eslint": "^9.0.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-import-resolver-typescript": "^3.5.1", "eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.0.1", "eslint-plugin-promise": "^6.0.1",
"eslint-plugin-regex": "^1.10.0", "eslint-plugin-regex": "^1.10.0",
"eslint-plugin-sonarjs": "^0.19.0", "eslint-plugin-sonarjs": "^0.25.1",
"eslint-plugin-unicorn": "^47.0.0", "eslint-plugin-unicorn": "^52.0.0",
"eslint-plugin-vue": "^9.12.0", "eslint-plugin-vue": "^9.12.0",
"postcss": "^8.4.24", "lodash": "^4.17.21",
"postcss": "8",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"stylelint": "14.15.0", "stylelint": "16.3.1",
"stylelint-config-idiomatic-order": "9.0.0", "stylelint-config-idiomatic-order": "10.0.0",
"stylelint-config-standard-scss": "6.1.0", "stylelint-config-standard-scss": "13.1.0",
"stylelint-use-logical-spec": "4.1.0", "stylelint-use-logical-spec": "5.0.1",
"type-fest": "^3.10.0", "type-fest": "^4.15.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"unplugin-auto-import": "^0.15.1", "unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.24.1", "unplugin-vue-components": "^0.26.0",
"vite": "^4.3.5", "vite": "^5.2.8",
"vite-plugin-pages": "^0.29.0", "vite-plugin-pages": "^0.32.1",
"vite-plugin-vue-layouts": "^0.8.0", "vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "1.0.2", "vite-plugin-vuetify": "2.0.3",
"vue-shepherd": "^3.0.0", "vue-shepherd": "^3.0.0",
"vue-tsc": "^1.6.5" "vue-tsc": "^2.0.10"
}, },
"packageManager": "yarn@1.22.18", "packageManager": "yarn@1.22.18",
"resolutions": { "resolutions": {

View File

@@ -75,6 +75,26 @@ http {
# 超时设置 # 超时设置
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /cookiecloud {
# 后端cookiecloud地址
proxy_pass http://backend_api;
rewrite ^.+mock-server/?(.*)$ /$1 break;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_redirect off;
proxy_set_header Connection "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
# 超时设置
proxy_read_timeout 600s;
}
} }
upstream backend_api { upstream backend_api {

View File

@@ -25,6 +25,16 @@ app.use(
}) })
); );
// 配置代理中间件将CookieCloud请求转发给后端API
app.use(
'/cookiecloud',
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /cookiecloud 前缀
proxyReqPathResolver: (req) => {
return `/cookiecloud${req.url}`
}
})
);
// 处理根路径的请求 // 处理根路径的请求
app.get('/', (req, res) => { app.get('/', (req, res) => {

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
// 定义触发的自定义事件 // 定义触发的自定义事件
const emit = defineEmits(['click']) const emit = defineEmits(['click', 'update:modelValue'])
// 按钮点击 // 按钮点击
function onClick() { function onClick() {
emit('update:modelValue', false)
emit('click') emit('click')
} }
</script> </script>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
progress: Number,
text: String
})
</script>
<template>
<div
class="w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!props.text"
size="48"
indeterminate
color="primary"
/>
<VProgressCircular
v-if="props.progress"
class="mb-3"
color="primary"
:model-value="props.progress"
size="64"
/>
<span>{{ props.text }}</span>
</div>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
interface Props {
color?: string
message?: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="absolute top-2 right-2 flex items-center justify-between p-2 shadow">
<VBadge :color="props.color" bordered>
<template #badge>
<VIcon icon="mdi-pulse"></VIcon>
</template>
</VBadge>
</div>
</template>

View File

@@ -0,0 +1,18 @@
/**
* 浏览器兼容性处理
*/
/**
* 修复低版本Safari等浏览器数组不支持at函数的问题
*/
export function fixArrayAt() {
if (!Array.prototype.at) {
Array.prototype.at = function(index: number) {
if (index >= 0) {
return this[index]
} else {
return this[this.length + index]
}
}
}
}

View File

@@ -1,5 +1,12 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import ZH_CN from 'dayjs/locale/zh-cn'
import { isToday } from './index' import { isToday } from './index'
dayjs.extend(relativeTime)
dayjs.locale(ZH_CN)
export function avatarText(value: string) { export function avatarText(value: string) {
if (!value) if (!value)
return '' return ''
@@ -19,7 +26,7 @@ export function kFormatter(num: number) {
* Format and return date in Humanize format * Format and return date in Humanize format
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format * Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
* Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat * Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
* @param {String} value date to format * @param {string} value date to format
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with * @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
*/ */
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) { export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
@@ -32,8 +39,8 @@ export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions
/** /**
* Return short human friendly month representation of date * Return short human friendly month representation of date
* Can also convert date to only time if date is of today (Better UX) * Can also convert date to only time if date is of today (Better UX)
* @param {String} value date to format * @param {string} value date to format
* @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current * @param {boolean} toTimeForCurrentDay Shall convert to time if day is today/current
*/ */
export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) { export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) {
const date = new Date(value) const date = new Date(value)
@@ -107,7 +114,7 @@ export function formatBytes(bytes: number, decimals = 2) {
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}` return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
} }
// 格式化剧集列表 // 格式化剧集列表
@@ -147,3 +154,24 @@ export function formatEp(nums: number[]): string {
return formattedRanges.join('、') return formattedRanges.join('、')
} }
// 将yyyy-mm-dd hh:mm:ss转换为时间差1小时前1天前
export function formatDateDifference(dateString: string): string {
// const timeDifference = dayjs().millisecond() - dayjs(dateString).millisecond()
// const secondsDifference = Math.floor(timeDifference / 1000)
// const minutesDifference = Math.floor(secondsDifference / 60)
// const hoursDifference = Math.floor(minutesDifference / 60)
// const daysDifference = Math.floor(hoursDifference / 24)
// if (daysDifference > 0)
// return `${daysDifference}天前`
// else if (hoursDifference > 0)
// return `${hoursDifference}小时前`
// else if (minutesDifference > 0)
// return `${minutesDifference}分钟前`
// else
// return '刚刚'
if (!dateString)
return ''
return dayjs(dateString).fromNow()
}

View File

@@ -33,12 +33,16 @@ export function isToday(date: Date) {
) )
} }
// 计算时间差返回xx天/xx小时/xx分钟/xx秒 /**
* 计算时间差返回xx天/xx小时/xx分钟/xx秒
*
* @deprecated 建议使用:@core/utils/formatters.ts formatDateDifference
*/
export function calculateTimeDifference(inputTime: string): string { export function calculateTimeDifference(inputTime: string): string {
if (!inputTime) if (!inputTime)
return '' return ''
const inputDate = new Date(inputTime) const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
const currentDate = new Date() const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime() const timeDifference = currentDate.getTime() - inputDate.getTime()
@@ -70,7 +74,7 @@ export function calculateTimeDiff(inputTime: string): string {
return '' return ''
// 使用当前时区 // 使用当前时区
const inputDate = new Date(inputTime) const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
const currentDate = new Date() const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime() const timeDifference = currentDate.getTime() - inputDate.getTime()

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Component } from 'vue' import type { Component } from 'vue'
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import logo from '@images/logo.svg?raw' import logo from '@images/logo.svg?raw'
@@ -85,6 +84,7 @@ function handleNavScroll(evt: Event) {
.visible { .visible {
visibility: visible !important; visibility: visible !important;
} }
// 👉 Vertical Nav // 👉 Vertical Nav
.layout-vertical-nav { .layout-vertical-nav {
position: fixed; position: fixed;
@@ -96,8 +96,8 @@ function handleNavScroll(evt: Event) {
inset-block-start: 0; inset-block-start: 0;
inset-inline-start: 0; inset-inline-start: 0;
transition: transform 0.25s ease-in-out, inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out; transition: transform 0.25s ease-in-out, inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
will-change: transform, inline-size;
visibility: hidden; visibility: hidden;
will-change: transform, inline-size;
&:not(.overlay-nav) { &:not(.overlay-nav) {
visibility: visible; visibility: visible;

View File

@@ -52,7 +52,7 @@ export default defineComponent({
'main', 'main',
{ class: 'layout-page-content' }, { class: 'layout-page-content' },
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true },
h('section', { class: 'page-content-container' }, slots.default?.()), () => h('section', { class: 'page-content-container' }, slots.default?.()),
), ),
) )

View File

@@ -19,7 +19,7 @@ $layout-horizontal-nav-layout-navbar-z-index: 11 !default;
$layout-boxed-content-width: 90rem !default; $layout-boxed-content-width: 90rem !default;
// 👉Footer // 👉Footer
$layout-vertical-nav-footer-height: 3.5rem !default; $layout-vertical-nav-footer-height: 0rem !default;
// 👉 Layout overlay // 👉 Layout overlay
$layout-overlay-z-index: 11 !default; $layout-overlay-z-index: 11 !default;

View File

@@ -1,3 +1,2 @@
@use "_global"; @use "_global";
@use "vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.min.css";
@use "_classes"; @use "_classes";

View File

@@ -1,9 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify' import { useTheme } from 'vuetify'
import api from '@/api'
import store from './store' import store from './store'
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -36,10 +37,30 @@ function startSSEMessager() {
} }
} }
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem("MP_DASHBOARD")) {
localStorage.setItem("MP_DASHBOARD", data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem("MP_DASHBOARD")) {
return
}
await loadDashboardConfig()
}
// 页面加载时,加载当前用户数据 // 页面加载时,加载当前用户数据
onBeforeMount(async () => { onBeforeMount(async () => {
setTheme() setTheme()
startSSEMessager() startSSEMessager()
await tryLoadDashboardConfig()
}) })
</script> </script>

View File

@@ -81,13 +81,16 @@ export interface Subscribe {
best_version: any best_version: any
// 使用 imdbid 搜索 // 使用 imdbid 搜索
search_imdbid?: boolean search_imdbid?: any
// 当前优先级 // 当前优先级
current_priority: number current_priority: number
// 保存目录 // 保存目录
save_path: string save_path: string
// 时间
date: string
} }
// 历史记录 // 历史记录
@@ -181,6 +184,9 @@ export interface MediaInfo {
// 豆瓣ID // 豆瓣ID
douban_id?: string douban_id?: string
// Bangumi ID
bangumi_id?: string
// 媒体原语种 // 媒体原语种
original_language?: string original_language?: string
@@ -282,6 +288,9 @@ export interface MediaInfo {
// 下一集 // 下一集
next_episode_to_air?: object next_episode_to_air?: object
// 别名
names?: string[]
} }
// TMDB季信息 // TMDB季信息
@@ -422,6 +431,28 @@ export interface DoubanPerson {
} }
// Bangumi人物信息
export interface BangumiPerson {
// ID
id?: number
// 名称
name?: string
// 类型
type?: number
// 角色
career?: string[]
// images large/normal
images?: { [key: string]: string }
// 关系
relation?: string
}
// 站点 // 站点
export interface Site { export interface Site {
@@ -477,6 +508,33 @@ export interface Site {
is_active: boolean is_active: boolean
} }
// 站点使用统计
export interface SiteStatistic {
// 站点主域名Key
domain?: string
// 成功次数
success?: number
// 失败次数
fail?: number
// 平均耗时
seconds?: number
// 最后一次访问状态 0-成功 1-失败
lst_state?: number
// 最后访问时间
lst_mod_date?: string
// 耗时记录 JSON
note?: string
}
// 正在下载 // 正在下载
export interface DownloadingInfo { export interface DownloadingInfo {
@@ -513,8 +571,11 @@ export interface DownloadingInfo {
// 媒体信息 // 媒体信息
media: { [key: string]: any } media: { [key: string]: any }
// 下载用户 // 下载用户ID
userid?: string userid?: string
// 下载用户名称
username?: string
} }
// 缺失剧集信息 // 缺失剧集信息
@@ -581,6 +642,9 @@ export interface Plugin {
// 插件仓库地址 // 插件仓库地址
repo_url?: string repo_url?: string
// 变更历史
history?: { [key: string]: string }
} }
// 种子信息 // 种子信息
@@ -799,18 +863,37 @@ export interface Context {
// 用户信息 // 用户信息
export interface User { export interface User {
// 用户ID
id: number id: number
// 用户名称
name: string name: string
// 用户密码
password: string password: string
// 用户邮箱
email: string email: string
// 是否激活
is_active: boolean is_active: boolean
// 是否管理员
is_superuser: boolean is_superuser: boolean
// 头像
avatar: string avatar: string
// 是否开启双重验证
is_otp: boolean
} }
// 存储空间 // 存储空间
export interface Storage { export interface Storage {
// 总空间
total_storage: number total_storage: number
// 已使用空间
used_storage: number used_storage: number
} }
@@ -904,6 +987,7 @@ export interface NotificationSwitch {
telegram: boolean telegram: boolean
slack: boolean slack: boolean
synologychat: boolean synologychat: boolean
vocechat: boolean
} }
// 环境设置 // 环境设置
@@ -914,45 +998,132 @@ export interface Setting {
// 文件浏览接口 // 文件浏览接口
export interface EndPoints { export interface EndPoints {
// 文件列表
list: any list: any
// 创建目录
mkdir: any mkdir: any
// 删除文件
delete: any delete: any
// 下载文件
download: any download: any
// 图片预览
image: any image: any
// 重命名
rename: any rename: any
} }
// 文件浏览项目 // 文件浏览项目
export interface FileItem { export interface FileItem {
// 类型
type: string type: string
// 文件名
name: string name: string
// 文件名不含扩展名
basename: string basename: string
// 文件路径
path: string path: string
// 文件扩展名
extension: string extension: string
// 文件大小
size: number size: number
// 文件子元素
children: FileItem[] children: FileItem[]
// 文件创建时间
modify_time: number modify_time: number
} }
// 媒体服务器播放条目 // 媒体服务器播放条目
export interface MediaServerPlayItem { export interface MediaServerPlayItem {
// ID
id?: string | number id?: string | number
// 标题
title: string title: string
// 副标题
subtitle?: string subtitle?: string
// 类型
type?: string type?: string
// 海报
image?: string image?: string
// 链接
link?: string link?: string
// 播放百分比
percent?: number percent?: number
} }
// 媒体服务器媒体库 // 媒体服务器媒体库
export interface MediaServerLibrary { export interface MediaServerLibrary {
// 服务器名称
server: string server: string
// ID
id?: string | number id?: string | number
// 名称
name: string name: string
// 路径
path?: string path?: string
// 类型
type?: string type?: string
// 图片
image?: string image?: string
// 图片列表
image_list?: string[] image_list?: string[]
// 链接
link?: string link?: string
} }
// 消息通知
export interface Message {
// 消息类型
mtype?: string
// 消息标题
title?: string
// 消息内容
text?: string
// 消息链接
link?: string
// 消息图片
image?: string
// 消息时间
date?: string
// 登记时间
reg_time?: string
// 用户ID
userid?: string
// 消息方向0-接收1-发送
action?: number
// JSON
note?: string
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -1,10 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Axios } from 'axios' import type { Axios } from 'axios'
import axios from 'axios' import axios from 'axios'
import List from './filebrowser/List.vue' import FileList from './filebrowser/FileList.vue'
import FileToolbar from './filebrowser/FileToolbar.vue'
import Toolbar from './filebrowser/Toolbar.vue'
import Tree from './filebrowser/Tree.vue'
import type { EndPoints } from '@/api/types' import type { EndPoints } from '@/api/types'
// 输入参数 // 输入参数
@@ -70,12 +68,10 @@ const storagesArray = computed(() => {
// 方法 // 方法
function loadingChanged(loading: number) { function loadingChanged(loading: number) {
if (loading) { if (loading)
loading++ loading++
} else if (loading > 0)
else if (loading > 0) {
loading-- loading--
}
} }
function storageChanged(storage: string) { function storageChanged(storage: string) {
@@ -103,7 +99,7 @@ onMounted(() => {
<template> <template>
<VCard class="mx-auto" :loading="loading > 0 || !path"> <VCard class="mx-auto" :loading="loading > 0 || !path">
<div v-if="path"> <div v-if="path">
<Toolbar <FileToolbar
:path="path" :path="path"
:storages="storagesArray" :storages="storagesArray"
:storage="activeStorage" :storage="activeStorage"
@@ -114,38 +110,20 @@ onMounted(() => {
@foldercreated="refreshPending = true" @foldercreated="refreshPending = true"
@sortchanged="sortChanged" @sortchanged="sortChanged"
/> />
<VRow no-gutters> <FileList
<VCol v-if="tree" sm="auto" class="d-none d-md-block"> :path="path"
<Tree :storage="activeStorage"
:path="path" :icons="fileIcons"
:storage="activeStorage" :endpoints="endpoints"
:icons="fileIcons" :axios="axiosInstance"
:endpoints="endpoints" :refreshpending="refreshPending"
:axios="axiosInstance" :sort="sort"
:refreshpending="refreshPending" @pathchanged="pathChanged"
@pathchanged="pathChanged" @loading="loadingChanged"
@loading="loadingChanged" @refreshed="refreshPending = false"
@refreshed="refreshPending = false" @filedeleted="refreshPending = true"
/> @renamed="refreshPending = true"
</VCol> />
<VDivider v-if="tree" vertical />
<VCol>
<List
:path="path"
:storage="activeStorage"
:icons="fileIcons"
:endpoints="endpoints"
:axios="axiosInstance"
:refreshpending="refreshPending"
:sort="sort"
@pathchanged="pathChanged"
@loading="loadingChanged"
@refreshed="refreshPending = false"
@filedeleted="refreshPending = true"
@renamed="refreshPending = true"
/>
</VCol>
</VRow>
</div> </div>
</VCard> </VCard>
</template> </template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import miscpose from '@images/pages/pose-fs-9.png' import image from '@images/misc/teamwork.png'
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -11,25 +11,24 @@ interface Props {
</script> </script>
<template> <template>
<div class="flex flex-col"> <VEmptyState
<ErrorHeader :image="image"
:error-code="props.errorCode" size="250"
:error-title="props.errorTitle" >
:error-description="props.errorDescription" <template #title>
/> <div class="mt-8 text-2xl">
{{ props.errorTitle }}
</div>
</template>
<!-- 👉 Image --> <template #text>
<div class="text-center"> <div class="text-subtitle">
<VImg {{ props.errorDescription }}
:src="miscpose" </div>
class="mx-auto pt-10" </template>
max-width="250"
cover <template #actions>
/>
<slot name="button" /> <slot name="button" />
</div> </template>
</div> </VEmptyState>
</template> </template>
<style lang="scss">
</style>

View File

@@ -25,7 +25,7 @@ function goPlay() {
// 计算图片地址 // 计算图片地址
const getImgUrl = computed(() => { const getImgUrl = computed(() => {
const image = props.media?.image || '' const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0` return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
}) })
</script> </script>

View File

@@ -0,0 +1,95 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { BangumiPerson } from '@/api/types'
const personProps = defineProps({
person: Object as PropType<BangumiPerson>,
width: String,
height: String,
})
// 当前人物
const personInfo = ref(personProps.person)
// 人物图片是否加载
const isImageLoaded = ref(false)
// 人物图片地址
function getPersonImage() {
if (!personInfo.value?.images)
return personIcon
return personInfo.value?.images?.medium
}
// 使用、拼装人物角色
function getPersonCharacter() {
if (!personInfo.value?.career)
return ''
return personInfo.value?.career.join('、')
}
// 打开人物详情
function goPersonDetail() {
if (!personInfo.value?.id)
return
window.open(`https://bangumi.tv/person/${personInfo.value?.id}`, '_blank')
}
</script>
<template>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:height="personProps.height"
:width="personProps.width"
class="rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105': hover.isHovering,
}"
@click.stop="goPersonDetail"
>
<div
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-bottom: 150%;">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
<VAvatar
size="120"
:class="{
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
</VAvatar>
</div>
<div class="w-full truncate text-center font-bold">
{{ personInfo?.name }}
</div>
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
{{ getPersonCharacter() }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
</div>
</div>
</div>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.person-card {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
}
.person-card:hover {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
}
</style>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { kFormatter } from '@core/utils/formatters'
interface Props {
title: string
color?: string
icon: string
stats: number
change: number
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
size="44"
rounded
:color="props.color"
variant="tonal"
class="me-4"
>
<VIcon
:icon="props.icon"
size="30"
/>
</VAvatar>
<div>
<span class="text-caption">{{ props.title }}</span>
<div class="d-flex align-center flex-wrap">
<span class="text-h6 font-weight-semibold">{{ kFormatter(props.stats) }}</span>
<div
v-if="props.change"
:class="`${isPositive ? 'text-success' : 'text-error'} mt-1`"
>
<VIcon :icon="isPositive ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
<span class="text-caption font-weight-semibold">{{ Math.abs(props.change) }}%</span>
</div>
</div>
</div>
</VCardText>
</VCard>
</template>

View File

@@ -1,56 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
color?: string
icon: string
stats: string
change: number
subtitle: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
v-if="props.icon"
size="38"
:color="props.color"
>
<VIcon
:icon="props.icon"
size="24"
/>
</VAvatar>
<VSpacer />
<MoreBtn class="me-n3 mt-n1" />
</VCardText>
<VCardText>
<h6 class="text-sm font-weight-semibold mb-2">
{{ props.title }}
</h6>
<div
v-if="props.change"
class="d-flex align-center mb-2"
>
<span class="font-weight-semibold text-h5 me-2">{{ props.stats }}</span>
<span
:class="isPositive ? 'text-success' : 'text-error'"
class="text-caption"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<span class="text-caption">{{ props.subtitle }}</span>
</VCardText>
</VCard>
</template>

View File

@@ -1,65 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
subtitle: string
stats: string
change: number
image: string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard class="overflow-visible">
<div class="d-flex position-relative">
<VCardText>
<h6 class="text-base font-weight-semibold mb-4">
{{ props.title }}
</h6>
<div class="d-flex align-center flex-wrap mb-4">
<h5 class="text-h5 font-weight-semibold me-2">
{{ props.stats }}
</h5>
<span
class="text-caption"
:class="isPositive ? 'text-success' : 'text-error'"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<VChip
v-if="props.subtitle"
size="small"
:color="props.color"
>
{{ props.subtitle }}
</VChip>
</VCardText>
<VSpacer />
<div class="illustrator-img">
<VImg
v-if="props.image"
:src="props.image"
:width="110"
/>
</div>
</div>
</VCard>
</template>
<style lang="scss">
.illustrator-img {
position: absolute;
inset-block-end: 0;
inset-inline-end: 5%;
}
</style>

View File

@@ -30,7 +30,7 @@ function goPersonDetail() {
</script> </script>
<template> <template>
<VHover v-bind="personProps"> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <VCard
v-bind="hover.props" v-bind="hover.props"

View File

@@ -25,8 +25,8 @@ const isDownloading = ref(props.info?.state === 'downloading')
// 监听props.info?.state的变化 // 监听props.info?.state的变化
watch(() => props.info?.state, (newValue) => { watch(() => props.info?.state, (newValue) => {
isDownloading.value = newValue === 'downloading'; isDownloading.value = newValue === 'downloading'
}); })
// 图片是否加载完成 // 图片是否加载完成
const imageLoaded = ref(false) const imageLoaded = ref(false)

View File

@@ -36,6 +36,7 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' }, { title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' }, { title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' }, { title: '国语配音', value: ' CNVOI ' },
{ title: '官种', value: ' GZ ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' }, { title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' }, { title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' }, { title: '排除: 粤语配音', value: ' !HKVOI ' },

View File

@@ -56,7 +56,7 @@ function getImgUrl(url: string) {
if (!url) if (!url)
return getDefaultImage() return getDefaultImage()
else else
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(url)}/0` return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
} }
// 根据多张图片生成媒体库封面 // 根据多张图片生成媒体库封面
@@ -68,7 +68,7 @@ async function drawImages(imageList: string[]) {
// 为所有图片添加system/img前缀 // 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++) for (let i = 0; i < IMAGES.length; i++)
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(IMAGES[i])}/0` IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
// canvas // canvas
const canvas = canvasRef.value const canvas = canvasRef.value

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType, Ref } from 'vue' import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue' import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
@@ -16,11 +16,6 @@ const props = defineProps({
height: String, height: String,
}) })
// 订阅规则
const subscribeRules = ref({
show_edit_dialog: false,
})
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -57,6 +52,15 @@ const seasonInfos = ref<TmdbSeason[]>([])
// 选中的订阅季 // 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([]) const seasonsSelected = ref<TmdbSeason[]>([])
// 获得mediaid
function getMediaId() {
return props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: props.media?.douban_id
? `douban:${props.media?.douban_id}`
: `bangumi:${props.media?.bangumi_id}`
}
// 订阅弹窗选择的多季 // 订阅弹窗选择的多季
function subscribeSeasons() { function subscribeSeasons() {
subscribeSeasonDialog.value = false subscribeSeasonDialog.value = false
@@ -131,6 +135,7 @@ async function addSubscribe(season = 0) {
year: props.media?.year, year: props.media?.year,
tmdbid: props.media?.tmdb_id, tmdbid: props.media?.tmdb_id,
doubanid: props.media?.douban_id, doubanid: props.media?.douban_id,
bangumiid: props.media?.bangumi_id,
season, season,
best_version, best_version,
}) })
@@ -151,15 +156,19 @@ async function addSubscribe(season = 0) {
) )
// 弹出订阅编辑弹窗 // 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1 && subscribeRules.value.show_edit_dialog) { if (result.success && seasonsSelected.value.length <= 1) {
subscribeId.value = result.data.id const show_edit_dialog = await queryDefaultSubscribeConfig()
subscribeEditDialog.value = true if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
} }
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
} finally {
doneNProgress()
} }
doneNProgress()
} }
// 弹出添加订阅提示 // 弹出添加订阅提示
@@ -186,9 +195,7 @@ async function removeSubscribe() {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
const mediaid = props.media?.tmdb_id const mediaid = getMediaId()
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const result: { [key: string]: any } = await api.delete( const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`, `subscribe/media/${mediaid}`,
@@ -249,9 +256,7 @@ async function handleCheckExists() {
// 调用API检查是否已订阅电视剧需要指定季 // 调用API检查是否已订阅电视剧需要指定季
async function checkSubscribe(season = 0) { async function checkSubscribe(season = 0) {
try { try {
const mediaid = props.media?.tmdb_id const mediaid = getMediaId()
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, { const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: { params: {
@@ -308,17 +313,23 @@ async function getMediaSeasons() {
} }
// 查询订阅弹窗规则 // 查询订阅弹窗规则
async function querySubscribeRules() { async function queryDefaultSubscribeConfig() {
try { try {
const result: { [key: string]: any } = await api.get( let subscribe_config_url = ''
'system/setting/DefaultFilterRules', if (props.media?.type === '电影')
) subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value) if (result.data?.value)
subscribeRules.value = result.data?.value return result.data.value.show_edit_dialog
} }
catch (error) { catch (error) {
console.log(error) console.log(error)
} }
return false
} }
// 爱心订阅按钮响应 // 爱心订阅按钮响应
@@ -358,18 +369,16 @@ function getExistText(season: number) {
} }
// 打开详情页 // 打开详情页
function goMediaDetail() { function goMediaDetail(isHovering = false) {
router.push({ if (isHovering) {
path: '/media', router.push({
query: { path: '/media',
mediaid: `${ query: {
props.media?.tmdb_id mediaid: getMediaId(),
? `tmdb:${props.media?.tmdb_id}` type: props.media?.type,
: `douban:${props.media?.douban_id}` },
}`, })
type: props.media?.type, }
},
})
} }
// 开始搜索 // 开始搜索
@@ -377,13 +386,10 @@ function handleSearch() {
router.push({ router.push({
path: '/resource', path: '/resource',
query: { query: {
keyword: `${ keyword: getMediaId(),
props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
}`,
type: props.media?.type, type: props.media?.type,
area: 'title', area: 'title',
season: props.media?.season,
}, },
}) })
} }
@@ -392,7 +398,6 @@ function handleSearch() {
onBeforeMount(() => { onBeforeMount(() => {
handleCheckSubscribe() handleCheckSubscribe()
handleCheckExists() handleCheckExists()
querySubscribeRules()
}) })
// 计算图片地址 // 计算图片地址
@@ -402,7 +407,7 @@ const getImgUrl: Ref<string> = computed(() => {
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
// 如果地址中包含douban则使用中转代理 // 如果地址中包含douban则使用中转代理
if (url.includes('doubanio.com')) if (url.includes('doubanio.com'))
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}` return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
return url return url
}) })
@@ -418,20 +423,20 @@ function getSeasonPoster(posterPath: string) {
function formatAirDate(airDate: string) { function formatAirDate(airDate: string) {
if (!airDate) if (!airDate)
return '' return ''
const date = new Date(airDate) const date = new Date(airDate.replaceAll(/-/g, '/'))
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}` return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`
} }
// 从yyyy-mm-dd中提取年份 // 从yyyy-mm-dd中提取年份
function getYear(airDate: string) { function getYear(airDate: string) {
if (!airDate) if (!airDate)
return '' return ''
const date = new Date(airDate) const date = new Date(airDate.replaceAll(/-/g, '/'))
return date.getFullYear() return date.getFullYear()
} }
</script> </script>
<template> <template>
<VHover v-bind="props"> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <VCard
v-bind="hover.props" v-bind="hover.props"
@@ -442,6 +447,7 @@ function getYear(airDate: string) {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering, 'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded, 'ring-1': isImageLoaded,
}" }"
@click.stop="goMediaDetail(hover.isHovering)"
> >
<VImg <VImg
aspect-ratio="2/3" aspect-ratio="2/3"
@@ -457,60 +463,60 @@ function getYear(airDate: string) {
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" /> <VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div> </div>
</template> </template>
<!-- 类型角标 -->
<VChip
v-show="isImageLoaded"
variant="elevated"
size="small"
:class="getChipColor(props.media?.type || '')"
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
>
{{ props.media?.type }}
</VChip>
<!-- 本地存在标识 -->
<ExistIcon v-if="isExists" />
<!-- 评分角标 -->
<VChip
v-if="isImageLoaded && props.media?.vote_average && !isExists"
variant="elevated"
size="small"
:class="getChipColor('rating')"
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
>
{{ props.media?.vote_average }}
</VChip>
<!-- 详情 -->
<VCardText
v-show="hover.isHovering || imageLoadError"
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
@click.stop="goMediaDetail"
>
<span class="font-bold">{{ props.media?.year }}</span>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }}
</h1>
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
{{ props.media?.overview }}
</p>
<div class="flex align-center justify-between">
<IconBtn
icon="mdi-magnify"
color="white"
@click.stop="handleSearch"
/>
<IconBtn
icon="mdi-heart"
:color="isSubscribed ? 'error' : 'white'"
@click.stop="handleSubscribe"
/>
</div>
</VCardText>
</VImg> </VImg>
<!-- 类型角标 -->
<VChip
v-show="isImageLoaded"
variant="elevated"
size="small"
:class="getChipColor(props.media?.type || '')"
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
>
{{ props.media?.type }}
</VChip>
<!-- 本地存在标识 -->
<ExistIcon v-if="isExists" />
<!-- 评分角标 -->
<VChip
v-if="isImageLoaded && props.media?.vote_average && !isExists"
variant="elevated"
size="small"
:class="getChipColor('rating')"
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
>
{{ props.media?.vote_average }}
</VChip>
<!-- 详情 -->
<VCardText
v-show="hover.isHovering || imageLoadError"
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
>
<span class="font-bold">{{ props.media?.year }}</span>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }}
</h1>
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
{{ props.media?.overview }}
</p>
<div class="flex align-center justify-between">
<IconBtn
icon="mdi-magnify"
color="white"
@click.stop="handleSearch"
/>
<IconBtn
icon="mdi-heart"
:color="isSubscribed ? 'error' : 'white'"
@click.stop="handleSubscribe"
/>
</div>
</VCardText>
</VCard> </VCard>
</template> </template>
</VHover> </VHover>
<!-- 订阅季弹窗 --> <!-- 订阅季弹窗 -->
<VBottomSheet <VBottomSheet
v-if="subscribeSeasonDialog"
v-model="subscribeSeasonDialog" v-model="subscribeSeasonDialog"
inset inset
scrollable scrollable
@@ -593,7 +599,8 @@ function getYear(airDate: string) {
</VCard> </VCard>
</VBottomSheet> </VBottomSheet>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditForm <SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog" v-model="subscribeEditDialog"
:subid="subscribeId" :subid="subscribeId"
@close="subscribeEditDialog = false" @close="subscribeEditDialog = false"

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { Message } from '@/api/types'
import { formatDateDifference } from '@core/utils/formatters'
// 输入参数
const props = defineProps({
message: Object as PropType<Message>,
width: String,
height: String,
})
// 图片是否加载完成
const isImageLoaded = ref(false)
// 图片是否加载失败
const imageLoadError = ref(false)
// 图片加载完成
async function imageLoaded() {
isImageLoaded.value = true
}
// 链接打开新窗口
function openLink() {
if (props.message?.link)
window.open(props.message.link, '_blank')
}
// 将note转换为json
function noteToJson() {
if (props.message?.note) {
try {
return JSON.parse(props.message.note)
}
catch (error) {
console.error(error)
}
}
return {}
}
// 将\n转换为html属性的换行符
function replaceNewLine(value: string) {
if (!value)
return ''
return value.replace(/\n/g, '<br/>')
}
</script>
<template>
<VCard
:width="props.width"
:height="props.height"
variant="tonal"
@click="openLink"
>
<div
v-if="props.message?.image"
class="relative text-center card-cover-blurred"
>
<VImg
:src="props.message?.image"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</div>
<VCardTitle v-if="props.message?.title" class="whitespace-break-spaces">
{{ props.message?.title }}
</VCardTitle>
<VAlert
v-if="props.message?.text && props.message?.action === 0"
variant="tonal"
type="success"
>
<template #prepend />
{{ props.message?.text }}
</VAlert>
<VCardText
v-if="props.message?.text && props.message?.action === 1"
v-html="replaceNewLine(props.message?.text)"
/>
<VCardText v-if="props.message?.note">
<VList>
<VListItem
v-for="(value, key) in noteToJson()"
:key="key"
two-line
>
<VListItemTitle v-if="value.title_year" class="font-bold">
{{ key + 1 }}. {{ value.title_year }}
</VListItemTitle>
<VListItemTitle v-if="value.enclosure" class="font-bold whitespace-break-spaces">
{{ key + 1 }}. {{ value.title }} {{ value.volume_factor }} {{ value.seeders }}
</VListItemTitle>
<VListItemSubtitle v-if="value.type">
类型{{ value.type }} 评分{{ value.vote_average }}
</VListItemSubtitle>
<VListItemSubtitle v-if="value.enclosure" class="whitespace-break-spaces">
{{ value.description }}
</VListItemSubtitle>
</VListItem>
</VList>
</VCardText>
<div class="text-end">
<span v-if="props.message?.action === 0" class="text-sm italic me-2">{{ props.message?.userid }}</span>
<span class="text-sm italic me-2">{{ formatDateDifference(props.message?.reg_time || props.message?.date || '') }}</span>
</div>
</VCard>
</template>

View File

@@ -1,15 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import VersionHistory from '../misc/VersionHistory.vue'
import api from '@/api' import api from '@/api'
import type { Plugin } from '@/api/types' import type { Plugin } from '@/api/types'
import noImage from '@images/logos/plugin.png' import noImage from '@images/logos/plugin.png'
import { getDominantColor } from '@/@core/utils/image' import { getDominantColor } from '@/@core/utils/image'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
plugin: Object as PropType<Plugin>, plugin: Object as PropType<Plugin>,
width: String, width: String,
height: String, height: String,
count: Number,
}) })
// 定义触发的自定义事件 // 定义触发的自定义事件
@@ -36,6 +39,9 @@ const isImageLoaded = ref(false)
// 图片是否加载失败 // 图片是否加载失败
const imageLoadError = ref(false) const imageLoadError = ref(false)
// 更新日志弹窗
const releaseDialog = ref(false)
// 图片加载完成 // 图片加载完成
async function imageLoaded() { async function imageLoaded() {
isImageLoaded.value = true isImageLoaded.value = true
@@ -49,7 +55,7 @@ async function installPlugin() {
try { try {
// 显示等待提示框 // 显示等待提示框
progressDialog.value = true progressDialog.value = true
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...` progressText.value = `正在安装 ${props.plugin?.plugin_name} v${props?.plugin?.plugin_version} ...`
const result: { [key: string]: any } = await api.get( const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`, `plugin/install/${props.plugin?.id}`,
@@ -85,7 +91,7 @@ const iconPath: Ref<string> = computed(() => {
return noImage return noImage
// 如果是网络图片则使用代理后返回 // 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http')) if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1` return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}` return `./plugin_icon/${props.plugin?.plugin_icon}`
}) })
@@ -117,15 +123,29 @@ function visitPluginPage() {
window.open(repoUrl, '_blank') window.open(repoUrl, '_blank')
} }
// 显示更新日志
function showUpdateHistory() {
releaseDialog.value = true
}
// 弹出菜单 // 弹出菜单
const dropdownItems = ref([ const dropdownItems = ref([
{ {
title: '查看详情', title: '项目主页',
value: 1, value: 1,
show: true,
props: { props: {
prependIcon: 'mdi-information-outline', prependIcon: 'mdi-github',
click: visitPluginPage, click: visitPluginPage,
}, },
}, {
title: '更新说明',
value: 2,
show: !isNullOrEmptyObject(props.plugin?.history || {}),
props: {
prependIcon: 'mdi-update',
click: showUpdateHistory,
},
}, },
]) ])
</script> </script>
@@ -150,6 +170,7 @@ const dropdownItems = ref([
<VList> <VList>
<VListItem <VListItem
v-for="(item, i) in dropdownItems" v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i" :key="i"
variant="plain" variant="plain"
@click="item.props.click" @click="item.props.click"
@@ -163,15 +184,6 @@ const dropdownItems = ref([
</VMenu> </VMenu>
</IconBtn> </IconBtn>
</div> </div>
<div
v-if="props.plugin?.has_update"
class="me-n3 absolute top-0 left-1"
>
<VIcon
icon="mdi-new-box"
class="text-white"
/>
</div>
<VAvatar <VAvatar
size="8rem" size="8rem"
> >
@@ -186,20 +198,28 @@ const dropdownItems = ref([
/> />
</VAvatar> </VAvatar>
</div> </div>
<VCardTitle>{{ props.plugin?.plugin_name }}</VCardTitle> <VCardTitle>
{{ props.plugin?.plugin_name }}
<VCardText> <span class="text-sm text-gray-500">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
<VCardText class="pb-2">
{{ props.plugin?.plugin_desc }} {{ props.plugin?.plugin_desc }}
</VCardText> </VCardText>
<VCardText> <VCardText class="flex items-center justify-start pb-2">
作者<a <span>
:href="props.plugin?.author_url" <VIcon icon="mdi-account" class="me-1" />
target="_blank" <a
@click.stop :href="props.plugin?.author_url"
> target="_blank"
{{ props.plugin?.plugin_author }} @click.stop
</a><br> >
版本{{ props.plugin?.plugin_version }} {{ props.plugin?.plugin_author }}
</a>
</span>
<span v-if="props.count" class="ms-3">
<VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</span>
</VCardText> </VCardText>
</VCard> </VCard>
<!-- 安装插件进度框 --> <!-- 安装插件进度框 -->
@@ -221,6 +241,19 @@ const dropdownItems = ref([
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 更新日志 -->
<VDialog
v-if="releaseDialog"
v-model="releaseDialog"
width="600"
scrollable
>
<VCard>
<DialogCloseBtn @click="releaseDialog = false" />
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
<VersionHistory :history="props.plugin?.history" />
</VCard>
</VDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,24 +1,32 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog' import { useConfirm } from 'vuetify-use-dialog'
import { VIcon } from 'vuetify/lib/components/index.mjs'
import api from '@/api' import api from '@/api'
import type { Plugin } from '@/api/types' import type { Plugin } from '@/api/types'
import FormRender from '@/components/render/FormRender.vue' import FormRender from '@/components/render/FormRender.vue'
import PageRender from '@/components/render/PageRender.vue' import PageRender from '@/components/render/PageRender.vue'
import VersionHistory from '@/components/misc/VersionHistory.vue'
import { isNullOrEmptyObject } from '@core/utils' import { isNullOrEmptyObject } from '@core/utils'
import noImage from '@images/logos/plugin.png' import noImage from '@images/logos/plugin.png'
import { getDominantColor } from '@/@core/utils/image' import { getDominantColor } from '@/@core/utils/image'
import store from '@/store' import store from '@/store'
import { useDisplay } from 'vuetify'
// 显示器宽度
const displayWidth = useDisplay().width
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
plugin: Object as PropType<Plugin>, plugin: Object as PropType<Plugin>,
count: Number, // 下载次数
action: Boolean, // 动作标识
width: String, width: String,
height: String, height: String,
}) })
// 定义触发的自定义事件 // 定义触发的自定义事件
const emit = defineEmits(['remove', 'save']) const emit = defineEmits(['remove', 'save', 'actionDone'])
// 背景颜色 // 背景颜色
const backgroundColor = ref('#28A9E1') const backgroundColor = ref('#28A9E1')
@@ -41,12 +49,18 @@ const pluginConfigDialog = ref(false)
// 插件配置表单数据 // 插件配置表单数据
const pluginConfigForm = ref({}) const pluginConfigForm = ref({})
// 进度框
const progressDialog = ref(false)
// 插件表单配置项 // 插件表单配置项
let pluginFormItems = reactive([]) let pluginFormItems = reactive([])
// 插件数据页面 // 插件数据页面
const pluginInfoDialog = ref(false) const pluginInfoDialog = ref(false)
// 进度框文本
const progressText = ref('正在更新插件...')
// 插件数据页面配置项 // 插件数据页面配置项
let pluginPageItems = reactive([]) let pluginPageItems = reactive([])
@@ -56,6 +70,17 @@ const isImageLoaded = ref(false)
// 图片是否加载失败 // 图片是否加载失败
const imageLoadError = ref(false) const imageLoadError = ref(false)
// 更新日志弹窗
const releaseDialog = ref(false)
// 监听动作标识如为true则打开详情
watch(() => props.action, (newAction, oldAction) => {
if (newAction && !oldAction) {
openPluginDetail()
emit('actionDone')
}
})
// 图片加载完成 // 图片加载完成
async function imageLoaded() { async function imageLoaded() {
isImageLoaded.value = true isImageLoaded.value = true
@@ -64,6 +89,16 @@ async function imageLoaded() {
backgroundColor.value = await getDominantColor(imageElement) backgroundColor.value = await getDominantColor(imageElement)
} }
// 显示更新日志
function showUpdateHistory() {
// 检查当前版本是否有更新日志
if (isNullOrEmptyObject(props.plugin?.history)) {
updatePlugin()
} else{
releaseDialog.value = true
}
}
// 调用API卸载插件 // 调用API卸载插件
async function uninstallPlugin() { async function uninstallPlugin() {
const isConfirmed = await createConfirm({ const isConfirmed = await createConfirm({
@@ -83,7 +118,12 @@ async function uninstallPlugin() {
return return
try { try {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在卸载 ${props.plugin?.plugin_name} ...`
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`) const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
// 隐藏等待提示框
progressDialog.value = false
if (result.success) { if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`) $toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
@@ -140,15 +180,20 @@ async function loadPluginConf() {
// 调用API保存配置数据 // 调用API保存配置数据
async function savePluginConf() { async function savePluginConf() {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在保存 ${props.plugin?.plugin_name} 配置...`
try { try {
const result: { [key: string]: any } = await api.put(`plugin/${props.plugin?.id}`, pluginConfigForm.value) const result: { [key: string]: any } = await api.put(`plugin/${props.plugin?.id}`, pluginConfigForm.value)
if (result.success) { if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`) progressDialog.value = false
pluginConfigDialog.value = false pluginConfigDialog.value = false
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
// 通知父组件刷新 // 通知父组件刷新
emit('save') emit('save')
} }
else { else {
progressDialog.value = false
$toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`) $toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`)
} }
} }
@@ -182,7 +227,7 @@ const iconPath: Ref<string> = computed(() => {
return noImage return noImage
// 如果是网络图片则使用代理后返回 // 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http')) if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1` return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}` return `./plugin_icon/${props.plugin?.plugin_icon}`
}) })
@@ -221,6 +266,42 @@ async function resetPlugin() {
} }
} }
// 更新插件
async function updatePlugin() {
try {
releaseDialog.value = false
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在更新 ${props.plugin?.plugin_name} ...`
const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`,
{
params: {
repo_url: props.plugin?.repo_url,
force: true,
},
},
)
// 隐藏等待提示框
progressDialog.value = false
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 更新成功!`)
// 通知父组件刷新
emit('save')
}
else {
$toast.error(`插件 ${props.plugin?.plugin_name} 更新失败:${result.message}`)
}
}
catch (error) {
console.error(error)
}
}
// 访问作者主页 // 访问作者主页
function visitAuthorPage() { function visitAuthorPage() {
window.open(props.plugin?.author_url, '_blank') window.open(props.plugin?.author_url, '_blank')
@@ -233,6 +314,14 @@ function openLoggerWindow() {
window.open(url, '_blank') window.open(url, '_blank')
} }
// 打开插件详情
function openPluginDetail() {
if (props.plugin?.has_page)
showPluginInfo()
else
showPluginConfig()
}
// 弹出菜单 // 弹出菜单
const dropdownItems = ref([ const dropdownItems = ref([
{ {
@@ -254,8 +343,18 @@ const dropdownItems = ref([
}, },
}, },
{ {
title: '重置', title: '更新',
value: 3, value: 3,
show: props.plugin?.has_update,
props: {
prependIcon: 'mdi-arrow-up-circle-outline',
color: 'success',
click: showUpdateHistory,
},
},
{
title: '重置',
value: 4,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-cancel', prependIcon: 'mdi-cancel',
@@ -265,7 +364,7 @@ const dropdownItems = ref([
}, },
{ {
title: '卸载', title: '卸载',
value: 4, value: 5,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-trash-can-outline', prependIcon: 'mdi-trash-can-outline',
@@ -275,7 +374,7 @@ const dropdownItems = ref([
}, },
{ {
title: '查看日志', title: '查看日志',
value: 5, value: 6,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-file-document-outline', prependIcon: 'mdi-file-document-outline',
@@ -286,7 +385,7 @@ const dropdownItems = ref([
}, },
{ {
title: '作者主页', title: '作者主页',
value: 5, value: 7,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-home-circle-outline', prependIcon: 'mdi-home-circle-outline',
@@ -294,6 +393,13 @@ const dropdownItems = ref([
}, },
}, },
]) ])
// 监听插件状态变化
watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
if (updateItemIndex !== -1)
dropdownItems.value[updateItemIndex].show = newHasUpdate
})
</script> </script>
<template> <template>
@@ -302,17 +408,21 @@ const dropdownItems = ref([
v-if="isVisible" v-if="isVisible"
:width="props.width" :width="props.width"
:height="props.height" :height="props.height"
@click="() => { @click="openPluginDetail"
if (props.plugin?.has_page)
showPluginInfo()
else
showPluginConfig()
}"
> >
<div <div
class="relative pa-4 text-center card-cover-blurred" class="relative pa-4 text-center card-cover-blurred"
:style="{ background: `${backgroundColor}` }" :style="{ background: `${backgroundColor}` }"
> >
<div
v-if="props.plugin?.has_update"
class="me-n3 absolute top-0 left-1"
>
<VIcon
icon="mdi-new-box"
class="text-white"
/>
</div>
<div class="me-n3 absolute top-0 right-3"> <div class="me-n3 absolute top-0 right-3">
<IconBtn> <IconBtn>
<VIcon icon="mdi-dots-vertical" class="text-white" /> <VIcon icon="mdi-dots-vertical" class="text-white" />
@@ -352,10 +462,14 @@ const dropdownItems = ref([
/> />
</VAvatar> </VAvatar>
</div> </div>
<span v-if="props.count" class="absolute bottom-1 right-2 flex items-center">
<VIcon icon="mdi-fire" />
<span class="text-sm ms-1">{{ props.count?.toLocaleString() }}</span>
</span>
<VCardItem class="py-2"> <VCardItem class="py-2">
<VCardTitle class="flex items-center flex-row"> <VCardTitle class="flex items-center flex-row">
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" /> <VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">{{ props.plugin?.plugin_version }}</span> {{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle> </VCardTitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
@@ -367,12 +481,13 @@ const dropdownItems = ref([
v-model="pluginConfigDialog" v-model="pluginConfigDialog"
scrollable scrollable
max-width="60rem" max-width="60rem"
:fullscreen="displayWidth < (60 * 16)"
> >
<VCard <VCard
:title="`${props.plugin?.plugin_name} - 配置`" :title="`${props.plugin?.plugin_name} - 配置`"
class="rounded-t" class="rounded-t"
> >
<DialogCloseBtn @click="pluginConfigDialog = false" /> <DialogCloseBtn v-model='pluginConfigDialog' />
<VCardText> <VCardText>
<FormRender <FormRender
v-for="(item, index) in pluginFormItems" v-for="(item, index) in pluginFormItems"
@@ -401,12 +516,13 @@ const dropdownItems = ref([
v-model="pluginInfoDialog" v-model="pluginInfoDialog"
scrollable scrollable
max-width="80rem" max-width="80rem"
:fullscreen="displayWidth < (80 * 16)"
> >
<VCard <VCard
:title="`${props.plugin?.plugin_name}`" :title="`${props.plugin?.plugin_name}`"
class="rounded-t" class="rounded-t"
> >
<DialogCloseBtn @click="pluginInfoDialog = false" /> <DialogCloseBtn v-model='pluginInfoDialog' />
<VCardText> <VCardText>
<PageRender <PageRender
v-for="(item, index) in pluginPageItems" v-for="(item, index) in pluginPageItems"
@@ -430,6 +546,49 @@ const dropdownItems = ref([
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 更新插件进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
<!-- 更新日志 -->
<VDialog
v-if="releaseDialog"
v-model="releaseDialog"
width="600"
scrollable
>
<VCard>
<DialogCloseBtn @click="releaseDialog = false" />
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
<VersionHistory :history="props.plugin?.history" />
<VCardText>
<VBtn
@click="updatePlugin"
block
>
<template #prepend>
<VIcon icon="mdi-arrow-up-circle-outline" />
</template>
更新到最新版本
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -31,12 +31,12 @@ const getImgUrl = computed(() => {
if (imageLoadError.value) if (imageLoadError.value)
return noImage return noImage
const image = props.media?.image || '' const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0` return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
}) })
// 跳转播放 // 跳转播放
function goPlay() { function goPlay(isHovering = false) {
if (props.media?.link) if (props.media?.link && isHovering)
window.open(props.media?.link, '_blank') window.open(props.media?.link, '_blank')
} }
</script> </script>
@@ -53,7 +53,7 @@ function goPlay() {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering, 'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded, 'ring-1': isImageLoaded,
}" }"
@click.stop="goPlay" @click.stop="goPlay(hover.isHovering)"
> >
<VImg <VImg
aspect-ratio="2/3" aspect-ratio="2/3"
@@ -69,8 +69,9 @@ function goPlay() {
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" /> <VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div> </div>
</template> </template>
<!-- 类型角标 --> </VImg>
<VChip <!-- 类型角标 -->
<VChip
v-show="isImageLoaded" v-show="isImageLoaded"
variant="elevated" variant="elevated"
size="small" size="small"
@@ -89,7 +90,6 @@ function goPlay() {
{{ props.media?.title }} {{ props.media?.title }}
</h1> </h1>
</VCardText> </VCardText>
</VImg>
</VCard> </VCard>
</template> </template>
</VHover> </VHover>

View File

@@ -1,12 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue' import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
import { formatFileSize } from '@core/utils/formatters' import SiteTorrentTable from '../table/SiteTorrentTable.vue'
import { requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { Site, TorrentInfo } from '@/api/types' import type { Site, SiteStatistic } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue' import ExistIcon from '@core/components/ExistIcon.vue'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDisplay } from 'vuetify'
// 显示器宽度
const displayWidth = useDisplay().width
// 输入参数 // 输入参数
const cardProps = defineProps({ const cardProps = defineProps({
@@ -51,31 +56,6 @@ const progressDialog = ref(false)
// 进度文本 // 进度文本
const progressText = ref('请稍候 ...') const progressText = ref('请稍候 ...')
// 资源浏览表头
const resourceHeaders = [
{ title: '标题', key: 'title', sortable: false },
{ title: '时间', key: 'pubdate', sortable: true },
{ title: '大小', key: 'size', sortable: true },
{ title: '做种', key: 'seeders', sortable: true },
{ title: '下载', key: 'peers', sortable: true },
{ title: '', key: 'actions', sortable: false },
]
// 数据列表
const resourceDataList = ref<TorrentInfo[]>([])
// 搜索
const resourceSearch = ref('')
// 加载状态
const resourceLoading = ref(false)
// 总条数
const resourceTotalItems = ref(0)
// 每页条数
const resourceItemsPerPage = ref(25)
// 用户名密码表单 // 用户名密码表单
const userPwForm = ref({ const userPwForm = ref({
username: '', username: '',
@@ -83,15 +63,8 @@ const userPwForm = ref({
code: '', code: '',
}) })
// 打开种子详情页面 // 站点使用统计
function openTorrentDetail(page_url: string) { const siteStats = ref<SiteStatistic>({})
window.open(page_url, '_blank')
}
// 下载种子文件
async function downloadTorrentFile(enclosure: string) {
window.open(enclosure, '_blank')
}
// 查询站点图标 // 查询站点图标
async function getSiteIcon() { async function getSiteIcon() {
@@ -117,6 +90,18 @@ async function testSite() {
testButtonText.value = '测试' testButtonText.value = '测试'
testButtonDisable.value = false testButtonDisable.value = false
getSiteStats()
}
catch (error) {
console.error(error)
}
}
// 查询站点使用统计
async function getSiteStats() {
try {
siteStats.value = (await api.get(`site/statistic/${cardProps.site?.domain}`))
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
@@ -131,7 +116,6 @@ async function handleSiteUpdate() {
// 打开资源浏览弹窗 // 打开资源浏览弹窗
async function handleResourceBrowse() { async function handleResourceBrowse() {
resourceDialog.value = true resourceDialog.value = true
getResourceList()
} }
// 调用API更新站点Cookie UA // 调用API更新站点Cookie UA
@@ -171,38 +155,38 @@ async function updateSiteCookie() {
} }
} }
// 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0)
return 'text-white bg-lime-500'
else if (downloadVolume < 1)
return 'text-white bg-green-500'
else if (uploadVolume !== 1)
return 'text-white bg-sky-500'
else
return 'text-white bg-gray-500'
}
// 调用API查询站点资源
async function getResourceList() {
resourceLoading.value = true
try {
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
resourceLoading.value = false
}
catch (error) {
console.error(error)
}
}
// 打开站点页面 // 打开站点页面
function openSitePage() { function openSitePage() {
window.open(cardProps.site?.url, '_blank') window.open(cardProps.site?.url, '_blank')
} }
// 根据站点状态显示不同的状态图标
const statColor = computed(() => {
if (isNullOrEmptyObject(siteStats.value)){
return 'secondary'
}
if (siteStats.value?.lst_state == 1){
return 'error'
}
else if (siteStats.value?.lst_state == 0){
if (!siteStats.value?.seconds)
return 'secondary'
if (siteStats.value?.seconds >= 5)
return 'warning'
return 'success'
}
})
// 监听resourceDialog如果为false则重新查询站点使用统计
watch(resourceDialog, (value) => {
if (!value)
getSiteStats()
})
// 装载时查询站点图标 // 装载时查询站点图标
onMounted(() => { onMounted(() => {
getSiteIcon() getSiteIcon()
getSiteStats()
}) })
</script> </script>
@@ -210,7 +194,7 @@ onMounted(() => {
<VCard <VCard
:height="cardProps.height" :height="cardProps.height"
:width="cardProps.width" :width="cardProps.width"
:flat="!cardProps.site?.is_active" :variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
class="overflow-hidden" class="overflow-hidden"
@click="siteEditDialog = true" @click="siteEditDialog = true"
> >
@@ -223,16 +207,17 @@ onMounted(() => {
<VImg :src="siteIcon" /> <VImg :src="siteIcon" />
</VAvatar> </VAvatar>
</template> </template>
<VCardItem> <VCardItem>
<VCardTitle class="font-bold"> <VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span> <span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle> </VCardTitle>
<VCardSubtitle> <VCardSubtitle>
{{ cardProps.site?.url }} <span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
</VCardSubtitle> </VCardSubtitle>
</VCardItem> </VCardItem>
<ExistIcon v-if="cardProps.site?.is_active" /> <StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
<VCardText class="py-2"> <VCardText class="py-2">
<VTooltip <VTooltip
@@ -329,9 +314,11 @@ onMounted(() => {
<VDialog <VDialog
v-model="siteCookieDialog" v-model="siteCookieDialog"
max-width="50rem" max-width="50rem"
:fullscreen="displayWidth < (50 * 16)"
> >
<!-- Dialog Content --> <!-- Dialog Content -->
<VCard title="更新站点Cookie & UA"> <VCard title="更新站点Cookie & UA">
<DialogCloseBtn @click="siteCookieDialog=false" />
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<VRow> <VRow>
@@ -385,7 +372,8 @@ onMounted(() => {
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<SiteAddEditForm <SiteAddEditDialog
v-if="siteEditDialog"
v-model="siteEditDialog" v-model="siteEditDialog"
:siteid="cardProps.site?.id" :siteid="cardProps.site?.id"
@save="siteEditDialog = false; emit('update')" @save="siteEditDialog = false; emit('update')"
@@ -394,130 +382,18 @@ onMounted(() => {
/> />
<!-- 站点资源弹窗 --> <!-- 站点资源弹窗 -->
<VDialog <VDialog
v-if="resourceDialog"
v-model="resourceDialog" v-model="resourceDialog"
max-width="80rem" max-width="80rem"
scrollable scrollable
z-index="1010"
:fullscreen="displayWidth < (80 * 16)"
> >
<!-- Dialog Content --> <!-- Dialog Content -->
<VCard :title="`浏览站点 - ${cardProps.site?.name}`"> <VCard :title="`浏览站点 - ${cardProps.site?.name}`">
<DialogCloseBtn @click="resourceDialog = false" /> <DialogCloseBtn @click="resourceDialog = false" />
<VCardText class="pt-2"> <VCardText class="pt-2">
<VDataTable <SiteTorrentTable :site="cardProps.site?.id" />
v-model:items-per-page="resourceItemsPerPage"
:headers="resourceHeaders"
:items="resourceDataList"
:items-length="resourceTotalItems"
:search="resourceSearch"
:loading="resourceLoading"
density="compact"
item-value="title"
return-object
fixed-header
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
>
<template #item.title="{ item }">
<div class="text-high-emphasis pt-1">
{{ item.raw.title }}
</div>
<div class="text-sm my-1">
{{ item.raw.description }}
</div>
<VChip
v-if="item.raw?.hit_and_run"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-black"
>
H&R
</VChip>
<VChip
v-if="item.raw?.freedate_diff"
variant="elevated"
color="secondary"
size="small"
class="me-1 mb-1"
>
{{ item.raw?.freedate_diff }}
</VChip>
<VChip
v-for="(label, index) in item.raw?.labels"
:key="index"
variant="elevated"
size="small"
color="primary"
class="me-1 mb-1"
>
{{ label }}
</VChip>
<VChip
v-if="item.raw?.downloadvolumefactor !== 1 || item.raw?.uploadvolumefactor !== 1"
:class="
getVolumeFactorClass(item.raw?.downloadvolumefactor, item.raw?.uploadvolumefactor)
"
variant="elevated"
size="small"
class="me-1 mb-1"
>
{{ item.raw?.volume_factor }}
</VChip>
</template>
<template #item.pubdate="{ item }">
<div>{{ item.raw.date_elapsed }}</div>
<div class="text-sm">
{{ item.raw.pubdate }}
</div>
</template>
<template #item.size="{ item }">
<div class="text-nowrap whitespace-nowrap">
{{ formatFileSize(item.raw.size) }}
</div>
</template>
<template #item.seeders="{ item }">
<div>{{ item.raw.seeders }}</div>
</template>
<template #item.peers="{ item }">
<div>{{ item.raw.peers }}</div>
</template>
<template #item.actions="{ item }">
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="openTorrentDetail(item.raw.page_url)"
>
<template #prepend>
<VIcon icon="mdi-information" />
</template>
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="item.raw.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile(item.raw.enclosure)"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子文件</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
<template #no-data>
没有数据
</template>
</VDataTable>
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>

View File

@@ -1,7 +1,7 @@
<script lang='ts' setup> <script lang='ts' setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue' import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { calculateTimeDifference } from '@/@core/utils' import { formatDateDifference } from '@/@core/utils/formatters'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import type { Subscribe } from '@/api/types' import type { Subscribe } from '@/api/types'
@@ -26,11 +26,9 @@ const subscribeEditDialog = ref(false)
// 上一次更新时间 // 上一次更新时间
const lastUpdateText = ref( const lastUpdateText = ref(
`${ props.media && props.media.last_update
props.media?.last_update ? formatDateDifference(props.media.last_update)
? `${calculateTimeDifference(props.media?.last_update || '')}` : '',
: ''
}`,
) )
// 图片加载完成响应 // 图片加载完成响应
@@ -284,7 +282,8 @@ const dropdownItems = ref([
/> />
</VCard> </VCard>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditForm <SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog" v-model="subscribeEditDialog"
:subid="props.media?.id" :subid="props.media?.id"
@remove="() => { emit('remove');subscribeEditDialog = false; }" @remove="() => { emit('remove');subscribeEditDialog = false; }"

View File

@@ -36,7 +36,7 @@ function goPersonDetail() {
</script> </script>
<template> <template>
<VHover v-bind="personProps"> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <VCard
v-bind="hover.props" v-bind="hover.props"
@@ -61,7 +61,6 @@ function goPersonDetail() {
}" }"
> >
<VImg <VImg
v-img
:src="getPersonImage()" :src="getPersonImage()"
cover cover
@load="isImageLoaded = true" @load="isImageLoaded = true"

View File

@@ -5,7 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
import { formatFileSize } from '@/@core/utils/formatters' import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { Context } from '@/api/types' import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -36,6 +36,9 @@ const meta = ref(props.torrent?.meta_info)
// 站点图标 // 站点图标
const siteIcon = ref('') const siteIcon = ref('')
// 存储是否已经下载过的记录
const downloaded = ref<String[]>([])
// 查询站点图标 // 查询站点图标
async function getSiteIcon() { async function getSiteIcon() {
try { try {
@@ -76,7 +79,7 @@ async function handleAddDownload(_site: any = undefined,
} }
// 添加下载 // 添加下载
async function addDownload(_media: any, _torrent: any) { async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
startNProgress() startNProgress()
try { try {
const result: { [key: string]: any } = await api.post('download/', { const result: { [key: string]: any } = await api.post('download/', {
@@ -87,6 +90,7 @@ async function addDownload(_media: any, _torrent: any) {
if (result.success) { if (result.success) {
// 添加下载成功 // 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`) $toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
downloaded.value.push(_torrent?.enclosure || '')
} }
else { else {
// 添加下载失败 // 添加下载失败
@@ -131,6 +135,7 @@ onMounted(() => {
<VCard <VCard
:width="props.width" :width="props.width"
:height="props.height" :height="props.height"
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'elevated'"
@click="handleAddDownload" @click="handleAddDownload"
> >
<template <template
@@ -288,7 +293,7 @@ onMounted(() => {
<VExpandTransition> <VExpandTransition>
<div v-show="showMoreTorrents"> <div v-show="showMoreTorrents">
<VDivider /> <VDivider />
<VChipGroup class="p-3"> <VChipGroup class="p-3" column>
<VChip <VChip
v-for="(item, index) in props.more" v-for="(item, index) in props.more"
:key="index" :key="index"

View File

@@ -5,7 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
import { formatFileSize } from '@/@core/utils/formatters' import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { Context } from '@/api/types' import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -33,6 +33,9 @@ const meta = ref(props.torrent?.meta_info)
// 站点图标 // 站点图标
const siteIcon = ref('') const siteIcon = ref('')
// 存储是否已经下载过的记录
const downloaded = ref<String[]>([])
// 查询站点图标 // 查询站点图标
async function getSiteIcon() { async function getSiteIcon() {
try { try {
@@ -73,7 +76,7 @@ async function handleAddDownload(_site: any = undefined,
} }
// 添加下载 // 添加下载
async function addDownload(_media: any, _torrent: any) { async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
startNProgress() startNProgress()
try { try {
const result: { [key: string]: any } = await api.post('download/', { const result: { [key: string]: any } = await api.post('download/', {
@@ -84,6 +87,7 @@ async function addDownload(_media: any, _torrent: any) {
if (result.success) { if (result.success) {
// 添加下载成功 // 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`) $toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
downloaded.value.push(_torrent?.enclosure || '')
} }
else { else {
// 添加下载失败 // 添加下载失败
@@ -125,7 +129,10 @@ onMounted(() => {
</script> </script>
<template> <template>
<VListItem @click="handleAddDownload"> <VListItem
@click="handleAddDownload"
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
>
<template <template
v-if="!showMoreTorrents" v-if="!showMoreTorrents"
#prepend #prepend

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
title: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('close')
}
</script>
<template>
<VDialog
width="40rem"
scrollable
max-height="85vh"
>
<VCard
:title="props.title"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="handleImport"
>
导入
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -1,9 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue' import TmdbSelector from '../misc/TmdbSelector.vue'
import store from '@/store' import store from '@/store'
import api from '@/api' import api from '@/api'
import { numberValidator } from '@/@validators' import { numberValidator } from '@/@validators'
import { useDisplay } from 'vuetify'
//
const displayWidth = useDisplay().width
// //
const props = defineProps({ const props = defineProps({
@@ -144,6 +148,7 @@ async function transfer() {
<VDialog <VDialog
scrollable scrollable
max-width="60rem" max-width="60rem"
:fullscreen="displayWidth < (60 * 16)"
> >
<VCard <VCard
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`" :title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
@@ -161,6 +166,7 @@ async function transfer() {
v-model="transferForm.target" v-model="transferForm.target"
label="目的路径" label="目的路径"
placeholder="留空自动" placeholder="留空自动"
hint="留空将自动整理到媒体库目录"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -204,6 +210,7 @@ async function transfer() {
placeholder="留空自动识别" placeholder="留空自动识别"
:rules="[numberValidator]" :rules="[numberValidator]"
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
hint="点击图标按名称搜索,留空将自动重新识别"
@click:append-inner="tmdbSelectorDialog = true" @click:append-inner="tmdbSelectorDialog = true"
/> />
</VCol> </VCol>
@@ -225,6 +232,7 @@ async function transfer() {
v-model="transferForm.episode_format" v-model="transferForm.episode_format"
label="集数定位" label="集数定位"
placeholder="使用{ep}定位集数" placeholder="使用{ep}定位集数"
hint="使用{ep}定位文件名中的集数部分,其余相同部分直接填写,不同部分使用{a}进行忽略,例如:{a}葬送的芙莉莲_Sousou no Frieren 第{ep}话{b}"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -232,6 +240,7 @@ async function transfer() {
v-model="transferForm.episode_detail" v-model="transferForm.episode_detail"
label="指定集数" label="指定集数"
placeholder="起始集,终止集如1或1,2" placeholder="起始集,终止集如1或1,2"
hint="直接指定集数或者范围,格式:起始集,终止集如1或1,2"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -239,6 +248,7 @@ async function transfer() {
v-model="transferForm.episode_part" v-model="transferForm.episode_part"
label="指定Part" label="指定Part"
placeholder="如part1" placeholder="如part1"
hint="指定集数的Part如part1"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -246,6 +256,7 @@ async function transfer() {
v-model.number="transferForm.episode_offset" v-model.number="transferForm.episode_offset"
label="集数偏移" label="集数偏移"
placeholder="如-10" placeholder="如-10"
hint="对集数进行偏移运算,如-10表示文件名中的集数减10为整理后集数"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -254,6 +265,7 @@ async function transfer() {
label="最小文件大小MB" label="最小文件大小MB"
:rules="[numberValidator]" :rules="[numberValidator]"
placeholder="0" placeholder="0"
hint="最小文件大小,小于此大小的文件将被忽略不进行整理"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -297,8 +309,9 @@ async function transfer() {
v-model="tmdbSelectorDialog" v-model="tmdbSelectorDialog"
width="40rem" width="40rem"
scrollable scrollable
max-height="85vh"
> >
<TmdbSelectorCard <TmdbSelector
v-model="transferForm.tmdbid" v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false" @close="tmdbSelectorDialog = false"
/> />

View File

@@ -4,6 +4,10 @@ import type { Site } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
import { numberValidator, requiredValidator } from '@/@validators' import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import { useDisplay } from 'vuetify'
//
const displayWidth = useDisplay().width
// //
const props = defineProps({ const props = defineProps({
@@ -125,6 +129,7 @@ async function updateSiteInfo() {
persistent persistent
eager eager
max-width="60rem" max-width="60rem"
:fullscreen="displayWidth < (60 * 16)"
> >
<VCard <VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`" :title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
@@ -142,6 +147,7 @@ async function updateSiteInfo() {
v-model="siteForm.url" v-model="siteForm.url"
label="站点地址" label="站点地址"
:rules="[requiredValidator]" :rules="[requiredValidator]"
hint="格式http://www.example.com/"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -153,6 +159,7 @@ async function updateSiteInfo() {
label="优先级" label="优先级"
:items="priorityItems" :items="priorityItems"
:rules="[requiredValidator]" :rules="[requiredValidator]"
hint="站点资源下载优先级,优先级数字越小越优先下载"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -171,18 +178,21 @@ async function updateSiteInfo() {
<VTextField <VTextField
v-model="siteForm.rss" v-model="siteForm.rss"
label="RSS地址" label="RSS地址"
hint="订阅模式为站点RSS时将会使用此地址获取站点种子资源该地址一般会自动获取也可手动补充"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<VTextarea <VTextarea
v-model="siteForm.cookie" v-model="siteForm.cookie"
label="站点Cookie" label="站点Cookie"
hint="浏览器打开站点首页打开开发人员工具刷新页面后在网络选项中找到首页地址在请求头中获取Cookie信息"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<VTextField <VTextField
v-model="siteForm.ua" v-model="siteForm.ua"
label="站点User-Agent" label="站点User-Agent"
hint="在开发人员工具网络请求头中获取User-Agent信息需与站点Cookie配套使用"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -195,6 +205,7 @@ async function updateSiteInfo() {
v-model="siteForm.limit_interval" v-model="siteForm.limit_interval"
label="单位周期(秒)" label="单位周期(秒)"
:rules="[numberValidator]" :rules="[numberValidator]"
hint="设定站点限流的单位周期单位为秒0为不限流"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -205,6 +216,7 @@ async function updateSiteInfo() {
v-model="siteForm.limit_count" v-model="siteForm.limit_count"
label="访问次数" label="访问次数"
:rules="[numberValidator]" :rules="[numberValidator]"
hint="设定单位周期内站点允许的访问次数0为不限制"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -215,6 +227,7 @@ async function updateSiteInfo() {
v-model="siteForm.limit_seconds" v-model="siteForm.limit_seconds"
label="访问间隔(秒)" label="访问间隔(秒)"
:rules="[numberValidator]" :rules="[numberValidator]"
hint="设定单位周期内每次站点访问需间隔时间单位为秒0为不限制"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -226,6 +239,7 @@ async function updateSiteInfo() {
<VSwitch <VSwitch
v-model="siteForm.proxy" v-model="siteForm.proxy"
label="代理" label="代理"
hint="站点是否需要代理访问,需要设置好代理服务器信息"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -235,6 +249,7 @@ async function updateSiteInfo() {
<VSwitch <VSwitch
v-model="siteForm.render" v-model="siteForm.render"
label="仿真" label="仿真"
hint="站点是否需要使用浏览器模拟访问,开启可以一定程度上提升连通性,但会大大增加站点请求时间"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -3,10 +3,16 @@ import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators' import { numberValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { Site, Subscribe } from '@/api/types' import type { Site, Subscribe } from '@/api/types'
import { useDisplay } from 'vuetify'
//
const displayWidth = useDisplay().width
// //
const props = defineProps({ const props = defineProps({
subid: Number, subid: Number,
default: Boolean,
type: String,
}) })
// //
@@ -30,7 +36,7 @@ const subscribeForm = ref<Subscribe>({
total_episode: 0, total_episode: 0,
start_episode: 0, start_episode: 0,
best_version: 0, best_version: 0,
search_imdbid: false, search_imdbid: 0,
sites: [], sites: [],
type: '', type: '',
name: '', name: '',
@@ -41,6 +47,8 @@ const subscribeForm = ref<Subscribe>({
username: '', username: '',
current_priority: 0, current_priority: 0,
save_path: '', save_path: '',
date: '',
show_edit_dialog: false
}) })
// //
@@ -63,6 +71,50 @@ async function updateSubscribeInfo() {
} }
} }
//
async function saveDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (props.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.post(
subscribe_config_url,
subscribeForm.value)
if (result.success)
$toast.success(`${props.type}订阅默认规则保存成功`)
else
$toast.error(`${props.type}订阅默认规则保存失败!`)
//
emit('save')
}
catch (error) {
console.log(error)
}
}
//
async function queryDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (props.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data.value)
subscribeForm.value = result.data?.value ?? ''
}
catch (error) {
console.log(error)
}
}
// //
async function loadSites() { async function loadSites() {
try { try {
@@ -100,6 +152,7 @@ async function getSubscribeInfo() {
) )
subscribeForm.value = result subscribeForm.value = result
subscribeForm.value.best_version = subscribeForm.value.best_version === 1 subscribeForm.value.best_version = subscribeForm.value.best_version === 1
subscribeForm.value.search_imdbid = subscribeForm.value.search_imdbid === 1
} }
catch (e) { catch (e) {
console.log(e) console.log(e)
@@ -207,11 +260,13 @@ const effectOptions = ref([
}, },
]) ])
watchEffect(() => { onMounted(() => {
if (props.subid) { getSiteList()
getSiteList() if (props.subid)
getSubscribeInfo() getSubscribeInfo()
}
if (props.default)
queryDefaultSubscribeConfig()
}) })
</script> </script>
@@ -219,9 +274,10 @@ watchEffect(() => {
<VDialog <VDialog
scrollable scrollable
max-width="60rem" max-width="60rem"
:fullscreen="displayWidth < (60 * 16)"
> >
<VCard <VCard
:title="`编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`" :title="`${props.default ? `${props.type}默认订阅规则` : `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`}`"
class="rounded-t" class="rounded-t"
> >
<VCardText class="pt-2"> <VCardText class="pt-2">
@@ -233,8 +289,10 @@ watchEffect(() => {
md="8" md="8"
> >
<VTextField <VTextField
v-if="!props.default"
v-model="subscribeForm.keyword" v-model="subscribeForm.keyword"
label="搜索关键词" label="搜索关键词"
hint="设定搜索关键词后将使用此关键词搜索站点资源否则自动使用themoviedb中的名称搜索"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -246,6 +304,7 @@ watchEffect(() => {
v-model="subscribeForm.total_episode" v-model="subscribeForm.total_episode"
label="总集数" label="总集数"
:rules="[numberValidator]" :rules="[numberValidator]"
hint="设定剧集的总集数以应对themoviedb中剧集信息未维护完整导致提前结束订阅的情况"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -257,6 +316,7 @@ watchEffect(() => {
v-model="subscribeForm.start_episode" v-model="subscribeForm.start_episode"
label="开始集数" label="开始集数"
:rules="[numberValidator]" :rules="[numberValidator]"
hint="只订阅下载此集数及之后的剧集"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -300,6 +360,7 @@ watchEffect(() => {
<VTextField <VTextField
v-model="subscribeForm.include" v-model="subscribeForm.include"
label="包含(关键字、正则式)" label="包含(关键字、正则式)"
hint="支持正则表达式,多个关键字用 | 分隔表示或"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -309,6 +370,7 @@ watchEffect(() => {
<VTextField <VTextField
v-model="subscribeForm.exclude" v-model="subscribeForm.exclude"
label="排除(关键字、正则式)" label="排除(关键字、正则式)"
hint="支持正则表达式,多个关键字用 | 分隔表示或"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -321,6 +383,7 @@ watchEffect(() => {
chips chips
label="订阅站点" label="订阅站点"
multiple multiple
hint="只订阅选中的订阅站点,不选则订阅所有可订阅站点"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -331,6 +394,7 @@ watchEffect(() => {
<VTextField <VTextField
v-model="subscribeForm.save_path" v-model="subscribeForm.save_path"
label="保存路径" label="保存路径"
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -342,6 +406,7 @@ watchEffect(() => {
<VSwitch <VSwitch
v-model="subscribeForm.best_version" v-model="subscribeForm.best_version"
label="洗版" label="洗版"
hint="开启后不管媒体库是否存在,均会根据洗版优先级进行过滤下载,直到下载到了最高优先级的资源为止"
/> />
</VCol> </VCol>
<VCol <VCol
@@ -351,6 +416,17 @@ watchEffect(() => {
<VSwitch <VSwitch
v-model="subscribeForm.search_imdbid" v-model="subscribeForm.search_imdbid"
label="使用 ImdbID 搜索" label="使用 ImdbID 搜索"
hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持"
/>
</VCol>
<VCol v-if="props.default"
cols="12"
md="4"
>
<VSwitch
v-model="subscribeForm.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后将在添加订阅后弹出编辑订阅的对话框,方便用户编辑订阅规则"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -358,13 +434,13 @@ watchEffect(() => {
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<VBtn color="error" @click="removeSubscribe"> <VBtn v-if="!props.default" color="error" @click="removeSubscribe">
取消订阅 取消订阅
</VBtn> </VBtn>
<VSpacer /> <VSpacer />
<VBtn <VBtn
variant="tonal" variant="tonal"
@click="updateSubscribeInfo" @click="`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
> >
保存 保存
</VBtn> </VBtn>

View File

@@ -0,0 +1,243 @@
<script lang="ts" setup>
import api from '@/api';
import { Subscribe } from '@/api/types';
import { formatDateDifference } from '@core/utils/formatters'
import { useDisplay } from 'vuetify'
// 显示器宽度
const displayWidth = useDisplay().width
// 输入参数
const props = defineProps({
type: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['close', 'save'])
// 订阅历史列表
const historyList = ref<Subscribe[]>([])
// 当前加载数据
const currData = ref<Subscribe[]>([])
// 当前页
const currentPage = ref(1)
// 每页数量
const pageSize = ref(30)
// 是否加载中
const loading = ref(false)
// 是否加载完成
const isRefreshed = ref(false)
// 进度框
const progressDialog = ref(false)
// 进度文字
const progressText = ref('正在重新订阅...')
// 调用API查询列表
async function loadHistory({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 调用API查询列表
try {
// 设置加载中
loading.value = true
currData.value = await api.get(`subscribe/history/${props.type}`, {
params: {
page: currentPage.value,
count: pageSize.value,
},
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
historyList.value = [...historyList.value, ...currData.value]
// 页码+1
currentPage.value++
// 返回加载成功
done('ok')
}
// 取消加载中
loading.value = false
} catch (e) {
console.error(e)
// 返回加载失败
done('error')
}
}
// 重新订阅
async function reSubscribe(item: Subscribe) {
if (item.type === '电影')
progressText.value = `正在重新订阅 ${item.name} ...`
else
progressText.value = `正在重新订阅 ${item.name}${item.season} 季 ...`
progressDialog.value = true
try {
const result: {[key: string]: any} = await api.post('subscribe', item)
if (result.success){
emit('save')
}
} catch (e) {
console.error(e)
}
progressDialog.value = false
}
// 删除记录
async function deleteHistory(item: Subscribe) {
try {
const result: {[key: string]: any} = await api.delete(`subscribe/history/${item.id}`)
if (result.success){
historyList.value = historyList.value.filter((i) => i.id !== item.id)
}
} catch (e) {
console.error(e)
}
}
// 弹出菜单
const dropdownItems = ref([
{
title: '重新订阅',
value: 1,
color: '',
props: {
prependIcon: 'mdi-redo',
click: reSubscribe,
},
},
{
title: '删除',
value: 2,
color: 'error',
props: {
prependIcon: 'mdi-delete',
click: deleteHistory,
},
}
])
</script>
<template>
<VDialog
scrollable
max-width="50rem"
:fullscreen="displayWidth < (50 * 16)"
>
<VCard
class="mx-auto"
width="100%"
>
<VCardItem class="pb-0">
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
</VCardItem>
<DialogCloseBtn @click="() => { emit('close') }" />
<VList
lines="two"
>
<VInfiniteScroll
mode="intersect"
side="end"
:items="historyList"
class="overflow-hidden"
@load="loadHistory"
>
<template #loading>
<LoadingBanner />
</template>
<template #empty />
<template v-for="(item, i) in historyList" :key="i">
<VListItem>
<template #prepend>
<VImg
height="75"
width="50"
:src="item.poster"
aspect-ratio="2/3"
class="object-cover rounded shadow ring-gray-500 me-3"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</template>
<VListItemTitle v-if="item.type == '电视剧'">
{{ item.name }} <span class="text-sm"> {{ item.season }} </span>
</VListItemTitle>
<VListItemTitle v-else>
{{ item.name }}
</VListItemTitle>
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VListItem>
</template>
</VInfiniteScroll>
</VList>
</VCard>
<!-- 进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
</VDialog>
</template>

View File

@@ -4,7 +4,7 @@ import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog' import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios' import axios from 'axios'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import ReorganizeForm from '../form/ReorganizeForm.vue' import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters' import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types' import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store' import store from '@/store'
@@ -73,6 +73,9 @@ const nameTestResult = ref<Context>()
// //
const nameTestDialog = ref(false) const nameTestDialog = ref(false)
//
const defer = (_: number) => true
// //
const dirs = computed(() => const dirs = computed(() =>
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)), items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
@@ -343,6 +346,36 @@ onMounted(() => {
<template> <template>
<VCard class="d-flex flex-column"> <VCard class="d-flex flex-column">
<VToolbar v-if="!loading" density="compact" flat color="gray">
<VTextField
v-if="!isFile"
v-model="filter"
hide-details
flat
density="compact"
variant="solo-filled"
placeholder="搜索 ..."
prepend-inner-icon="mdi-filter-outline"
class="me-2"
rounded="0"
/>
<VSpacer v-if="isFile" />
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
<VIcon color="primary">
mdi-text-recognition
</VIcon>
</IconBtn>
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
<VIcon color="primary">
mdi-download
</VIcon>
</IconBtn>
<IconBtn v-if="!isFile" @click="load">
<VIcon color="primary">
mdi-refresh
</VIcon>
</IconBtn>
</VToolbar>
<VCardText <VCardText
v-if="loading" v-if="loading"
class="text-center flex flex-col items-center" class="text-center flex flex-col items-center"
@@ -374,175 +407,92 @@ onMounted(() => {
<VImg :src="getImgLink(path)" max-width="100%" max-height="100%" /> <VImg :src="getImgLink(path)" max-width="100%" max-height="100%" />
</VCardText> </VCardText>
<VCardText v-else-if="dirs.length || files.length" class="p-0"> <VCardText v-else-if="dirs.length || files.length" class="p-0">
<VList v-if="dirs.length" subheader> <VList subheader>
<VListSubheader>目录</VListSubheader> <VVirtualScroll class="virtual-scroll-div" :items="[...dirs, ...files]">
<VHover <template #default="{ item }">
v-for="(item, index) in dirs" <VHover>
:key="index" <template #default="hover">
> <VListItem
<template #default="hover"> v-bind="hover.props"
<VListItem class="px-3 pe-1"
v-bind="hover.props" @click="changePath(item.path)"
class="px-3 pe-1" >
@click="changePath(item.path)" <template #prepend>
> <VIcon v-if="inProps.icons && item.extension" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
<template #prepend> <VIcon v-else icon="mdi-folder-outline" />
<VIcon icon="mdi-folder-outline" /> </template>
</template> <VListItemTitle v-text="item.name" />
<VListItemTitle v-text="item.name" /> <VListItemSubtitle v-if="item.size">
<template #append> {{ formatBytes(item.size) }}
<IconBtn class="d-sm-none"> </VListItemSubtitle>
<VIcon <template #append>
icon="mdi-dots-vertical" <IconBtn class="d-sm-none">
/> <VIcon
<VMenu icon="mdi-dots-vertical"
activator="parent" />
close-on-content-click <VMenu
> activator="parent"
<VList> close-on-content-click
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
> >
<template #prepend> <VList>
<VIcon :icon="menu.props.prependIcon" /> <VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<span v-if="hover.isHovering" class="flex">
<VTooltip text="识别">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
</template> </template>
<VListItemTitle v-text="menu.title" /> </VTooltip>
</VListItem> <VTooltip text="刮削">
</VList> <template #activator="{ props }">
</VMenu> <IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
</IconBtn> <VIcon icon="mdi-auto-fix" />
<span v-show="hover.isHovering" class="flex"> </IconBtn>
<VTooltip text="识别">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="刮削">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
<VIcon icon="mdi-auto-fix" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="重命名">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="整理">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="删除">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" color="error" />
</IconBtn>
</template>
</VTooltip>
</span>
</template>
</VListItem>
</template>
</VHover>
</VList>
<VDivider v-if="dirs.length && files.length" />
<VList v-if="files.length" subheader>
<VListSubheader>文件</VListSubheader>
<VHover
v-for="(item, index) in files"
:key="index"
>
<template #default="hover">
<VListItem
v-bind="hover.props"
class="pl-3 pe-1"
@click="changePath(item.path)"
>
<template #prepend>
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
</template>
<VListItemTitle v-text="item.name" />
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
<template #append>
<IconBtn class="d-sm-none">
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template> </template>
<VListItemTitle v-text="menu.title" /> </VTooltip>
</VListItem> <VTooltip text="重命名">
</VList> <template #activator="{ props }">
</VMenu> <IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
</IconBtn> <VIcon icon="mdi-rename" />
<span v-show="hover.isHovering" class="flex"> </IconBtn>
<VTooltip text="识别"> </template>
<template #activator="{ props }"> </VTooltip>
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)"> <VTooltip text="整理">
<VIcon icon="mdi-text-recognition" /> <template #activator="{ props }">
</IconBtn> <IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
</template> <VIcon icon="mdi-folder-arrow-right" />
</VTooltip> </IconBtn>
<VTooltip text="刮削"> </template>
<template #activator="{ props }"> </VTooltip>
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)"> <VTooltip text="删除">
<VIcon icon="mdi-auto-fix" /> <template #activator="{ props }">
</IconBtn> <IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
</template> <VIcon icon="mdi-delete-outline" color="error" />
</VTooltip> </IconBtn>
<VTooltip text="重命名"> </template>
<template #activator="{ props }"> </VTooltip>
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)"> </span>
<VIcon icon="mdi-rename" /> </template>
</IconBtn> </VListItem>
</template>
</VTooltip>
<VTooltip text="整理">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="删除">
<template #activator="{ props }">
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" color="error" />
</IconBtn>
</template>
</VTooltip>
</span>
</template> </template>
</VListItem> </VHover>
</template> </template>
</VHover> </VVirtualScroll>
</VList> </VList>
</VCardText> </VCardText>
<VCardText <VCardText
@@ -557,39 +507,10 @@ onMounted(() => {
> >
空目录 空目录
</VCardText> </VCardText>
<VDivider v-if="path" />
<VToolbar v-if="!loading" density="compact" flat color="gray">
<VTextField
v-if="!isFile"
v-model="filter"
hide-details
flat
density="compact"
variant="solo-filled"
placeholder="搜索 ..."
prepend-inner-icon="mdi-filter-outline"
class="me-2"
/>
<VSpacer v-if="isFile" />
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
<VIcon color="primary">
mdi-text-recognition
</VIcon>
</IconBtn>
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
<VIcon color="primary">
mdi-download
</VIcon>
</IconBtn>
<IconBtn v-if="!isFile" @click="load">
<VIcon color="primary">
mdi-refresh
</VIcon>
</IconBtn>
</VToolbar>
</VCard> </VCard>
<!-- 重命名弹窗 --> <!-- 重命名弹窗 -->
<VDialog <VDialog
v-if="renamePopper"
v-model="renamePopper" v-model="renamePopper"
max-width="50rem" max-width="50rem"
> >
@@ -614,7 +535,8 @@ onMounted(() => {
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 文件整理弹窗 --> <!-- 文件整理弹窗 -->
<ReorganizeForm <ReorganizeDialog
v-if="transferPopper"
v-model="transferPopper" v-model="transferPopper"
:path="currentItem?.path" :path="currentItem?.path"
@done="transferPopper = false; load()" @done="transferPopper = false; load()"
@@ -642,6 +564,7 @@ onMounted(() => {
</VDialog> </VDialog>
<!-- 识别结果对话框 --> <!-- 识别结果对话框 -->
<VDialog <VDialog
v-if="nameTestDialog"
v-model="nameTestDialog" v-model="nameTestDialog"
width="50rem" width="50rem"
> >
@@ -656,9 +579,21 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.v-card { .v-card {
height: 100%; block-size: 100%;
} }
.v-toolbar{ .v-toolbar{
background: rgb(var(--v-table-header-background)); background: rgb(var(--v-table-header-background));
} }
.virtual-scroll-div {
block-size: calc(100vh - 14rem);
}
@media (width <= 768px) {
.virtual-scroll-div {
block-size: calc(100vh - 17rem);
}
}
</style> </style>

View File

@@ -1,203 +0,0 @@
<script lang="ts" setup>
import type { Axios } from 'axios'
import type { EndPoints, FileItem } from '@/api/types'
// 输入参数
const props = defineProps({
icons: Object,
storage: String,
path: String,
endpoints: Object as PropType<EndPoints>,
axios: Object as PropType<Axios>,
refreshpending: Boolean,
})
// 对外事件
const emit = defineEmits(['pathchanged', 'loading', 'refreshed'])
// 变量
const open = ref<string[]>([])
// 活跃的文件夹
const active = ref<string[]>([])
// 内容
const items = ref<FileItem[]>([])
// 过滤
const filter = ref('')
// 方法
function init() {
open.value = []
items.value = [{
type: 'dir',
path: '/',
basename: 'root',
extension: '',
name: 'root',
children: [],
size: 0,
modify_time: 0,
}]
}
// 调用API读取文件夹
async function readFolder(item: FileItem) {
emit('loading', true)
const url = props.endpoints?.list.url
.replace(/{storage}/g, props.storage)
.replace(/{path}/g, item.path)
const config = {
url,
method: props.endpoints?.list.method || 'get',
}
const response: FileItem[] = await props.axios?.request(config) ?? []
item.children = response.map((item: FileItem) => {
if (item.type === 'dir')
item.children = []
return item
})
emit('loading', false)
}
// 选中变化
function activeChanged(_active: string[]) {
let path = ''
if (active.value.length)
path = active.value[0]
if (props.path !== path)
emit('pathchanged', path)
}
// 查找文件
function findItem(path: string) {
const stack: FileItem[] = []
stack.push(items.value[0])
while (stack.length > 0) {
const node = stack.pop()
if (node?.path === path) {
return node
}
else if (node?.children && node.children.length) {
for (const element of node.children)
stack.push(element)
}
}
return null
}
// 监听存储空间变量
watch(() => props.storage, () => {
init()
})
// 监听路径变化
watch(
() => props.path,
() => {
if (props.path) {
active.value = [props.path]
if (!open.value.includes(props.path))
open.value.push(props.path)
}
})
// 监听 refreshPending
watch(
() => props.refreshpending,
async () => {
if (props.refreshpending && props.path) {
const item = findItem(props.path)
if (item) {
await readFolder(item)
emit('refreshed')
}
}
},
)
onMounted(() => {
init()
})
</script>
<template>
<VCard flat width="250" min-height="500" class="d-flex flex-column folders-tree-card">
<div class="grow scroll-x">
<VTreeview
:open="open"
:active="active"
:items="items"
:search="filter"
:load-children="readFolder"
item-key="path"
item-text="basename"
dense
activatable
transition
class="folders-tree"
@update:active="activeChanged"
>
<template #prepend="{ item, open }">
<VIcon
v-if="item.type === 'dir'"
>
{{ open ? 'mdi-folder-open-outline' : 'mdi-folder-outline' }}
</VIcon>
<VIcon v-else-if="props.icons" :icon="props.icons[item.extension.toLowerCase()] || props.icons.other" />
</template>
<template #label="{ item }">
{{ item.basename }}
<VBtn
v-if="item.type === 'dir'"
icon
class="ml-1"
@click.stop="readFolder(item)"
>
<VIcon class="pa-0 mdi-18px" color="grey lighten-1">
mdi-refresh
</VIcon>
</VBtn>
</template>
</VTreeview>
</div>
<VDivider />
<VToolbar
density="compact"
>
<VBtn icon @click="init">
<VIcon icon="mdi-collapse-all-outline" />
</VBtn>
</VToolbar>
</VCard>
</template>
<style lang="scss" scoped>
.folders-tree-card {
height: 100%;
.scroll-x {
overflow-x: auto;
}
::v-deep .folders-tree {
width: fit-content;
min-width: 250px;
.v-treeview-node {
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.02);
}
}
}
}
.v-toolbar{
background: rgb(var(--v-table-header-background));
}
</style>

View File

@@ -1,39 +0,0 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
title: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('close')
}
</script>
<template>
<VCard
:title="props.title"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="handleImport"
>
导入
</VBtn>
</VCardActions>
</VCard>
</template>

View File

@@ -93,7 +93,7 @@ onMounted(() => {
single-line single-line
placeholder="电影或电视剧名称" placeholder="电影或电视剧名称"
variant="solo" variant="solo"
append-inner-icon="mdi-magnify" prepend-inner-icon="mdi-magnify"
flat flat
class="mx-1" class="mx-1"
:loading="loading" :loading="loading"
@@ -101,7 +101,7 @@ onMounted(() => {
@keydown.enter="searchMedias" @keydown.enter="searchMedias"
/> />
</VToolbar> </VToolbar>
<DialogCloseBtn @click="() => { emit('close') }" />
<VList <VList
v-if="items.length > 0" v-if="items.length > 0"
lines="three" lines="three"
@@ -131,7 +131,6 @@ onMounted(() => {
</VListItemTitle> </VListItemTitle>
<VListItemSubtitle class="mt-2" v-html="item.overview" /> <VListItemSubtitle class="mt-2" v-html="item.overview" />
</VListItem> </VListItem>
<VDivider v-if="i < items.length - 1" class="mt-1" inset />
</template> </template>
</VList> </VList>
</VCard> </VCard>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
// 输入参数
const props = defineProps({
history: Object as PropType<{ [key: string]: string }>,
})
</script>
<template>
<VCardItem>
<VList>
<VListItem
v-for="(value, key) in props.history"
:key="key"
>
<VListItemTitle>{{ key }}</VListItemTitle>
<div class="text-gray-500">
{{ value }}
</div>
</VListItem>
</VList>
</VCardItem>
</template>

View File

@@ -7,6 +7,7 @@ interface RenderProps {
text: string text: string
html: string html: string
content?: any content?: any
slots?: any
props?: any props?: any
} }
@@ -22,6 +23,7 @@ const formItem = ref<RenderProps>(elementProps.config ?? {
html: '', html: '',
props: {}, props: {},
content: [], content: [],
slots: {},
}) })
</script> </script>
@@ -32,6 +34,15 @@ const formItem = ref<RenderProps>(elementProps.config ?? {
v-bind="formItem.props" v-bind="formItem.props"
> >
{{ formItem.text }} {{ formItem.text }}
<template v-for="(content, name) in (formItem.slots || [])" :key="name" v-slot:[name]="{_props}">
<slot :name="name" v-bind="_props">
<PageRender
v-for="(slotItem, slotIndex) in (content || [])"
:key="slotIndex"
:config="slotItem"
/>
</slot>
</template>
<PageRender <PageRender
v-for="(innerItem, innerIndex) in (formItem.content || [])" v-for="(innerItem, innerIndex) in (formItem.content || [])"
:key="innerIndex" :key="innerIndex"

View File

@@ -1,12 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue' import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
// 输入参数
const props = defineProps({
linkurl: String,
title: String,
})
// 元素 // 元素
const slideview_content = ref() const slideview_content = ref()
// 分页切换状态 // 分页切换状态
@@ -95,7 +89,7 @@ onActivated(() => {
<template> <template>
<div class="flex justify-between mt-3"> <div class="flex justify-between mt-3">
<slot name="title"> <slot name="title">
<SlideViewTitle v-bind="props" /> <SlideViewTitle />
</slot> </slot>
<div v-if="disabled !== 3" class="me-1 d-none d-md-flex"> <div v-if="disabled !== 3" class="me-1 d-none d-md-flex">
<VBtn <VBtn

View File

@@ -1,9 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
// 输入参数 // 输入参数
const props = defineProps({ const props = inject('rankingPropsKey')
linkurl: String,
title: String,
})
</script> </script>
<template> <template>

View File

@@ -0,0 +1,245 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import api from '@/api'
import type { TorrentInfo } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatFileSize } from '@core/utils/formatters'
// 输入参数
const props = defineProps({
site: Number,
width: String,
height: String,
})
// 提示框
const $toast = useToast()
// 确认框
const createConfirm = useConfirm()
// 数据列表
const resourceDataList = ref<TorrentInfo[]>([])
// 搜索
const resourceSearch = ref('')
// 总条数
const resourceTotalItems = ref(0)
// 每页条数
const resourceItemsPerPage = ref(25)
// 加载状态
const resourceLoading = ref(false)
// 资源浏览表头
const resourceHeaders = [
{ title: '标题', key: 'title', sortable: false },
{ title: '时间', key: 'pubdate', sortable: true },
{ title: '大小', key: 'size', sortable: true },
{ title: '做种', key: 'seeders', sortable: true },
{ title: '下载', key: 'peers', sortable: true },
{ title: '', key: 'actions', sortable: false },
]
// 打开种子详情页面
function openTorrentDetail(page_url: string) {
window.open(page_url, '_blank')
}
// 下载种子文件
async function downloadTorrentFile(enclosure: string) {
window.open(enclosure, '_blank')
}
// 调用API查询站点资源
async function getResourceList() {
resourceLoading.value = true
try {
resourceDataList.value = await api.get(`site/resource/${props.site}`)
resourceLoading.value = false
}
catch (error) {
console.error(error)
}
}
// 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0)
return 'text-white bg-lime-500'
else if (downloadVolume < 1)
return 'text-white bg-green-500'
else if (uploadVolume !== 1)
return 'text-white bg-sky-500'
else
return 'text-white bg-gray-500'
}
// 添加下载
async function addDownload(_torrent: any) {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认下载【${_torrent.site_name}${_torrent?.title} ?`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
return
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download/add', _torrent)
if (result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
}
else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败:${result.message || '未知错误'}`)
}
}
catch (error) {
console.error(error)
}
doneNProgress()
}
// 装载时查询站点图标
onMounted(() => {
getResourceList()
})
</script>
<template>
<VDataTable
v-model:items-per-page="resourceItemsPerPage"
:headers="resourceHeaders"
:items="resourceDataList"
:items-length="resourceTotalItems"
:search="resourceSearch"
:loading="resourceLoading"
density="compact"
item-value="title"
return-object
fixed-header
hover
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
>
<template #item.title="{ item }">
<a href="javascript:void(0)" @click.stop="addDownload(item)">
<div class="text-high-emphasis pt-1">
{{ item.title }}
</div>
<div class="text-sm my-1">
{{ item.description }}
</div>
<VChip
v-if="item.hit_and_run"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-black"
>
H&R
</VChip>
<VChip
v-if="item.freedate_diff"
variant="elevated"
color="secondary"
size="small"
class="me-1 mb-1"
>
{{ item.freedate_diff }}
</VChip>
<VChip
v-for="(label, index) in item.labels"
:key="index"
variant="elevated"
size="small"
color="primary"
class="me-1 mb-1"
>
{{ label }}
</VChip>
<VChip
v-if="item.downloadvolumefactor !== 1 || item.uploadvolumefactor !== 1"
:class="
getVolumeFactorClass(item.downloadvolumefactor, item.uploadvolumefactor)
"
variant="elevated"
size="small"
class="me-1 mb-1"
>
{{ item.volume_factor }}
</VChip>
</a>
</template>
<template #item.pubdate="{ item }">
<div>{{ item.date_elapsed }}</div>
<div class="text-sm">
{{ item.pubdate }}
</div>
</template>
<template #item.size="{ item }">
<div class="text-nowrap whitespace-nowrap">
{{ formatFileSize(item.size) }}
</div>
</template>
<template #item.seeders="{ item }">
<div>{{ item.seeders }}</div>
</template>
<template #item.peers="{ item }">
<div>{{ item.peers }}</div>
</template>
<template #item.actions="{ item }">
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="openTorrentDetail(item.page_url || '')"
>
<template #prepend>
<VIcon icon="mdi-information" />
</template>
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="item.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile(item.enclosure)"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子文件</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
<template #no-data>
没有数据
</template>
</VDataTable>
</template>

View File

@@ -44,7 +44,7 @@ const superUser = store.state.auth.superUser
</IconBtn> </IconBtn>
<!-- 👉 Shortcuts --> <!-- 👉 Shortcuts -->
<ShortcutBar /> <ShortcutBar v-if="superUser" />
<!-- 👉 Theme --> <!-- 👉 Theme -->
<NavbarThemeSwitcher class="me-2" /> <NavbarThemeSwitcher class="me-2" />

View File

@@ -3,7 +3,14 @@ import NameTestView from '@/views/system/NameTestView.vue'
import NetTestView from '@/views/system/NetTestView.vue' import NetTestView from '@/views/system/NetTestView.vue'
import LoggingView from '@/views/system/LoggingView.vue' import LoggingView from '@/views/system/LoggingView.vue'
import RuleTestView from '@/views/system/RuleTestView.vue' import RuleTestView from '@/views/system/RuleTestView.vue'
import ModuleTestView from '@/views/system/ModuleTestView.vue'
import MessageView from '@/views/system/MessageView.vue'
import store from '@/store' import store from '@/store'
import api from '@/api'
import { useDisplay } from 'vuetify'
// 显示器宽度
const displayWidth = useDisplay().width
// App捷径 // App捷径
const appsMenu = ref(false) const appsMenu = ref(false)
@@ -20,11 +27,56 @@ const loggingDialog = ref(false)
// 过滤规则弹窗 // 过滤规则弹窗
const ruleTestDialog = ref(false) const ruleTestDialog = ref(false)
// 系统健康检查弹窗
const systemTestDialog = ref(false)
// 消息中心弹窗
const messageDialog = ref(false)
// 输入消息
const user_message = ref('')
// 发送按钮是否可用
const sendButtonDisabled = ref(false)
// 聊天容器
const chatContainer = ref<HTMLDivElement>()
// 滚动到底部
function scrollMessageToEnd() {
nextTick(() => {
if (chatContainer.value) {
const scrollDiv = chatContainer.value.$el
scrollDiv.scrollTop = scrollDiv.scrollHeight
}
})
}
// 拼接全部日志url // 拼接全部日志url
function allLoggingUrl() { function allLoggingUrl() {
const token = store.state.auth.token const token = store.state.auth.token
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1` return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
} }
// 发送消息
async function sendMessage() {
if (user_message.value) {
try {
sendButtonDisabled.value = true
await api.post(`message/web?text=${user_message.value}`)
user_message.value = ''
sendButtonDisabled.value = false
scrollMessageToEnd()
}
catch (error) {
console.error(error)
}
}
}
onMounted(() => {
scrollMessageToEnd()
})
</script> </script>
<template> <template>
@@ -81,23 +133,23 @@ function allLoggingUrl() {
</VCol> </VCol>
<VCol <VCol
cols="6" cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon" class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}" @click="() => {}"
> >
<VListItem <VListItem
class="pa-4" class="pa-4"
@click="netTestDialog = true" @click="ruleTestDialog = true"
> >
<VAvatar <VAvatar
size="48" size="48"
variant="tonal" variant="tonal"
> >
<VIcon icon="mdi-network-outline" /> <VIcon icon="mdi-filter-cog-outline" />
</VAvatar> </VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0"> <h6 class="text-base font-weight-medium mt-2 mb-0">
网络 优先级
</h6> </h6>
<span class="text-sm">测试网速连通性</span> <span class="text-sm">优先级规则测试</span>
</VListItem> </VListItem>
</VCol> </VCol>
</VRow> </VRow>
@@ -120,7 +172,51 @@ function allLoggingUrl() {
<h6 class="text-base font-weight-medium mt-2 mb-0"> <h6 class="text-base font-weight-medium mt-2 mb-0">
日志 日志
</h6> </h6>
<span class="text-sm">系统实时日志</span> <span class="text-sm">实时日志</span>
</VListItem>
</VCol>
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="netTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VIcon icon="mdi-network-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
网络
</h6>
<span class="text-sm">网速连通性测试</span>
</VListItem>
</VCol>
</VRow>
<VRow class="ma-0 mt-n1 border-t">
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="systemTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VIcon icon="mdi-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
系统
</h6>
<span class="text-sm">健康检查</span>
</VListItem> </VListItem>
</VCol> </VCol>
<VCol <VCol
@@ -130,18 +226,18 @@ function allLoggingUrl() {
> >
<VListItem <VListItem
class="pa-4" class="pa-4"
@click="ruleTestDialog = true" @click="messageDialog = true"
> >
<VAvatar <VAvatar
size="48" size="48"
variant="tonal" variant="tonal"
> >
<VIcon icon="mdi-filter-cog-outline" /> <VIcon icon="mdi-message-outline" />
</VAvatar> </VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0"> <h6 class="text-base font-weight-medium mt-2 mb-0">
优先级 消息
</h6> </h6>
<span class="text-sm">优先级规则测试</span> <span class="text-sm">消息中心</span>
</VListItem> </VListItem>
</VCol> </VCol>
</VRow> </VRow>
@@ -150,8 +246,10 @@ function allLoggingUrl() {
</VMenu> </VMenu>
<!-- 名称测试弹窗 --> <!-- 名称测试弹窗 -->
<VDialog <VDialog
v-if="nameTestDialog"
v-model="nameTestDialog" v-model="nameTestDialog"
max-width="50rem" max-width="50rem"
:fullscreen="displayWidth < (50 * 16)"
> >
<VCard title="名称识别测试"> <VCard title="名称识别测试">
<DialogCloseBtn @click="nameTestDialog = false" /> <DialogCloseBtn @click="nameTestDialog = false" />
@@ -162,8 +260,10 @@ function allLoggingUrl() {
</VDialog> </VDialog>
<!-- 网络测试弹窗 --> <!-- 网络测试弹窗 -->
<VDialog <VDialog
v-if="netTestDialog"
v-model="netTestDialog" v-model="netTestDialog"
max-width="35rem" max-width="35rem"
:fullscreen="displayWidth < (35 * 16)"
> >
<VCard title="网络测试"> <VCard title="网络测试">
<DialogCloseBtn @click="netTestDialog = false" /> <DialogCloseBtn @click="netTestDialog = false" />
@@ -174,9 +274,11 @@ function allLoggingUrl() {
</VDialog> </VDialog>
<!-- 实时日志弹窗 --> <!-- 实时日志弹窗 -->
<VDialog <VDialog
v-if="loggingDialog"
v-model="loggingDialog" v-model="loggingDialog"
class="w-full lg:w-4/5"
scrollable scrollable
max-width="70rem"
:fullscreen="displayWidth < (70 * 16)"
> >
<VCard> <VCard>
<DialogCloseBtn @click="loggingDialog = false" /> <DialogCloseBtn @click="loggingDialog = false" />
@@ -198,9 +300,11 @@ function allLoggingUrl() {
</VDialog> </VDialog>
<!-- 规则测试弹窗 --> <!-- 规则测试弹窗 -->
<VDialog <VDialog
v-if="ruleTestDialog"
v-model="ruleTestDialog" v-model="ruleTestDialog"
max-width="50rem" max-width="50rem"
scrollable scrollable
:fullscreen="displayWidth < (50 * 16)"
> >
<VCard title="优先级测试"> <VCard title="优先级测试">
<DialogCloseBtn @click="ruleTestDialog = false" /> <DialogCloseBtn @click="ruleTestDialog = false" />
@@ -209,4 +313,58 @@ function allLoggingUrl() {
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 系统健康检查弹窗 -->
<VDialog
v-if="systemTestDialog"
v-model="systemTestDialog"
max-width="50rem"
scrollable
:fullscreen="displayWidth < (50 * 16)"
>
<VCard title="系统健康检查">
<DialogCloseBtn @click="systemTestDialog = false" />
<VCardText>
<ModuleTestView />
</VCardText>
</VCard>
</VDialog>
<!-- 消息中心弹窗 -->
<VDialog
v-if="messageDialog"
v-model="messageDialog"
max-width="60rem"
scrollable
:fullscreen="displayWidth < (60 * 16)"
>
<VCard title="消息中心">
<DialogCloseBtn @click="messageDialog = false" />
<VCardText ref="chatContainer">
<MessageView @scroll="scrollMessageToEnd" />
</VCardText>
<VCardItem>
<VTextField
v-model="user_message"
placeholder="输入消息或命令"
outlined
hide-details
single-line
clearable
density="compact"
:disabled="sendButtonDisabled"
@keydown.enter="sendMessage"
>
<template #append>
<VBtn
color="primary"
:disabled="sendButtonDisabled"
@click="sendMessage"
>
发送
</VBtn>
</template>
</VTextField>
</VCardItem>
</VCard>
</VDialog>
</template> </template>

View File

@@ -15,7 +15,15 @@ import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss' import '@layouts/styles/index.scss'
import '@styles/styles.scss' import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css' import 'vue-toast-notification/dist/theme-bootstrap.css'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar';
import 'vue3-perfect-scrollbar/style.css';
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 加载字体
loadFonts() loadFonts()
// 创建Vue实例 // 创建Vue实例
@@ -24,6 +32,7 @@ const app = createApp(App)
// 注册全局组件 // 注册全局组件
app.component('VAceEditor', VAceEditor) app.component('VAceEditor', VAceEditor)
.component('VApexChart', VueApexCharts) .component('VApexChart', VueApexCharts)
.component('VDialogCloseBtn', DialogCloseBtn)
// 注册插件 // 注册插件
app app
@@ -34,5 +43,6 @@ app
position: 'bottom-right', position: 'bottom-right',
}) })
.use(VuetifyUseDialog) .use(VuetifyUseDialog)
.use(PerfectScrollbarPlugin)
.mount('#app') .mount('#app')
.$nextTick(() => removeEl('#loading-bg')) .$nextTick(() => removeEl('#loading-bg'))

View File

@@ -9,6 +9,12 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue' import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue' import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue' import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import api from '@/api'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDisplay } from 'vuetify'
// 显示器宽度
const displayWidth = useDisplay().width
// 仪表盘配置 // 仪表盘配置
const dashboard_names = { const dashboard_names = {
@@ -40,20 +46,38 @@ const default_config = {
playing: true, playing: true,
latest: true, latest: true,
} }
// 初始化默认值
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}')) const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
if (Object.keys(config.value).length === 0) { if (isNullOrEmptyObject(config.value)) {
config.value = default_config config.value = default_config
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
} }
// 设置项目 // 设置项目
function setDashboardConfig() { function setDashboardConfig() {
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value)) const data = JSON.stringify(config.value)
localStorage.setItem('MP_DASHBOARD', data)
// 保存到服务端
api.post('/user/config/Dashboard', data, {
headers: {
"Content-Type": "application/json"
}
})
dialog.value = false dialog.value = false
} }
</script> </script>
<template> <template>
<!-- 底部操作按钮 -->
<VFab
icon="mdi-view-dashboard-edit"
location="bottom end"
size="x-large"
fixed
app
appear
@click="dialog = true"
/>
<VRow class="match-height"> <VRow class="match-height">
<VCol <VCol
v-if="config.storage" v-if="config.storage"
@@ -132,15 +156,12 @@ function setDashboardConfig() {
<MediaServerLatest /> <MediaServerLatest />
</VCol> </VCol>
</VRow> </VRow>
<!-- 底部操作按钮 -->
<span class="fixed right-5 bottom-5">
<VBtn icon="mdi-view-dashboard-edit" class="me-2" color="primary" size="x-large" @click="dialog = true" />
</span>
<!-- 弹窗根据配置生成选项 --> <!-- 弹窗根据配置生成选项 -->
<VDialog <VDialog
v-model="dialog" v-model="dialog"
max-width="600" max-width="600"
scrollable scrollable
:fullscreen="displayWidth < 600"
> >
<VCard title="设置仪表板"> <VCard title="设置仪表板">
<VCardText> <VCardText>
@@ -150,6 +171,7 @@ function setDashboardConfig() {
:key="key" :key="key"
cols="12" cols="12"
md="4" md="4"
sm="4"
> >
<VCheckbox <VCheckbox
v-model="config[key]" v-model="config[key]"
@@ -168,6 +190,7 @@ function setDashboardConfig() {
<VSpacer /> <VSpacer />
<VBtn <VBtn
color="primary" color="primary"
variant="tonal"
@click="setDashboardConfig" @click="setDashboardConfig"
> >
保存 保存

View File

@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { debounce } from 'lodash'
import { VForm } from 'vuetify/components/VForm' import { VForm } from 'vuetify/components/VForm'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
@@ -13,6 +14,7 @@ const store = useStore()
const form = ref({ const form = ref({
username: '', username: '',
password: '', password: '',
otp_password: '',
remember: true, remember: true,
}) })
@@ -30,6 +32,12 @@ const backgroundImageUrl = ref('')
// 背景图片加载状态 // 背景图片加载状态
const isImageLoaded = ref(false) const isImageLoaded = ref(false)
// 是否开启双重验证
const isOTP = ref(false)
// 用户名称输入框
const usernameInput = ref()
// 获取背景图片 // 获取背景图片
async function fetchBackgroundImage() { async function fetchBackgroundImage() {
api api
@@ -41,20 +49,64 @@ async function fetchBackgroundImage() {
console.log(error) console.log(error)
}) })
} }
// 查询是否开启双重验证
const fetchOTP = debounce(async () => {
const userid = usernameInput.value?.value
if (!userid) {
isOTP.value = false
return
}
api
.get(`/user/otp/${userid}`)
.then((response: any) => {
isOTP.value = response.success
})
.catch((error: any) => {
console.log(error)
})
}, 500)
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem('MP_DASHBOARD')) {
localStorage.setItem('MP_DASHBOARD', data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem('MP_DASHBOARD')) {
return
}
await loadDashboardConfig()
}
async function afterLogin() {
// 尝试加载用户监控面板配置(本地无配置时才加载)
await tryLoadDashboardConfig()
// 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/')
}
// 登录获取token事件 // 登录获取token事件
function login() { function login() {
errorMessage.value = '' errorMessage.value = ''
// 进行表单校验 // 进行表单校验
if (!form.value.username || !form.value.password) if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) {
errorMessage.value = '请输入完整信息'
return return
}
// 用户名密码 // 用户名密码
const formData = new FormData() const formData = new FormData()
formData.append('username', form.value.username) formData.append('username', form.value.username)
formData.append('password', form.value.password) formData.append('password', form.value.password)
formData.append('otp_password', form.value.otp_password)
// 请求token // 请求token
api api
@@ -77,21 +129,16 @@ function login() {
store.dispatch('auth/updateUserName', username) store.dispatch('auth/updateUserName', username)
store.dispatch('auth/updateAvatar', avatar) store.dispatch('auth/updateAvatar', avatar)
// 跳转到首页或回原始页面 // 登录后处理
router.push(store.state.auth.originalPath) afterLogin()
}) })
.catch((error: any) => { .catch((error: any) => {
// 登录失败,显示错误提示 // 登录失败,显示错误提示
if (!error.response) if (!error.response) errorMessage.value = '登录失败,请检查网络连接'
errorMessage.value = '登录失败,请检查网络连接' else if (error.response.status === 401) errorMessage.value = '登录失败,请检查用户名、密码或双重验证是否正确'
else if (error.response.status === 401) else if (error.response.status === 403) errorMessage.value = '登录失败,您没有权限访问'
errorMessage.value = '登录失败,请检查用户名和密码是否正确' else if (error.response.status === 500) errorMessage.value = '登录失败,服务器错误'
else if (error.response.status === 403) else errorMessage.value = `登录失败 ${error.response.status},请检查用户名、密码或双重验证码是否正确`
errorMessage.value = '登录失败,您没有权限访问'
else if (error.response.status === 500)
errorMessage.value = '登录失败,服务器错误'
else
errorMessage.value = `登录失败 ${error.response.status},请检查用户名和密码是否正确`
}) })
} }
@@ -104,8 +151,7 @@ onMounted(() => {
// 如果token存在且保持登录状态为true则跳转到首页 // 如果token存在且保持登录状态为true则跳转到首页
if (token && remember) { if (token && remember) {
router.push('/') router.push('/')
} } else {
else {
// 获取背景图片 // 获取背景图片
fetchBackgroundImage() fetchBackgroundImage()
} }
@@ -134,64 +180,47 @@ onMounted(() => {
</div> </div>
</template> </template>
<VCardTitle class="font-weight-semibold text-2xl text-uppercase"> <VCardTitle class="font-weight-semibold text-2xl text-uppercase"> MoviePilot </VCardTitle>
MoviePilot
</VCardTitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<VForm <VForm ref="refForm" @submit.prevent="() => {}">
ref="refForm"
@submit.prevent="() => {}"
>
<VRow> <VRow>
<!-- username --> <!-- username -->
<VCol cols="12"> <VCol cols="12">
<VTextField <VTextField
ref="usernameInput"
v-model="form.username" v-model="form.username"
label="用户名" label="用户名"
type="text" type="text"
:rules="[requiredValidator]" :rules="[requiredValidator]"
@input="fetchOTP"
/> />
</VCol> </VCol>
<!-- password --> <!-- password -->
<VCol cols="12"> <VCol cols="12">
<VTextField <VTextField
v-model="form.password" v-model="form.password"
label="密码" label="密码"
:type="isPasswordVisible ? 'text' : 'password'" :type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon=" :append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
:rules="[requiredValidator]" :rules="[requiredValidator]"
@click:append-inner="isPasswordVisible = !isPasswordVisible" @click:append-inner="isPasswordVisible = !isPasswordVisible"
/> />
</VCol>
<div <VCol cols="12">
v-if="errorMessage" <VTextField v-if="isOTP" v-model="form.otp_password" label="双重验证码" type="input" />
class="text-error mt-1" <!-- remember me checkbox -->
> <div class="d-flex align-center justify-space-between flex-wrap">
<VCheckbox v-model="form.remember" label="保持登录" required />
</div>
</VCol>
<VCol cols="12">
<!-- login button -->
<VBtn block type="submit" @click="login"> 登录 </VBtn>
<div v-if="errorMessage" class="text-error mt-2 text-shadow">
{{ errorMessage }} {{ errorMessage }}
</div> </div>
<!-- remember me checkbox -->
<div class="d-flex align-center justify-space-between flex-wrap mt-1 mb-4">
<VCheckbox
v-model="form.remember"
label="保持登录"
required
/>
</div>
<!-- login button -->
<VBtn
block
type="submit"
@click="login"
>
登录
</VBtn>
</VCol> </VCol>
</VRow> </VRow>
</VForm> </VForm>
@@ -202,7 +231,7 @@ onMounted(() => {
</template> </template>
<style lang="scss"> <style lang="scss">
@use "@core/scss/pages/page-auth.scss"; @use '@core/scss/pages/page-auth.scss';
.v-card-item__prepend { .v-card-item__prepend {
padding-inline-end: 0 !important; padding-inline-end: 0 !important;

View File

@@ -1,79 +1,82 @@
<script setup lang="ts"> <script setup lang="ts">
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue' import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
const viewList = reactive<{apipath: string, linkurl: string, title: string}[]>([
{
apipath: 'tmdb/trending',
linkurl: "/browse/tmdb/trending?title=流行趋势",
title: "流行趋势",
},
{
apipath: "douban/showing",
linkurl: "/browse/douban/showing?title=正在热映",
title: "正在热映"
},
{
apipath: "bangumi/calendar",
linkurl: "/browse/bangumi/calendar?title=Bangumi每日放送",
title: "Bangumi每日放送"
},
{
apipath: "tmdb/movies",
linkurl: "/browse/tmdb/movies?title=热门电影",
title: "热门电影"
},
{
apipath: "tmdb/tvs?with_original_language=zh|en|ja|ko",
linkurl: "/browse/tmdb/tvs??with_original_language=zh|en|ja|ko&title=热门电视剧",
title: "热门电视剧"
},
{
apipath: "douban/movie_hot",
linkurl: "/browse/douban/movie_hot?title=热门电影",
title: "热门电影"
},
{
apipath: "douban/tv_hot",
linkurl: "/browse/douban/tv_hot?title=热门电视剧",
title: "热门电视剧"
},
{
apipath: "douban/tv_animation",
linkurl: "/browse/douban/tv_animation?title=热门动漫",
title: "热门动漫"
},
{
apipath: "douban/movies",
linkurl: "/browse/douban/movies?title=最新电影",
title: "最新电影"
},
{
apipath: "douban/tvs",
linkurl: "/browse/douban/tvs?title=最新电视剧",
title: "最新电视剧"
},
{
apipath: "douban/movie_top250",
linkurl: "/browse/douban/movie_top250?title=电影TOP250",
title: "电影TOP250"
},
{
apipath: "douban/tv_weekly_chinese",
linkurl: "/browse/douban/tv_weekly_chinese?title=国产剧集榜",
title: "国产剧集榜"
},
{
apipath: "douban/tv_weekly_global",
linkurl: "/browse/douban/tv_weekly_global?title=全球剧集榜",
title: "全球剧集榜"
}
])
</script> </script>
<template> <template>
<div> <div>
<MediaCardSlideView <MediaCardSlideView
apipath="tmdb/trending" v-for="item in viewList"
linkurl="/browse/tmdb/trending?title=流行趋势" :key="item.apipath"
title="流行趋势" v-bind="item"
/>
<MediaCardSlideView
apipath="douban/showing"
linkurl="/browse/douban/showing?title=正在热映"
title="正在热映"
/>
<MediaCardSlideView
apipath="tmdb/movies"
linkurl="/browse/tmdb/movies?title=热门电影"
title="热门电影"
/>
<MediaCardSlideView
apipath="tmdb/tvs?with_original_language=zh|en|ja|ko"
linkurl="/browse/tmdb/tvs??with_original_language=zh|en|ja|ko&title=热门电视剧"
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/movie_hot"
linkurl="/browse/douban/movie_hot?title=热门电影"
title="热门电影"
/>
<MediaCardSlideView
apipath="douban/tv_hot"
linkurl="/browse/douban/tv_hot?title=热门电视剧"
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/tv_animation"
linkurl="/browse/douban/tv_animation?title=热门动漫"
title="热门动漫"
/>
<MediaCardSlideView
apipath="douban/movies"
linkurl="/browse/douban/movies?title=最新电影"
title="最新电影"
/>
<MediaCardSlideView
apipath="douban/tvs"
linkurl="/browse/douban/tvs?title=最新电视剧"
title="最新电视剧"
/>
<MediaCardSlideView
apipath="douban/movie_top250"
linkurl="/browse/douban/movie_top250?title=电影TOP250"
title="电影TOP250"
/>
<MediaCardSlideView
apipath="douban/tv_weekly_chinese"
linkurl="/browse/douban/tv_weekly_chinese?title=国产剧集榜"
title="国产剧集榜"
/>
<MediaCardSlideView
apipath="douban/tv_weekly_global"
linkurl="/browse/douban/tv_weekly_global?title=全球剧集榜"
title="全球剧集榜"
/> />
</div> </div>
</template> </template>

View File

@@ -18,6 +18,9 @@ const type = route.query?.type?.toString() ?? ''
// 搜索字段 // 搜索字段
const area = route.query?.area?.toString() ?? '' const area = route.query?.area?.toString() ?? ''
// 搜索季
const season = route.query?.season?.toString() ?? ''
// 视图类型从localStorage中读取 // 视图类型从localStorage中读取
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card') const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
@@ -36,6 +39,12 @@ const progressValue = ref(0)
// 加载进度SSE // 加载进度SSE
const progressEventSource = ref<EventSource>() const progressEventSource = ref<EventSource>()
// 错误标题
const errorTitle = ref('没有数据')
// 错误描述
const errorDescription = ref('未搜索到任何资源')
// 使用SSE监听加载进度 // 使用SSE监听加载进度
function startLoadingProgress() { function startLoadingProgress() {
progressText.value = '正在搜索,请稍候...' progressText.value = '正在搜索,请稍候...'
@@ -75,13 +84,19 @@ async function fetchData() {
else { else {
startLoadingProgress() startLoadingProgress()
// 优先按TMDBID精确查询 // 优先按TMDBID精确查询
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) { if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
dataList.value = await api.get(`search/media/${keyword}`, { const result: {[key: string]: any} = await api.get(`search/media/${keyword}`, {
params: { params: {
mtype: type, mtype: type,
area, area,
season,
}, },
}) })
if (result.success){
dataList.value = result.data
} else {
errorDescription.value = result.message
}
} }
else { else {
// 按标题模糊查询 // 按标题模糊查询
@@ -105,16 +120,16 @@ onMounted(() => {
</script> </script>
<template> <template>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"> <LoadingBanner
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" /> v-if="!isRefreshed"
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" /> class="mt-12"
<span>{{ progressText }}</span> :text="progressText"
</div> :progress="progressValue"
/>
<NoDataFound <NoDataFound
v-if="dataList.length === 0 && isRefreshed" v-if="dataList.length === 0 && isRefreshed"
error-code="404" :error-title="errorTitle"
error-title="没有资源" :error-description="errorDescription"
error-description="没有搜索到符合条件的资源"
/> />
<div v-if="dataList.length > 0"> <div v-if="dataList.length > 0">
<TorrentRowListView <TorrentRowListView
@@ -127,20 +142,24 @@ onMounted(() => {
/> />
</div> </div>
<!-- 视图切换 --> <!-- 视图切换 -->
<span v-if="dataList.length > 0" class="fixed right-5 bottom-5"> <VFab
<VBtn v-if="viewType === 'list'"
v-if="viewType === 'list'" icon="mdi-view-grid"
size="x-large" location="bottom end"
icon="mdi-view-grid" size="x-large"
color="primary" fixed
@click="setViewType('card')" app
/> appear
<VBtn @click="setViewType('card')"
v-else />
size="x-large" <VFab
icon="mdi-view-list" v-else
color="primary" icon="mdi-view-list"
@click="setViewType('list')" location="bottom end"
/> size="x-large"
</span> fixed
app
appear
@click="setViewType('list')"
/>
</template> </template>

View File

@@ -80,6 +80,7 @@ export default {
// set v-rating default color to primary // set v-rating default color to primary
color: 'rgba(var(--v-theme-on-background),0.23)', color: 'rgba(var(--v-theme-on-background),0.23)',
activeColor: 'warning', activeColor: 'warning',
halfIncrements: true,
}, },
VProgressCircular: { VProgressCircular: {
// set v-progress-circular default color to primary // set v-progress-circular default color to primary

View File

@@ -5,39 +5,37 @@
#nprogress .bar { #nprogress .bar {
background: rgb(var(--v-theme-primary)) !important; background: rgb(var(--v-theme-primary)) !important;
top: env(safe-area-inset-top) !important; inset-block-start: env(safe-area-inset-top) !important;
} }
#nprogress .peg { #nprogress .peg {
box-shadow: 0 0 10px rgb(var(--v-theme-primary)), 0 0 5px rgb(var(--v-theme-primary)) !important; box-shadow: 0 0 10px rgb(var(--v-theme-primary)), 0 0 5px rgb(var(--v-theme-primary)) !important;
-webkit-transform: rotate(0deg) translate(0px, -1px); transform: rotate(0deg) translate(0, -1px);
-ms-transform: rotate(0deg) translate(0px, -1px);
transform: rotate(0deg) translate(0px, -1px);
} }
.v-toast--bottom { .v-toast--bottom {
margin-bottom: env(safe-area-inset-bottom);
z-index: 2500; z-index: 2500;
margin-block-end: env(safe-area-inset-bottom);
} }
.v-toast--top { .v-toast--top {
margin-top: env(safe-area-inset-top);
z-index: 2500; z-index: 2500;
margin-block-start: env(safe-area-inset-top);
} }
.v-dialog > .v-overlay__content { .v-dialog > .v-overlay__content {
margin-top: calc(env(safe-area-inset-top) + 1rem); margin-block-start: calc(env(safe-area-inset-top) + 1rem);
max-height: calc(100% - env(safe-area-inset-top) - 1rem); max-block-size: calc(100% - env(safe-area-inset-top) - 1rem);
} }
.v-dialog > .v-overlay__content{ .v-dialog > .v-overlay__content{
width: calc(100% - 1rem); inline-size: calc(100% - 1rem);
} }
.v-dialog--fullscreen > .v-overlay__content{ .v-dialog--fullscreen > .v-overlay__content{
margin-top: env(safe-area-inset-top); inline-size: 100%;
max-height: calc(100% - env(safe-area-inset-top)); margin-block-start: env(safe-area-inset-top);
width: 100%; max-block-size: calc(100% - env(safe-area-inset-top));
} }
/* router view transition fade-slide */ /* router view transition fade-slide */
@@ -62,21 +60,20 @@
} }
.text-moviepilot { .text-moviepilot {
background-clip: text;
background-image: linear-gradient(to bottom right,var(--tw-gradient-stops)); background-image: linear-gradient(to bottom right,var(--tw-gradient-stops));
color: transparent;
--tw-gradient-from: #818cf8; --tw-gradient-from: #818cf8;
--tw-gradient-to: rgba(129,140,248,0); --tw-gradient-to: rgba(129,140,248,0%);
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to); --tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to);
--tw-gradient-to: #c084fc; --tw-gradient-to: #c084fc;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
} }
.slider-header { .slider-header {
position: relative; position: relative;
margin-top: 1.5rem;
margin-bottom: 1rem;
display: flex; display: flex;
margin-block: 1.5rem 1rem;
} }
.slider-title { .slider-title {
@@ -87,25 +84,27 @@
line-height: 1.75rem; line-height: 1.75rem;
} }
@media (min-width: 640px){ @media (width >= 640px){
.slider-title { .slider-title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2.25rem; line-height: 2.25rem;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
// 美化滚动条 // 美化滚动条
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; block-size: 8px;
height: 8px; inline-size: 8px;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border-radius: 3px; border-radius: 3px;
background: rgb(var(--v-theme-perfect-scrollbar-thumb)); background: rgb(var(--v-theme-perfect-scrollbar-thumb));
-webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.2); box-shadow: inset 0 0 10px rgba(0,0,0,20%);
@media(hover){ @media(hover){
&:hover{ &:hover{
background: #a1a1a1; background: #a1a1a1;
@@ -120,10 +119,14 @@
.backdrop-blur { .backdrop-blur {
--tw-backdrop-blur: blur(8px)!important; --tw-backdrop-blur: blur(8px)!important;
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important; backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
} }
.v-toolbar{ .v-toolbar{
background: rgb(var(--v-table-header-background)); background: rgb(var(--v-table-header-background));
} }
.v-toast {
z-index: 2500 !important;
}

View File

@@ -52,70 +52,63 @@ async function fetchData({ done }: { done: any }) {
// 如果正在加载中,直接返回 // 如果正在加载中,直接返回
if (loading.value) { if (loading.value) {
done('ok') done('ok')
return return
} }
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错 // 加载到满屏或者加载出错
if (!hasScroll()) { if (!hasScroll()) {
// 加载多次 // 加载多次
while (!hasScroll()) { while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: getParams(), params: getParams(),
}) })
// 取消加载中
loading.value = false
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('ok') done('empty')
return return
} }
// 合并数据 // 合并数据
dataList.value = [...dataList.value, ...currData.value] dataList.value = [...dataList.value, ...currData.value]
// 页码+1 // 页码+1
page.value++ page.value++
// 返回加载成功
done('ok')
} }
} }
else { else {
// 加载一次 // 加载一次
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: getParams(), params: getParams(),
}) })
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok') done('ok')
return
} }
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
} }
// 取消加载中 // 取消加载中
loading.value = false loading.value = false
// 返回加载成功
done('ok')
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
// 返回加载失败 // 返回加载失败
done('error') done('error')
} }
@@ -123,16 +116,10 @@ async function fetchData({ done }: { done: any }) {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<VInfiniteScroll <VInfiniteScroll
mode="intersect" mode="intersect"
side="end" side="end"
@@ -141,6 +128,7 @@ async function fetchData({ done }: { done: any }) {
@load="fetchData" @load="fetchData"
> >
<template #loading /> <template #loading />
<template #empty />
<div <div
v-if="dataList.length > 0" v-if="dataList.length > 0"
class="grid gap-4 grid-media-card mx-3" class="grid gap-4 grid-media-card mx-3"

View File

@@ -11,6 +11,8 @@ const props = defineProps({
title: String, title: String,
}) })
provide('rankingPropsKey', reactive({...props}))
// 组件加载完成 // 组件加载完成
const componentLoaded = ref(false) const componentLoaded = ref(false)
@@ -39,12 +41,11 @@ onMounted(fetchData)
<template> <template>
<SlideView <SlideView
v-if="componentLoaded" v-if="componentLoaded"
v-bind="props"
> >
<template #content> <template #content>
<template <template
v-for="data in dataList" v-for="data in dataList"
:key="data.tmdb_id || data.douban_id" :key="data.tmdb_id || data.douban_id || data.bangumi_id"
> >
<MediaCard <MediaCard
:media="data" :media="data"

View File

@@ -8,7 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router' import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue' import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
// 输入参数 // 输入参数
const mediaProps = defineProps({ const mediaProps = defineProps({
@@ -46,10 +46,14 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号 // 订阅编号
const subscribeId = ref<number>() const subscribeId = ref<number>()
// 订阅规则 // 获得mediaid
const subscribeRules = ref({ function getMediaId() {
show_edit_dialog: false, return mediaDetail.value?.tmdb_id
}) ? `tmdb:${mediaDetail.value?.tmdb_id}`
: mediaDetail.value?.douban_id
? `douban:${mediaDetail.value?.douban_id}`
: `bangumi:${mediaDetail.value?.bangumi_id}`
}
// 调用API查询详情 // 调用API查询详情
async function getMediaDetail() { async function getMediaDetail() {
@@ -60,7 +64,7 @@ async function getMediaDetail() {
}, },
}) })
isRefreshed.value = true isRefreshed.value = true
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id) if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id)
return return
// 检查存在状态 // 检查存在状态
@@ -113,7 +117,7 @@ async function checkExists() {
// 查询当前媒体是否已订阅 // 查询当前媒体是否已订阅
async function checkSubscribe(season = 0) { async function checkSubscribe(season = 0) {
try { try {
const mediaid = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}` const mediaid = getMediaId()
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, { const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: { params: {
@@ -198,6 +202,7 @@ async function addSubscribe(season = 0) {
year: mediaDetail.value?.year, year: mediaDetail.value?.year,
tmdbid: mediaDetail.value?.tmdb_id, tmdbid: mediaDetail.value?.tmdb_id,
doubanid: mediaDetail.value?.douban_id, doubanid: mediaDetail.value?.douban_id,
bangumiid: mediaDetail.value?.bangumi_id,
season, season,
best_version, best_version,
}) })
@@ -220,9 +225,12 @@ async function addSubscribe(season = 0) {
) )
// 显示编辑弹窗 // 显示编辑弹窗
if (result.success && subscribeRules.value.show_edit_dialog) { if (result.success) {
subscribeId.value = result.data.id const show_edit_dialog = await queryDefaultSubscribeConfig()
subscribeEditDialog.value = true if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
} }
} }
catch (error) { catch (error) {
@@ -253,9 +261,7 @@ async function removeSubscribe(season: number) {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
const mediaid = mediaDetail.value?.tmdb_id const mediaid = getMediaId()
? `tmdb:${mediaDetail.value?.tmdb_id}`
: `douban:${mediaDetail.value?.douban_id}`
const result: { [key: string]: any } = await api.delete( const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`, `subscribe/media/${mediaid}`,
@@ -282,20 +288,6 @@ async function removeSubscribe(season: number) {
doneNProgress() doneNProgress()
} }
// 查询订阅弹窗规则
async function querySubscribeRules() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
subscribeRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 订阅按钮响应 // 订阅按钮响应
function handleSubscribe(season = 0) { function handleSubscribe(season = 0) {
if (isSubscribed.value) if (isSubscribed.value)
@@ -330,6 +322,11 @@ function getTvdbLink() {
return `https://www.thetvdb.com/series/${mediaDetail.value.tvdb_id}` return `https://www.thetvdb.com/series/${mediaDetail.value.tvdb_id}`
} }
// 拼装Bangumi地址
function getBangumiLink() {
return `https://bgm.tv/subject/${mediaDetail.value.bangumi_id}`
}
// 拼装集图片地址 // 拼装集图片地址
function getEpisodeImage(stillPath: string) { function getEpisodeImage(stillPath: string) {
if (!stillPath) if (!stillPath)
@@ -405,13 +402,14 @@ function joinArray(arr: string[]) {
// 开始搜索 // 开始搜索
function handleSearch(area: string) { function handleSearch(area: string) {
const keyword = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}` const keyword = getMediaId()
router.push({ router.push({
path: '/resource', path: '/resource',
query: { query: {
keyword, keyword,
type: mediaDetail.value.type, type: mediaDetail.value.type,
area, area,
season: mediaDetail.value.season,
}, },
}) })
} }
@@ -436,24 +434,36 @@ async function handlePlay() {
} }
} }
async function queryDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (mediaProps.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value)
return result.data.value.show_edit_dialog
}
catch (error) {
console.log(error)
}
return false
}
onBeforeMount(() => { onBeforeMount(() => {
getMediaDetail() getMediaDetail()
querySubscribeRules()
}) })
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular <div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
size="48"
indeterminate
color="primary"
/>
</div>
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path"> <template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96"> <div class="vue-media-back absolute left-0 top-0 w-full h-96">
<VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover /> <VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
@@ -492,7 +502,7 @@ onBeforeMount(() => {
</span> </span>
</div> </div>
<div class="media-actions"> <div class="media-actions">
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info" class="mb-2"> <VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" variant="tonal" color="info" class="mb-2">
<template #prepend> <template #prepend>
<VIcon icon="mdi-magnify" /> <VIcon icon="mdi-magnify" />
</template> </template>
@@ -518,7 +528,7 @@ onBeforeMount(() => {
</VList> </VList>
</VMenu> </VMenu>
</VBtn> </VBtn>
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)"> <VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
<template #prepend> <template #prepend>
<VIcon :icon="getSubscribeIcon" /> <VIcon :icon="getSubscribeIcon" />
</template> </template>
@@ -580,6 +590,12 @@ onBeforeMount(() => {
<span class="ms-1">TheTvDb</span> <span class="ms-1">TheTvDb</span>
</div> </div>
</a> </a>
<a v-if="mediaDetail.bangumi_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getBangumiLink()" target="_blank">
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
<VIcon icon="mdi-link" />
<span class="ms-1">Bangumi</span>
</div>
</a>
</div> </div>
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4"> <h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4">
@@ -618,16 +634,10 @@ onBeforeMount(() => {
</VExpansionPanelTitle> </VExpansionPanelTitle>
<VExpansionPanelText> <VExpansionPanelText>
<template #default> <template #default>
<div <LoadingBanner
v-if="!seasonEpisodesInfo[season.season_number || 0]" v-if="!seasonEpisodesInfo[season.season_number || 0]"
class="mt-3 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-3"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<div class="flex flex-col justify-center divide-y divide-gray-700"> <div class="flex flex-col justify-center divide-y divide-gray-700">
<div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"> <div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4">
<div class="flex-1"> <div class="flex-1">
@@ -740,6 +750,33 @@ onBeforeMount(() => {
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="mediaDetail.bangumi_id" class="media-overview-right">
<div class="media-facts">
<div v-if="mediaDetail.vote_average" class="media-ratings">
<VRating
v-model="mediaDetail.vote_average"
density="compact"
length="10"
class="ma-2"
readonly
/>
</div>
<div v-if="mediaDetail.bangumi_id" class="media-fact">
<span>ID</span>
<span class="media-fact-value">{{ mediaDetail.bangumi_id }}</span>
</div>
<div v-if="mediaDetail.original_title" class="media-fact">
<span>原始标题</span>
<span class="media-fact-value">{{ mediaDetail.original_title }}</span>
</div>
<div v-if="mediaDetail.release_date" class="media-fact border-b-0">
<span>上映日期</span>
<span class="media-fact-value">
{{ mediaDetail.release_date }}
</span>
</div>
</div>
</div>
</div> </div>
<div v-if="mediaDetail.tmdb_id"> <div v-if="mediaDetail.tmdb_id">
<PersonCardSlideView <PersonCardSlideView
@@ -757,6 +794,14 @@ onBeforeMount(() => {
type="douban" type="douban"
/> />
</div> </div>
<div v-else-if="mediaDetail.bangumi_id">
<PersonCardSlideView
:apipath="`bangumi/credits/${mediaDetail.bangumi_id}`"
:linkurl="`/credits/bangumi/credits/${mediaDetail.bangumi_id}?title=演员阵容&type=bangumi`"
title="演员阵容"
type="bangumi"
/>
</div>
<div v-if="mediaDetail.tmdb_id"> <div v-if="mediaDetail.tmdb_id">
<MediaCardSlideView <MediaCardSlideView
:apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`" :apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`"
@@ -771,6 +816,13 @@ onBeforeMount(() => {
title="推荐" title="推荐"
/> />
</div> </div>
<div v-else-if="mediaDetail.bangumi_id">
<MediaCardSlideView
:apipath="`bangumi/recommend/${mediaDetail.bangumi_id}`"
:linkurl="`/browse/bangumi/recommend/${mediaDetail.bangumi_id}?title=推荐`"
title="推荐"
/>
</div>
<div v-if="mediaDetail.tmdb_id"> <div v-if="mediaDetail.tmdb_id">
<MediaCardSlideView <MediaCardSlideView
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`" :apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
@@ -781,13 +833,13 @@ onBeforeMount(() => {
</div> </div>
</div> </div>
<NoDataFound <NoDataFound
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed" v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && !mediaDetail.bangumi_id && isRefreshed"
error-code="500" error-code="500"
error-title="出错啦" error-title="出错啦"
error-description="未识别到媒体信息" error-description="未识别到媒体信息"
/> />
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditForm <SubscribeEditDialog
v-model="subscribeEditDialog" v-model="subscribeEditDialog"
:subid="subscribeId" :subid="subscribeId"
@close="subscribeEditDialog = false" @close="subscribeEditDialog = false"
@@ -829,7 +881,7 @@ onBeforeMount(() => {
padding-block-start: 1rem; padding-block-start: 1rem;
} }
@media (min-width: 1280px) { @media (width >= 1280px) {
.media-header { .media-header {
flex-direction: row; flex-direction: row;
align-items: flex-end; align-items: flex-end;
@@ -839,65 +891,66 @@ onBeforeMount(() => {
.media-overview { .media-overview {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 2rem; padding-block: 2rem 1rem;
padding-bottom: 1rem;
} }
@media (min-width: 1024px) { @media (width >= 1024px) {
.media-overview { .media-overview {
flex-direction: row; flex-direction: row;
} }
} }
.media-poster { .media-poster {
width: 8rem;
overflow: hidden; overflow: hidden;
border-radius: .25rem; border-radius: .25rem;
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
inline-size: 8rem;
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 10%), 0 1px 2px -1px rgba(0, 0, 0, 10%);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
} }
@media (min-width: 1280px) { @media (width >= 1280px) {
.media-poster { .media-poster {
margin-right: 1rem; inline-size: 13rem;
width: 13rem; margin-inline-end: 1rem;
} }
} }
@media (min-width: 768px) { @media (width >= 768px) {
.media-poster { .media-poster {
width: 11rem;
border-radius: .5rem; border-radius: .5rem;
--tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, .25);
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
inline-size: 11rem;
--tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, 25%);
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
} }
} }
.media-title { .media-title {
margin-top: 1rem;
display: flex; display: flex;
flex: 1 1 0%; flex: 1 1 0%;
flex-direction: column; flex-direction: column;
margin-block-start: 1rem;
text-align: center; text-align: center;
} }
@media (min-width: 1280px) { @media (width >= 1280px) {
.media-title { .media-title {
margin-right: 1rem; margin-block-start: 0;
margin-top: 0; margin-inline-end: 1rem;
text-align: left; text-align: start;
} }
} }
.media-title>h1 { .media-title>h1 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem;
font-weight: 700; font-weight: 700;
line-height: 2rem;
} }
@media (min-width: 1280px) { @media (width >= 1280px) {
.media-title>h1 { .media-title>h1 {
font-size: 2.25rem; font-size: 2.25rem;
line-height: 2.5rem; line-height: 2.5rem;
@@ -905,23 +958,23 @@ onBeforeMount(() => {
} }
ul.media-crew { ul.media-crew {
margin-top: 1.5rem;
display: grid; display: grid;
grid-template-columns: repeat(2,minmax(0,1fr));
gap: 1.5rem; gap: 1.5rem;
grid-template-columns: repeat(2,minmax(0,1fr));
margin-block-start: 1.5rem;
} }
@media (min-width: 640px) { @media (width >= 640px) {
ul.media-crew { ul.media-crew {
grid-template-columns: repeat(3,minmax(0,1fr)); grid-template-columns: repeat(3,minmax(0,1fr));
} }
} }
ul.media-crew>li { ul.media-crew>li {
grid-column: span 1/span 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-weight: 700; font-weight: 700;
grid-column: span 1/span 1;
} }
a.crew-name { a.crew-name {
@@ -929,27 +982,27 @@ a.crew-name {
} }
.media-status { .media-status {
margin-bottom: .5rem; margin-block-end: .5rem;
} }
.media-attributes { .media-attributes {
margin-top: .25rem;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-block-start: .25rem;
} }
@media (min-width: 1280px) { @media (width >= 1280px) {
.media-attributes { .media-attributes {
margin-top: 0;
justify-content: flex-start; justify-content: flex-start;
font-size: 1rem; font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
margin-block-start: 0;
} }
} }
@media (min-width: 640px) { @media (width >= 640px) {
.media-attributes { .media-attributes {
font-size: .875rem; font-size: .875rem;
line-height: 1.25rem; line-height: 1.25rem;
@@ -958,21 +1011,21 @@ a.crew-name {
.media-actions { .media-actions {
position: relative; position: relative;
margin-top: 1rem;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-block-start: 1rem;
} }
@media (min-width: 1280px) { @media (width >= 1280px) {
.media-actions { .media-actions {
margin-top: 0; margin-block-start: 0;
} }
} }
@media (min-width: 640px) { @media (width >= 640px) {
.media-actions { .media-actions {
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: flex-end; justify-content: flex-end;
@@ -983,42 +1036,45 @@ a.crew-name {
flex: 1 1 0%; flex: 1 1 0%;
} }
@media (min-width: 1024px) { @media (width >= 1024px) {
.media-overview-left { .media-overview-left {
margin-right: 2rem; margin-inline-end: 2rem;
} }
} }
.media-overview-right { .media-overview-right {
margin-top: 2rem; inline-size: 100%;
width: 100%; margin-block-start: 2rem;
} }
@media (min-width: 1024px) { @media (width >= 1024px) {
.media-overview-right { .media-overview-right {
margin-top: 0; inline-size: 20rem;
width: 20rem; margin-block-start: 0;
} }
} }
.media-facts { .media-facts {
border-radius: 0.5rem;
border-width: 1px; border-width: 1px;
--tw-border-opacity: 1;
border-color: rgb(55 65 81/var(--tw-border-opacity)); border-color: rgb(55 65 81/var(--tw-border-opacity));
--tw-bg-opacity: 1; border-radius: 0.5rem;
font-size: .875rem; font-size: .875rem;
line-height: 1.25rem;
font-weight: 700; font-weight: 700;
line-height: 1.25rem;
--tw-border-opacity: 1;
--tw-bg-opacity: 1;
--tw-text-opacity: 1; --tw-text-opacity: 1;
} }
.media-ratings { .media-ratings {
border-bottom-width: 1px;
--tw-border-opacity: 1;
border-color: rgb(55 65 81/var(--tw-border-opacity)); border-color: rgb(55 65 81/var(--tw-border-opacity));
padding: 0.5rem 1rem; border-block-end-width: 1px;
font-weight: 500; font-weight: 500;
padding-block: 0.5rem;
padding-inline: 1rem;
--tw-border-opacity: 1;
} }
.media-ratings { .media-ratings {
@@ -1030,19 +1086,21 @@ a.crew-name {
.media-fact { .media-fact {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-bottom-width: 1px;
--tw-border-opacity: 1;
border-color: rgb(55 65 81/var(--tw-border-opacity)); border-color: rgb(55 65 81/var(--tw-border-opacity));
padding: 0.5rem 1rem; border-block-end-width: 1px;
padding-block: 0.5rem;
padding-inline: 1rem;
--tw-border-opacity: 1;
} }
.media-overview h2 { .media-overview h2 {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700; font-weight: 700;
line-height: 1.75rem;
} }
@media (min-width: 640px) { @media (width >= 640px) {
.media-overview h2 { .media-overview h2 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
@@ -1050,13 +1108,13 @@ a.crew-name {
} }
.tagline { .tagline {
margin-bottom: 1rem;
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem;
font-style: italic; font-style: italic;
line-height: 1.75rem;
margin-block-end: 1rem;
} }
@media (min-width: 1024px) { @media (width >= 1024px) {
.tagline { .tagline {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;

View File

@@ -42,74 +42,67 @@ async function fetchData({ done }: { done: any }) {
// 如果正在加载中,直接返回 // 如果正在加载中,直接返回
if (loading.value) { if (loading.value) {
done('ok') done('ok')
return return
} }
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错 // 加载到满屏或者加载出错
if (!hasScroll()) { if (!hasScroll()) {
// 加载多次 // 加载多次
while (!hasScroll()) { while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: { params: {
page: page.value, page: page.value,
}, },
}) })
// 取消加载中
loading.value = false
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok') done('ok')
return
} }
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
} }
} }
else { else {
// 加载一次 // 加载一次
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: { params: {
page: page.value, page: page.value,
}, },
}) })
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok') done('ok')
return
} }
// 取消加载中
// 合并数据 loading.value = false
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
} }
// 取消加载中
loading.value = false
// 返回加载成功
done('ok')
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
// 返回加载失败 // 返回加载失败
done('error') done('error')
} }
@@ -117,16 +110,10 @@ async function fetchData({ done }: { done: any }) {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<VInfiniteScroll <VInfiniteScroll
mode="intersect" mode="intersect"
side="end" side="end"
@@ -135,6 +122,7 @@ async function fetchData({ done }: { done: any }) {
@load="fetchData" @load="fetchData"
> >
<template #loading /> <template #loading />
<template #empty />
<div <div
v-if="dataList.length > 0 && props.type === 'tmdb'" v-if="dataList.length > 0 && props.type === 'tmdb'"
class="grid gap-4 grid-media-card mx-3" class="grid gap-4 grid-media-card mx-3"

View File

@@ -3,6 +3,7 @@ import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
import api from '@/api' import api from '@/api'
import SlideView from '@/components/slide/SlideView.vue' import SlideView from '@/components/slide/SlideView.vue'
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue' import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
import BangumiPersonCard from '@/components/cards/BangumiPersonCard.vue'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -12,6 +13,8 @@ const props = defineProps({
type: String, type: String,
}) })
provide('rankingPropsKey', reactive({...props}))
// 组件加载完成 // 组件加载完成
const componentLoaded = ref(false) const componentLoaded = ref(false)
@@ -40,7 +43,6 @@ onMounted(fetchData)
<template> <template>
<SlideView <SlideView
v-if="componentLoaded" v-if="componentLoaded"
v-bind="props"
> >
<template #content> <template #content>
<template <template
@@ -59,6 +61,12 @@ onMounted(fetchData)
height="15rem" height="15rem"
width="10rem" width="10rem"
/> />
<BangumiPersonCard
v-if="props.type === 'bangumi'"
:person="data"
height="15rem"
width="10rem"
/>
</template> </template>
</template> </template>
</SlideView> </SlideView>

View File

@@ -48,16 +48,10 @@ onBeforeMount(() => {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<div v-if="personDetail.id" class="max-w-8xl mx-auto px-4"> <div v-if="personDetail.id" class="max-w-8xl mx-auto px-4">
<div class="relative z-10 mt-4 mb-8 flex flex-col items-center lg:flex-row "> <div class="relative z-10 mt-4 mb-8 flex flex-col items-center lg:flex-row ">
<VAvatar <VAvatar

View File

@@ -68,6 +68,14 @@ function initOptions(data: Context) {
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix) optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
} }
// 对季过滤选项进行排序
const sortSeasonFilterOptions = computed(() => {
return seasonFilterOptions.value.sort((a, b) => {
// 按字符串升序排序
return a.localeCompare(b, 'zh-Hans-CN', { sensitivity: 'accent' })
})
})
// 计算分组后的列表 // 计算分组后的列表
onMounted(() => { onMounted(() => {
// 数据分组 // 数据分组
@@ -154,7 +162,7 @@ watchEffect(() => {
<VCol v-if="seasonFilterOptions.length > 0" cols="6" md=""> <VCol v-if="seasonFilterOptions.length > 0" cols="6" md="">
<VSelect <VSelect
v-model="filterForm.season" v-model="filterForm.season"
:items="seasonFilterOptions" :items="sortSeasonFilterOptions"
size="small" size="small"
density="compact" density="compact"
chips chips

View File

@@ -104,16 +104,16 @@ onMounted(() => {
<template> <template>
<VRow> <VRow>
<VCol> <VCol>
<VList v-if="dataList.length === 0" lines="three" class="rounded"> <VList v-if="dataList.length === 0" lines="three" class="rounded p-0">
<VListItem> <VListItem>
<VListItemTitle>没有附合当前过滤条件的资源</VListItemTitle> <VListItemTitle>没有附合当前过滤条件的资源</VListItemTitle>
</VListItem> </VListItem>
</VList> </VList>
<div> <VList v-if="dataList.length !== 0" lines="three" class="rounded p-0">
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"> <div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
<TorrentItem v-if="defer(index)" :torrent="item" /> <TorrentItem v-if="defer(index)" :torrent="item" />
</div> </div>
</div> </VList>
</VCol> </VCol>
<VCol xl="2" md="3" class="d-none d-md-block"> <VCol xl="2" md="3" class="d-none d-md-block">
<VList lines="one" class="rounded"> <VList lines="one" class="rounded">

View File

@@ -1,9 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api' import api from '@/api'
import type { Plugin } from '@/api/types' import type { Plugin } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import PluginAppCard from '@/components/cards/PluginAppCard.vue' import PluginAppCard from '@/components/cards/PluginAppCard.vue'
import PluginCard from '@/components/cards/PluginCard.vue' import PluginCard from '@/components/cards/PluginCard.vue'
import noImage from '@images/logos/plugin.png'
// 已安装插件列表 // 已安装插件列表
const dataList = ref<Plugin[]>([]) const dataList = ref<Plugin[]>([])
@@ -20,16 +22,118 @@ const isAppMarketLoaded = ref(false)
// APP市场窗口 // APP市场窗口
const PluginAppDialog = ref(false) const PluginAppDialog = ref(false)
// 插件安装统计
const PluginStatistics = ref<{ [key: string]: number }>({})
// 搜索窗口
const SearchDialog = ref(false)
// 搜索关键字
const keyword = ref('')
// 每一个插件的图标加载状态
const pluginIconLoaded = ref<{ [key: string]: boolean }>({})
// 每一个插件的动作标识
const pluginActions = ref<{ [key: string]: boolean }>({})
// 提示框
const $toast = useToast()
// 进度框
const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在安装插件...')
// 关闭插件市场窗口 // 关闭插件市场窗口
function pluginDialogClose() { function pluginDialogClose() {
PluginAppDialog.value = false PluginAppDialog.value = false
} }
// 安装插件
async function installPlugin(item: Plugin) {
try {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在安装 ${item?.plugin_name} v${item?.plugin_version} ...`
const result: { [key: string]: any } = await api.get(
`plugin/install/${item?.id}`,
{
params: {
repo_url: item?.repo_url,
force: item?.has_update,
},
},
)
// 隐藏等待提示框
progressDialog.value = false
if (result.success) {
$toast.success(`插件 ${item?.plugin_name} 安装成功!`)
// 刷新
refreshData()
}
else {
$toast.error(`插件 ${item?.plugin_name} 安装失败:${result.message}`)
}
}
catch (error) {
console.error(error)
}
}
// 打开插件搜索结果
function openPlugin(item: Plugin) {
// 如果是已安装插件则打开插件详情
if (item.installed === true) {
// 标记插件动作
pluginActions.value[item.id || '0'] = true
}
else {
// 如果是未安装插件则安装
installPlugin(item)
}
closeSearchDialog()
}
// 关闭插件搜索窗口
function closeSearchDialog() {
SearchDialog.value = false
}
// 插件图标加载错误
function pluginIconError(item: Plugin) {
pluginIconLoaded.value[item.id || '0'] = false
}
// 插件图标地址
function pluginIcon(item: Plugin) {
// 如果图片加载错误
if (pluginIconLoaded.value[item.id || '0'] === false)
return noImage
// 如果是网络图片则使用代理后返回
if (item?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(item?.plugin_icon)}`
return `./plugin_icon/${item?.plugin_icon}`
}
// 过滤插件
const filterPlugins = computed(() => {
const all_list = [...dataList.value, ...uninstalledList.value]
return all_list.filter((item: Plugin) => {
return item.plugin_name?.includes(keyword.value) || item.plugin_desc?.includes(keyword.value)
})
})
// 新安装了插件 // 新安装了插件
function pluginInstalled() { function pluginInstalled() {
fetchInstalledPlugins()
pluginDialogClose() pluginDialogClose()
fetchUninstalledPlugins() refreshData()
} }
// 获取插件列表数据 // 获取插件列表数据
@@ -55,42 +159,75 @@ async function fetchUninstalledPlugins() {
state: 'market', state: 'market',
}, },
}) })
// 设置APP市场加载完成
isAppMarketLoaded.value = true isAppMarketLoaded.value = true
// 设置更新状态
for (const uninstalled of uninstalledList.value) {
for (const data of dataList.value) {
if (uninstalled.id === data.id) {
data.has_update = true
data.repo_url = uninstalled.repo_url
data.history = uninstalled.history
}
}
}
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
} }
} }
// 加载时获取数据 // 加载插件统计数据
onBeforeMount(() => { async function getPluginStatistics() {
try {
PluginStatistics.value = await api.get('plugin/statistic')
}
catch (error) {
console.error(error)
}
}
// 加载所有数据
function refreshData() {
fetchInstalledPlugins() fetchInstalledPlugins()
fetchUninstalledPlugins() fetchUninstalledPlugins()
}
// 对uninstalledList进行排序按PluginStatistics倒序
const sortedUninstalledList = computed(() => {
const list = uninstalledList.value.filter(item => !item.has_update)
if (PluginStatistics.value.length === 0)
return list
return list.sort((a, b) => {
return PluginStatistics.value[b.id || '0'] - PluginStatistics.value[a.id || '0']
})
})
// 加载时获取数据
onBeforeMount(() => {
refreshData()
getPluginStatistics()
}) })
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<div <div
v-if="dataList.length > 0" v-if="dataList.length > 0"
class="grid gap-4 grid-plugin-card" class="grid gap-4 grid-plugin-card"
> >
<PluginCard <PluginCard
v-for="data in dataList" v-for="data in dataList"
:key="data.id" :key="`${data.id}_v${data.plugin_version}`"
:count="PluginStatistics[data.id || '0']"
:plugin="data" :plugin="data"
@remove="fetchInstalledPlugins" :action="pluginActions[data.id || '0']"
@save="fetchInstalledPlugins" @remove="refreshData"
@save="refreshData"
@action-done="pluginActions[data.id || '0'] = false"
/> />
</div> </div>
<NoDataFound <NoDataFound
@@ -100,7 +237,17 @@ onBeforeMount(() => {
error-description="点击右下角按钮前往插件市场安装插件" error-description="点击右下角按钮前往插件市场安装插件"
/> />
<!-- App市场 --> <!-- App市场 -->
<VFab
icon="mdi-store-plus"
location="bottom end"
size="x-large"
fixed
app
appear
@click="PluginAppDialog = true"
/>
<VDialog <VDialog
v-if="PluginAppDialog"
v-model="PluginAppDialog" v-model="PluginAppDialog"
fullscreen fullscreen
scrollable scrollable
@@ -108,16 +255,6 @@ onBeforeMount(() => {
:z-index="1010" :z-index="1010"
transition="dialog-bottom-transition" transition="dialog-bottom-transition"
> >
<!-- Dialog Activator -->
<template #activator="{ props }">
<VBtn
icon="mdi-store-plus"
v-bind="props"
size="x-large"
class="fixed right-5 bottom-5"
/>
</template>
<!-- Dialog Content --> <!-- Dialog Content -->
<VCard> <VCard>
<!-- Toolbar --> <!-- Toolbar -->
@@ -141,22 +278,16 @@ onBeforeMount(() => {
</VToolbar> </VToolbar>
</div> </div>
<VCardText> <VCardText>
<div <LoadingBanner
v-if="!isAppMarketLoaded" v-if="!isAppMarketLoaded"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isAppMarketLoaded"
size="48"
indeterminate
color="primary"
/>
</div>
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card"> <div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
<PluginAppCard <PluginAppCard
v-for="data in uninstalledList" v-for="data in sortedUninstalledList"
:key="data.id" :key="`${data.id}_v${data.plugin_version}`"
:plugin="data" :plugin="data"
:count="PluginStatistics[data.id || '0']"
@install="pluginInstalled" @install="pluginInstalled"
/> />
</div> </div>
@@ -169,11 +300,106 @@ onBeforeMount(() => {
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 插件搜索 -->
<VFab
icon="mdi-magnify"
color="info"
location="bottom end"
class="mb-2"
size="x-large"
fixed
app
appear
@click="SearchDialog = true"
/>
<VDialog
v-if="SearchDialog"
v-model="SearchDialog"
scrollable
:z-index="1010"
max-width="40rem"
max-height="85vh"
>
<VCard
class="mx-auto"
width="100%"
>
<VToolbar flat class="p-0">
<VTextField
v-model="keyword"
label="搜索插件"
single-line
placeholder="插件名称或描述"
variant="solo"
prepend-inner-icon="mdi-magnify"
flat
class="mx-1"
/>
</VToolbar>
<DialogCloseBtn @click="closeSearchDialog" />
<VList
v-if="filterPlugins.length > 0"
lines="two"
>
<template v-for="(item, i) in filterPlugins" :key="i">
<VListItem
@click="openPlugin(item)"
>
<template #prepend>
<VAvatar>
<VImg
:src="pluginIcon(item)"
@error="pluginIconError(item)"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
</div>
</template>
</VImg>
</VAvatar>
</template>
<VListItemTitle>
{{ item.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ item?.plugin_version }}</span>
<VIcon
v-if="item.installed"
color="success"
icon="mdi-check-circle"
class="ms-2"
size="small"
/>
</VListItemTitle>
<VListItemSubtitle class="mt-2" v-html="item.plugin_desc" />
</VListItem>
</template>
</VList>
</VCard>
</VDialog>
<!-- 安装插件进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
</template> </template>
<style lang="scss"> <style lang="scss">
.grid-plugin-card { .grid-plugin-card {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
padding-block-end: 1rem; padding-block-end: 1rem;
} }
</style> </style>

View File

@@ -44,7 +44,7 @@ const filteredDataList = computed(() => {
if (superUser) if (superUser)
return dataList.value return dataList.value
else else
return dataList.value.filter(data => data.userid === userName) return dataList.value.filter(data => data.userid === userName || data.username === userName)
}) })
// 加载时获取数据 // 加载时获取数据
@@ -67,17 +67,10 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<PullRefresh <PullRefresh
v-model="loading" v-model="loading"
@refresh="onRefresh" @refresh="onRefresh"

View File

@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { debounce } from 'lodash'
import { ref, unref } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import api from '@/api' import api from '@/api'
import type { TransferHistory } from '@/api/types' import type { TransferHistory } from '@/api/types'
import ReorganizeForm from '@/components/form/ReorganizeForm.vue' import ReorganizeDialog from '@/components/dialog/ReorganizeDialog.vue'
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -57,6 +58,14 @@ const headers = [
}, },
] ]
const pageRange = [
{ title: '25', value: 25 },
{ title: '50', value: 50 },
{ title: '100', value: 100 },
{ title: '1000', value: 1000 },
{ title: 'All', value: -1 },
]
// 数据列表 // 数据列表
const dataList = ref<TransferHistory[]>([]) const dataList = ref<TransferHistory[]>([])
@@ -93,42 +102,6 @@ const deleteConfirmDialog = ref(false)
// 确认框标题 // 确认框标题
const confirmTitle = ref('') const confirmTitle = ref('')
// 获取订阅列表数据
async function fetchData({ page, itemsPerPage }: { page: number; itemsPerPage: number }) {
loading.value = true
try {
currentPage.value = page
const result: { [key: string]: any } = await api.get('history/transfer', {
params: {
page,
count: itemsPerPage,
title: search.value,
},
})
dataList.value = result.data.list
totalItems.value = result.data.total
searchHintList.value = ['失败', '成功', ...new Set(dataList.value.map(item => item.title || ''))].filter(
title => title !== '',
)
}
catch (error) {
console.error(error)
}
loading.value = false
}
// 根据 type 返回不同的图标
function getIcon(type: string) {
if (type === '电影')
return 'mdi-movie'
else if (type === '电视剧')
return 'mdi-television-classic'
else
return 'mdi-help-circle'
}
// 转移方式字典 // 转移方式字典
const TransferDict: { [key: string]: string } = { const TransferDict: { [key: string]: string } = {
copy: '复制', copy: '复制',
@@ -139,6 +112,61 @@ const TransferDict: { [key: string]: string } = {
rclone_move: 'Rclone移动', rclone_move: 'Rclone移动',
} }
// 分页提示
const pageTip = computed(() => {
const begin = unref(itemsPerPage) * (unref(currentPage) - 1) + 1
const end = unref(itemsPerPage) * unref(currentPage) === -1 ? 'ALL' : unref(itemsPerPage) * unref(currentPage)
return {
begin,
end,
}
})
// 分页总数
const totalPage = computed(() => {
const total = Math.ceil(unref(totalItems) / unref(itemsPerPage))
return total
})
// 切换页签和搜索词
watch(
[() => currentPage.value, () => itemsPerPage.value, () => search.value],
debounce(async () => {
await fetchData()
}, 1000),
)
// 获取订阅列表数据
async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
loading.value = true
try {
const result: { [key: string]: any } = await api.get('history/transfer', {
params: {
page,
count,
title: search.value,
},
})
dataList.value = result.data?.list
totalItems.value = result.data?.total
searchHintList.value = ['失败', '成功', ...new Set(dataList.value.map(item => item.title || ''))].filter(
title => title !== '',
)
} catch (error) {
console.error(error)
}
loading.value = false
}
// 根据 type 返回不同的图标
function getIcon(type: string) {
if (type === '电影') return 'mdi-movie'
else if (type === '电视剧') return 'mdi-television-classic'
else return 'mdi-help-circle'
}
// 删除历史记录 // 删除历史记录
async function removeHistory(item: TransferHistory) { async function removeHistory(item: TransferHistory) {
currentHistory.value = item currentHistory.value = item
@@ -156,10 +184,8 @@ async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boo
data: item, data: item,
}) })
if (!result.success) if (!result.success) $toast.error(`删除失败: ${result.msg}`)
$toast.error(`删除失败: ${result.msg}`) } catch (error) {
}
catch (error) {
console.error(error) console.error(error)
} }
} }
@@ -168,16 +194,12 @@ async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boo
async function removeSingle(deleteSrc: boolean, deleteDest: boolean) { async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
// 关闭弹窗 // 关闭弹窗
deleteConfirmDialog.value = false deleteConfirmDialog.value = false
if (!currentHistory.value) if (!currentHistory.value) return
return
// 删除 // 删除
await remove(currentHistory.value, deleteSrc, deleteDest) await remove(currentHistory.value, deleteSrc, deleteDest)
// 刷新 // 刷新
fetchData({ fetchData()
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
} }
// 批量删除记录 // 批量删除记录
@@ -186,8 +208,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
deleteConfirmDialog.value = false deleteConfirmDialog.value = false
// 总条数 // 总条数
const total = selected.value.length const total = selected.value.length
if (total === 0) if (total === 0) return
return
// 已处理条数 // 已处理条数
let handled = 0 let handled = 0
@@ -207,24 +228,18 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
// 隐藏进度条 // 隐藏进度条
progressDialog.value = false progressDialog.value = false
// 重新获取数据 // 重新获取数据
fetchData({ fetchData()
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
} }
// 响应删除操作 // 响应删除操作
async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) { async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
if (currentHistory.value) if (currentHistory.value) await removeSingle(deleteSrc, deleteDest)
await removeSingle(deleteSrc, deleteDest) else await removeBatch(deleteSrc, deleteDest)
else
await removeBatch(deleteSrc, deleteDest)
} }
// 批量删除历史记录 // 批量删除历史记录
async function removeHistoryBatch() { async function removeHistoryBatch() {
if (selected.value.length === 0) if (selected.value.length === 0) return
return
// 清空当前操作记录 // 清空当前操作记录
currentHistory.value = undefined currentHistory.value = undefined
@@ -235,26 +250,20 @@ async function removeHistoryBatch() {
// 计算根路径 // 计算根路径
function getRootPath(path: string, type: string, category: string) { function getRootPath(path: string, type: string, category: string) {
if (!path) if (!path) return ''
return ''
let index = -2 let index = -2
if (type !== '电影') if (type !== '电影') index = -3
index = -3
if (category) if (category) index -= 1
index -= 1
if (path.includes('/')) if (path.includes('/')) return path.split('/').slice(0, index).join('/')
return path.split('/').slice(0, index).join('/') else return path.split('\\').slice(0, index).join('\\')
else
return path.split('\\').slice(0, index).join('\\')
} }
// 批量重新整理 // 批量重新整理
async function retransferBatch() { async function retransferBatch() {
if (selected.value.length === 0) if (selected.value.length === 0) return
return
// 清空当前操作记录 // 清空当前操作记录
currentHistory.value = undefined currentHistory.value = undefined
@@ -270,8 +279,7 @@ async function retransferBatch() {
const category = selected.value[0].category ?? '' const category = selected.value[0].category ?? ''
// 计算根路径 // 计算根路径
redoTarget.value = getRootPath(dest, mediaType, category) redoTarget.value = getRootPath(dest, mediaType, category)
} } else {
else {
redoTarget.value = '' redoTarget.value = ''
} }
// 打开识别弹窗 // 打开识别弹窗
@@ -304,25 +312,26 @@ const dropdownItems = ref([
}, },
}, },
]) ])
// 初始加载数据
onMounted(fetchData)
</script> </script>
<template> <template>
<VCard class="pb-5"> <VCard>
<VCardItem> <VCardItem>
<VCardTitle> <VCardTitle>
<VRow> <VRow>
<VCol cols="4" md="6"> <VCol cols="4" md="6"> 历史记录 </VCol>
历史记录 <VCol cols="8" md="6" class="flex">
</VCol>
<VCol cols="8" md="6">
<VCombobox <VCombobox
key="search_navbar" key="search_navbar"
v-model="search" v-model="search"
:items="searchHintList" :items="searchHintList"
class="text-disabled" class="text-disabled"
density="compact" density="compact"
label="搜索标题、状态" label="搜索目录、状态"
append-inner-icon="mdi-magnify" prepend-inner-icon="mdi-magnify"
variant="solo-filled" variant="solo-filled"
single-line single-line
hide-details hide-details
@@ -334,58 +343,52 @@ const dropdownItems = ref([
</VRow> </VRow>
</VCardTitle> </VCardTitle>
</VCardItem> </VCardItem>
<VDataTableServer <VDataTableVirtual
v-model="selected" v-model="selected"
v-model:items-per-page="itemsPerPage"
:headers="headers" :headers="headers"
:items="dataList" :items="dataList"
:items-length="totalItems"
:search="search"
:loading="loading" :loading="loading"
density="compact" density="compact"
item-value="id"
return-object return-object
fixed-header fixed-header
show-select show-select
items-per-page-text="每页条数" loading-text="加载中..."
page-text="{0}-{1} {2} " class="data-table-div"
@update:options="fetchData"
> >
<template #item.title="{ item }"> <template #item.title="{ item }">
<div class="d-flex align-center"> <div class="d-flex align-center">
<VAvatar> <VAvatar>
<VIcon :icon="getIcon(item.value.type || '')" /> <VIcon :icon="getIcon(item.type || '')" />
</VAvatar> </VAvatar>
<div class="d-flex flex-column ms-1"> <div class="d-flex flex-column ms-1">
<span class="d-block text-high-emphasis"> <span v-if="item.type === '电视剧'" class="d-block text-high-emphasis min-w-20">
{{ item.value.title }} {{ item.value.seasons }}{{ item.value.episodes }} {{ item?.title }} {{ item?.seasons }}{{ item?.episodes }}
</span> </span>
<small>{{ item.value.category }}</small> <span v-else class="d-block text-high-emphasis min-w-20">
{{ item?.title }}
</span>
<small>{{ item?.category }}</small>
</div> </div>
</div> </div>
</template> </template>
<template #item.src="{ item }"> <template #item.src="{ item }">
<small>{{ item.value.src }} <br>=> {{ item.value.dest }}</small> <small>{{ item?.src }} <br />=> {{ item?.dest }}</small>
</template> </template>
<template #item.mode="{ item }"> <template #item.mode="{ item }">
<VChip variant="outlined" color="primary" size="small"> <VChip variant="outlined" color="primary" size="small">
{{ TransferDict[item.value.mode] }} {{ TransferDict[item?.mode || ''] }}
</VChip> </VChip>
</template> </template>
<template #item.status="{ item }"> <template #item.status="{ item }">
<VChip v-if="item.value.status" color="success" size="small"> <VChip v-if="item?.status" color="success" size="small"> 成功 </VChip>
成功 <v-tooltip v-else :text="item?.errmsg">
</VChip>
<v-tooltip v-else :text="item.value.errmsg">
<template #activator="{ props }"> <template #activator="{ props }">
<VChip v-bind="props" color="error" size="small"> <VChip v-bind="props" color="error" size="small"> 失败 </VChip>
失败
</VChip>
</template> </template>
</v-tooltip> </v-tooltip>
</template> </template>
<template #item.date="{ item }"> <template #item.date="{ item }">
<small>{{ item.value.date }}</small> <small>{{ item?.date }}</small>
</template> </template>
<template #item.actions="{ item }"> <template #item.actions="{ item }">
<IconBtn> <IconBtn>
@@ -397,7 +400,7 @@ const dropdownItems = ref([
:key="i" :key="i"
variant="plain" variant="plain"
:base-color="menu.props.color" :base-color="menu.props.color"
@click="menu.props.click(item.value)" @click="menu.props.click(item)"
> >
<template #prepend> <template #prepend>
<VIcon :icon="menu.props.prependIcon" /> <VIcon :icon="menu.props.prependIcon" />
@@ -408,24 +411,23 @@ const dropdownItems = ref([
</VMenu> </VMenu>
</IconBtn> </IconBtn>
</template> </template>
<template #no-data> <template #no-data> 没有数据 </template>
没有数据 </VDataTableVirtual>
</template> <div class="flex items-center justify-end">
</VDataTableServer> <div class="w-auto">
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" variant="solo" flat />
</div>
<div class="w-auto text-sm">{{ pageTip.begin }}-{{ pageTip.end }} / {{ totalItems }}</div>
<VPagination
v-model="currentPage"
show-first-last-page
:length="totalPage"
@next="currentPage + 1"
@prev="currentPage - 1"
>
</VPagination>
</div>
</VCard> </VCard>
<!-- 底部操作按钮 -->
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
<VTooltip text="批量重新整理">
<template #activator="{ props }">
<VBtn v-bind="props" icon="mdi-redo-variant" class="me-2" color="primary" size="x-large" @click="retransferBatch" />
</template>
</VTooltip>
<VTooltip text="批量删除">
<template #activator="{ props }">
<VBtn v-bind="props" icon="mdi-trash-can-outline" color="error" size="x-large" @click="removeHistoryBatch" />
</template>
</VTooltip>
</span>
<!-- 底部弹窗 --> <!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset> <VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center rounded-t"> <VCard class="text-center rounded-t">
@@ -434,12 +436,8 @@ const dropdownItems = ref([
{{ confirmTitle }} {{ confirmTitle }}
</VCardTitle> </VCardTitle>
<div class="d-flex flex-column flex-lg-row justify-center my-3"> <div class="d-flex flex-column flex-lg-row justify-center my-3">
<VBtn color="primary" class="mb-2 mx-2" @click="deleteConfirmHandler(false, false)"> <VBtn color="primary" class="mb-2 mx-2" @click="deleteConfirmHandler(false, false)"> 仅删除历史记录 </VBtn>
仅删除历史记录 <VBtn color="warning" class="mb-2 mx-2" @click="deleteConfirmHandler(true, false)"> 删除历史记录和源文件 </VBtn>
</VBtn>
<VBtn color="warning" class="mb-2 mx-2" @click="deleteConfirmHandler(true, false)">
删除历史记录和源文件
</VBtn>
<VBtn color="info" class="mb-2 mx-2" @click="deleteConfirmHandler(false, true)"> <VBtn color="info" class="mb-2 mx-2" @click="deleteConfirmHandler(false, true)">
删除历史记录和媒体库文件 删除历史记录和媒体库文件
</VBtn> </VBtn>
@@ -450,7 +448,8 @@ const dropdownItems = ref([
</VCard> </VCard>
</VBottomSheet> </VBottomSheet>
<!-- 文件整理弹窗 --> <!-- 文件整理弹窗 -->
<ReorganizeForm <ReorganizeDialog
v-if="redoDialog"
v-model="redoDialog" v-model="redoDialog"
:logids="redoIds" :logids="redoIds"
:target="redoTarget" :target="redoTarget"
@@ -461,18 +460,50 @@ const dropdownItems = ref([
currentHistory = undefined currentHistory = undefined
selected = [] selected = []
// 刷新 // 刷新
fetchData({ fetchData()
page: currentPage,
itemsPerPage,
})
} }
" "
@close="redoDialog = false" @close="redoDialog = false"
/> />
<!-- 底部操作按钮 -->
<span>
<VFab
v-if="selected.length > 0"
icon="mdi-trash-can-outline"
color="error"
location="bottom end"
size="x-large"
fixed
app
appear
@click="removeHistoryBatch"
/>
<VFab
v-if="selected.length > 0"
class="mb-2"
icon="mdi-redo-variant"
location="bottom end"
size="x-large"
fixed
app
appear
@click="retransferBatch"
/>
</span>
</template> </template>
<style lang="scss"> <style lang="scss">
.v-table th { .v-table th {
white-space: nowrap; white-space: nowrap;
} }
.data-table-div {
block-size: calc(100vh - 14rem);
}
@media (width <= 768px) {
.data-table-div {
block-size: calc(100vh - 17rem);
}
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { calculateTimeDifference } from '@/@core/utils' import { formatDateDifference } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
// 系统环境变量 // 系统环境变量
@@ -62,7 +62,7 @@ async function queryAllRelease() {
// 计算发布时间 // 计算发布时间
function releaseTime(releaseDate: string) { function releaseTime(releaseDate: string) {
// 上一次更新时间 // 上一次更新时间
return `${calculateTimeDifference(releaseDate)}` return formatDateDifference(releaseDate)
} }
onMounted(() => { onMounted(() => {

View File

@@ -1,5 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import QrcodeVue from 'qrcode.vue'
import { VForm } from 'vuetify/lib/components/index.mjs'
import { requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { User } from '@/api/types' import type { User } from '@/api/types'
@@ -19,6 +21,18 @@ const refInputEl = ref<HTMLElement>()
// 新增用户窗口 // 新增用户窗口
const addUserDialog = ref(false) const addUserDialog = ref(false)
// 开启双重验证窗口
const otpDialog = ref(false)
// otp uri
const otpUri = ref('')
// otp secret
const secret = ref('')
// 确认双重验证密码
const otpPassword = ref('')
// 新增用户表单 // 新增用户表单
const userForm = reactive({ const userForm = reactive({
name: '', name: '',
@@ -35,11 +49,15 @@ const accountInfo = ref<User>({
is_active: false, is_active: false,
is_superuser: false, is_superuser: false,
avatar: '', avatar: '',
is_otp: false,
}) })
// 所有用户信息 // 所有用户信息
const allUsers = ref<User[]>([]) const allUsers = ref<User[]>([])
// 二维码信息
const qrCode = ref('')
// changeAvatar function // changeAvatar function
function changeAvatar(file: Event) { function changeAvatar(file: Event) {
const fileReader = new FileReader() const fileReader = new FileReader()
@@ -65,7 +83,7 @@ function resetAvatar() {
async function loadAccountInfo() { async function loadAccountInfo() {
try { try {
const user: User = await api.get('user/current') const user: User = await api.get('user/current')
console.log(user)
accountInfo.value = user accountInfo.value = user
if (!accountInfo.value.avatar) if (!accountInfo.value.avatar)
accountInfo.value.avatar = avatar1 accountInfo.value.avatar = avatar1
@@ -167,6 +185,65 @@ async function addUser() {
} }
} }
// 为当前用户获取Otp Uri
async function getOtpUri() {
try {
const result: { [key: string]: any } = await api.post('user/otp/generate')
if (result.success) {
otpUri.value = result.data.uri
secret.value = result.data.secret
qrCode.value = result.data.uri
otpDialog.value = true
}
else {
$toast.error(`获取otp uri失败${result.message}`)
}
}
catch (error) {
console.log(error)
}
}
// 关闭当前用户的双重验证
async function disableOtp() {
try {
const result: { [key: string]: any } = await api.post('user/otp/disable')
if (result.success) {
accountInfo.value.is_otp = false
$toast.success('关闭登录双重验证成功!')
}
else {
$toast.error(`关闭otp失败${result.message}`)
}
}
catch (error) {
console.log(error)
}
}
// 启用Otp
async function judgeOtpPassword() {
if (!otpPassword.value) {
$toast.error('请填写6位验证码')
return
}
try {
const result: { [key: string]: any } = await api.post('user/otp/judge', { uri: otpUri.value, otpPassword: otpPassword.value })
if (result.success) {
$toast.success('开启登录双重验证成功!')
otpDialog.value = false
accountInfo.value.is_otp = true
}
else {
$toast.error(`开启otp失败${result.message}`)
}
}
catch (error) {
console.log(error)
}
}
// 加载当前用户数据 // 加载当前用户数据
onMounted(() => { onMounted(() => {
loadAccountInfo() loadAccountInfo()
@@ -175,298 +252,364 @@ onMounted(() => {
</script> </script>
<template> <template>
<VRow> <div>
<VCol cols="12"> <VRow>
<VCard title="个人信息"> <VCol cols="12">
<VCardText class="d-flex"> <VCard title="个人信息">
<!-- 👉 Avatar --> <VCardText class="d-flex">
<VAvatar <!-- 👉 Avatar -->
rounded="lg" <VAvatar
size="100" rounded="lg"
class="me-6" size="100"
:image="accountInfo.avatar" class="me-6"
/> :image="accountInfo.avatar"
/>
<!-- 👉 Upload Photo --> <!-- 👉 Upload Photo -->
<form class="d-flex flex-column justify-center gap-5"> <form class="d-flex flex-column justify-center gap-5">
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<VBtn <VBtn
color="primary" color="primary"
@click="refInputEl?.click()" @click="refInputEl?.click()"
> >
<VIcon <VIcon
icon="mdi-cloud-upload-outline" icon="mdi-cloud-upload-outline"
class="d-sm-none" />
/> <span class="d-none d-sm-block ms-2">上传头像</span>
<span class="d-none d-sm-block">上传头像</span>
</VBtn>
<input
ref="refInputEl"
type="file"
name="file"
accept=".jpeg,.png,.jpg,GIF"
hidden
@input="changeAvatar"
>
<VBtn
type="reset"
color="error"
variant="tonal"
@click="resetAvatar"
>
<span class="d-none d-sm-block">重置</span>
<VIcon
icon="mdi-refresh"
class="d-sm-none"
/>
</VBtn>
</div>
<p class="text-body-1 mb-0">
允许 JPGGIF PNG 格式 最大尺寸 800K
</p>
</form>
</VCardText>
<VDivider />
<VCardText>
<!-- 👉 Form -->
<VForm class="mt-6">
<VRow>
<!-- 👉 Name -->
<VCol
md="6"
cols="12"
>
<VTextField
v-model="accountInfo.name"
readonly
label="用户名"
/>
</VCol>
<!-- 👉 Email -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="accountInfo.email"
label="邮箱"
type="email"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 new password -->
<VTextField
v-model="newPassword"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
label="新密码"
autocomplete="new-password"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 confirm password -->
<VTextField
v-model="confirmPassword"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
label="确认新密码"
@click:append-inner="
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
/>
</VCol>
<!-- 👉 Form Actions -->
<VCol
cols="12"
class="d-flex flex-wrap gap-4"
>
<VBtn @click="saveAccountInfo">
保存
</VBtn> </VBtn>
<input
ref="refInputEl"
type="file"
name="file"
accept=".jpeg,.png,.jpg,GIF"
hidden
@input="changeAvatar"
>
<VBtn
type="reset"
color="error"
variant="tonal"
@click="resetAvatar"
>
<VIcon
icon="mdi-refresh"
/>
<span class="d-none d-sm-block ms-2">重置</span>
</VBtn>
<VBtn
:color="accountInfo.is_otp ? 'error' : 'info'"
variant="tonal"
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
>
<VIcon
icon="mdi-account-key"
/>
<span class="d-none d-sm-block ms-2">{{ accountInfo.is_otp ? "关闭验证" : "双重验证" }}</span>
</VBtn>
</div>
<p class="text-body-1 mb-0">
允许 JPGGIF PNG 格式 最大尺寸 800K
</p>
</form>
</VCardText>
<VDivider />
<VCardText>
<!-- 👉 Form -->
<VForm class="mt-6">
<VRow>
<!-- 👉 Name -->
<VCol
md="6"
cols="12"
>
<VTextField
v-model="accountInfo.name"
readonly
label="用户名"
/>
</VCol>
<!-- 👉 Email -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="accountInfo.email"
label="邮箱"
type="email"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 new password -->
<VTextField
v-model="newPassword"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
label="新密码"
autocomplete=""
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 confirm password -->
<VTextField
v-model="confirmPassword"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
label="确认新密码"
@click:append-inner="
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
/>
</VCol>
<!-- 👉 Form Actions -->
<VCol
cols="12"
class="d-flex flex-wrap gap-4"
>
<VBtn @click="saveAccountInfo">
保存
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VCol>
<VCol
v-if="accountInfo.is_superuser"
cols="12"
>
<!-- 👉 Accounts -->
<VCard title="所有用户">
<template #append>
<IconBtn @click.stop="addUserDialog = true">
<VIcon icon="mdi-plus" />
</IconBtn>
</template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">
用户名
</th>
<th scope="col">
邮箱
</th>
<th scope="col">
状态
</th>
<th scope="col">
管理员
</th>
<th
scope="col"
class="w-5"
/>
</tr>
</thead>
<tbody>
<tr
v-for="user in allUsers"
:key="user.name"
>
<td>
{{ user.name }}
</td>
<td>{{ user.email }}</td>
<td>
<VChip
v-if="user.is_active"
color="success"
text-color="white"
>
激活
</VChip>
<VChip
v-else
color="error"
text-color="white"
>
冻结
</VChip>
</td>
<td>{{ user.is_superuser ? "是" : "否" }}</td>
<td>
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name !== user.name">
<VIcon icon="mdi-dots-vertical" />
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="deactivateUser(user)"
>
<template #prepend>
<VIcon icon="mdi-lock" />
</template>
<VListItemTitle>
{{
user.is_active ? "冻结" : "解冻"
}}
</VListItemTitle>
</VListItem>
<VListItem
variant="plain"
base-color="error"
@click="deleteUser(user)"
>
<template #prepend>
<VIcon icon="mdi-delete" />
</template>
<VListItemTitle>删除</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
</VCard>
</VCol>
</VRow>
<!-- =弹窗 -->
<VDialog
v-model="addUserDialog"
max-width="50rem"
persistent
z-index="1010"
>
<!-- Dialog Content -->
<VCard title="新增用户">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.name"
label="用户名"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.password"
label="密码"
:rules="[requiredValidator]"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.email"
:rules="[requiredValidator]"
label="邮箱"
/>
</VCol> </VCol>
</VRow> </VRow>
</VForm> </VForm>
</VCardText> </VCardText>
<VCardActions>
<VBtn @click="addUserDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="addUser">
确定
</VBtn>
</VCardActions>
</VCard> </VCard>
</VCol> </VDialog>
<VCol <!-- 双重验证弹窗 -->
v-if="accountInfo.is_superuser" <VDialog
cols="12" v-model="otpDialog"
max-width="45rem"
persistent
z-index="1010"
> >
<!-- 👉 Accounts --> <!-- 开启双重验证弹窗内容 -->
<VCard title="所有用户"> <VCard>
<template #append> <DialogCloseBtn @click="otpDialog = false" />
<IconBtn @click.stop="addUserDialog = true"> <VCardText>
<VIcon icon="mdi-plus" /> <h4 class="text-h4 text-center mb-6 mt-5">
</IconBtn> 登录双重验证
</template> </h4><h5 class="text-h5 font-weight-medium mb-2">
<VTable class="text-no-wrap"> 身份验证器
<thead> </h5>
<tr> <p class="mb-6">
<th scope="col"> 使用像Google AuthenticatorMicrosoft AuthenticatorAuthy或1Password这样的身份验证器应用程序扫描二维码它将为您生成一个6位数的代码供您在下方输入
用户名 </p>
</th> <div class="my-6">
<th scope="col"> <QrcodeVue class="mx-auto" :value="qrCode" :size="200" max-width="25rem" />
邮箱 </div>
</th> <VAlert
<th scope="col"> :title="secret"
状态 variant="tonal"
</th> type="warning"
<th scope="col"> class="my-4"
管理员 text="如果您在使用二维码时遇到困难,请在您的应用程序中选择手动输入以上代码。"
</th> >
<th <template #prepend />
scope="col" </VAlert>
class="w-5" <VForm>
/> <VTextField
</tr> v-model="otpPassword"
</thead> type="text"
<tbody> label="输入验证码以确认开启双重验证"
<tr autocomplete=""
v-for="user in allUsers" class="mb-8"
:key="user.name" variant="outlined"
> />
<td> <div class="d-flex justify-end flex-wrap gap-4">
{{ user.name }} <VBtn variant="outlined" color="secondary" @click="otpDialog = false">
</td> 取消
<td>{{ user.email }}</td> </VBtn>
<td> <VBtn @click="judgeOtpPassword">
<VChip 确定
v-if="user.is_active" <template #append>
color="success" <VIcon icon="mdi-check" />
text-color="white" </template>
> </VBtn>
激活 </div>
</VChip> </VForm>
<VChip </VCardText>
v-else
color="error"
text-color="white"
>
冻结
</VChip>
</td>
<td>{{ user.is_superuser ? "是" : "否" }}</td>
<td>
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name != user.name">
<VIcon icon="mdi-dots-vertical" />
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="deactivateUser(user)"
>
<template #prepend>
<VIcon icon="mdi-lock" />
</template>
<VListItemTitle>
{{
user.is_active ? "冻结" : "解冻"
}}
</VListItemTitle>
</VListItem>
<VListItem
variant="plain"
base-color="error"
@click="deleteUser(user)"
>
<template #prepend>
<VIcon icon="mdi-delete" />
</template>
<VListItemTitle>删除</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
</VCard> </VCard>
</VCol> </VDialog>
</VRow> </div>
<!-- 站点编辑弹窗 -->
<VDialog
v-model="addUserDialog"
max-width="50rem"
persistent
>
<!-- Dialog Content -->
<VCard title="新增用户">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.name"
label="用户名"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.password"
label="密码"
:rules="[requiredValidator]"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.email"
:rules="[requiredValidator]"
label="邮箱"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="addUserDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="addUser">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template> </template>

View File

@@ -29,6 +29,9 @@ const notificationSettings = ref({
SLACK_CHANNEL: '', SLACK_CHANNEL: '',
SYNOLOGYCHAT_WEBHOOK: '', SYNOLOGYCHAT_WEBHOOK: '',
SYNOLOGYCHAT_TOKEN: '', SYNOLOGYCHAT_TOKEN: '',
VOCECHAT_HOST: '',
VOCECHAT_API_KEY: '',
VOCECHAT_CHANNEL_ID: '',
}) })
// 消息渠道 // 消息渠道
@@ -49,6 +52,10 @@ const NotificationChannels = [
title: 'SynologyChat', title: 'SynologyChat',
value: 'synologychat', value: 'synologychat',
}, },
{
title: 'VoceChat',
value: 'vocechat',
},
] ]
// 提示框 // 提示框
@@ -89,7 +96,7 @@ async function loadNotificationSettings() {
try { try {
const result1: { [key: string]: any } = await api.get('system/setting/MESSAGER') const result1: { [key: string]: any } = await api.get('system/setting/MESSAGER')
if (result1.success) if (result1.success)
selectedChannels.value = result1.data?.value?.split(',') selectedChannels.value = result1.data && result1.data.value ? result1.data.value.split(',') : []
const result2: { [key: string]: any } = await api.get('system/env') const result2: { [key: string]: any } = await api.get('system/env')
if (result2.success) { if (result2.success) {
@@ -110,6 +117,9 @@ async function loadNotificationSettings() {
SLACK_CHANNEL, SLACK_CHANNEL,
SYNOLOGYCHAT_WEBHOOK, SYNOLOGYCHAT_WEBHOOK,
SYNOLOGYCHAT_TOKEN, SYNOLOGYCHAT_TOKEN,
VOCECHAT_HOST,
VOCECHAT_API_KEY,
VOCECHAT_CHANNEL_ID,
} = result2.data } = result2.data
notificationSettings.value = { notificationSettings.value = {
WECHAT_CORPID, WECHAT_CORPID,
@@ -128,6 +138,9 @@ async function loadNotificationSettings() {
SLACK_CHANNEL, SLACK_CHANNEL,
SYNOLOGYCHAT_WEBHOOK, SYNOLOGYCHAT_WEBHOOK,
SYNOLOGYCHAT_TOKEN, SYNOLOGYCHAT_TOKEN,
VOCECHAT_HOST,
VOCECHAT_API_KEY,
VOCECHAT_CHANNEL_ID,
} }
} }
} }
@@ -196,6 +209,7 @@ onMounted(() => {
chips chips
:items="NotificationChannels" :items="NotificationChannels"
label="当前使用通知渠道" label="当前使用通知渠道"
hint="选中的渠道才会按消息类型的设定发送消息"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -217,6 +231,9 @@ onMounted(() => {
<VTab value="synologychat"> <VTab value="synologychat">
SynologyChat SynologyChat
</VTab> </VTab>
<VTab value="vocechat">
VoceChat
</VTab>
</VTabs> </VTabs>
<VWindow <VWindow
v-model="messagerTab" v-model="messagerTab"
@@ -230,36 +247,42 @@ onMounted(() => {
<VTextField <VTextField
v-model="notificationSettings.WECHAT_CORPID" v-model="notificationSettings.WECHAT_CORPID"
label="企业ID" label="企业ID"
hint="登录企业微信后台,在 https://work.weixin.qq.com/wework_admin/frame#profile 中查看"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="notificationSettings.WECHAT_APP_SECRET" v-model="notificationSettings.WECHAT_APP_SECRET"
label="应用密钥" label="应用Secret"
hint="在企业微信中创建应用查看应用的Secret"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="notificationSettings.WECHAT_APP_ID" v-model="notificationSettings.WECHAT_APP_ID"
label="应用ID" label="应用 AgentId"
hint="在企业微信中创建应用查看应用的AgentId"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="notificationSettings.WECHAT_PROXY" v-model="notificationSettings.WECHAT_PROXY"
label="代理地址" label="代理地址"
hint="由于微信官方限制2022年6月20日后创建的企业微信应用需要有固定的公网IP地址并加入IP白名单后才能接收消息使用有固定公网IP的代理服务器转发可解决该问题代理服务器需自行搭建搭建方法参考项目主页说明不使用代理需保留默认值"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="notificationSettings.WECHAT_TOKEN" v-model="notificationSettings.WECHAT_TOKEN"
label="Token" label="Token"
hint="在微信企业应用管理后台-接收消息设置页面生成"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="notificationSettings.WECHAT_ENCODING_AESKEY" v-model="notificationSettings.WECHAT_ENCODING_AESKEY"
label="EncodingAESKey" label="EncodingAESKey"
hint="在微信企业应用管理后台-接收消息设置页面生成所有信息填入完成后保存然后再在企业微信应用消息接收服务中输入回调地址http(s)://domain:port/api/v1/message/"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -267,6 +290,7 @@ onMounted(() => {
v-model="notificationSettings.WECHAT_ADMINS" v-model="notificationSettings.WECHAT_ADMINS"
label="管理员白名单" label="管理员白名单"
placeholder="多个用,分隔" placeholder="多个用,分隔"
hint="只有在白名单中的用户才能使用菜单管理功能,不填写则所有用户都能使用,菜单会自动生成,不需要手动创建"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -279,12 +303,14 @@ onMounted(() => {
<VTextField <VTextField
v-model="notificationSettings.TELEGRAM_TOKEN" v-model="notificationSettings.TELEGRAM_TOKEN"
label="Bot Token" label="Bot Token"
hint="Telegram机器人的token关注BotFather创建机器人并获取token格式为123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="notificationSettings.TELEGRAM_CHAT_ID" v-model="notificationSettings.TELEGRAM_CHAT_ID"
label="Chat ID" label="Chat ID"
hint="接受消息通知的用户、群组或频道Chat ID关注@getidsbot获取"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -292,6 +318,7 @@ onMounted(() => {
v-model="notificationSettings.TELEGRAM_USERS" v-model="notificationSettings.TELEGRAM_USERS"
label="用户白名单" label="用户白名单"
placeholder="多个用,分隔" placeholder="多个用,分隔"
hint="只有在白名单中的用户才能使用Telegram机器人不填写则所有用户都能使用多个用户用英文,分隔"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -299,6 +326,7 @@ onMounted(() => {
v-model="notificationSettings.TELEGRAM_ADMINS" v-model="notificationSettings.TELEGRAM_ADMINS"
label="管理员白名单" label="管理员白名单"
placeholder="多个用,分隔" placeholder="多个用,分隔"
hint="只有在白名单中的用户才能使用管理功能,不填写则所有用户都能使用,多个用户用英文,分隔。菜单会自动生成,不需要手动创建"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -311,12 +339,16 @@ onMounted(() => {
<VTextField <VTextField
v-model="notificationSettings.SLACK_OAUTH_TOKEN" v-model="notificationSettings.SLACK_OAUTH_TOKEN"
label="Slack Bot User OAuth Token" label="Slack Bot User OAuth Token"
placeholder="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
hint="在 https://api.slack.com/apps 中创建应用查看OAuth & Permissions页面中的Bot User OAuth Token"
/> />
</VCol> </VCol>
<VCol cols="12" md="5"> <VCol cols="12" md="5">
<VTextField <VTextField
v-model="notificationSettings.SLACK_APP_TOKEN" v-model="notificationSettings.SLACK_APP_TOKEN"
label="Slack App-Level Token" label="Slack App-Level Token"
placeholder="xapp-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
hint="在 https://api.slack.com/apps 中创建应用查看OAuth & Permissions页面中的App-Level Token"
/> />
</VCol> </VCol>
<VCol cols="12" md="2"> <VCol cols="12" md="2">
@@ -324,6 +356,7 @@ onMounted(() => {
v-model="notificationSettings.SLACK_CHANNEL" v-model="notificationSettings.SLACK_CHANNEL"
label="频道名称" label="频道名称"
placeholder="全体" placeholder="全体"
hint="消息发送到的频道名称,不填写则发送到全体频道"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -335,13 +368,42 @@ onMounted(() => {
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="notificationSettings.SYNOLOGYCHAT_WEBHOOK" v-model="notificationSettings.SYNOLOGYCHAT_WEBHOOK"
label="Webhook" label="机器人传入URL"
hint="在Synology Chat中创建机器人获取机器人传入URL"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="notificationSettings.SYNOLOGYCHAT_TOKEN" v-model="notificationSettings.SYNOLOGYCHAT_TOKEN"
label="Token" label="令牌"
hint="在Synology Chat中创建机器人获取机器人令牌"
/>
</VCol>
</VRow>
</VForm>
</VWindowItem>
<VWindowItem value="vocechat">
<VForm>
<VRow>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.VOCECHAT_HOST"
label="地址"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.VOCECHAT_API_KEY"
label="机器人密钥"
hint="在VoceChat中创建机器人获取机器人密钥"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.VOCECHAT_CHANNEL_ID"
label="频道ID"
placeholder="不包含#号"
hint="在VoceChat中创建频道获取频道ID不包含#号"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -389,6 +451,9 @@ onMounted(() => {
<th scope="col"> <th scope="col">
SynologyChat SynologyChat
</th> </th>
<th scope="col">
VoceChat
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -411,10 +476,13 @@ onMounted(() => {
<td> <td>
<VCheckbox v-model="message.synologychat" /> <VCheckbox v-model="message.synologychat" />
</td> </td>
<td>
<VCheckbox v-model="message.vocechat" />
</td>
</tr> </tr>
<tr v-if="messagemTypes.length === 0"> <tr v-if="messagemTypes.length === 0">
<td <td
colspan="5" colspan="6"
class="text-center" class="text-center"
> >
没有设置任何通知渠道 没有设置任何通知渠道

View File

@@ -4,7 +4,7 @@ import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue' import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types' import type { Site } from '@/api/types'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import ImportCodeForm from '@/components/form/ImportCodeForm.vue' import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型 // 规则卡片类型
interface FilterCard { interface FilterCard {
@@ -390,6 +390,7 @@ onMounted(() => {
v-model="defaultFilterRules.include" v-model="defaultFilterRules.include"
type="text" type="text"
label="包含(关键字、正则式)" label="包含(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -397,6 +398,7 @@ onMounted(() => {
v-model="defaultFilterRules.exclude" v-model="defaultFilterRules.exclude"
type="text" type="text"
label="排除(关键字、正则式)" label="排除(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -418,7 +420,7 @@ onMounted(() => {
width="60rem" width="60rem"
scrollable scrollable
> >
<ImportCodeForm <ImportCodeDialog
v-model="importCodeString" v-model="importCodeString"
title="导入优先级规则" title="导入优先级规则"
@close="importCodeDialog = false" @close="importCodeDialog = false"

View File

@@ -24,6 +24,7 @@ const cookieCloudSetting = ref({
COOKIECLOUD_PASSWORD: '', COOKIECLOUD_PASSWORD: '',
COOKIECLOUD_INTERVAL: 0, COOKIECLOUD_INTERVAL: 0,
USER_AGENT: '', USER_AGENT: '',
COOKIECLOUD_ENABLE_LOCAL: '',
}) })
// 种子优先规则下拉框 // 种子优先规则下拉框
@@ -108,6 +109,7 @@ async function loadCookieCloudSettings() {
COOKIECLOUD_PASSWORD, COOKIECLOUD_PASSWORD,
COOKIECLOUD_INTERVAL, COOKIECLOUD_INTERVAL,
USER_AGENT, USER_AGENT,
COOKIECLOUD_ENABLE_LOCAL,
} = result.data } = result.data
cookieCloudSetting.value = { cookieCloudSetting.value = {
COOKIECLOUD_HOST, COOKIECLOUD_HOST,
@@ -115,6 +117,7 @@ async function loadCookieCloudSettings() {
COOKIECLOUD_PASSWORD, COOKIECLOUD_PASSWORD,
COOKIECLOUD_INTERVAL, COOKIECLOUD_INTERVAL,
USER_AGENT, USER_AGENT,
COOKIECLOUD_ENABLE_LOCAL,
} }
} }
} }
@@ -155,18 +158,30 @@ onMounted(() => {
<VCardSubtitle> 从CookieCloud快速同步站点数据 </VCardSubtitle> <VCardSubtitle> 从CookieCloud快速同步站点数据 </VCardSubtitle>
<VCardText> <VCardText>
<VForm> <VForm>
<VRow>
<VCol cols="12" md="6">
<VCheckbox
v-model="cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL"
label="启用本地CookieCloud服务器"
hint="启用后将使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud"
/>
</VCol>
</VRow>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="cookieCloudSetting.COOKIECLOUD_HOST" v-model="cookieCloudSetting.COOKIECLOUD_HOST"
label="CookieCloud服务器地址" label="远程CookieCloud服务器地址"
placeholder="https://movie-pilot.org/cookiecloud" placeholder="https://movie-pilot.org/cookiecloud"
:disabled="!!cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL"
hint="格式https://movie-pilot.org/cookiecloud"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="cookieCloudSetting.COOKIECLOUD_KEY" v-model="cookieCloudSetting.COOKIECLOUD_KEY"
label="用户KEY" label="用户KEY"
hint="在CookieCloud浏览器插件中生成"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -174,6 +189,7 @@ onMounted(() => {
v-model="cookieCloudSetting.COOKIECLOUD_PASSWORD" v-model="cookieCloudSetting.COOKIECLOUD_PASSWORD"
type="password" type="password"
label="端对端加密密码" label="端对端加密密码"
hint="在CookieCloud浏览器插件中生成"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -181,12 +197,14 @@ onMounted(() => {
v-model="cookieCloudSetting.COOKIECLOUD_INTERVAL" v-model="cookieCloudSetting.COOKIECLOUD_INTERVAL"
label="自动同步间隔" label="自动同步间隔"
:items="CookieCloudIntervalItems" :items="CookieCloudIntervalItems"
hint="设置定时从CookieCloud服务器同步站点Cookie到MoviePilot的时间周期"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<VTextField <VTextField
v-model="cookieCloudSetting.USER_AGENT" v-model="cookieCloudSetting.USER_AGENT"
label="浏览器User-Agent" label="浏览器User-Agent"
hint="设置为CookieCloud插件所在的浏览器的User-Agent用于模拟浏览器请求正确填写后有助于提升站点访问成功率"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -213,6 +231,7 @@ onMounted(() => {
v-model="selectedTorrentPriority" v-model="selectedTorrentPriority"
:items="TorrentPriorityItems" :items="TorrentPriorityItems"
label="当前使用下载优先规则" label="当前使用下载优先规则"
hint="站点优先:优先下载站点优先级最高的站点的种子;做种数优先:优先下载做种数量最多的种子。注意下载优先级仍然低于搜索和订阅中设定的优先级规则"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -232,7 +251,11 @@ onMounted(() => {
<VCard title="站点重置"> <VCard title="站点重置">
<VCardText> <VCardText>
<div> <div>
<VCheckbox v-model="isConfirmResetSites" label="确认删除所有站点数据并重新同步。" /> <VCheckbox
v-model="isConfirmResetSites"
label="确认删除所有站点数据并重新同步。"
hint="删除所有站点数据并重新同步站点图标短时间内会因数缓存而混乱重启或者等待2两时自动恢复。"
/>
</div> </div>
<VBtn :disabled="!isConfirmResetSites || resetSitesDisabled" color="error" class="mt-3" @click="resetSites"> <VBtn :disabled="!isConfirmResetSites || resetSitesDisabled" color="error" class="mt-3" @click="resetSites">

View File

@@ -4,7 +4,7 @@ import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue' import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types' import type { Site } from '@/api/types'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import ImportCodeForm from '@/components/form/ImportCodeForm.vue' import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型 // 规则卡片类型
interface FilterCard { interface FilterCard {
@@ -41,7 +41,7 @@ const defaultFilterRules = ref({
exclude: '', exclude: '',
movie_size: '', movie_size: '',
tv_size: '', tv_size: '',
show_edit_dialog: false, min_seeders: 0
}) })
// 订阅模式选择项 // 订阅模式选择项
@@ -410,6 +410,7 @@ onMounted(() => {
v-model="selectedSubscribeMode" v-model="selectedSubscribeMode"
:items="subscribeModeItems" :items="subscribeModeItems"
label="订阅模式" label="订阅模式"
hint="自动系统自动爬取站点首页资源站点RSS使用站点RSS订阅资源站点RSS会自动获取也可手动在站点管理中补全"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -417,6 +418,7 @@ onMounted(() => {
v-model="selectedRssInterval" v-model="selectedRssInterval"
:items="rssIntervalItems" :items="rssIntervalItems"
label="站点RSS周期" label="站点RSS周期"
hint="设置站点RSS运行周期在订阅模式为站点RSS时生效"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -425,6 +427,7 @@ onMounted(() => {
<VSwitch <VSwitch
v-model="enableIntervalSearch" v-model="enableIntervalSearch"
label="开启订阅定时搜索" label="开启订阅定时搜索"
hint="开启后系统每隔24小时将按名称搜索全站补全订阅可能漏掉的资源"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -580,6 +583,7 @@ onMounted(() => {
v-model="defaultFilterRules.include" v-model="defaultFilterRules.include"
type="text" type="text"
label="包含(关键字、正则式)" label="包含(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -587,28 +591,34 @@ onMounted(() => {
v-model="defaultFilterRules.exclude" v-model="defaultFilterRules.exclude"
type="text" type="text"
label="排除(关键字、正则式)" label="排除(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="defaultFilterRules.movie_size" v-model="defaultFilterRules.movie_size"
type="text" type="text"
label="电影文件大小GB" label="电影文件大小GB"
placeholder="0-30" placeholder="0-30"
hint="格式0-30表示0到30GB之间的资源"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="defaultFilterRules.tv_size" v-model="defaultFilterRules.tv_size"
type="text" type="text"
label="剧集单集文件大小GB" label="剧集单集文件大小GB"
placeholder="0-10" placeholder="0-10"
hint="格式0-10表示0到10GB之间的资源"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VSwitch <VTextField
v-model="defaultFilterRules.show_edit_dialog" v-model="defaultFilterRules.min_seeders"
label="订阅时编辑更多规则" type="text"
label="最小做种数"
placeholder="0"
hint="小于该值的资源将被过滤掉0表示不过滤"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -630,7 +640,7 @@ onMounted(() => {
width="60rem" width="60rem"
scrollable scrollable
> >
<ImportCodeForm <ImportCodeDialog
v-model="importCodeString" v-model="importCodeString"
title="导入优先级规则" title="导入优先级规则"
@close="importCodeDialog = false" @close="importCodeDialog = false"

View File

@@ -8,6 +8,9 @@ import { requiredValidator } from '@/@validators'
// 选中的媒体服务器 // 选中的媒体服务器
const selectedMediaServers = ref([]) const selectedMediaServers = ref([])
// 选中的下载器
const selectedDownloaders = ref([])
// 下载器选中标签页 // 下载器选中标签页
const downloaderTab = ref('qbittorrent') const downloaderTab = ref('qbittorrent')
@@ -33,7 +36,6 @@ const mediaSettings = ref({
// 下载器设置项 // 下载器设置项
const downloaderSettings = ref({ const downloaderSettings = ref({
DOWNLOADER: '',
DOWNLOADER_MONITOR: true, DOWNLOADER_MONITOR: true,
TORRENT_TAG: '', TORRENT_TAG: '',
QB_HOST: '', QB_HOST: '',
@@ -182,12 +184,15 @@ async function saveMediaSetting() {
} }
// 调用API查询下载器设置 // 调用API查询下载器设置
async function loadDownladerSetting() { async function loadDownloaderSetting() {
try { try {
const result: { [key: string]: any } = await api.get('system/env') const result1: { [key: string]: any } = await api.get('system/setting/DOWNLOADER')
if (result.success) { if (result1.success)
selectedDownloaders.value = result1.data?.value?.split(',')
const result2: { [key: string]: any } = await api.get('system/env')
if (result2.success) {
const { const {
DOWNLOADER,
DOWNLOADER_MONITOR, DOWNLOADER_MONITOR,
TORRENT_TAG, TORRENT_TAG,
QB_HOST, QB_HOST,
@@ -199,9 +204,8 @@ async function loadDownladerSetting() {
TR_HOST, TR_HOST,
TR_USER, TR_USER,
TR_PASSWORD, TR_PASSWORD,
} = result.data } = result2.data
downloaderSettings.value = { downloaderSettings.value = {
DOWNLOADER,
DOWNLOADER_MONITOR, DOWNLOADER_MONITOR,
TORRENT_TAG, TORRENT_TAG,
QB_HOST, QB_HOST,
@@ -214,7 +218,6 @@ async function loadDownladerSetting() {
TR_USER, TR_USER,
TR_PASSWORD, TR_PASSWORD,
} }
downloaderTab.value = DOWNLOADER === 'qbittorrent' ? 'qbittorrent' : 'transmission'
} }
} }
catch (error) { catch (error) {
@@ -225,12 +228,16 @@ async function loadDownladerSetting() {
// 调用API保存下载器设置 // 调用API保存下载器设置
async function saveDownloaderSetting() { async function saveDownloaderSetting() {
try { try {
const result: { [key: string]: any } = await api.post( const result1: { [key: string]: any } = await api.post(
'system/setting/DOWNLOADER',
selectedDownloaders.value.join(','),
)
const result2: { [key: string]: any } = await api.post(
'system/env', 'system/env',
downloaderSettings.value, downloaderSettings.value,
) )
if (result.success) { if (result1.success && result2.success) {
$toast.success('保存下载器设置成功') $toast.success('保存下载器设置成功')
reloadModule() reloadModule()
} }
@@ -323,7 +330,7 @@ async function reloadModule() {
// 加载数据 // 加载数据
onMounted(() => { onMounted(() => {
loadDownladerSetting() loadDownloaderSetting()
loadMediaServerSetting() loadMediaServerSetting()
loadMediaSettings() loadMediaSettings()
}) })
@@ -333,21 +340,25 @@ onMounted(() => {
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VCard title="下载器"> <VCard title="下载器">
<VCardSubtitle>只有选中的下载器才会被默认使用</VCardSubtitle> <VCardSubtitle>只有选中的第1个下载器才会被默认使用</VCardSubtitle>
<VCardText> <VCardText>
<VForm> <VForm>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSelect <VSelect
v-model="downloaderSettings.DOWNLOADER" v-model="selectedDownloaders"
multiple
chips
:items="Downloaders" :items="Downloaders"
label="当前使用下载器" label="当前使用下载器"
hint="MoviePilot自动添加的下载任务将使用选中的第1个下载器"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="downloaderSettings.TORRENT_TAG" v-model="downloaderSettings.TORRENT_TAG"
label="下载器种子标签" label="下载器种子标签"
hint="设置种子标签用于区分MoviePilot添加的下载任务默认标签为`MOVIEPILOT`"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -355,7 +366,8 @@ onMounted(() => {
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSwitch <VSwitch
v-model="downloaderSettings.DOWNLOADER_MONITOR" v-model="downloaderSettings.DOWNLOADER_MONITOR"
label="监控下载器" label="监控默认下载器"
hint="监控选中的第1个下载器当任务下载完成时自动整理文件到媒体库"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -385,6 +397,7 @@ onMounted(() => {
v-model="downloaderSettings.QB_HOST" v-model="downloaderSettings.QB_HOST"
label="地址" label="地址"
placeholder="IP:PORT" placeholder="IP:PORT"
hint="格式IP:PORT如启用了HTTPS请使用https://IP:PORT"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -392,6 +405,7 @@ onMounted(() => {
v-model="downloaderSettings.QB_USER" v-model="downloaderSettings.QB_USER"
label="用户名" label="用户名"
placeholder="admin" placeholder="admin"
hint="QB的登录用户名"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -399,24 +413,28 @@ onMounted(() => {
v-model="downloaderSettings.QB_PASSWORD" v-model="downloaderSettings.QB_PASSWORD"
type="password" type="password"
label="密码" label="密码"
hint="QB的登录密码"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VSwitch <VSwitch
v-model="downloaderSettings.QB_CATEGORY" v-model="downloaderSettings.QB_CATEGORY"
label="自动分类管理" label="自动分类管理"
hint="开启后下载目录将由QB控制自动下载到分类到目录此时MoviePilot的下载目录设定无效需在QB中提前创建分类"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VSwitch <VSwitch
v-model="downloaderSettings.QB_SEQUENTIAL" v-model="downloaderSettings.QB_SEQUENTIAL"
label="顺序下载" label="顺序下载"
hint="开启后QB将按照文件顺序依次下载"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VSwitch <VSwitch
v-model="downloaderSettings.QB_FORCE_RESUME" v-model="downloaderSettings.QB_FORCE_RESUME"
label="强制继续" label="强制继续"
hint="开启后QB将设置为强制继续、强制上传模式带[F]标识)"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -430,6 +448,7 @@ onMounted(() => {
v-model="downloaderSettings.TR_HOST" v-model="downloaderSettings.TR_HOST"
label="地址" label="地址"
placeholder="IP:PORT" placeholder="IP:PORT"
hint="格式IP:PORT如启用了HTTPS请使用https://IP:PORT"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -437,6 +456,7 @@ onMounted(() => {
v-model="downloaderSettings.TR_USER" v-model="downloaderSettings.TR_USER"
label="用户名" label="用户名"
placeholder="admin" placeholder="admin"
hint="TR的登录用户名"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -444,6 +464,7 @@ onMounted(() => {
v-model="downloaderSettings.TR_PASSWORD" v-model="downloaderSettings.TR_PASSWORD"
type="password" type="password"
label="密码" label="密码"
hint="TR的登录密码"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -483,6 +504,7 @@ onMounted(() => {
chips chips
:items="MediaServers" :items="MediaServers"
label="当前使用媒体服务器" label="当前使用媒体服务器"
hint="媒体服务器用于搜索下载等判断库中是否已存在,以避免重复下载"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -490,6 +512,7 @@ onMounted(() => {
v-model="mediaServerSettings.MEDIASERVER_SYNC_INTERVAL" v-model="mediaServerSettings.MEDIASERVER_SYNC_INTERVAL"
:items="syncIntervalItems" :items="syncIntervalItems"
label="同步周期" label="同步周期"
hint="设置后数据将定时同步到MoviePilot数据库以便展示媒体库是否存在标识"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -497,6 +520,7 @@ onMounted(() => {
v-model="mediaServerSettings.MEDIASERVER_SYNC_BLACKLIST" v-model="mediaServerSettings.MEDIASERVER_SYNC_BLACKLIST"
label="媒体库同步黑名单" label="媒体库同步黑名单"
placeholder="使用,分隔" placeholder="使用,分隔"
hint="设置不同步数据的媒体库名称,使用,分隔,如:电影,电视剧"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -529,6 +553,7 @@ onMounted(() => {
v-model="mediaServerSettings.EMBY_HOST" v-model="mediaServerSettings.EMBY_HOST"
label="地址" label="地址"
placeholder="IP:PORT" placeholder="IP:PORT"
hint="格式IP:PORT 或 http(s)://IP:PORT/"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -536,12 +561,14 @@ onMounted(() => {
v-model="mediaServerSettings.EMBY_PLAY_HOST" v-model="mediaServerSettings.EMBY_PLAY_HOST"
label="外网播放地址" label="外网播放地址"
placeholder="http(s)://domain:port" placeholder="http(s)://domain:port"
hint="格式http(s)://domain:port设置后跳转Emby时将优先使用此地址"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="mediaServerSettings.EMBY_API_KEY" v-model="mediaServerSettings.EMBY_API_KEY"
label="API密钥" label="API密钥"
hint="Emby的API密钥在 Emby设置->高级->API 密钥 中生成"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -555,6 +582,7 @@ onMounted(() => {
v-model="mediaServerSettings.JELLYFIN_HOST" v-model="mediaServerSettings.JELLYFIN_HOST"
label="地址" label="地址"
placeholder="IP:PORT" placeholder="IP:PORT"
hint="格式IP:PORT 或 http(s)://IP:PORT/"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -562,12 +590,14 @@ onMounted(() => {
v-model="mediaServerSettings.JELLYFIN_PLAY_HOST" v-model="mediaServerSettings.JELLYFIN_PLAY_HOST"
label="外网播放地址" label="外网播放地址"
placeholder="http(s)://domain:port" placeholder="http(s)://domain:port"
hint="格式http(s)://domain:port设置后跳转Jellyfin时将优先使用此地址"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="mediaServerSettings.JELLYFIN_API_KEY" v-model="mediaServerSettings.JELLYFIN_API_KEY"
label="API密钥" label="API密钥"
hint="Jellyfin的API密钥在 Jellyfin设置->高级->API 密钥 中生成"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -581,6 +611,7 @@ onMounted(() => {
v-model="mediaServerSettings.PLEX_HOST" v-model="mediaServerSettings.PLEX_HOST"
label="地址" label="地址"
placeholder="IP:PORT" placeholder="IP:PORT"
hint="格式IP:PORT 或 http(s)://IP:PORT/"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -588,12 +619,14 @@ onMounted(() => {
v-model="mediaServerSettings.PLEX_PLAY_HOST" v-model="mediaServerSettings.PLEX_PLAY_HOST"
label="外网播放地址" label="外网播放地址"
placeholder="http(s)://domain:port" placeholder="http(s)://domain:port"
hint="格式http(s)://domain:port设置后跳转Plex时将优先使用此地址"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="mediaServerSettings.PLEX_TOKEN" v-model="mediaServerSettings.PLEX_TOKEN"
label="API密钥" label="API密钥"
hint="Plex网页Url中的X-Plex-Token通过浏览器F12->网络从请求URL中获取"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -631,30 +664,35 @@ onMounted(() => {
v-model="mediaSettings.DOWNLOAD_PATH" v-model="mediaSettings.DOWNLOAD_PATH"
label="下载目录" label="下载目录"
:rules="[requiredValidator]" :rules="[requiredValidator]"
hint="MoviePilot添加的下载任务的默认保存目录必须设置"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="mediaSettings.DOWNLOAD_MOVIE_PATH" v-model="mediaSettings.DOWNLOAD_MOVIE_PATH"
label="电影下载目录" label="电影下载目录"
hint="为电影设置单独的下载保存目录,不设置则使用下载目录"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="mediaSettings.DOWNLOAD_TV_PATH" v-model="mediaSettings.DOWNLOAD_TV_PATH"
label="电视剧下载目录" label="电视剧下载目录"
hint="为电视剧设置单独的下载保存目录,不设置则使用下载目录"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="mediaSettings.DOWNLOAD_ANIME_PATH" v-model="mediaSettings.DOWNLOAD_ANIME_PATH"
label="动漫下载目录" label="动漫下载目录"
hint="为动漫设置单独的下载保存目录,不设置则使用下载目录"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSwitch <VSwitch
v-model="mediaSettings.DOWNLOAD_CATEGORY" v-model="mediaSettings.DOWNLOAD_CATEGORY"
label="下载目录自动分类" label="下载目录自动分类"
hint="开启后,下载任务保存目录将根据二级分类策略自动分类存放到下载目录的二级子目录中,二级分类策略需要编辑配置文件目录下的`category.yml`文件,插件市场有提供文件编辑插件"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -664,6 +702,7 @@ onMounted(() => {
v-model="mediaSettings.TRANSFER_TYPE" v-model="mediaSettings.TRANSFER_TYPE"
:items="transferTypeItems" :items="transferTypeItems"
label="整理方式" label="整理方式"
hint="硬链接需要确保下载目录和媒体库目录不跨盘、不跨共享目录、不分别映射rclone需要手动在容器中完成配置且配置名为`MP`"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -671,12 +710,14 @@ onMounted(() => {
v-model="mediaSettings.OVERWRITE_MODE" v-model="mediaSettings.OVERWRITE_MODE"
:items="overwriteModeItems" :items="overwriteModeItems"
label="覆盖模式" label="覆盖模式"
hint="从不覆盖:不覆盖已存在的文件;按大小覆盖:大文件将覆盖小文件;总是覆盖:总是覆盖已存在的文件;仅保留最新版本:保留最新版本的文件,删除其它版本的文件"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSwitch <VSwitch
v-model="mediaSettings.SCRAP_METADATA" v-model="mediaSettings.SCRAP_METADATA"
label="自动刮削媒体信息" label="自动刮削媒体信息"
hint="开启后,整理完成后将自动刮削媒体信息,如海报、简介等"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -687,6 +728,7 @@ onMounted(() => {
label="媒体库目录" label="媒体库目录"
placeholder="多个目录使用,分隔" placeholder="多个目录使用,分隔"
:rules="[requiredValidator]" :rules="[requiredValidator]"
hint="整理完成后的媒体文件存放的根目录,所有整理场景下未设定目的目录时都将整理到该目录下,必须设置"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -694,6 +736,7 @@ onMounted(() => {
v-model="mediaSettings.LIBRARY_MOVIE_NAME" v-model="mediaSettings.LIBRARY_MOVIE_NAME"
label="电影目录名称" label="电影目录名称"
placeholder="电影" placeholder="电影"
hint="设置电影的存放一级目录名称,不设置则使用使用`电影`做为目录名称"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -701,6 +744,7 @@ onMounted(() => {
v-model="mediaSettings.LIBRARY_TV_NAME" v-model="mediaSettings.LIBRARY_TV_NAME"
label="电视剧目录名称" label="电视剧目录名称"
placeholder="电视剧" placeholder="电视剧"
hint="设置电视剧的存放一级目录名称,不设置则使用使用`电视剧`做为目录名称"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -708,12 +752,14 @@ onMounted(() => {
v-model="mediaSettings.LIBRARY_ANIME_NAME" v-model="mediaSettings.LIBRARY_ANIME_NAME"
label="动漫目录名称" label="动漫目录名称"
placeholder="动漫" placeholder="动漫"
hint="设置动漫的存放一级目录名称,不设置则使用使用`动漫`做为目录名称"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSwitch <VSwitch
v-model="mediaSettings.LIBRARY_CATEGORY" v-model="mediaSettings.LIBRARY_CATEGORY"
label="媒体库目录自动分类" label="媒体库目录自动分类"
hint="开启后,整理完成后的媒体文件将根据二级分类策略自动分类存放到媒体库一级目录的二级子目录中,二级分类策略需要编辑配置文件目录下的`category.yml`文件,插件市场有提供文件编辑插件"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -167,6 +167,7 @@ onMounted(() => {
v-model="customIdentifiers" v-model="customIdentifiers"
auto-grow auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组" placeholder="支持正则表达式,特殊字符需要\转义,一行为一组"
hint="支持正则表达式,特殊字符需要\转义,一行为一组"
/> />
</VCardItem> </VCardItem>
<VCardItem> <VCardItem>
@@ -204,6 +205,7 @@ onMounted(() => {
v-model="customReleaseGroups" v-model="customReleaseGroups"
auto-grow auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组" placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
hint="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
/> />
</VCardItem> </VCardItem>
<VCardItem> <VCardItem>
@@ -224,6 +226,7 @@ onMounted(() => {
v-model="customization" v-model="customization"
auto-grow auto-grow
placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义" placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
hint="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
/> />
</VCardItem> </VCardItem>
<VCardItem> <VCardItem>
@@ -244,6 +247,7 @@ onMounted(() => {
v-model="transferExcludeWords" v-model="transferExcludeWords"
auto-grow auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词" placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
hint="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
/> />
</VCardItem> </VCardItem>
<VCardItem> <VCardItem>

View File

@@ -3,7 +3,8 @@ import api from '@/api'
import type { Site } from '@/api/types' import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue' import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue' import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import { useDefer } from '@/@core/utils/dom'
// 数据列表 // 数据列表
const dataList = ref<Site[]>([]) const dataList = ref<Site[]>([])
@@ -14,11 +15,15 @@ const isRefreshed = ref(false)
// 新增站点对话框 // 新增站点对话框
const siteAddDialog = ref(false) const siteAddDialog = ref(false)
// 延迟加载
let defer = (_: number) => true
// 获取站点列表数据 // 获取站点列表数据
async function fetchData() { async function fetchData() {
try { try {
dataList.value = await api.get('site/') dataList.value = await api.get('site/')
isRefreshed.value = true isRefreshed.value = true
defer = useDefer(dataList.value.length)
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
@@ -30,28 +35,26 @@ onBeforeMount(fetchData)
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<div <div
v-if="dataList.length > 0" v-if="dataList.length > 0"
class="grid gap-3 grid-site-card" class="grid gap-3 grid-site-card"
> >
<SiteCard <div
v-for="data in dataList" v-for="(data, index) in dataList"
:key="data.id" :key="index"
:site="data" >
@remove="fetchData" <SiteCard
@update="fetchData" v-if="defer(index)"
/> :key="data.id"
:site="data"
@remove="fetchData"
@update="fetchData"
/>
</div>
</div> </div>
<NoDataFound <NoDataFound
v-if="dataList.length === 0 && isRefreshed" v-if="dataList.length === 0 && isRefreshed"
@@ -60,14 +63,17 @@ onBeforeMount(fetchData)
error-description="已添加并支持的站点将会在这里显示" error-description="已添加并支持的站点将会在这里显示"
/> />
<!-- 新增站点按钮 --> <!-- 新增站点按钮 -->
<VBtn <VFab
icon="mdi-plus" icon="mdi-plus"
location="bottom end"
size="x-large" size="x-large"
class="fixed right-5 bottom-5" fixed
oper="add" app
appear
@click="siteAddDialog = true" @click="siteAddDialog = true"
/> />
<SiteAddEditForm <!-- 新增站点弹窗 -->
<SiteAddEditDialog
v-if="siteAddDialog" v-if="siteAddDialog"
v-model="siteAddDialog" v-model="siteAddDialog"
oper="add" oper="add"

View File

@@ -20,6 +20,7 @@ const calendarOptions: Ref<CalendarOptions> = ref({
], ],
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
weekends: true, weekends: true,
firstDay: 1,
headerToolbar: { headerToolbar: {
left: 'prev', left: 'prev',
center: 'title', center: 'title',
@@ -154,7 +155,7 @@ onMounted(() => {
</VCard> </VCard>
</div> </div>
<div class="md:hidden"> <div class="md:hidden">
<VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`"> <VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`">
<template #activator="{ props }"> <template #activator="{ props }">
<VImg <VImg
height="60" height="60"
@@ -197,6 +198,11 @@ onMounted(() => {
--fc-event-border-color: currentcolor; --fc-event-border-color: currentcolor;
} }
// 当天背景渐变
.fc-day-today {
background-image: linear-gradient(to bottom, #AF85FD ,rgba(var(--v-theme-on-surface), 0.04));
}
.v-application .fc a { .v-application .fc a {
color: inherit; color: inherit;
} }
@@ -378,8 +384,8 @@ onMounted(() => {
} }
.v-application .fc .fc-daygrid-day-number { .v-application .fc .fc-daygrid-day-number {
padding-block: 0rem; padding-block: 0;
padding-inline: 0rem; padding-inline: 0;
} }
.v-application .fc .fc-list-event-dot { .v-application .fc .fc-list-event-dot {
@@ -429,7 +435,7 @@ onMounted(() => {
margin-inline-end: 0.25rem; margin-inline-end: 0.25rem;
} }
@media (max-width: 1264px) { @media (width <= 1264px) {
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-drawerToggler-button { .v-application .fc .fc-toolbar-chunk .fc-button-group .fc-drawerToggler-button {
display: block !important; display: block !important;
} }
@@ -475,10 +481,10 @@ onMounted(() => {
} }
.v-application .fc .fc-button-primary { .v-application .fc .fc-button-primary {
background-color: transparent;
border: none; border: none;
outline: none; background-color: transparent;
color: var(--v-theme-on-surface); color: var(--v-theme-on-surface);
outline: none;
} }
.v-application .fc .fc-button-primary:hover { .v-application .fc .fc-button-primary:hover {
@@ -486,7 +492,7 @@ onMounted(() => {
color: rgb(var(--v-theme-primary)); color: rgb(var(--v-theme-primary));
} }
@media (max-width: 776px) { @media (width <= 776px) {
.fc-daygrid-event-harness { .fc-daygrid-event-harness {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -4,6 +4,8 @@ import api from '@/api'
import type { Subscribe } from '@/api/types' import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue' import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import store from '@/store' import store from '@/store'
// 输入参数 // 输入参数
@@ -17,6 +19,12 @@ const isRefreshed = ref(false)
// 数据列表 // 数据列表
const dataList = ref<Subscribe[]>([]) const dataList = ref<Subscribe[]>([])
// 弹窗
const subscribeEditDialog = ref(false)
// 历史记录弹窗
const historyDialog = ref(false)
// 获取订阅列表数据 // 获取订阅列表数据
async function fetchData() { async function fetchData() {
try { try {
@@ -49,22 +57,15 @@ const filteredDataList = computed(() => {
if (superUser) if (superUser)
return dataList.value.filter(data => data.type === props.type) return dataList.value.filter(data => data.type === props.type)
else else
return dataList.value.filter(data => data.type === props.type && data.username === userName) return dataList.value.filter(data => data.type === props.type && (data.username === userName))
}) })
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<PullRefresh <PullRefresh
v-model="loading" v-model="loading"
@refresh="onRefresh" @refresh="onRefresh"
@@ -88,6 +89,44 @@ const filteredDataList = computed(() => {
error-description="请通过搜索添加电影电视剧订阅" error-description="请通过搜索添加电影电视剧订阅"
/> />
</PullRefresh> </PullRefresh>
<!-- 底部操作按钮 -->
<VFab
icon="mdi-file-document-edit"
location="bottom end"
size="x-large"
fixed
app
appear
@click="subscribeEditDialog = true"
/>
<VFab
icon="mdi-history"
color="info"
location="bottom end"
class="mb-2"
size="x-large"
fixed
app
appear
@click="historyDialog = true"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:default="true"
:type="props.type"
@save="subscribeEditDialog = false"
@close="subscribeEditDialog = false"
/>
<!-- 历史记录弹窗 -->
<SubscribeHistoryDialog
v-if="historyDialog"
v-model="historyDialog"
:type="props.type"
@close="historyDialog = false"
@save="() => {historyDialog = false; fetchData()}"
/>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -4,11 +4,14 @@ import store from '@/store'
// 日志列表 // 日志列表
const logs = ref<string[]>([]) const logs = ref<string[]>([])
// SSE消息对象
let eventSource: EventSource | null = null
// SSE持续获取日志 // SSE持续获取日志
function startSSELogging() { function startSSELogging() {
const token = store.state.auth.token const token = store.state.auth.token
if (token) { if (token) {
const eventSource = new EventSource( eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`, `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
) )
@@ -17,10 +20,6 @@ function startSSELogging() {
if (message) if (message)
logs.value.push(message) logs.value.push(message)
}) })
onBeforeUnmount(() => {
eventSource.close()
})
} }
} }
@@ -34,7 +33,7 @@ function extractLogDetailsFromLogs(logs: string[]): { level: string; time: strin
const matches = RegExp(logPattern).exec(log) const matches = RegExp(logPattern).exec(log)
if (matches && matches.length === 5) { if (matches && matches.length === 5) {
const [_, level, time, program, content] = matches const [_, level, time, program, content] = matches
logDetails.push({ level, time, program, content }) logDetails.unshift({ level, time, program, content })
} }
} }
@@ -65,6 +64,11 @@ const extractLogDetails = computed(() => {
onMounted(() => { onMounted(() => {
startSSELogging() startSSELogging()
}) })
onBeforeUnmount(() => {
if (eventSource)
eventSource.close()
})
</script> </script>
<template> <template>

View File

@@ -0,0 +1,155 @@
<script lang="ts" setup>
import store from '@/store'
import type { Message } from '@/api/types'
import MessageCard from '@/components/cards/MessageCard.vue'
import api from '@/api'
// 定义事件
const emit = defineEmits(['scroll'])
// 消息列表
const messages = ref<Message[]>([])
// 当前页数据
const currData = ref<Message[]>([])
// 是否完成加载
const isLoaded = ref(false)
// 是否加载中
const loading = ref(false)
// 当前页码
const page = ref(1)
// 存量消息最新时间
const lastTime = ref('')
// SSE消息对象
let eventSource: EventSource | null = null
// SSE持续获取消息
function startSSEMessager() {
const token = store.state.auth.token
if (token) {
eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}&role=user`,
)
eventSource.addEventListener('message', (event) => {
const message = event.data
if (message) {
const object = JSON.parse(message)
if (compareTime(object.date, lastTime.value) <= 0)
return
messages.value.push(object)
emit('scroll')
}
})
}
}
// 调用API加载存量消息
async function loadMessages({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
try {
// 设置加载中
loading.value = true
currData.value = await api.get('message/web', {
params: {
page: page.value,
size: 20,
},
})
// 已加载过
isLoaded.value = true
if (currData.value.length > 0) {
// 取最后一条时间为存量消息最新时间
lastTime.value = currData.value[currData.value.length - 1].reg_time ?? ''
// 合并数据
messages.value = [...currData.value, ...messages.value]
if (page.value === 1) {
// 滚动到底部
emit('scroll')
}
// 页码+1
page.value++
// 完成
done('ok')
}
else {
// 没有新数据
done('empty')
}
// 取消加载中
loading.value = false
// 监听SSE消息
startSSEMessager()
}
catch (error) {
console.error(error)
}
}
// 比较yyyy-MM-dd HH:mm:ss时间大小
function compareTime(time1: string, time2: string) {
if (!time1)
return -1
if (!time2)
return 1
return new Date(time1.replaceAll(/-/g, '/')).getTime() - new Date(time2.replaceAll(/-/g, '/')).getTime()
}
onBeforeUnmount(() => {
if (eventSource)
eventSource.close()
})
</script>
<template>
<VInfiniteScroll
:mode="!isLoaded ? 'intersect' : 'manual'"
side="start"
:items="messages"
class="overflow-hidden"
@load="loadMessages"
load-more-text="加载更多 ..."
>
<template #loading>
<LoadingBanner />
</template>
<template #empty>
没有更多数据
</template>
<div>
<VRow
v-for="(msg, index) in messages"
:key="index"
:class="{
'justify-end': msg.action === 0,
'justify-start': msg.action === 1,
}"
>
<VCol
cols="10"
lg="6"
xl="4"
style="position: relative;"
>
<MessageCard
:message="msg"
/>
</VCol>
</VRow>
</div>
<div
v-if="messages.length === 0 && isLoaded && !loading"
class="w-full text-center flex flex-col items-center"
>
<span class="mb-3">当前没有消息</span>
</div>
</VInfiniteScroll>
</template>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import api from '@/api'
// 定义所有的模块ID、名称列表
const modules = ref([
{ id: 'FileTransferModule', name: '媒体目录', state: '', errmsg: '', loading: false },
{ id: 'IndexerModule', name: '站点索引', state: '', errmsg: '', loading: false },
{ id: 'DoubanModule', name: '豆瓣', state: '', errmsg: '', loading: false },
{ id: 'TheMovieDbModule', name: 'TheMovieDb', state: '', errmsg: '', loading: false },
{ id: 'TheTvDbModule', name: 'TheTvDb', state: '', errmsg: '', loading: false },
{ id: 'FanartModule', name: 'Fanart', state: '', errmsg: '', loading: false },
{ id: 'EmbyModule', name: 'Emby', state: '', errmsg: '', loading: false },
{ id: 'JellyfinModule', name: 'Jellyfin', state: '', errmsg: '', loading: false },
{ id: 'PlexModule', name: 'Plex', state: '', errmsg: '', loading: false },
{ id: 'WechatModule', name: '微信', state: '', errmsg: '', loading: false },
{ id: 'TelegramModule', name: 'Telegram', state: '', errmsg: '', loading: false },
{ id: 'SlackModule', name: 'Slack', state: '', errmsg: '', loading: false },
{ id: 'SynologyChatModule', name: 'Synology Chat', state: '', errmsg: '', loading: false },
{ id: 'VoceChatModule', name: 'VoceChat', state: '', errmsg: '', loading: false },
{ id: 'QbittorrentModule', name: 'Qbittorrent', state: '', errmsg: '', loading: false },
{ id: 'TransmissionModule', name: 'Transmission', state: '', errmsg: '', loading: false },
])
// 调用API测试模块
async function moduleTest(index: number) {
try {
const target = modules.value[index]
const moduleid = target.id
target.loading = true
const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`)
target.loading = false
if (result.success) {
target.state = 'success'
target.name = `${target.name} - 正常`
}
else if (result.message?.includes('模块未加载')) {
target.state = ''
target.name = `${target.name} - 未启用`
}
else {
target.state = 'error'
target.name = `${target.name} - 错误!`
target.errmsg = result.message
}
}
catch (error) {
console.error(error)
}
}
// 加载
onMounted(async () => {
// 逐个检查所有模块
for (let i = 0; i < modules.value.length; i++)
await moduleTest(i)
})
</script>
<template>
<VAlert
v-for="(module, index) in modules"
:key="index"
:type="module.state"
:title="module.name"
class="mb-2"
variant="tonal"
>
{{ module.errmsg }}
<template #append>
<VProgressCircular
v-if="module.loading"
indeterminate
/>
</template>
</VAlert>
</template>

Some files were not shown because too many files have changed in this diff Show More