Compare commits

..

644 Commits
v1.5.4 ... main

Author SHA1 Message Date
jxxghp
6fdbc8104c v1.9.17 2024-09-18 17:37:57 +08:00
jxxghp
433c14679c 更新 package.json 2024-09-08 13:09:25 +08:00
jxxghp
fcaa4476f0 更新 package.json 2024-09-05 06:59:13 +08:00
jxxghp
85c5c3058c 更新 AccountSettingDirectory.vue 2024-09-05 06:57:32 +08:00
jxxghp
035122a08e Merge pull request #172 from flowclouds/main 2024-08-16 12:07:18 +08:00
jxxghp
0a76875f8e chore: update package.json version to 1.9.14-2 2024-08-16 10:10:41 +08:00
lj
218eac54ce fix: 优化代码移除相关警告 2024-08-15 15:52:32 +08:00
jxxghp
84deeff4f5 chore: update package.json version to 1.9.14-1 2024-08-12 18:13:47 +08:00
jxxghp
0c72d026f6 chore: update package.json version to 1.9.14 2024-08-08 14:37:40 +08:00
jxxghp
aec9ea83c5 更新 package.json 2024-07-30 06:33:53 +08:00
jxxghp
effd13aedd Merge pull request #171 from InfinityPacer/main 2024-07-22 22:12:19 +08:00
InfinityPacer
42b43d65d7 fix(FilterRuleCard): 移除规则中的空白字符并保留前后的空格 2024-07-22 21:47:42 +08:00
jxxghp
c501d824dd Merge pull request #170 from InfinityPacer/main 2024-07-17 06:53:43 +08:00
InfinityPacer
384ac2faf1 feat: ace-editor support python 2024-07-17 01:48:18 +08:00
jxxghp
dd2c4dd24b v1.9.12 2024-07-16 07:56:07 +08:00
jxxghp
356ffddb1c release 2024-07-09 08:00:11 +08:00
jxxghp
de69be7c4e Merge pull request #168 from s0urcelab/main 2024-07-09 06:28:30 +08:00
s0urce
e962f555ae fix: issue #167 2024-07-09 02:56:48 +08:00
jxxghp
1987246585 Merge pull request #166 from JavaZeroo/main 2024-07-08 19:05:23 +08:00
JavaZero
393264f66b 实现根据操作系统动态显示不同的搜索快捷键提示 2024-07-08 13:51:00 +08:00
jxxghp
9b50020b3b Merge pull request #165 from BrettDean/main 2024-07-05 15:26:31 +08:00
Dean
5e5545fe01 优化"最近入库"数量显示 2024-07-05 15:19:50 +08:00
jxxghp
0e8da35b0a fix bug 2024-07-01 10:55:46 +08:00
jxxghp
4d2cf73330 fix https://github.com/jxxghp/MoviePilot/issues/2471 2024-07-01 10:20:50 +08:00
jxxghp
5df89f2ce4 release 2024-06-28 10:48:20 +08:00
jxxghp
045c0b4c0c fix ui 2024-06-28 10:47:50 +08:00
jxxghp
8b4ffa0795 Update SubscribeCard.vue 2024-06-28 10:03:28 +08:00
jxxghp
14359a37ae Update package.json 2024-06-26 16:14:24 +08:00
jxxghp
a8e4a1c2e0 Merge pull request #162 from jxxghp/main
fix bugs
2024-06-24 12:59:49 +08:00
jxxghp
9048d181af Merge branch 'dev' into main 2024-06-24 12:59:22 +08:00
jxxghp
1cb02994bf fix 登录失败的提示信息 2024-06-24 11:51:39 +08:00
jxxghp
6fad85e957 feat:仪表盘不活跃时不刷新 && 网盘整理联动刮削 2024-06-24 09:13:22 +08:00
jxxghp
db9b2ee6b3 init 2024-06-23 09:35:00 +08:00
jxxghp
8efeb77102 v1.9.8 2024-06-23 09:08:19 +08:00
jxxghp
0215a800e2 fix scrape 2024-06-23 09:06:14 +08:00
jxxghp
87d282f98b fix bug 2024-06-21 21:22:42 +08:00
jxxghp
60c392d3d0 fix 2024-06-21 19:15:40 +08:00
jxxghp
34c3aa25da fix: 修复刮削功能中的路径错误 2024-06-21 12:17:57 +08:00
jxxghp
80690d4cc8 fix win 2024-06-21 11:02:42 +08:00
jxxghp
18f3dc2d44 fix buttons 2024-06-20 17:40:51 +08:00
jxxghp
e8256b4e1a fix bug 2024-06-20 15:34:30 +08:00
jxxghp
4f67bb0250 feat:文件管理批量选择 2024-06-20 15:32:17 +08:00
jxxghp
5dd071adf4 fix bug 2024-06-20 14:01:20 +08:00
jxxghp
aaf5e7f49d feat:阿里云盘支持备份盘 2024-06-20 13:16:05 +08:00
jxxghp
6a5958409a fix:优化文件管理 2024-06-20 11:39:25 +08:00
jxxghp
e0ff98b1d7 fix store 2024-06-20 08:11:47 +08:00
jxxghp
a815e07cdd fix store 2024-06-20 07:08:47 +08:00
jxxghp
aa2fe9740c fix 2024-06-19 18:02:47 +08:00
jxxghp
75a358a4d2 feat: improve QR code UI and add loading skeleton 2024-06-19 16:07:37 +08:00
jxxghp
d5646be6f8 fix qrcode ui 2024-06-19 15:58:26 +08:00
jxxghp
cb04ebcd95 批量重命名进度条 2024-06-19 15:20:50 +08:00
jxxghp
9889ccfc74 feat: add keepAlive meta property to filemanager route 2024-06-19 14:43:38 +08:00
jxxghp
f528bd861a chore: update file list layout for renaming feature 2024-06-19 13:45:08 +08:00
jxxghp
f793654bd8 fix 115 2024-06-19 13:02:24 +08:00
jxxghp
8d064a2165 add storage type 2024-06-19 07:12:35 +08:00
jxxghp
1240899b08 fix api path 2024-06-18 19:19:47 +08:00
jxxghp
558752b890 feat:文件管理批量重命名 2024-06-18 16:46:20 +08:00
jxxghp
997548b7d6 feat:自动识别命名 2024-06-18 13:56:34 +08:00
jxxghp
865d597fe8 add thumbnail 2024-06-18 13:05:25 +08:00
jxxghp
b0a043b464 fix 2024-06-18 12:04:10 +08:00
jxxghp
e003b6f9a7 fix aliyunpan ui 2024-06-18 12:01:38 +08:00
jxxghp
9e9e940dfd fix filelist dropdownmenu 2024-06-18 07:12:33 +08:00
jxxghp
d6dac704eb fix 2024-06-18 07:03:29 +08:00
jxxghp
9aa8dff650 fix aliyunpan 2024-06-17 21:04:13 +08:00
jxxghp
14c2503b0d add aliyun 2024-06-17 19:46:21 +08:00
jxxghp
cb282c6f9a Merge pull request #160 from falling/main 2024-06-17 15:55:54 +08:00
falling
66a5a40482 TorrentCardListView.vue
使用VInfiniteScroll
2024-06-17 15:39:13 +08:00
falling
8d211ed20b Merge branch 'jxxghp:main' into main 2024-06-17 09:54:30 +08:00
falling
bbf2814285 TorrentCardListView.vue
显示性能优化,默认只显示前面20个,页面滚到底部才会再加载新的数据。
2024-06-17 09:53:58 +08:00
jxxghp
a15e479a3e Merge pull request #159 from xiangt920/patch-1
修复使用带subpath的反向代理时api可能无法访问的问题
2024-06-17 09:06:31 +08:00
xiangt920
505d6ec010 fix regex expression for denyList 2024-06-16 19:53:30 -05:00
jxxghp
314ac65e23 Merge pull request #158 from falling/main
TorrentRowListView 筛选bug 以及季集选项排序
2024-06-16 20:18:25 +08:00
falling
118a9a2c5d RowListView:
修复搜索页筛选显示bug以及过滤选项季集排序。
2024-06-16 20:09:50 +08:00
jxxghp
347f47bbef fix apps drag 2024-06-16 14:03:39 +08:00
jxxghp
a73c35468d fix login ui 2024-06-16 13:44:47 +08:00
jxxghp
f9a1446ed5 fix login ui 2024-06-16 09:48:56 +08:00
jxxghp
874ba45034 Merge pull request #157 from falling/main 2024-06-15 23:26:35 +08:00
falling
febe08eb9d 修复搜索页筛选显示bug 2024-06-15 22:50:37 +08:00
jxxghp
9123b34c82 add COOKIECLOUD_BLACKLIST 设置 2024-06-15 21:20:29 +08:00
jxxghp
c66d7cafa6 fix ui 2024-06-15 21:06:56 +08:00
jxxghp
73c54992e2 fix subscribe card 2024-06-15 19:13:26 +08:00
jxxghp
be1a44ad61 fix keepalive 2024-06-15 18:10:56 +08:00
jxxghp
28b307fb98 back arrow 2024-06-14 22:51:50 +08:00
jxxghp
a1dc723445 add select-none 2024-06-14 19:50:28 +08:00
jxxghp
23f4a70693 add drag delay 2024-06-14 19:40:58 +08:00
jxxghp
be5b4b39e5 fix ui 2024-06-14 15:51:38 +08:00
jxxghp
cf706e0e30 fix ui 2024-06-14 15:36:25 +08:00
jxxghp
8bc80d2088 fix ui 2024-06-14 15:12:11 +08:00
jxxghp
b94f8c92f0 fix https://github.com/jxxghp/MoviePilot/issues/2335 2024-06-14 14:38:30 +08:00
jxxghp
c3be75bed1 fix loading ui 2024-06-14 12:30:15 +08:00
jxxghp
91c8d8077f fix search ui 2024-06-14 11:39:00 +08:00
jxxghp
f598eed149 fix ui 2024-06-14 08:27:42 +08:00
jxxghp
971bae3be0 更新 Footer.vue 2024-06-14 07:44:54 +08:00
jxxghp
9a6abf4d5a 更新 Footer.vue 2024-06-14 07:27:09 +08:00
jxxghp
d756077a48 更新 Footer.vue 2024-06-14 07:26:55 +08:00
jxxghp
a1fc87bb1e fix footer ui 2024-06-14 07:15:17 +08:00
jxxghp
07186d2ae1 fix app mode margin 2024-06-13 20:39:27 +08:00
jxxghp
d2164d9ada fix ui 2024-06-13 20:29:53 +08:00
jxxghp
7eacaf8fc5 fix ui 2024-06-13 19:52:31 +08:00
jxxghp
9aa2de526e fix 2024-06-13 19:24:04 +08:00
jxxghp
12dfc5b407 fix app mode ui 2024-06-13 19:11:00 +08:00
jxxghp
1fc964ec16 add app mode 2024-06-13 17:30:50 +08:00
jxxghp
7f2f7b100b 更新 AccountSettingNotification.vue 2024-06-13 07:11:03 +08:00
jxxghp
8292140f1f 更新 package.json 2024-06-13 07:07:45 +08:00
jxxghp
c26e610a23 更新 MediaDetailView.vue 2024-06-13 07:06:49 +08:00
jxxghp
c96cfe81ab Merge pull request #154 from Mattoids/main 2024-06-11 18:43:06 +08:00
liufei
bb1cc0b60e 修复 套件版本无法添加用户的问题 2024-06-11 17:59:49 +08:00
jxxghp
1e74073344 更新 SearchBarView.vue 2024-06-10 17:05:11 +08:00
jxxghp
d83d1dd888 fix 榜单 & 订阅弹窗 & 订阅重置 2024-06-10 09:36:42 +08:00
jxxghp
e34573e72f fix webpush仅限管理员 2024-06-08 12:35:05 +08:00
jxxghp
9d3f4879ef feat:增加域名设置 2024-06-08 10:56:29 +08:00
jxxghp
6317277a70 v1.9.4-1 2024-06-08 07:46:20 +08:00
jxxghp
a1130ec60b feat:捷径根据参数自动打开 2024-06-08 07:45:45 +08:00
jxxghp
a1a3ccf6fb fix 2024-06-07 20:24:02 +08:00
jxxghp
aedb8bee9c fix service worker 2024-06-07 20:22:59 +08:00
jxxghp
6620d1c8fe fix service-worker 2024-06-07 08:34:09 +08:00
jxxghp
0ecc7dfead remove defer 2024-06-06 14:07:25 +08:00
jxxghp
9f5859ee93 feat:订阅重置 2024-06-06 07:57:45 +08:00
jxxghp
d559e1717c fix service worker 2024-06-05 22:21:27 +08:00
jxxghp
e649be58a2 add webpush switch 2024-06-05 18:42:39 +08:00
jxxghp
157c37c862 add service worker 2024-06-05 18:12:07 +08:00
jxxghp
da910ac670 Merge pull request #151 from hotlcc/develop-20240604-2 2024-06-04 17:53:02 +08:00
Allen
3831363815 删除和整理场景路由参数未改变,reloadPage不会生效,需要fetchData刷新数据 2024-06-04 17:41:09 +08:00
jxxghp
94a6ea13bd rollback 2024-06-04 16:18:06 +08:00
jxxghp
06c1ad0f69 更新 main.ts 2024-06-04 16:14:25 +08:00
jxxghp
d6873781e8 更新 SearchBarView.vue 2024-06-04 15:46:36 +08:00
jxxghp
ab6c9647a7 Merge pull request #149 from hotlcc/develop-20240604-1 2024-06-04 15:42:36 +08:00
Allen
59b0350993 针对异形屏做了优化 2024-06-04 15:28:17 +08:00
jxxghp
df0be4c070 更新 package.json 2024-06-04 14:02:59 +08:00
jxxghp
87f3ef4353 Merge pull request #148 from hotlcc/develop-20240604-1 2024-06-04 14:00:49 +08:00
Allen
2611bbaea4 弹窗 VDialog 在低版本 iOS Safari 浏览器下宽度异常问题处理 2024-06-04 13:54:48 +08:00
jxxghp
7c0d8cf792 Merge pull request #147 from hotlcc/develop-20240604-1 2024-06-04 12:54:09 +08:00
Allen
2d17baccd2 低版本safari主菜单样式兼容性处理 2024-06-04 12:33:18 +08:00
jxxghp
fe31723726 fix #145 2024-06-04 11:41:38 +08:00
jxxghp
bb10b22421 fix bug 2024-06-04 08:01:10 +08:00
jxxghp
6445f3a634 Merge pull request #144 from falling/main 2024-06-03 21:11:33 +08:00
falling
d1f28d9c94 资源搜索里的季集下拉列表,从字符串排序改成按季集排序 2024-06-03 21:01:12 +08:00
jxxghp
1e5366123c feat:近期搜索记忆 2024-06-03 16:35:32 +08:00
jxxghp
7feff7c90b fix 2024-06-03 11:45:15 +08:00
jxxghp
429b3bc045 Merge pull request #142 from hotlcc/develop-20240603
Develop 20240603
2024-06-03 11:36:01 +08:00
Allen
e76f1b89da fix number 2024-06-03 11:33:53 +08:00
Allen
f25e8595c3 fix number 2024-06-03 11:17:54 +08:00
jxxghp
6977ce55a3 Merge pull request #141 from hotlcc/develop-20240603
Develop 20240603
2024-06-03 11:09:51 +08:00
Allen
222e0e5ff2 fix encodeURIComponent 2024-06-03 11:04:17 +08:00
Allen
6996d9bbe2 历史记录页面搜索关键字、页码、页大小参数路优化,方便外部定位,同时为了解决支持kbar后路由参数和搜索框内容不一致的问题 2024-06-03 11:00:00 +08:00
jxxghp
f70e08adac Merge pull request #140 from hotlcc/develop-20240603
Develop 20240603
2024-06-03 10:47:04 +08:00
jxxghp
223ecc0e6b fix dialog persistent-hint 2024-06-03 10:43:28 +08:00
Allen
43f36f556c kbar支持历史记录 2024-06-03 10:16:01 +08:00
jxxghp
4579e00283 fix persistent-hint 2024-06-03 10:14:03 +08:00
Allen
b5e9b14048 站点卡片代理和仿真图标顺序与配置界面保存一致 2024-06-03 09:21:03 +08:00
Allen
2288e72c5f 站点卡片有代理等图标时高度保持一致 2024-06-03 09:19:52 +08:00
jxxghp
4882cc0417 release v1.9.3 2024-06-03 08:21:15 +08:00
jxxghp
499d3d0424 fix ui 2024-06-03 08:09:03 +08:00
jxxghp
d6b17debb4 fix loading banner 2024-06-02 21:27:24 +08:00
jxxghp
8f970e0008 feat:支持直接搜索站点资源 2024-06-02 21:10:02 +08:00
jxxghp
18d778a1cc feat:聚合搜索支持订阅 2024-06-02 19:50:28 +08:00
jxxghp
d667c4e45d v1.9.3 2024-06-02 18:50:55 +08:00
jxxghp
b7f8ffd56f fix 聚合搜索 2024-06-02 18:45:50 +08:00
jxxghp
c20f9d527f fix icon 2024-06-02 15:41:09 +08:00
jxxghp
b859d00cb9 fix 聚合搜索 2024-06-02 14:58:58 +08:00
jxxghp
a2d28ad360 feat:聚合搜索(working...) 2024-06-02 11:13:03 +08:00
jxxghp
c6702fbc18 更新 package.json 2024-06-01 22:31:44 +08:00
jxxghp
5018f96786 fix VFab 2024-06-01 22:07:19 +08:00
jxxghp
f29f408b67 feat:目录选择组件 2024-05-31 20:35:57 +08:00
jxxghp
a475a3b851 fix DirectoryTreeInput 2024-05-31 18:31:45 +08:00
jxxghp
9335f79c30 add DirectoryTreeInput 2024-05-31 15:06:58 +08:00
jxxghp
9dab691649 Merge pull request #139 from hotlcc/develop-20240531
vuetify升级至3.6.8后,设定中tab的选中样式改变,修复为原版选中样式
2024-05-31 14:10:36 +08:00
Allen
16abc65f49 vuetify升级至3.6.8后,设定中tab的选中样式改变,修复为原版选中样式 2024-05-31 13:00:35 +08:00
jxxghp
23ac80886d upgrade vuetify to 3.6.8 2024-05-31 11:46:27 +08:00
jxxghp
b242e757e0 add kbar 2024-05-31 11:26:19 +08:00
jxxghp
a69965a605 Merge pull request #138 from hotlcc/develop-20240531 2024-05-31 11:25:14 +08:00
Allen
3321427eb4 修正tab路由参数为query,解决原先采用params路由参数时导致主菜单选中状态不同步的问题 2024-05-31 11:18:44 +08:00
jxxghp
3ffe354770 v1.9.2-3 2024-05-31 08:04:48 +08:00
jxxghp
52e0d3a4bc fix tab route 2024-05-31 08:04:27 +08:00
jxxghp
e865a5ca62 Merge pull request #136 from hotlcc/develop-20240530 2024-05-30 15:24:06 +08:00
Allen
528a4ddb03 完善设定tab精确路由 2024-05-30 15:16:49 +08:00
jxxghp
36f3b649c6 Merge pull request #135 from hotlcc/develop-20240530 2024-05-30 11:59:54 +08:00
Allen
ce91c0cc30 await接口请求后才重新获取插件仪表板,解决仪表板调整配置保存时出现重复插件请求的问题 2024-05-30 11:36:13 +08:00
jxxghp
e31e9e3520 更新 dashboard.vue 2024-05-29 15:30:04 +08:00
jxxghp
df313ebe7f fix 2024-05-29 15:26:59 +08:00
jxxghp
e1cf36e952 v1.9.2-2 2024-05-29 15:17:10 +08:00
jxxghp
493194652c 仪表板组件高度拉齐开关 && 无边框组件背景不拉平 && 修复多次定时问题 2024-05-29 15:15:15 +08:00
jxxghp
5030e75c2c fix ui 2024-05-29 09:17:22 +08:00
jxxghp
3c70eac7ca fix 种子剧集过滤 2024-05-27 09:12:46 +08:00
jxxghp
f9b22962a4 fix hover 2024-05-27 08:48:25 +08:00
jxxghp
7ce0c21b0c fix 2024-05-26 18:38:41 +08:00
jxxghp
7a7a8c923f fix 2024-05-26 18:15:41 +08:00
jxxghp
d5d5e28f7e fix 文件管理路径 2024-05-26 18:14:31 +08:00
jxxghp
b22ac27075 fix 2024-05-26 17:55:38 +08:00
jxxghp
3cb5f4bdfe fix 默认下载路径 2024-05-26 17:41:41 +08:00
jxxghp
d355e4575d fix #2179 根据路径自动匹配刮削开关 2024-05-26 09:37:42 +08:00
jxxghp
bdbb118e55 fix https://github.com/jxxghp/MoviePilot-Frontend/issues/131 2024-05-26 08:09:47 +08:00
jxxghp
9a174d99db Update MediaDirectoryCard.vue 2024-05-25 07:07:13 +08:00
jxxghp
9c8725066c Merge pull request #130 from hotlcc/develop-20240524-插件支持多仪表板组件 2024-05-24 15:48:16 +08:00
Allen
9f0f3de864 一个插件支持透出多个仪表板控件,并兼容历史 2024-05-24 14:56:33 +08:00
jxxghp
ac84ed2d6a v1.9.1-1 2024-05-24 11:20:16 +08:00
jxxghp
9d7e15f4df feat:同盘优先选项 2024-05-24 11:18:30 +08:00
jxxghp
c3563f4501 v1.9.1 2024-05-24 09:00:42 +08:00
jxxghp
a543202edc feat:订阅保存路径支持下拉选择 2024-05-24 08:16:10 +08:00
jxxghp
52cf517a91 站点拖动排序 2024-05-23 19:39:33 +08:00
jxxghp
11b649dc8c fix 手动整理选择目录
fix https://github.com/jxxghp/MoviePilot/issues/2145
2024-05-23 12:39:35 +08:00
jxxghp
19663bacb1 更新 TransferHistoryView.vue 2024-05-23 10:34:38 +08:00
jxxghp
41c276d0e0 更新 AccountSettingDirectory.vue 2024-05-23 09:17:11 +08:00
jxxghp
6bb73add28 release-beta 2024-05-23 08:42:00 +08:00
jxxghp
2c16b6c078 fix manual_transfer 2024-05-23 08:09:48 +08:00
jxxghp
5ddc955805 feat:目录设置UI 2024-05-22 18:01:53 +08:00
jxxghp
6a3afa4240 fix dashboard refresh 2024-05-21 20:20:51 +08:00
jxxghp
deabd7b83c fix ui 2024-05-21 10:51:10 +08:00
jxxghp
422e5858ef fix ui 2024-05-19 14:30:20 +08:00
jxxghp
3c019d1376 feat:自定义主题 2024-05-19 14:20:01 +08:00
jxxghp
f676e8423e 更新 package.json 2024-05-18 11:10:41 +08:00
jxxghp
f687d1de01 更新 manifest.json 2024-05-18 11:09:59 +08:00
jxxghp
6fe28bc2ef fix 2024-05-17 14:12:44 +08:00
jxxghp
86b5af3423 去除无用package 2024-05-17 13:59:00 +08:00
jxxghp
8f3dce058c v1.8.9 2024-05-17 12:19:59 +08:00
jxxghp
825b8bb4a5 Merge pull request #128 from hotlcc/develop-20240517-页面优化
仪表板组件拖拽按钮按照hover进行展示
2024-05-17 10:56:16 +08:00
jxxghp
05320d1070 Merge branch 'main' into develop-20240517-页面优化 2024-05-17 10:56:08 +08:00
jxxghp
33d2a396ce 仪表板支持自定义标题 2024-05-17 10:54:19 +08:00
jxxghp
ae4cce8abf 安装到桌面时支持操作按钮 2024-05-17 10:41:35 +08:00
Allen
b85950e4ca 仪表板组件拖拽按钮按照hover进行展示 2024-05-17 10:23:57 +08:00
jxxghp
aecf52551b fix 2024-05-17 07:37:10 +08:00
jxxghp
fc877ed836 fix ui 2024-05-16 20:31:30 +08:00
jxxghp
5580921b7d 站点超时时间设置 2024-05-16 14:42:35 +08:00
jxxghp
6b7d0a0fe2 fix Module Test 2024-05-16 14:18:32 +08:00
jxxghp
f55efbe1e2 feat:种子页面排序 2024-05-16 13:08:55 +08:00
jxxghp
8e6fc3c417 Merge pull request #127 from dh336699/main 2024-05-16 12:25:24 +08:00
hao.dai
7943ab6017 fix: 只有一季以及多季只订阅一季订阅成功无提示问题 2024-05-16 11:29:15 +08:00
jxxghp
81725a58cf Merge pull request #126 from hotlcc/develop-20240516-页面优化 2024-05-16 10:57:33 +08:00
jxxghp
5cbcf46aaa Merge pull request #124 from hotlcc/develop-20240515-页面优化 2024-05-16 10:56:46 +08:00
Allen
49dd3f726a 解决路由回跳缺陷(1、手动退出后重新登录会错误地回到上次丢失认证时记录的路由页面;2、后端接口403时会错误地回到上次丢失认证时记录的路由页面而不是当前页面) 2024-05-16 10:54:10 +08:00
Allen
73f9ebc709 插件仪表板支持自定义子标题 2024-05-15 10:29:34 +08:00
Allen
f6884ba4f9 插件仪表板组件卸载时取消刷新定时器 2024-05-15 10:27:24 +08:00
jxxghp
5d39d0e139 fix subscribe card 2024-05-14 15:54:53 +08:00
jxxghp
6a1463ef17 fix subscribe card 2024-05-14 15:47:29 +08:00
jxxghp
5d00f23cb3 fix bug 2024-05-14 12:19:05 +08:00
jxxghp
6ea106b25d feat:优先级规则支持拖动排序 2024-05-14 11:32:28 +08:00
jxxghp
d501bf7506 feat:仪表板组件支持无边框 2024-05-13 20:23:12 +08:00
jxxghp
1408060053 fix dashboard ui 2024-05-13 12:17:48 +08:00
jxxghp
0c37c01496 feat: add new media cards and components 2024-05-13 07:06:37 +08:00
jxxghp
d2049f7839 fix dashboard refresh 2024-05-12 20:22:20 +08:00
jxxghp
33cdf672b3 fix ui 2024-05-11 17:34:08 +08:00
jxxghp
145c89acc3 release beta 2024-05-11 13:46:55 +08:00
jxxghp
706d7d6dc1 fix apexchats datalabels 2024-05-11 13:46:16 +08:00
jxxghp
2c35d0f897 fix layout ui 2024-05-11 12:53:42 +08:00
jxxghp
f227ae89ec fix 2024-05-10 20:32:07 +08:00
jxxghp
ac43d53884 fix #2045 2024-05-10 20:08:04 +08:00
jxxghp
4b70549bcb fix sort 2024-05-09 19:05:45 +08:00
jxxghp
ea601ae404 fix mobile 2024-05-09 18:54:19 +08:00
jxxghp
201411841c fix versions ui 2024-05-09 18:39:44 +08:00
jxxghp
d857acc58e fix drag handle 2024-05-09 18:30:25 +08:00
jxxghp
d005252f13 fix bug 2024-05-09 15:21:46 +08:00
jxxghp
2065992b17 仪表板组件支持拖动排序 2024-05-09 14:45:12 +08:00
jxxghp
74e96980e6 插件仪表板支持自动刷新 & 仅管理员可见 2024-05-09 08:03:01 +08:00
jxxghp
09110d1ef7 支持插件扩展仪表板 2024-05-08 21:03:00 +08:00
jxxghp
bcf55e63f1 调整热门订阅热度显示样式 2024-05-08 08:08:08 +08:00
jxxghp
dd22b2580e fix nodata svg 2024-05-07 19:30:43 +08:00
jxxghp
62a0e46698 release 2024-05-07 16:10:13 +08:00
jxxghp
14b68135fb fix VDivder 2024-05-07 13:50:49 +08:00
jxxghp
d44b62e489 fix btnui 2024-05-07 13:36:13 +08:00
jxxghp
b0f5c2a493 feat:显示流行度 2024-05-07 12:34:30 +08:00
jxxghp
d6cfbc60a8 更新 PluginCard.vue 2024-05-06 18:43:16 +08:00
jxxghp
fe51f5ced4 更新 SubscribeEditDialog.vue 2024-05-06 18:39:28 +08:00
jxxghp
b257b0453e 更新 SiteAddEditDialog.vue 2024-05-06 18:38:57 +08:00
jxxghp
a88105a086 更新 ReorganizeDialog.vue 2024-05-06 18:38:02 +08:00
jxxghp
2dc792690e Update button styles in PluginCard.vue, ReorganizeDialog.vue, SiteAddEditDialog.vue, and SubscribeEditDialog.vue 2024-05-06 18:26:52 +08:00
jxxghp
aa146b1cdf Update confirmation dialog styles and props in UserProfile.vue, FileList.vue, PluginCard.vue, and dashboard.vue 2024-05-06 18:18:05 +08:00
jxxghp
c44b20bae3 Update confirmation dialog styles and props in UserProfile.vue, FileList.vue, PluginCard.vue, and dashboard.vue 2024-05-06 17:37:47 +08:00
jxxghp
cad8964841 Remove unused styles in setting.vue 2024-05-06 16:41:41 +08:00
jxxghp
ec9a989214 Update card cover size and alignment in PluginAppCard and PluginCard components 2024-05-06 16:14:44 +08:00
jxxghp
7f05932fb9 remove github icon 2024-05-06 13:22:16 +08:00
jxxghp
d51694e1cb fix text 2024-05-06 12:37:34 +08:00
jxxghp
3079483e6b feat:订阅统计共享 2024-05-06 11:37:52 +08:00
jxxghp
bee4264a39 Update styling in setting.vue and PluginCardListView.vue 2024-05-05 20:20:37 +08:00
jxxghp
c949ea2667 Update version to 1.8.6 in package.json 2024-05-05 19:53:52 +08:00
jxxghp
2bcb28d0c0 Update card cover size in PluginAppCard and PluginCard components 2024-05-05 19:51:36 +08:00
jxxghp
bd257554cd Update grid-template-columns in TorrentCardListView and SubscribeListView components 2024-05-05 19:37:57 +08:00
jxxghp
68a27e0b61 remove defer for plugins & sites 2024-05-05 19:35:26 +08:00
jxxghp
8b589bdb9c feat:媒体详情显示集存在标识 2024-05-05 13:45:30 +08:00
jxxghp
1a25710aac fix user/current 调用过多 2024-05-05 12:01:01 +08:00
jxxghp
271d59ca51 Fix issues with MediaCard and SubscribeListView components 2024-05-05 11:54:20 +08:00
jxxghp
37e5e57d5b fix #2011 2024-05-05 11:22:10 +08:00
jxxghp
f817b20545 fix 历史记录进度条 2024-04-30 18:16:09 +08:00
jxxghp
5f8619805e fix mediainfocard 2024-04-30 11:56:15 +08:00
jxxghp
c9d4629bfa fix ui 2024-04-30 11:27:51 +08:00
jxxghp
c9c27c83d4 rollback 2024-04-30 09:27:09 +08:00
jxxghp
7a6a985c47 fix 2024-04-30 09:19:07 +08:00
jxxghp
225df7b1e6 插件支持最近更新排序 2024-04-30 08:35:29 +08:00
jxxghp
97ede69609 fix cols 2024-04-30 07:17:01 +08:00
jxxghp
c5ded86d8a fix:站点信息维护增加属性 2024-04-29 20:26:56 +08:00
jxxghp
b4f049ecda fix searchbar size 2024-04-29 15:12:15 +08:00
jxxghp
56692eb6cb feat 搜索框记忆 2024-04-29 14:51:00 +08:00
jxxghp
7c22c60190 plugins defer render 2024-04-29 13:45:21 +08:00
jxxghp
2f2c4d4a44 fix 2024-04-29 13:37:20 +08:00
jxxghp
c1c71916db fix dialog 2024-04-29 13:31:27 +08:00
jxxghp
4b15a7454c fix ui 2024-04-29 12:47:52 +08:00
jxxghp
22e723587d 调整插件页面UI 2024-04-29 12:30:21 +08:00
jxxghp
969adaf5bb Merge pull request #122 from hotlcc/develop-20240429-页面优化
升级vuetify-use-dialog至v0.6.11,解决弹出Confirm后点击overlay关闭后无法再次弹出Confirm的问题
2024-04-29 11:30:55 +08:00
Allen
c2214e8300 升级vuetify-use-dialog至v0.6.11,解决弹出Confirm后点击overlay关闭后无法再次弹出Confirm的问题 2024-04-29 11:28:37 +08:00
jxxghp
10af659227 fix hint 2024-04-29 10:51:34 +08:00
jxxghp
5cd3757f4f rollback 2024-04-29 09:55:25 +08:00
jxxghp
81f674ea01 test 2024-04-29 07:16:16 +08:00
jxxghp
1846ee0ffe fix ui 2024-04-28 21:45:16 +08:00
jxxghp
14a825093a 更新 package.json 2024-04-28 21:07:57 +08:00
jxxghp
d70f477bc1 fix theme 2024-04-28 18:36:10 +08:00
jxxghp
c9c897ffb5 Merge pull request #121 from hotlcc/develop-20240428-页面优化
主题配置保存到服务端,使各端主题一致
2024-04-28 18:10:28 +08:00
Allen
462dea3e05 解决媒体卡片右上角评分和入库标记冲突,hover的时候展示评分 2024-04-28 17:40:39 +08:00
Allen
4e7a0084dd 兼容处理不支持黑白配色的老旧设备 2024-04-28 16:40:47 +08:00
Allen
0268df0e24 主题配置保存到服务端,使各端主题一致 2024-04-28 16:18:12 +08:00
jxxghp
f926ca66c0 fix ui 2024-04-28 14:05:51 +08:00
jxxghp
16b5898928 fix ui 2024-04-28 13:48:16 +08:00
jxxghp
c1bb66cc9d fix 插件过滤 & 标签 2024-04-28 13:40:49 +08:00
jxxghp
f7502d0d18 Merge pull request #118 from thsrite/main 2024-04-28 10:26:46 +08:00
thsrite
b4975f649c fix 默认过滤规则支持最少做种人数生效发布时间,防止过滤掉到最新发布的种子 2024-04-28 10:21:46 +08:00
jxxghp
89353c1f7e feat:媒体搜索聚合开关设置 2024-04-28 08:56:00 +08:00
jxxghp
fce10b6dca media card icon 2024-04-27 18:16:05 +08:00
jxxghp
2cf95c6706 feat:发现内容聚合 2024-04-27 15:19:50 +08:00
jxxghp
58ab1599db fix person cards 2024-04-27 08:44:39 +08:00
jxxghp
9745c2ea1a fix hint 2024-04-27 08:18:58 +08:00
jxxghp
9db46e2949 fix ui 2024-04-27 08:16:36 +08:00
jxxghp
7949505104 fix ui 2024-04-26 23:58:06 +08:00
jxxghp
db0d5133e8 feat:人物搜索 2024-04-26 20:35:40 +08:00
jxxghp
54415377ee Merge pull request #117 from thsrite/main
fix 搜索默认规则支持最小做种数
2024-04-25 16:21:52 +08:00
thsrite
d7f55477da fix 搜索默认规则支持最小做种数 2024-04-25 16:19:50 +08:00
jxxghp
faca586fa7 Merge pull request #116 from hotlcc/develop-20240425-页面优化
修改“本地存在未安装的旧版本插件且云端有更新时”不会在插件市场展示的问题
2024-04-25 16:09:16 +08:00
jxxghp
5f3ba7b9c7 fix bug 2024-04-25 16:06:47 +08:00
Allen
abace4a58d 修改“本地存在未安装的旧版本插件且云端有更新时”不会在插件市场展示的问题 2024-04-25 15:43:30 +08:00
jxxghp
5895cea587 feat:进度条公共组件 2024-04-25 15:07:35 +08:00
jxxghp
cdbcef5232 release 2024-04-25 13:52:12 +08:00
jxxghp
d5d6bfdc56 feat:插件详情支持动态API调用 2024-04-25 13:30:25 +08:00
jxxghp
75ae7f0c15 Merge pull request #115 from hotlcc/develop-20240425-页面优化 2024-04-25 12:07:22 +08:00
Allen
6931451f18 媒体详情页面搜索资源按钮当下拉选项只有一个时不触发下拉框直接搜索 2024-04-25 11:25:52 +08:00
jxxghp
f5625e1354 fix bug 2024-04-25 10:26:20 +08:00
jxxghp
d1be4a30b6 feat:前端版本号显示 2024-04-25 10:19:09 +08:00
jxxghp
5c13362db2 fix fullscreen 2024-04-25 09:48:12 +08:00
jxxghp
6c71dce80c 历史记录可排序 2024-04-25 08:51:02 +08:00
jxxghp
790c397951 fix ui 2024-04-25 08:44:36 +08:00
jxxghp
e28e74b874 Merge pull request #113 from hotlcc/develop-20240424-1-插件搜索框宽度优化 2024-04-24 21:42:10 +08:00
Allen
b99ea22d89 插件搜索弹出框小屏下全屏 2024-04-24 21:28:15 +08:00
jxxghp
8938195c5d fix 2024-04-24 18:21:08 +08:00
jxxghp
887b4a7862 fix ui 2024-04-24 18:02:30 +08:00
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
jxxghp
058b32a263 release 2024-03-05 17:27:27 +08:00
jxxghp
e7b960838e Merge pull request #82 from honue/main 2024-03-05 15:24:46 +08:00
honue
14e776a287 登录后返回原始页面功能 2024-03-05 15:20:27 +08:00
jxxghp
73f11b920f Merge pull request #81 from WangEdward/main
feat: add search_imdbid
2024-03-05 12:57:44 +08:00
jxxghp
5c93040a8e fanart nettest 2024-03-05 12:57:02 +08:00
WangEdward
a517769e8a feat: add search_imdbid 2024-03-04 21:36:06 +08:00
jxxghp
4bb59a9f05 fix 2024-03-03 09:02:41 +08:00
jxxghp
b37879d2d4 release 2024-03-03 08:48:04 +08:00
jxxghp
05defc39d7 rollback VTooltip 2024-03-03 08:47:20 +08:00
jxxghp
18bfad07d2 fix 2024-03-02 09:58:42 +08:00
jxxghp
b83591255d Merge pull request #80 from maicss/main 2024-03-02 09:14:56 +08:00
maicss
804350bc81 feat: search box add border for contrast 2024-03-01 23:33:50 +08:00
maicss
46e1cae0bb feat: add session valid chack 2024-03-01 23:33:50 +08:00
maicss
81062d4580 fix: enhance some interactive experiences 2024-03-01 23:33:50 +08:00
maicss
55481db2ee Merge branch 'jxxghp:main' into main 2024-03-01 20:16:10 +08:00
jxxghp
ecdd12f5a9 fix 2024-02-29 20:21:30 +08:00
jxxghp
ef92cdc183 fix torrent type 2024-02-29 16:15:28 +08:00
jxxghp
08f4a6cf2c Merge pull request #78 from maicss/maicss 2024-02-29 11:16:48 +08:00
maicss
38889acb4e fix: toast message not visible
When install plugin failed, it will yield an error toast. But attribute `z-index` of toast is `1090`, while `v-dialog` is `2400`
2024-02-29 09:51:20 +08:00
maicss
c0517cd29a fix: toast message not visible
When install plugin failed, `z-index` of toast is `1090` while `v-dialog` is `2400`.
2024-02-29 09:48:23 +08:00
jxxghp
084449ccf3 rebuild 2024-02-28 18:54:35 +08:00
jxxghp
0e8203ae03 fix app 2024-02-27 19:25:12 +08:00
jxxghp
236440be52 fix form 2024-02-27 15:23:07 +08:00
jxxghp
6f7e4bb272 release 2024-02-26 13:21:37 +08:00
jxxghp
38dcd3635a fix text 2024-02-25 17:17:53 +08:00
jxxghp
a3f3330dad fix 2024-02-25 08:58:46 +08:00
jxxghp
bbc6c57c08 Merge pull request #77 from cikezhu/main 2024-02-24 06:47:02 +08:00
叮叮当
2f36a8edef fix 2024-02-23 22:27:01 +08:00
叮叮当
df637fb887 定时作业添加提供者条目 2024-02-23 22:26:16 +08:00
jxxghp
be74c92a35 更新 package.json 2024-02-23 16:22:57 +08:00
jxxghp
a219a64e20 Merge pull request #76 from alkalixin/main 2024-02-23 16:22:19 +08:00
alkalixin
25c22a276a 给部分按钮增加浮动提示 2024-02-23 15:07:01 +08:00
jxxghp
6e6be057ca fix ui 2024-02-23 10:45:21 +08:00
jxxghp
af69efa48b feat:插件日志单独查看 2024-02-22 13:32:23 +08:00
jxxghp
c551083fa4 优化插件页面加载速度 2024-02-21 17:50:02 +08:00
jxxghp
9767feed29 更新 package.json 2024-02-18 15:19:52 +08:00
jxxghp
4392818e92 Merge pull request #75 from honue/main 2024-02-18 15:17:53 +08:00
honue
8d22bafeb6 订阅卡片增加剧集详情跳转功能 2024-02-18 15:08:22 +08:00
jxxghp
89ddd1fb78 更新 package.json 2024-02-17 13:19:04 +08:00
jxxghp
24513fa22b Merge pull request #74 from honue/main 2024-02-17 13:18:46 +08:00
honue
cddde0c2a0 fix 2024-02-17 13:09:48 +08:00
jxxghp
9c674e0018 更新 package.json 2024-02-15 21:06:19 +08:00
jxxghp
0c6476d283 更新 AccountSettingSystem.vue 2024-02-15 21:05:58 +08:00
jxxghp
bf0c529a59 fix ui 2024-02-15 20:01:51 +08:00
jxxghp
877bb4d4a2 fix ui 2024-02-15 18:58:22 +08:00
jxxghp
dc4db0b2b3 fix settings 2024-02-15 18:17:36 +08:00
jxxghp
a738d4a3b9 feat:系统设置面板 2024-02-15 15:04:59 +08:00
jxxghp
e9866a04df v1.6.4 2024-02-14 21:07:09 +08:00
jxxghp
4f5193d602 feat:更新Cookie支持两步验证 2024-02-14 21:06:42 +08:00
jxxghp
37b92c55ba feat:文件管理手动刮削 2024-02-10 19:32:49 +08:00
jxxghp
9299f1bcb6 release 2024-02-10 09:48:45 +08:00
jxxghp
7fe12192df add apexcharts 2024-02-10 09:36:54 +08:00
jxxghp
1169644ab3 fix render 2024-02-09 08:43:36 +08:00
jxxghp
6f7770ed43 fix 2024-02-08 20:58:41 +08:00
jxxghp
8059fd6f90 fix 2024-02-08 20:57:09 +08:00
jxxghp
556dbd8d78 fix bug 2024-02-08 20:07:23 +08:00
jxxghp
6695fd8c14 add ace-editor 2024-02-08 19:56:28 +08:00
jxxghp
3ab0229275 fix ui 2024-02-08 13:43:37 +08:00
jxxghp
99467127a0 更新 package.json 2024-02-08 13:28:11 +08:00
jxxghp
90d73b7bd5 Merge pull request #73 from cikezhu/main 2024-02-08 13:27:40 +08:00
叮叮当
2e326e1798 fix 全部日志url反向代理时被serviceWorker拦截 2024-02-08 12:45:52 +08:00
jxxghp
251eac93c7 更新 package.json 2024-02-08 07:11:49 +08:00
jxxghp
c74d70808c Merge pull request #72 from cikezhu/main 2024-02-08 07:10:48 +08:00
叮叮当
e63b2d7152 新窗口打开全部日志 2024-02-08 00:06:52 +08:00
jxxghp
16b29b56a5 更新 package.json 2024-01-29 23:11:57 +08:00
jxxghp
6d79c4fe2f Merge pull request #71 from cikezhu/main 2024-01-29 23:11:34 +08:00
叮叮当
4b1fb60ee3 fix 媒体信息页面person跳转问题 2024-01-29 23:01:22 +08:00
jxxghp
1d2be54f9e 更新 package.json 2024-01-29 11:05:38 +08:00
jxxghp
83547e32db Merge pull request #70 from cikezhu/main 2024-01-29 11:05:10 +08:00
叮叮当
70ddb929f2 fix plugin_icon 2024-01-28 00:47:37 +08:00
叮叮当
8b22961394 支持基于路径的反向代理 2024-01-27 14:47:00 +08:00
jxxghp
c15d42c179 Merge pull request #69 from falling/main 2024-01-24 18:43:37 +08:00
falling
098e473cab 从qbt后台的返回值来更新下载状态 2024-01-21 19:48:47 +08:00
jxxghp
f6f3d9368a fix bug 2024-01-08 13:22:42 +08:00
jxxghp
9558a420e9 fix image proxy 2024-01-08 12:25:06 +08:00
jxxghp
4d3b69ca34 更新 package.json 2024-01-08 11:33:32 +08:00
jxxghp
fdcc4a44c8 Merge pull request #68 from jjjokin/feat-auto-switch-theme 2024-01-08 11:01:26 +08:00
jokin
5de0494538 增加主题自适应选项 2024-01-07 22:17:11 +08:00
jxxghp
2045f833e4 fix bug 2024-01-06 11:07:23 +08:00
jxxghp
cc4f89aac1 fix 2024-01-06 11:02:03 +08:00
jxxghp
1c2f2c17d4 fix plex image 2024-01-06 10:58:35 +08:00
jxxghp
ace7a6621f image proxy 2024-01-05 21:35:33 +08:00
jxxghp
d02fe55a1e fix ui 2024-01-05 20:46:23 +08:00
jxxghp
9b753a8f5b fix 2024-01-05 20:40:44 +08:00
jxxghp
11e82582b8 fix 2024-01-05 17:34:52 +08:00
jxxghp
419358863e feat: dashboard 2024-01-05 17:32:30 +08:00
jxxghp
1d0d7f9975 dashboard cards 2024-01-05 15:59:03 +08:00
jxxghp
c5f564372b Merge pull request #66 from thofx/thofx_fix_ui 2024-01-04 21:38:30 +08:00
thofx
a50f0cd727 fix: 列表使用useDefer渲染 2024-01-04 21:35:35 +08:00
jxxghp
96f6f55138 fix type 2024-01-04 20:50:51 +08:00
jxxghp
6a45c8b358 fix service.js 2024-01-04 20:46:42 +08:00
jxxghp
165937596e fix safari window.open 2024-01-04 08:12:32 +08:00
jxxghp
fb976f043b fix 2024-01-03 21:30:00 +08:00
jxxghp
ecb9c4e51a fix safari 2024-01-03 21:29:40 +08:00
jxxghp
9e8c3b495c 更新 MediaDetailView.vue 2024-01-03 21:11:56 +08:00
jxxghp
24a37fc33c 更新 MediaDetailView.vue 2024-01-03 18:58:16 +08:00
jxxghp
d09a21114d fix 播放跳转 2024-01-03 18:39:39 +08:00
jxxghp
6e2b12501f feat:订阅弹窗开关 2024-01-03 18:11:13 +08:00
jxxghp
2a56e116cf fix 2024-01-03 17:30:49 +08:00
jxxghp
6de4f238d8 feat:size limit 2024-01-03 17:05:01 +08:00
jxxghp
1b426c5957 fix bug 2024-01-03 16:52:35 +08:00
jxxghp
82454a650c feat:dashboard可编辑 2024-01-03 13:09:28 +08:00
jxxghp
227b6bd7ef v1.5.7 2024-01-03 12:46:23 +08:00
jxxghp
9554025daf feat:在线播放 2024-01-03 12:45:44 +08:00
jxxghp
0eb5d607bf v1.5.6 2024-01-01 20:06:37 +08:00
jxxghp
750f4bc276 fix ui 2023-12-30 10:07:02 +08:00
jxxghp
d0aada1d3d 更新 package.json 2023-12-29 15:41:50 +08:00
jxxghp
8a4848387c Merge pull request #65 from thofx/thofx_fix_ui 2023-12-28 20:07:51 +08:00
thofx
6904fc7da3 fix: 文件管理初始化两次的bug 2023-12-28 20:05:50 +08:00
jxxghp
28c55a05e6 Merge pull request #64 from thofx/thofx_fix_ui 2023-12-28 07:04:14 +08:00
jxxghp
562c829267 Merge pull request #63 from honue/main 2023-12-28 07:04:04 +08:00
thofx
b200ed242d fix: 解决历史记录不居中的问题
历史历史错误信息使用tooltip展示
fix:解决当搜索结果过多导致页面卡顿问题
2023-12-27 23:39:12 +08:00
honue
815cfe55df fix 2023-12-27 17:18:58 +08:00
honue
40a1094d74 fix 更新数量为1时不显示角标 2023-12-27 13:39:23 +08:00
honue
346650c091 feat:更改多集展示样式 2023-12-27 13:26:26 +08:00
172 changed files with 16984 additions and 11326 deletions

View File

@@ -1 +1,2 @@
VITE_API_BASE_URL=http://localhost:3001/api/v1/
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM

View File

@@ -1 +1,2 @@
VITE_API_BASE_URL=/api/v1/
VITE_API_BASE_URL=api/v1/
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: '18'
node-version: '20'
cache: 'yarn'
- name: Download Icons
@@ -39,6 +39,7 @@ jobs:
run: |
yarn
yarn build
echo "$frontend_version" > dist/version.txt
zip -r dist.zip dist
- name: Generate Release

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ node_modules
.DS_Store
dist
dist-ssr
dev-dist
*.local
/cypress/videos/

View File

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

12
.vscode/settings.json vendored
View File

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

View File

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

322
auto-imports.d.ts vendored
View File

@@ -1,6 +1,7 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
@@ -41,6 +42,7 @@ declare global {
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
@@ -77,6 +79,7 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -105,6 +108,7 @@ declare global {
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -143,6 +147,7 @@ declare global {
const useCeil: typeof import('@vueuse/math')['useCeil']
const useClamp: typeof import('@vueuse/math')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
@@ -308,11 +313,13 @@ declare global {
// for type re-export
declare global {
// @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
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
@@ -351,6 +358,7 @@ declare module 'vue' {
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']>
@@ -387,6 +395,7 @@ declare module 'vue' {
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']>
@@ -415,6 +424,7 @@ declare module 'vue' {
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']>
@@ -453,6 +463,316 @@ declare module 'vue' {
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 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 useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>

6
components.d.ts vendored
View File

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

View File

@@ -1,214 +1,162 @@
<!DOCTYPE html>
<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>
<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)">
<head>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="expires" content="0">
<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" />
<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" />
<link rel="preload" href="index.js" as="script">
</head>
<body>
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
<svg width="10rem" height="10rem" 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
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">
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" />
</clipPath>
<g clip-path="url(#_clip5)">
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
<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"
/>
</clipPath>
<g clip-path="url(#_clip5)">
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
<path
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)"
/>
</g>
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
<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"
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>
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)" />
</g>
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
<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"
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>
<defs>
<linearGradient
id="_Linear1"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"
>
<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" />
</linearGradient>
<linearGradient
id="_Linear2"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
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="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
</linearGradient>
<linearGradient
id="_Linear3"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"
>
<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="_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>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
<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" />
</linearGradient>
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
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="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
</linearGradient>
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
<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="_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 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'
<div class="loading">
<div class="effect-1 effects"></div>
<div class="effect-2 effects"></div>
<div class="effect-3 effects"></div>
</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)
</script>
</body>
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
</script>
</body>
</html>

View File

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

View File

@@ -3,10 +3,9 @@ body {
}
html {
overflow: hidden auto;
background: var(--initial-loader-bg, #fff);
min-block-size: calc(100% + env(safe-area-inset-top));
overflow-x: hidden;
overflow-y: auto;
}
#loading-bg {
@@ -20,8 +19,8 @@ html {
.loading-logo {
position: absolute;
inset-block-start: 40%;
inset-inline-start: calc(50% - 50px);
inset-block-start: 35%;
inset-inline-start: calc(50% - 5rem);
}
.loading {
@@ -83,4 +82,4 @@ html {
opacity: 1;
transform: rotate(1turn);
}
}
}

View File

@@ -1,80 +0,0 @@
{
"name": "MoviePilot",
"short_name": "MoviePilot",
"start_url": "./",
"icons": [
{
"src": "./android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-192x192_maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-512x512_maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#28243D",
"background_color": "#28243D",
"display": "standalone",
"shortcuts": [
{
"name": "推荐",
"url": "./ranking",
"icons": [
{
"src": "./sparkles-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "电影订阅",
"url": "./subscribe-movie",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "电视剧订阅",
"url": "./subscribe-tv",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "设置",
"url": "./setting",
"icons": [
{
"src": "./cog-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
]
}

View File

@@ -11,6 +11,13 @@ http {
keepalive_timeout 3600;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_proxied any;
gzip_min_length 256;
gzip_vary on;
gzip_comp_level 6;
server {
include mime.types;
@@ -28,9 +35,16 @@ http {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
# 静态资源
expires 1y;
add_header Cache-Control "public, immutable";
root html;
}
location /assets {
# 静态资源
expires 7d;
expires 1y;
add_header Cache-Control "public";
root html;
}
@@ -75,6 +89,26 @@ http {
# 超时设置
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 {

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -25,10 +25,25 @@ app.use(
})
);
// 配置代理中间件将CookieCloud请求转发给后端API
app.use(
'/cookiecloud',
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /cookiecloud 前缀
proxyReqPathResolver: (req) => {
return `/cookiecloud${req.url}`
}
})
);
// 处理根路径的请求
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
res.sendFile(path.join(__dirname, 'index.html'))
})
// 处理所有其他请求,重定向到前端入口文件
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'))
})
app.listen(port, () => {

View File

@@ -1,16 +1,21 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
// 是否显示
innerClass: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['click'])
const emit = defineEmits(['click', 'update:modelValue'])
// 按钮点击
function onClick() {
emit('update:modelValue', false)
emit('click')
}
</script>
<template>
<IconBtn
class="absolute right-3 top-3"
:class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'"
@click.stop="onClick"
>
<VIcon icon="mdi-close" />

View File

@@ -0,0 +1,15 @@
<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 || !props.progress" class="mb-3" size="64" 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

@@ -1,7 +1,14 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useTheme } from 'vuetify'
import { ref } from 'vue'
import { useDisplay, useTheme } from 'vuetify'
import type { ThemeSwitcherTheme } from '@layouts/types'
import api from '@/api'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { useToast } from 'vue-toast-notification'
import { VAceEditor } from 'vue3-ace-editor'
// 显示器宽度
const display = useDisplay()
const props = defineProps<{
themes: ThemeSwitcherTheme[]
@@ -11,59 +18,33 @@ const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
const {
state: currentThemeName,
next: getNextThemeName,
index: currentThemeIndex,
} = useCycleList(
const { state: currentThemeName, next: getNextThemeName } = useCycleList(
props.themes.map(t => t.name),
{ initialValue: savedTheme.value },
)
function changeTheme() {
const nextTheme = getNextThemeName()
const $toast = useToast()
globalTheme.name.value = nextTheme
savedTheme.value = nextTheme
localStorage.setItem('theme', nextTheme)
// 修改载入时背景色
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
// 自定义CSS弹窗
const cssDialog = ref(false)
themeTransition()
}
// 自定义 CSS
const customCSS = ref('')
// Update icon if theme is changed from other sources
watch(
() => globalTheme.name.value,
(val) => {
currentThemeName.value = val
},
)
// Apply saved theme on page load
// onMounted(() => {
// globalTheme.name.value = savedTheme.value
// })
function hasScrollbar(el?: Element | null) {
if (!el || el.nodeType !== Node.ELEMENT_NODE)
return false
const style = window.getComputedStyle(el)
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
}
// 编辑器主题
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
// 主题切换动画
function themeTransition() {
const x = performance.now()
for (let i = 0; i++ < 1e7; (i << 9) & ((9 % 9) * 9 + 9));
const cost = performance.now() - x
if (cost > 10)
return
if (cost > 10) return
const el: HTMLElement = document.querySelector('[data-v-app]')!
const children = el.querySelectorAll('*') as NodeListOf<HTMLElement>
children.forEach((el) => {
children.forEach(el => {
if (hasScrollbar(el)) {
el.dataset.scrollX = String(el.scrollLeft)
el.dataset.scrollY = String(el.scrollTop)
@@ -95,7 +76,7 @@ function themeTransition() {
})
document.body.append(copy)
; (copy.querySelectorAll('[data-scroll-x], [data-scroll-y]') as NodeListOf<HTMLElement>).forEach((el) => {
;(copy.querySelectorAll('[data-scroll-x], [data-scroll-y]') as NodeListOf<HTMLElement>).forEach(el => {
el.scrollLeft = +el.dataset.scrollX!
el.scrollTop = +el.dataset.scrollY!
})
@@ -113,12 +94,145 @@ function themeTransition() {
el.addEventListener('transitionend', onTransitionend)
el.addEventListener('transitioncancel', onTransitionend)
}
// 更新主题
function updateTheme() {
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
globalTheme.name.value = theme
savedTheme.value = theme
themeTransition()
// 保存主题到本地
localStorage.setItem('theme', theme)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
}
// 切换主题
function changeTheme(theme: string) {
let nextTheme = theme
if (!theme) nextTheme = getNextThemeName()
currentThemeName.value = nextTheme
// 保存主题到服务端
try {
api.post('/user/config/theme', nextTheme, {
headers: {
'Content-Type': 'text/plain',
},
})
} catch (e) {
console.error('保存主题到服务端失败')
}
}
// 是否有滚动条
function hasScrollbar(el?: Element | null) {
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false
const style = window.getComputedStyle(el)
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
}
// 监听系统主题变化
try {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
} catch (e) {
console.error('当前设备不支持监听系统主题变化')
}
// 查询当前主题的图标
const getThemeIcon = computed(() => {
const theme = props.themes.find(t => t.name === currentThemeName.value)
return theme?.icon ?? 'mdi-circle'
})
// 监听设置主题变化
watch(
() => currentThemeName.value,
() => updateTheme(),
)
// 获取自定义 CSS
async function getCustomCSS() {
try {
const result: { [key: string]: any } = await api.get('system/setting/UserCustomCSS')
if (result && result.success && result.data?.value) {
customCSS.value = result.data?.value ?? ''
if (customCSS.value) {
const style = document.createElement('style')
style.innerHTML = result.data?.value ?? ''
document.head.appendChild(style)
}
}
} catch (error) {
console.error(error)
}
}
// 保存自定义 CSS
async function saveCustomCSS() {
cssDialog.value = false
try {
const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', customCSS.value, {
headers: {
'Content-Type': 'text/plain',
},
})
if (result.success) $toast.success('自定义CSS保存成功')
} catch (e) {
console.error('保存自定义 CSS 到服务端失败')
}
}
onMounted(() => {
getCustomCSS()
})
</script>
<template>
<IconBtn @click="changeTheme">
<VIcon :icon="props.themes[currentThemeIndex].icon" />
</IconBtn>
<VMenu v-if="props.themes">
<template v-slot:activator="{ props }">
<IconBtn v-bind="props">
<VIcon :icon="getThemeIcon" />
</IconBtn>
</template>
<VList>
<VListItem v-for="theme in props.themes" :key="theme.name" @click="changeTheme(theme.name)">
<template #prepend>
<VIcon :icon="theme.icon" />
</template>
<VListItemTitle>{{ theme.title }}</VListItemTitle>
</VListItem>
<VListItem @click="cssDialog = true">
<template #prepend>
<VIcon icon="mdi-palette" />
</template>
<VListItemTitle>自定义</VListItemTitle>
</VListItem>
</VList>
</VMenu>
<!-- 自定义 CSS -- -->
<VDialog v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard title="自定义主题风格">
<DialogCloseBtn @click="cssDialog = false" />
<VDivider />
<VAceEditor
v-model:value="customCSS"
lang="css"
:theme="editorTheme"
style="block-size: 100%; min-block-size: 30rem"
/>
<VDivider />
<VCardText class="text-center">
<VBtn @click="saveCustomCSS" class="w-1/2">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="sass">

View File

@@ -3,5 +3,5 @@
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
/* stylelint-enable */
background-color: rgb(var(--v-theme-surface), 0.9);
background-color: rgb(var(--v-theme-surface), 0.8);
}

View File

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

21
src/@core/utils/dom.ts Normal file
View File

@@ -0,0 +1,21 @@
export function removeEl(selector: string) {
if (selector) {
const el = document.querySelector(selector)
el?.parentNode?.removeChild(el)
}
}
export function useDefer(maxFrameCount = 1) {
const frameCount = ref(0)
const refreshFrameCount = () => {
requestAnimationFrame(() => {
frameCount.value++
if (frameCount.value < maxFrameCount)
refreshFrameCount()
})
}
refreshFrameCount()
return function (showInFrameCount: number) {
return frameCount.value >= showInFrameCount
}
}

View File

@@ -1,8 +1,14 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import ZH_CN from 'dayjs/locale/zh-cn'
import { isToday } from './index'
dayjs.extend(relativeTime)
dayjs.locale(ZH_CN)
export function avatarText(value: string) {
if (!value)
return ''
if (!value) return ''
const nameArray = value.split(' ')
return nameArray.map(word => word.charAt(0).toUpperCase()).join('')
@@ -12,19 +18,23 @@ export function avatarText(value: string) {
export function kFormatter(num: number) {
const regex = /\B(?=(\d{3})+(?!\d))/g
return Math.abs(num) > 9999 ? `${Math.sign(num) * +((Math.abs(num) / 1000).toFixed(1))}k` : Math.abs(num).toFixed(0).replace(regex, ',')
return Math.abs(num) > 9999
? `${Math.sign(num) * +(Math.abs(num) / 1000).toFixed(1)}k`
: Math.abs(num).toFixed(0).replace(regex, ',')
}
/**
* 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 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
*/
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
if (!value)
return value
export function formatDate(
value: string,
formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' },
) {
if (!value) return value
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
}
@@ -32,28 +42,26 @@ export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions
/**
* Return short human friendly month representation of date
* Can also convert date to only time if date is of today (Better UX)
* @param {String} value date to format
* @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current
* @param {string} value date to format
* @param {boolean} toTimeForCurrentDay Shall convert to time if day is today/current
*/
export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) {
const date = new Date(value)
let formatting: Record<string, string> = { month: 'short', day: 'numeric' }
if (toTimeForCurrentDay && isToday(date))
formatting = { hour: 'numeric', minute: 'numeric' }
if (toTimeForCurrentDay && isToday(date)) formatting = { hour: 'numeric', minute: 'numeric' }
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
}
export const prefixWithPlus = (value: number) => value > 0 ? `+${value}` : value
export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : value)
// 格式化为Sxx
export const formatSeason = (value: string) => value ? `S${value.padStart(2, '0')}` : ''
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
// 格式化为xx[TGMK]B
export function formatFileSize(bytes: number) {
if (bytes < 0)
throw new Error('字节数不能为负数。')
if (bytes < 0) throw new Error('字节数不能为负数。')
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = bytes
@@ -75,22 +83,18 @@ export function formatSeconds(seconds: number) {
let formattedTime = ''
if (hours > 0)
formattedTime += `${hours}小时`
if (hours > 0) formattedTime += `${hours}小时`
if (minutes > 0)
formattedTime += `${minutes}`
if (minutes > 0) formattedTime += `${minutes}`
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0)
formattedTime += `${remainingSeconds}`
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0) formattedTime += `${remainingSeconds}`
return formattedTime
}
// YYYY-MM-DD 转化为Date
export function parseDate(dateString: string): Date | null {
if (!dateString)
return null
if (!dateString) return null
const [year, month, day] = dateString.split('-').map(Number)
return new Date(year, month - 1, day)
@@ -98,8 +102,7 @@ export function parseDate(dateString: string): Date | null {
// 文件大小格式化
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0)
return '0 bytes'
if (bytes === 0) return '0 bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
@@ -107,5 +110,40 @@ export function formatBytes(bytes: number, decimals = 2) {
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]}`
}
// 格式化剧集列表
export function formatEp(nums: number[]): string {
if (!nums.length) return ''
if (nums.length === 1) return nums[0].toString()
// 将数组升序排序
nums.sort((a, b) => a - b)
const formattedRanges: string[] = []
let start = nums[0]
let end = nums[0]
for (let i = 1; i < nums.length; i++) {
if (nums[i] === end + 1) {
end = nums[i]
} else {
if (start === end) formattedRanges.push(start.toString())
else formattedRanges.push(`${start.toString()}-${end.toString()}`)
start = end = nums[i]
}
}
if (start === end) formattedRanges.push(start.toString())
else formattedRanges.push(`${start.toString()}-${end.toString()}`)
return formattedRanges.join('、')
}
// 将yyyy-mm-dd hh:mm:ss转换为时间差1小时前1天前
export function formatDateDifference(dateString: string): string {
if (!dateString) return ''
return dayjs(dateString).fromNow()
}

View File

@@ -1,7 +1,6 @@
// 👉 IsEmpty
export function isEmpty(value: unknown): boolean {
if (value === null || value === undefined || value === '')
return true
if (value === null || value === undefined || value === '') return true
return !!(Array.isArray(value) && value.length === 0)
}
@@ -33,37 +32,6 @@ export function isToday(date: Date) {
)
}
// 计算时间差返回xx天xx小时xx分钟
export function calculateTimeDifference(inputTime: string): string {
if (!inputTime)
return ''
const inputDate = new Date(inputTime)
const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime()
const secondsDifference = Math.floor(timeDifference / 1000)
if (secondsDifference < 60) {
return `${secondsDifference}`
}
else if (secondsDifference < 3600) {
const minutes = Math.floor(secondsDifference / 60)
return `${minutes}分钟`
}
else if (secondsDifference < 86400) {
const hours = Math.floor(secondsDifference / 3600)
return `${hours}小时`
}
else {
const days = Math.floor(secondsDifference / 86400)
return `${days}`
}
}
// 判断一个数组subArray是不是在另一个数组mainArray中
export function isContained(subArray: any[], mainArray: any[]): boolean {
return subArray.every(element => mainArray.includes(element))
@@ -76,9 +44,24 @@ export function isIntersected(array1: any[], array2: any[]): boolean {
export function isNullOrEmptyObject(obj: any): boolean {
// 首先判断是否为 null 或 undefined
if (obj === null || obj === undefined)
return true
if (obj === null || obj === undefined) return true
// 然后判断是否为空对象
return !!(typeof obj === 'object' && Object.keys(obj).length === 0)
}
// 判断系统配置色是否是黑暗的
export function checkPrefersColorSchemeIsDark(): boolean {
try {
return window.matchMedia('(prefers-color-scheme: dark)').matches
} catch (e) {
return false
}
}
// 从URL中获取参数值
export function getQueryValue(key: string, url = window.location.href): string {
const reg = new RegExp(`[?&]${key}=([^&#]*)`, 'i')
const res = reg.exec(url)
return res ? res[1] : ''
}

View File

@@ -28,3 +28,17 @@ export async function copyToClipboard(content: string) {
document.body.removeChild(input)
}
}
// VAPID公钥转Uint8Array
export function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}

View File

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

View File

@@ -52,7 +52,7 @@ export default defineComponent({
'main',
{ class: 'layout-page-content' },
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

@@ -33,7 +33,10 @@ defineProps<{
.nav-link a {
display: flex;
align-items: center;
border-radius: 0 3.125rem 3.125rem 0 !important;
cursor: pointer;
margin-inline-end: 1.125em;
padding-inline: 1.375rem 1rem;
}
}
</style>

View File

@@ -18,3 +18,12 @@ defineProps<{
</div>
</li>
</template>
<style lang="scss">
.layout-vertical-nav {
.nav-section-title {
padding-left: 1.375rem;
padding-right: 1rem;
}
}
</style>

View File

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

View File

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

View File

@@ -6,19 +6,19 @@ export interface UserConfig {
app: {
title: Lowercase<string>
logo: VNode
contentWidth: typeof ContentWidth[keyof typeof ContentWidth]
contentLayoutNav: typeof AppContentLayoutNav[keyof typeof AppContentLayoutNav]
contentWidth: (typeof ContentWidth)[keyof typeof ContentWidth]
contentLayoutNav: (typeof AppContentLayoutNav)[keyof typeof AppContentLayoutNav]
overlayNavFromBreakpoint: number
enableI18n: boolean
isRtl: boolean
iconRenderer?: Component
}
navbar: {
type: typeof NavbarType[keyof typeof NavbarType]
type: (typeof NavbarType)[keyof typeof NavbarType]
navbarBlur: boolean
}
footer: {
type:typeof FooterType[keyof typeof FooterType]
type: (typeof FooterType)[keyof typeof FooterType]
}
verticalNav: {
isVerticalNavCollapsed: boolean
@@ -120,6 +120,12 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
disable?: boolean
}
export interface NavMenu extends NavLink {
header: string
admin: boolean
description?: string
}
// 👉 Vertical nav group
export interface NavGroup extends Partial<AclProperties> {
title: string
@@ -143,7 +149,7 @@ interface I18nLanguage {
// avatar | text | icon
// Thanks: https://stackoverflow.com/a/60617060/10796681
type Notification = {
id:number
id: number
title: string
subtitle: string
time: string
@@ -157,5 +163,6 @@ type Notification = {
interface ThemeSwitcherTheme {
name: string
title: string
icon: string
}

View File

@@ -1,38 +1,49 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify'
import store from './store'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
// 第一时间应用主题
const { global: globalTheme } = useTheme()
globalTheme.name.value = localStorage.getItem('theme') || 'light'
// 提示框
const $toast = useToast()
// 生效主题
async function setTheme() {
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
}
// SSE持续接收消息
function startSSEMessager() {
const token = store.state.auth.token
if (token) {
const eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`,
)
// ApexCharts 全局配置
declare global {
interface Window {
Apex: any
}
}
eventSource.addEventListener('message', (event) => {
const message = event.data
if (message)
$toast.info(message)
})
onBeforeUnmount(() => {
eventSource.close()
})
if (window.Apex) {
// 数据标签
window.Apex.dataLabels = {
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
// 如果有小数点,保留两位小数,否则保留整数
const data = w.config.series[seriesIndex]
return data.toFixed(data % 1 === 0 ? 0 : 1)
},
}
// 图例
window.Apex.legend = {
labels: {
useSeriesColors: true,
},
}
// 标题
window.Apex.title = {
style: {
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
},
}
}
// 页面加载时,加载当前用户数据
onBeforeMount(async () => {
startSSEMessager()
setTheme()
})
</script>

69
src/ace-config.ts Normal file
View File

@@ -0,0 +1,69 @@
import ace from 'ace-builds'
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'
import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
import modePythonUrl from 'ace-builds/src-noconflict/mode-python?url'
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'
import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
import workerCssUrl from 'ace-builds/src-noconflict/worker-css?url'
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?url'
import snippetsPythonUrl from 'ace-builds/src-noconflict/snippets/python?url'
import 'ace-builds/src-noconflict/ext-language_tools'
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
ace.config.setModuleUrl('ace/mode/python', modePythonUrl)
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl)
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
ace.config.setModuleUrl('ace/mode/css_worker', workerCssUrl)
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
ace.config.setModuleUrl('ace/snippets/python', snippetsPythonUrl)
ace.require('ace/ext/language_tools')

View File

@@ -8,32 +8,32 @@ const api = axios.create({
})
// 添加请求拦截器
api.interceptors.request.use((config) => {
api.interceptors.request.use(config => {
// 在请求头中添加token
const token = store.state.auth.token
if (token)
config.headers.Authorization = `Bearer ${token}`
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 添加响应拦截器
api.interceptors.response.use((response) => {
return response.data
}, (error) => {
if (!error.response) {
// 请求超时
api.interceptors.response.use(
response => {
return response.data
},
error => {
if (!error.response) {
// 请求超时
return Promise.reject(new Error(error))
} else if (error.response.status === 403) {
// 清除登录状态信息
store.dispatch('auth/logout')
// token验证失败跳转到登录页面
router.push('/login')
}
return Promise.reject(error)
}
else if (error.response.status === 403) {
// 清除登录状态信息
store.dispatch('auth/clearToken')
// token验证失败跳转到登录页面
router.push('/login')
}
return Promise.reject(error)
})
},
)
export default api

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 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,21 +1,28 @@
<script lang="ts" setup>
import type { Axios } from 'axios'
import axios from 'axios'
import Toolbar from './filebrowser/Toolbar.vue'
import Tree from './filebrowser/Tree.vue'
import List from './filebrowser/List.vue'
import type { EndPoints } from '@/api/types'
import FileList from './filebrowser/FileList.vue'
import FileToolbar from './filebrowser/FileToolbar.vue'
import type { EndPoints, FileItem } from '@/api/types'
import api from '@/api'
import AliyunAuthDialog from './dialog/AliyunAuthDialog.vue'
import U115AuthDialog from './dialog/U115AuthDialog.vue'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数
const props = defineProps({
storages: String,
storage: String,
path: String,
tree: Boolean,
endpoints: Object as PropType<EndPoints>,
axios: Object as PropType<Axios>,
axios: {
type: Object as PropType<Axios>,
required: true,
},
axiosconfig: Object,
item: {
type: Object as PropType<FileItem>,
required: true,
},
itemstack: Array as PropType<FileItem[]>,
})
// 对外事件
@@ -27,6 +34,16 @@ const availableStorages = [
code: 'local',
icon: 'mdi-folder-multiple-outline',
},
{
name: '阿里云盘',
code: 'aliyun',
icon: 'mdi-cloud-outline',
},
{
name: '115网盘',
code: 'u115',
icon: 'mdi-cloud-outline',
},
]
const fileIcons = {
@@ -59,8 +76,14 @@ const activeStorage = ref('local')
const refreshPending = ref(false)
// 排序
const sort = ref('name')
// axios实例
const axiosInstance = ref<Axios>()
// 阿里云盘认证对话框
const aliyunAuthDialog = ref(false)
// 阿里云盘用户信息
const aliyunUserInfo = ref<{ [key: string]: any }>({})
// 115网盘认证对话框
const u115AuthDialog = ref(false)
// 115网盘用户信息
const u115UserInfo = ref<{ [key: string]: any }>({})
// 计算属性
const storagesArray = computed(() => {
@@ -70,19 +93,56 @@ const storagesArray = computed(() => {
// 方法
function loadingChanged(loading: number) {
if (loading)
loading++
else if (loading > 0)
loading--
if (loading) loading++
else if (loading > 0) loading--
}
function storageChanged(storage: string) {
// 查询阿里云
async function loadAliyunUserInfo() {
try {
const result: { [key: string]: any } = await api.get('aliyun/userinfo')
if (result.success) {
aliyunUserInfo.value = result
}
} catch (error) {
console.log(error)
}
}
// 查询115
async function loadU115UserInfo() {
try {
const result: { [key: string]: any } = await api.get('u115/storage')
if (result.success) {
u115UserInfo.value = result
}
} catch (error) {
console.log(error)
}
}
// 存储切换
async function storageChanged(storage: string) {
if (storage == 'aliyun') {
await loadAliyunUserInfo()
if (isNullOrEmptyObject(aliyunUserInfo.value)) {
aliyunAuthDialog.value = true
return
}
} else if (storage == 'u115') {
await loadU115UserInfo()
if (isNullOrEmptyObject(u115UserInfo.value)) {
u115AuthDialog.value = true
return
}
}
activeStorage.value = storage
emit('pathchanged', { path: '/', fileid: 'root' })
}
// 路径变化
function pathChanged(_path: string) {
emit('pathchanged', _path)
function pathChanged(item: FileItem) {
emit('pathchanged', item)
}
// 排序变化
@@ -91,57 +151,55 @@ function sortChanged(s: string) {
refreshPending.value = true
}
// 初始化
onBeforeMount(() => {
activeStorage.value = props.storage ?? 'local'
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
})
// aliyun认证完成
function aliyunAuthDone() {
aliyunAuthDialog.value = false
activeStorage.value = 'aliyun'
}
// u115认证完成
function u115AuthDone() {
u115AuthDialog.value = false
activeStorage.value = 'u115'
}
</script>
<template>
<VCard class="mx-auto" :loading="loading > 0">
<Toolbar
:path="props.path"
:storages="storagesArray"
:storage="activeStorage"
:endpoints="props.endpoints"
:axios="axiosInstance"
@storagechanged="storageChanged"
@pathchanged="pathChanged"
@foldercreated="refreshPending = true"
@sortchanged="sortChanged"
/>
<VRow no-gutters>
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
<Tree
:path="props.path"
:storage="activeStorage"
:icons="fileIcons"
:endpoints="endpoints"
:axios="axiosInstance"
:refreshpending="refreshPending"
@pathchanged="pathChanged"
@loading="loadingChanged"
@refreshed="refreshPending = false"
/>
</VCol>
<VDivider v-if="tree" vertical />
<VCol>
<List
:path="props.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 v-if="activeStorage && item">
<FileToolbar
:item="item"
:itemstack="itemstack"
:storages="storagesArray"
:storage="activeStorage"
:endpoints="endpoints"
:axios="axios"
@storagechanged="storageChanged"
@pathchanged="pathChanged"
@foldercreated="refreshPending = true"
@sortchanged="sortChanged"
/>
<FileList
:item="item"
:storage="activeStorage"
:icons="fileIcons"
:endpoints="endpoints"
:axios="axios"
:refreshpending="refreshPending"
:sort="sort"
@pathchanged="pathChanged"
@loading="loadingChanged"
@refreshed="refreshPending = false"
@filedeleted="refreshPending = true"
@renamed="refreshPending = true"
/>
</div>
</VCard>
<AliyunAuthDialog
v-if="aliyunAuthDialog"
v-model="aliyunAuthDialog"
@close="aliyunAuthDialog = false"
@done="aliyunAuthDone"
/>
<U115AuthDialog v-if="u115AuthDialog" v-model="u115AuthDialog" @close="u115AuthDialog = false" @done="u115AuthDone" />
</template>

View File

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

View File

@@ -0,0 +1,81 @@
<script lang="ts" setup>
import type { MediaServerPlayItem } from '@/api/types'
// 输入参数
const props = defineProps({
media: Object as PropType<MediaServerPlayItem>,
width: String,
height: String,
})
// 图片是否加载完成
const imageLoaded = ref(false)
// 图片加载完成响应
function imageLoadHandler() {
imageLoaded.value = true
}
// 跳转播放
function goPlay() {
if (props.media?.link) window.open(props.media?.link, '_blank')
}
// 计算图片地址
const getImgUrl = computed(() => {
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
})
</script>
<template>
<VHover v-bind="props">
<template #default="hover">
<VCard
v-bind="hover.props"
:height="props.height"
:width="props.width"
class="ring-gray-500"
:class="{
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': imageLoaded,
}"
@click="goPlay"
>
<template #image>
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
</div>
</template>
<VCardText
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
>
<h1
class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ..."
>
{{ props.media?.title }}
</h1>
<span class="text-shadow">{{ props.media?.subtitle }}</span>
</VCardText>
</VImg>
</template>
<div class="w-full absolute bottom-0">
<VProgressLinear
v-if="props.media?.percent"
:model-value="props.media?.percent"
bg-color="success"
color="success"
/>
</div>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.text-shadow {
text-shadow: 1px 1px #777;
}
</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

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MediaDirectory } from '@/api/types'
import { VTextField } from 'vuetify/lib/components/index.mjs'
// 输入参数
const props = defineProps({
type: String, // download/library
directory: {
type: Object as PropType<MediaDirectory>,
required: true, // 必填参数
},
categories: {
type: Object as PropType<{ [key: string]: any }>,
required: true,
},
width: String,
height: String,
})
// 路径
const path = ref<string>('')
// 类型下拉字典
const typeItems = [
{ title: '全部', value: '' },
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
]
// 定义触发的自定义事件
const emit = defineEmits(['close', 'changed', 'update:modelValue'])
// 按钮点击
function onClose() {
emit('close')
}
// 路径更新
function updatePath(value: string) {
path.value = value
emit('update:modelValue', value)
}
// 根据选中的媒体类型,获取对应的媒体类别
const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }]
if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value
return default_value.concat(props.categories[props.directory.media_type ?? ''])
})
</script>
<template>
<VCard variant="tonal" :width="props.width" :height="props.height">
<DialogCloseBtn @click="onClose" />
<VCardItem>
<VTextField
v-model="props.directory.name"
variant="underlined"
label="别名"
class="me-20 text-high-emphasis font-weight-bold"
/>
<span class="absolute top-3 right-12">
<IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn>
</span>
</VCardItem>
<VCardText>
<VForm>
<VRow>
<VCol>
<VPathField @update:modelValue="updatePath">
<template #activator="{ menuprops }">
<VTextField v-model="props.directory.path" v-bind="menuprops" variant="underlined" label="路径" />
</template>
</VPathField>
</VCol>
</VRow>
<VRow>
<VCol cols="4">
<VSelect
v-model="props.directory.media_type"
variant="underlined"
:items="typeItems"
label="媒体类型"
@update:modelValue="props.directory.category = ''"
/>
</VCol>
<VCol>
<VSelect v-model="props.directory.category" variant="underlined" :items="getCategories" label="媒体类别" />
</VCol>
</VRow>
<VRow>
<VCol v-if="!props.directory.category || props.directory.category === ''">
<VSwitch v-model="props.directory.auto_category" label="自动分类"></VSwitch>
</VCol>
<VCol v-if="type === 'library'">
<VSwitch v-model="props.directory.scrape" label="刮削元数据"></VSwitch>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</template>

View File

@@ -1,88 +0,0 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { DoubanPerson } from '@/api/types'
const personProps = defineProps({
person: Object as PropType<DoubanPerson>,
width: String,
height: String,
})
// 当前人物
const personInfo = ref(personProps.person)
// 人物图片是否加载
const isImageLoaded = ref(false)
// 人物图片地址
function getPersonImage() {
if (!personInfo.value?.avatar)
return personIcon
return personInfo.value?.avatar?.large
}
// 打开人物详情
function goPersonDetail() {
if (!personInfo.value?.id)
return
window.open(`https://movie.douban.com/celebrity/${personInfo.value?.id}/`, '_blank')
}
</script>
<template>
<VHover v-bind="personProps">
<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;">
{{ personInfo?.character }}
</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

@@ -23,6 +23,11 @@ function getSpeedText() {
// 下载状态
const isDownloading = ref(props.info?.state === 'downloading')
// 监听props.info?.state的变化
watch(() => props.info?.state, (newValue) => {
isDownloading.value = newValue === 'downloading'
})
// 图片是否加载完成
const imageLoaded = ref(false)

View File

@@ -9,33 +9,32 @@ const props = defineProps({
})
// 定义触发的自定义事件
const emit = defineEmits(['close', 'changed', 'levelup', 'leveldown'])
const emit = defineEmits(['close', 'changed'])
// 按钮点击
function onClose() {
emit('close')
}
// 上升优先级
function onLevelUp() {
emit('levelup', props.pri)
}
// 下降优先级
function onLevelDown() {
emit('leveldown', props.pri)
}
// 选项变化
function filtersChanged(value: string[]) {
emit('changed', props.pri, value)
}
// 清洗规则中的换行符和多余空格,并在前后添加空格
const cleanedRules = computed(() => {
return props.rules.map(rule => {
rule = rule ?? ''
return ` ${rule.replace(/[\r\n]/g, '').replace(/\s+/g, '')} `
})
})
// 过滤规则下拉框
const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '官种', value: ' GZ ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
@@ -75,18 +74,9 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
<template>
<VCard variant="tonal" :width="props.width" :height="props.height">
<span class="absolute top-3 right-14">
<IconBtn
v-if="props.pri !== '1'"
@click.stop="onLevelUp"
>
<VIcon icon="mdi-arrow-up" />
</IconBtn>
<IconBtn
v-if="props.pri !== props.maxpri"
@click.stop="onLevelDown"
>
<VIcon icon="mdi-arrow-down" />
<span class="absolute top-3 right-12">
<IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn>
</span>
<DialogCloseBtn @click="onClose" />
@@ -95,8 +85,7 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
<VRow>
<VCol>
<VSelect
:key="props.pri"
v-model="props.rules"
v-model="cleanedRules"
variant="underlined"
:items="selectFilterOptions"
chips

View File

@@ -0,0 +1,179 @@
<script lang="ts" setup>
import type { MediaServerLibrary } from '@/api/types'
import plex from '@images/misc/plex.png'
import emby from '@images/misc/emby.png'
import jellyfin from '@images/misc/jellyfin.png'
// 输入参数
const props = defineProps({
media: Object as PropType<MediaServerLibrary>,
width: String,
height: String,
})
// canvas
const canvasRef = ref<HTMLCanvasElement>()
// 图片地址
const imgUrl = ref('')
// 图片是否加载完成
const imageLoaded = ref(false)
// 图片是否加载错误
const imageError = ref(false)
// 图片加载完成响应
function imageLoadHandler() {
imageLoaded.value = true
}
// 图片加载错误
function imageErrorHandler() {
imageError.value = true
}
// 默认图片
function getDefaultImage() {
if (props.media?.server === 'plex') return plex
else if (props.media?.server === 'emby') return emby
else if (props.media?.server === 'jellyfin') return jellyfin
else return plex
}
// 跳转播放
function goPlay() {
if (props.media?.link) window.open(props.media?.link, '_blank')
}
// 生成图片代理路径
function getImgUrl(url: string) {
if (!url) return getDefaultImage()
else return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
}
// 根据多张图片生成媒体库封面
async function drawImages(imageList: string[]) {
// 图片
const IMAGES = imageList
if (IMAGES.length === 0) return getDefaultImage()
// 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++)
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
// canvas
const canvas = canvasRef.value
if (!canvas) return getDefaultImage()
// 画布参数
const POSTER_WIDTH = (canvas.width - 32) / 4
const POSTER_HEIGHT = canvas.height * 0.75 - 8
const MARGIN_WIDTH = 4
const MARGIN_HEIGHT = 4
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
// 获取画布上下文
const ctx = canvas.getContext('2d')
if (!ctx) return getDefaultImage()
// 设置背景色为黑色
ctx.fillStyle = '#000000'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 绘制图片
async function drawImageWithReflection(imgSrc: string, index: number) {
if (!canvas) return
if (!ctx) return
const img = new Image()
img.setAttribute('crossorigin', 'anonymous')
img.src = imgSrc
await new Promise(resolve => (img.onload = resolve))
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
const y = MARGIN_HEIGHT
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
ctx.save()
ctx.translate(0, canvas.height)
ctx.scale(1, -1)
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
x,
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
POSTER_WIDTH,
REFLECTION_HEIGHT,
)
const gradient = ctx.createLinearGradient(0, REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT, 0, REFLECTION_HEIGHT)
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
ctx.fillStyle = gradient
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
ctx.restore()
}
// 绘制多张图片
const loopCount = Math.min(4, IMAGES.length)
for (let i = 0; i < loopCount; i++) await drawImageWithReflection(IMAGES[i], i + 1)
// 转换为图片地址
return canvas.toDataURL('image/png')
}
onMounted(async () => {
if (props.media?.image_list && props.media?.image_list.length > 0)
imgUrl.value = await drawImages(props.media?.image_list || [])
else imgUrl.value = getImgUrl(props.media?.image || '')
})
</script>
<template>
<VHover v-bind="props" :height="props.height" :width="props.width">
<template #default="hover">
<VCard
v-bind="hover.props"
:height="props.height"
:width="props.width"
:class="{
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
}"
@click="goPlay"
>
<template #image>
<canvas ref="canvasRef" class="w-full h-full hidden" />
<VImg :src="imgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
</div>
</template>
<VCardText
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
>
<h1 class="mb-1 text-white text-shadow font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.name }}
</h1>
</VCardText>
</VImg>
</template>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.text-shadow {
text-shadow: 1px 1px #777;
}
</style>

View File

@@ -1,13 +1,16 @@
<script lang="ts" setup>
import type { PropType, Ref } from 'vue'
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 api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from '@/api/types'
import router from '@/router'
import noImage from '@images/no-image.jpeg'
import tmdbImage from '@images/logos/tmdb.png'
import doubanImage from '@images/logos/douban-black.png'
import bangumiImage from '@images/logos/bangumi.png'
// 输入参数
const props = defineProps({
@@ -16,6 +19,8 @@ const props = defineProps({
height: String,
})
const store = useStore()
// 提示框
const $toast = useToast()
@@ -52,22 +57,33 @@ const seasonInfos = ref<TmdbSeason[]>([])
// 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([])
// 来源角标字典
const sourceIconDict: { [key: string]: any } = {
themoviedb: tmdbImage,
douban: doubanImage,
bangumi: bangumiImage,
}
// 获得mediaid
function getMediaId() {
if (props.media?.tmdb_id) return `tmdb:${props.media?.tmdb_id}`
else if (props.media?.douban_id) return `douban:${props.media?.douban_id}`
else return `bangumi:${props.media?.bangumi_id}`
}
// 订阅弹窗选择的多季
function subscribeSeasons() {
subscribeSeasonDialog.value = false
seasonsSelected.value.forEach((season) => {
seasonsSelected.value.forEach(season => {
addSubscribe(season.season_number)
})
}
// 角标颜色
function getChipColor(type: string) {
if (type === '电影')
return 'border-blue-500 bg-blue-600'
else if (type === '电视剧')
return ' bg-indigo-500 border-indigo-600'
else
return 'border-purple-600 bg-purple-600'
if (type === '电影') return 'border-blue-500 bg-blue-600'
else if (type === '电视剧') return ' bg-indigo-500 border-indigo-600'
else return 'border-purple-600 bg-purple-600'
}
// 添加订阅处理
@@ -81,29 +97,24 @@ async function handleAddSubscribe() {
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
return
}
// 检查各季的缺失状态
await checkSeasonsNotExists()
if (!tmdbFlag.value)
return
if (!tmdbFlag.value) return
if (seasonInfos.value.length === 1) {
// 添加订阅
addSubscribe(1)
}
else {
} else {
// 弹出季选择列表,支持多选
seasonsSelected.value = []
subscribeSeasonDialog.value = true
}
}
else if (props.media?.type === '电视剧') {
} else if (props.media?.type === '电视剧') {
// 豆瓣电视剧,只会有一季
const season = props.media?.season ?? 1
// 添加订阅
addSubscribe(season)
}
else {
} else {
// 电影
addSubscribe()
}
@@ -126,6 +137,7 @@ async function addSubscribe(season = 0) {
year: props.media?.year,
tmdbid: props.media?.tmdb_id,
doubanid: props.media?.douban_id,
bangumiid: props.media?.bangumi_id,
season,
best_version,
})
@@ -137,43 +149,32 @@ async function addSubscribe(season = 0) {
}
// 提示
showSubscribeAddToast(
result.success,
props.media?.title ?? '',
season,
result.message,
best_version,
)
showSubscribeAddToast(result.success, props.media?.title ?? '', season, result.message, best_version)
// 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
const show_edit_dialog = await queryDefaultSubscribeConfig()
if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
}
catch (error) {
} catch (error) {
console.error(error)
} finally {
doneNProgress()
}
doneNProgress()
}
// 弹出添加订阅提示
function showSubscribeAddToast(result: boolean,
title: string,
season: number,
message: string,
best_version: number) {
if (season)
title = `${title} ${formatSeason(season.toString())}`
function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) {
if (season) title = `${title} ${formatSeason(season.toString())}`
let subname = '订阅'
if (best_version > 0)
subname = '洗版订阅'
if (best_version > 0) subname = '洗版订阅'
if (result && seasonsSelected.value.length > 1)
$toast.success(`${title} 添加${subname}成功`)
else if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`)
if (result) $toast.success(`${title} 添加${subname}成功!`)
else if (!result) $toast.error(`${title} 添加${subname}失败:${message}`)
}
// 调用API取消订阅
@@ -181,41 +182,33 @@ async function removeSubscribe() {
// 开始处理
startNProgress()
try {
const mediaid = props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const mediaid = getMediaId()
const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`,
{
params: {
season: props.media?.season,
},
const result: { [key: string]: any } = await api.delete(`subscribe/media/${mediaid}`, {
params: {
season: props.media?.season,
},
)
})
if (result.success) {
isSubscribed.value = false
$toast.success(`${props.media?.title} 已取消订阅!`)
}
else {
} else {
$toast.error(`${props.media?.title} 取消订阅失败:${result.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
} finally {
doneNProgress()
}
doneNProgress()
}
// 查询当前媒体是否已订阅
async function handleCheckSubscribe() {
try {
const result = await checkSubscribe(props.media?.season)
if (result)
isSubscribed.value = true
}
catch (error) {
if (result) isSubscribed.value = true
} catch (error) {
console.error(error)
}
}
@@ -223,7 +216,7 @@ async function handleCheckSubscribe() {
// 查询当前媒体是否已入库
async function handleCheckExists() {
try {
const result: { [key: string]: any } = await api.get('media/exists', {
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
params: {
tmdbid: props.media?.tmdb_id,
title: props.media?.title,
@@ -233,10 +226,8 @@ async function handleCheckExists() {
},
})
if (result.success)
isExists.value = true
}
catch (error) {
if (result.success) isExists.value = true
} catch (error) {
console.error(error)
}
}
@@ -244,9 +235,7 @@ async function handleCheckExists() {
// 调用API检查是否已订阅电视剧需要指定季
async function checkSubscribe(season = 0) {
try {
const mediaid = props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const mediaid = getMediaId()
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: {
@@ -256,101 +245,102 @@ async function checkSubscribe(season = 0) {
})
return result.id || null
}
catch (error) {
} catch (error) {
console.error(error)
}
return null
}
// 检查所有季的缺失状态
// 检查所有季的缺失状态(数据库)
async function checkSeasonsNotExists() {
// 开始处理
startNProgress()
try {
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
if (result) {
result.forEach((item) => {
result.forEach(item => {
// 0-已入库 1-部分缺失 2-全部缺失
let state = 0
if (item.episodes.length === 0)
state = 2
else if (item.episodes.length < item.total_episode)
state = 1
if (item.episodes.length === 0) state = 2
else if (item.episodes.length < item.total_episode) state = 1
seasonsNotExisted.value[item.season] = state
})
}
}
catch (error) {
} catch (error) {
$toast.error(`${props.media?.title}无法识别TMDB媒体信息`)
tmdbFlag.value = false
} finally {
// 处理完成
doneNProgress()
}
// 处理完成
doneNProgress()
}
// 查询TMDB的所有季信息
async function getMediaSeasons() {
try {
seasonInfos.value = await api.get(`tmdb/seasons/${props.media?.tmdb_id}`)
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 查询订阅弹窗规则
async function queryDefaultSubscribeConfig() {
// 非管理员不显示
if (!store.state.auth.superUser) return false
try {
let subscribe_config_url = ''
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) return result.data.value.show_edit_dialog
} catch (error) {
console.log(error)
}
return false
}
// 爱心订阅按钮响应
function handleSubscribe() {
if (isSubscribed.value)
removeSubscribe()
else
handleAddSubscribe()
if (isSubscribed.value) removeSubscribe()
else handleAddSubscribe()
}
// 计算存在状态的颜色
function getExistColor(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return 'success'
if (!state) return 'success'
if (state === 1)
return 'warning'
else if (state === 2)
return 'error'
else
return 'success'
if (state === 1) return 'warning'
else if (state === 2) return 'error'
else return 'success'
}
// 计算存在状态的文本
function getExistText(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return '已入库'
if (!state) return '已入库'
if (state === 1)
return '部分缺失'
else if (state === 2)
return '缺失'
else
return '已入库'
if (state === 1) return '部分缺失'
else if (state === 2) return '缺失'
else return '已入库'
}
// 打开详情页
function goMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: `${
props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
}`,
type: props.media?.type,
},
})
function goMediaDetail(isHovering = false) {
if (isHovering) {
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
type: props.media?.type,
},
})
}
}
// 开始搜索
@@ -358,13 +348,10 @@ function handleSearch() {
router.push({
path: '/resource',
query: {
keyword: `${
props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
}`,
keyword: getMediaId(),
type: props.media?.type,
area: 'title',
season: props.media?.season,
},
})
}
@@ -377,41 +364,37 @@ onBeforeMount(() => {
// 计算图片地址
const getImgUrl: Ref<string> = computed(() => {
if (imageLoadError.value)
return noImage
if (imageLoadError.value) return noImage
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
// 如果地址中包含douban则使用中转代理
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
})
// 拼装季图片地址
function getSeasonPoster(posterPath: string) {
if (!posterPath)
return ''
if (!posterPath) return ''
return `https://image.tmdb.org/t/p/w500${posterPath}`
}
// 将yyyy-mm-dd转换为yyyy年mm月dd日
function formatAirDate(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
if (!airDate) return ''
const date = new Date(airDate.replaceAll(/-/g, '/'))
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`
}
// 从yyyy-mm-dd中提取年份
function getYear(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
if (!airDate) return ''
const date = new Date(airDate.replaceAll(/-/g, '/'))
return date.getFullYear()
}
</script>
<template>
<VHover v-bind="props">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
@@ -422,6 +405,7 @@ function getYear(airDate: string) {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goMediaDetail(hover.isHovering)"
>
<VImg
aspect-ratio="2/3"
@@ -437,79 +421,69 @@ function getYear(airDate: string) {
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</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>
<!-- 类型角标 -->
<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 && !hover.isHovering" />
<!-- 评分角标 -->
<VChip
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
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>
<VAvatar
size="24"
density="compact"
class="absolute bottom-1 right-1"
tile
v-if="!hover.isHovering && isImageLoaded && props.media?.source"
>
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
</VAvatar>
</VCard>
</template>
</VHover>
<!-- 订阅季弹窗 -->
<VBottomSheet
v-model="subscribeSeasonDialog"
inset
scrollable
>
<VBottomSheet v-if="subscribeSeasonDialog" v-model="subscribeSeasonDialog" inset scrollable>
<VCard class="rounded-t">
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
<VCardTitle class="pe-10">
订阅 - {{ props.media?.title }}
</VCardTitle>
<VCardItem>
<VCardTitle class="pe-10"> 订阅 - {{ props.media?.title }} </VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VList
v-model:selected="seasonsSelected"
lines="three"
select-strategy="classic"
>
<VListItem
v-for="(item, i) in seasonInfos" :key="i"
:value="item"
>
<VList v-model:selected="seasonsSelected" lines="three" select-strategy="classic">
<VListItem v-for="(item, i) in seasonInfos" :key="i" :value="item">
<template #prepend>
<VImg
height="90"
@@ -526,16 +500,9 @@ function getYear(airDate: string) {
</template>
</VImg>
</template>
<VListItemTitle>
{{ item.season_number }}
</VListItemTitle>
<VListItemTitle> {{ item.season_number }} </VListItemTitle>
<VListItemSubtitle class="mt-1 me-2">
<VChip
v-if="item.vote_average"
color="primary"
size="small"
class="mb-1"
>
<VChip v-if="item.vote_average" color="primary" size="small" class="mb-1">
<VIcon icon="mdi-star" /> {{ item.vote_average }}
</VChip>
{{ getYear(item.air_date || '') }} {{ item.episode_count }}
@@ -544,12 +511,7 @@ function getYear(airDate: string) {
{{ media?.title }} {{ item.season_number }} 季于 {{ formatAirDate(item.air_date || '') }} 首播
</VListItemSubtitle>
<VListItemSubtitle>
<VChip
v-if="seasonsNotExisted"
class="mt-2"
size="small"
:color="getExistColor(item.season_number || 0)"
>
<VChip v-if="seasonsNotExisted" class="mt-2" size="small" :color="getExistColor(item.season_number || 0)">
{{ getExistText(item.season_number || 0) }}
</VChip>
</VListItemSubtitle>
@@ -562,23 +524,25 @@ function getYear(airDate: string) {
</VList>
</VCardText>
<div class="my-2 text-center">
<VBtn
:disabled="seasonsSelected.length === 0"
width="30%"
@click="subscribeSeasons"
>
<VBtn :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
{{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
</VBtn>
</div>
</VCard>
</VBottomSheet>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
@remove="
() => {
subscribeEditDialog = false
handleCheckSubscribe()
}
"
/>
</template>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import type { Context } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数
const props = defineProps({
@@ -9,15 +10,13 @@ const props = defineProps({
// TMDB图片转换为w500大小
function getW500Image(url = '') {
if (!url)
return ''
if (!url) return ''
return url.replace('original', 'w500')
}
// 打开TMDB详情页面
function openTmdbPage(type: string, tmdbId: number) {
if (!type || !tmdbId)
return
if (!type || !tmdbId) return
const url = `https://www.themoviedb.org/${type === '电影' ? 'movie' : 'tv'}/${tmdbId}`
window.open(url, '_blank')
@@ -31,10 +30,7 @@ function openTmdbPage(type: string, tmdbId: number) {
v-if="context?.meta_info?.name"
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
>
<div
v-if="context?.media_info?.poster_path"
class="ma-auto"
>
<div v-if="context?.media_info?.poster_path" class="ma-auto">
<VImg
width="10rem"
aspect-ratio="2/3"
@@ -75,16 +71,10 @@ function openTmdbPage(type: string, tmdbId: number) {
variant="elevated"
class="me-1 mb-1 text-white bg-blue-500"
>
{{
context?.media_info?.type || context?.meta_info?.type
}}
{{ context?.media_info?.type || context?.meta_info?.type }}
</VChip>
<!-- 二级分类 -->
<VChip
v-if="context?.media_info?.category"
variant="elevated"
class="me-1 mb-1 text-white bg-blue-500"
>
<VChip v-if="context?.media_info?.category" variant="elevated" class="me-1 mb-1 text-white bg-blue-500">
{{ context?.media_info?.category }}
</VChip>
<!-- TMDBID -->
@@ -98,18 +88,10 @@ function openTmdbPage(type: string, tmdbId: number) {
{{ context?.media_info?.tmdb_id }}
</VChip>
<!-- meta_info -->
<VChip
v-if="context?.meta_info?.edition"
variant="elevated"
class="me-1 mb-1 text-white bg-red-500"
>
<VChip v-if="context?.meta_info?.edition" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.meta_info?.edition }}
</VChip>
<VChip
v-if="context?.meta_info?.resource_pix"
variant="elevated"
class="me-1 mb-1 text-white bg-red-500"
>
<VChip v-if="context?.meta_info?.resource_pix" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.meta_info?.resource_pix }}
</VChip>
<VChip
@@ -126,36 +108,19 @@ function openTmdbPage(type: string, tmdbId: number) {
>
{{ context?.meta_info?.audio_encode }}
</VChip>
<VChip
v-if="context?.meta_info?.resource_team"
variant="elevated"
class="me-1 mb-1 text-white bg-cyan-500"
>
<VChip v-if="context?.meta_info?.resource_team" variant="elevated" class="me-1 mb-1 text-white bg-cyan-500">
{{ context?.meta_info?.resource_team }}
</VChip>
</VCardItem>
</div>
</div>
<VAlert
v-if="!context?.meta_info?.name"
icon="mdi-alert-circle-outline"
>
识别失败无法识别到有效信息
</VAlert>
<VAlert v-if="!context?.meta_info?.name" icon="mdi-alert-circle-outline"> 识别失败无法识别到有效信息 </VAlert>
</VCol>
<VExpansionPanels
v-show="context?.meta_info?.title !== context?.meta_info.org_string"
>
<VExpansionPanels v-show="!isNullOrEmptyObject(context?.meta_info.apply_words)">
<VExpansionPanel>
<VExpansionPanelTitle>
识别词应用详情
</VExpansionPanelTitle>
<VExpansionPanelTitle> 识别词应用详情 </VExpansionPanelTitle>
<VExpansionPanelText>
<VChip
variant="elevated"
class="me-1 mb-1 break-all"
color="primary"
>
<VChip variant="elevated" class="me-1 mb-1 break-all" color="primary">
{{ context?.meta_info.org_string }}
</VChip>
<VChip

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,10 +1,10 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { TmdbPerson } from '@/api/types'
import type { Person } from '@/api/types'
import router from '@/router'
const personProps = defineProps({
person: Object as PropType<TmdbPerson>,
person: Object as PropType<Person>,
width: String,
height: String,
})
@@ -17,26 +17,54 @@ const isImageLoaded = ref(false)
//
function getPersonImage() {
if (!personInfo.value?.profile_path)
if (personProps.person?.source === 'themoviedb') {
if (!personInfo.value?.profile_path) return personIcon
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personInfo.value?.profile_path}`
} else if (personProps.person?.source === 'douban') {
if (!personInfo.value?.avatar) return personIcon
if (typeof personInfo.value?.avatar === 'object') {
return personInfo.value?.avatar?.normal
} else {
return personInfo.value?.avatar
}
} else if (personProps.person?.source === 'bangumi') {
if (!personInfo.value?.images) return personIcon
return personInfo.value?.images?.medium
} else {
return personIcon
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personInfo.value?.profile_path}`
}
}
//
function getPersonName() {
return personInfo.value?.name
}
//
function getPersonCharacter() {
if (personProps.person?.source === 'bangumi') {
if (!personInfo.value?.career) return ''
return personInfo.value?.career.join('、')
} else {
return personInfo.value?.character
}
}
//
function goPersonDetail() {
if (!personInfo.value?.id)
return
if (!personInfo.value?.id) return
router.push({
path: '/person',
query: {
personid: personInfo.value?.id,
source: personInfo.value?.source,
},
})
}
</script>
<template>
<VHover v-bind="personProps">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
@@ -49,9 +77,9 @@ function goPersonDetail() {
@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"
class="person-card relative transform-gpu cursor-pointer rounded shadow transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-bottom: 150%;">
<div style="padding-block-end: 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
@@ -60,19 +88,17 @@ function goPersonDetail() {
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
<VImg :src="getPersonImage()" cover @load="isImageLoaded = true" />
</VAvatar>
</div>
<div class="w-full truncate text-center font-bold">
{{ personInfo?.name }}
{{ getPersonName() }}
</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;">
{{ personInfo?.character }}
<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>

View File

@@ -1,15 +1,19 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import VersionHistory from '../misc/VersionHistory.vue'
import api from '@/api'
import type { Plugin } from '@/api/types'
import noImage from '@images/logos/plugin.png'
import { getDominantColor } from '@/@core/utils/image'
import { isNullOrEmptyObject } from '@/@core/utils'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
// 输入参数
const props = defineProps({
plugin: Object as PropType<Plugin>,
width: String,
height: String,
count: Number,
})
// 定义触发的自定义事件
@@ -36,6 +40,15 @@ const isImageLoaded = ref(false)
// 图片是否加载失败
const imageLoadError = ref(false)
// 更新日志弹窗
const releaseDialog = ref(false)
// 计算插件标签
const pluginLabels = computed(() => {
if (!props.plugin?.plugin_label) return []
return props.plugin.plugin_label.split(',')
})
// 图片加载完成
async function imageLoaded() {
isImageLoaded.value = true
@@ -49,17 +62,14 @@ async function installPlugin() {
try {
// 显示等待提示框
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(
`plugin/install/${props.plugin?.id}`,
{
params: {
repo_url: props.plugin?.repo_url,
force: props.plugin?.has_update,
},
const result: { [key: string]: any } = await api.get(`plugin/install/${props.plugin?.id}`, {
params: {
repo_url: props.plugin?.repo_url,
force: props.plugin?.has_update,
},
)
})
// 隐藏等待提示框
progressDialog.value = false
@@ -69,25 +79,22 @@ async function installPlugin() {
// 通知父组件刷新
emit('install')
}
else {
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 计算图标路径
const iconPath: Ref<string> = computed(() => {
if (imageLoadError.value)
return noImage
if (imageLoadError.value) return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
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}`
})
// 访问插件页面
@@ -96,141 +103,126 @@ function visitPluginPage() {
let repoUrl = props.plugin?.repo_url
if (repoUrl) {
if (repoUrl.includes('raw.githubusercontent.com')) {
if (!repoUrl.endsWith('/'))
repoUrl += '/'
if (!repoUrl.endsWith('/')) repoUrl += '/'
if (repoUrl.split('/').length < 6)
repoUrl = `${repoUrl}main/`
if (repoUrl.split('/').length < 6) repoUrl = `${repoUrl}main/`
try {
const [user, repo] = repoUrl.split('/').slice(-4, -2)
repoUrl = `https://github.com/${user}/${repo}`
}
catch (error) {
} catch (error) {
return
}
}
}
else {
} else {
repoUrl = props.plugin?.author_url
}
window.open(repoUrl, '_blank')
}
// 显示更新日志
function showUpdateHistory() {
releaseDialog.value = true
}
// 弹出菜单
const dropdownItems = ref([
{
title: '查看详情',
title: '项目主页',
value: 1,
show: true,
props: {
prependIcon: 'mdi-information-outline',
prependIcon: 'mdi-github',
click: visitPluginPage,
},
},
{
title: '更新说明',
value: 2,
show: !isNullOrEmptyObject(props.plugin?.history || {}),
props: {
prependIcon: 'mdi-update',
click: showUpdateHistory,
},
},
])
</script>
<template>
<VCard
:width="props.width"
:height="props.height"
@click="installPlugin"
>
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
<div class="me-n3 absolute bottom-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
<div
class="relative pa-4 text-center card-cover-blurred"
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div class="me-n3 absolute top-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" class="text-white" />
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
<div
v-if="props.plugin?.has_update"
class="me-n3 absolute top-0 left-1"
>
<VIcon
icon="mdi-new-box"
class="text-white"
/>
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
></div>
<div class="relative flex-1 min-w-0">
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
{{ props.plugin?.plugin_name }}
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
{{ props.plugin?.plugin_desc }}
</VCardText>
</div>
<div class="relative flex-shrink-0 self-center">
<VAvatar size="64">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</div>
<VAvatar
size="8rem"
>
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</div>
<VCardTitle>{{ props.plugin?.plugin_name }}</VCardTitle>
<VCardText>
{{ props.plugin?.plugin_desc }}
</VCardText>
<VCardText>
作者<a
:href="props.plugin?.author_url"
target="_blank"
@click.stop
>
{{ props.plugin?.plugin_author }}
</a><br>
版本{{ props.plugin?.plugin_version }}
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
<span>
<VIcon icon="mdi-github" class="me-1" />
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
{{ 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>
</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>
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
<!-- 更新日志 -->
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
<DialogCloseBtn @click="releaseDialog = false" />
<VDivider />
<VersionHistory :history="props.plugin?.history" />
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.card-cover-blurred::before {
position: absolute;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background: rgba(29, 39, 59, 48%);
content: "";
inset: 0;
}
</style>

View File

@@ -1,23 +1,33 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { VIcon } from 'vuetify/lib/components/index.mjs'
import api from '@/api'
import type { Plugin } from '@/api/types'
import FormRender from '@/components/render/FormRender.vue'
import PageRender from '@/components/render/PageRender.vue'
import VersionHistory from '@/components/misc/VersionHistory.vue'
import { isNullOrEmptyObject } from '@core/utils'
import noImage from '@images/logos/plugin.png'
import { getDominantColor } from '@/@core/utils/image'
import store from '@/store'
import { useDisplay } from 'vuetify'
import ProgressDialog from '../dialog/ProgressDialog.vue'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
plugin: Object as PropType<Plugin>,
count: Number, // 下载次数
action: Boolean, // 动作标识
width: String,
height: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save'])
const emit = defineEmits(['remove', 'save', 'actionDone'])
// 背景颜色
const backgroundColor = ref('#28A9E1')
@@ -40,14 +50,20 @@ const pluginConfigDialog = ref(false)
// 插件配置表单数据
const pluginConfigForm = ref({})
// 进度框
const progressDialog = ref(false)
// 插件表单配置项
let pluginFormItems = reactive([])
// 插件数据页面
const pluginInfoDialog = ref(false)
// 进度框文本
const progressText = ref('正在更新插件...')
// 插件数据页面配置项
let pluginPageItems = reactive([])
let pluginPageItems = ref([])
// 图片是否加载完成
const isImageLoaded = ref(false)
@@ -55,6 +71,20 @@ const isImageLoaded = 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() {
isImageLoaded.value = true
@@ -63,37 +93,41 @@ async function imageLoaded() {
backgroundColor.value = await getDominantColor(imageElement)
}
// 显示更新日志
function showUpdateHistory() {
// 检查当前版本是否有更新日志
if (isNullOrEmptyObject(props.plugin?.history)) {
updatePlugin()
} else {
releaseDialog.value = true
}
}
// 调用API卸载插件
async function uninstallPlugin() {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认卸载插件 ${props.plugin?.plugin_name} ?`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
return
if (!isConfirmed) return
try {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在卸载 ${props.plugin?.plugin_name} ...`
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
// 隐藏等待提示框
progressDialog.value = false
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
// 通知父组件刷新
emit('remove')
}
else {
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 卸载失败:${result.message}}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -104,11 +138,9 @@ async function loadPluginForm() {
const result: { [key: string]: any } = await api.get(`plugin/form/${props.plugin?.id}`)
if (result) {
pluginFormItems = result.conf
if (result.model)
pluginConfigForm.value = result.model
if (result.model) pluginConfigForm.value = result.model
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -117,10 +149,8 @@ async function loadPluginForm() {
async function loadPluginPage() {
try {
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
if (result)
pluginPageItems = result
}
catch (error) {
if (result) pluginPageItems.value = result
} catch (error) {
console.error(error)
}
}
@@ -129,29 +159,30 @@ async function loadPluginPage() {
async function loadPluginConf() {
try {
const result: { [key: string]: any } = await api.get(`plugin/${props.plugin?.id}`)
if (!isNullOrEmptyObject(result))
pluginConfigForm.value = result
}
catch (error) {
if (!isNullOrEmptyObject(result)) pluginConfigForm.value = result
} catch (error) {
console.error(error)
}
}
// 调用API保存配置数据
async function savePluginConf() {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在保存 ${props.plugin?.plugin_name} 配置...`
try {
const result: { [key: string]: any } = await api.put(`plugin/${props.plugin?.id}`, pluginConfigForm.value)
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
progressDialog.value = false
pluginConfigDialog.value = false
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
// 通知父组件刷新
emit('save')
}
else {
} else {
progressDialog.value = false
$toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -177,13 +208,12 @@ async function showPluginConfig() {
// 计算图标路径
const iconPath: Ref<string> = computed(() => {
if (imageLoadError.value)
return noImage
if (imageLoadError.value) return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
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}`
})
// 重置插件
@@ -191,18 +221,9 @@ async function resetPlugin() {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认重置插件 ${props.plugin?.plugin_name} 的配置数据?`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
return
if (!isConfirmed) return
try {
const result: { [key: string]: any } = await api.get(`plugin/reset/${props.plugin?.id}`)
@@ -210,12 +231,41 @@ async function resetPlugin() {
$toast.success(`插件 ${props.plugin?.plugin_name} 数据已重置`)
// 通知父组件刷新
emit('save')
}
else {
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 重置失败:${result.message}}`)
}
} catch (error) {
console.error(error)
}
catch (error) {
}
// 更新插件
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)
}
}
@@ -225,6 +275,21 @@ function visitAuthorPage() {
window.open(props.plugin?.author_url, '_blank')
}
// 查看日志URL
function openLoggerWindow() {
const token = store.state.auth.token
const url = `${
import.meta.env.VITE_API_BASE_URL
}system/logging?token=${token}&length=-1&logfile=plugins/${props.plugin?.id?.toLowerCase()}.log`
window.open(url, '_blank')
}
// 打开插件详情
function openPluginDetail() {
if (props.plugin?.has_page) showPluginInfo()
else showPluginConfig()
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -246,8 +311,18 @@ const dropdownItems = ref([
},
},
{
title: '重置',
title: '更新',
value: 3,
show: props.plugin?.has_update,
props: {
prependIcon: 'mdi-arrow-up-circle-outline',
color: 'success',
click: showUpdateHistory,
},
},
{
title: '重置',
value: 4,
show: true,
props: {
prependIcon: 'mdi-cancel',
@@ -257,7 +332,7 @@ const dropdownItems = ref([
},
{
title: '卸载',
value: 4,
value: 5,
show: true,
props: {
prependIcon: 'mdi-trash-can-outline',
@@ -265,9 +340,20 @@ const dropdownItems = ref([
click: uninstallPlugin,
},
},
{
title: '查看日志',
value: 6,
show: true,
props: {
prependIcon: 'mdi-file-document-outline',
click: () => {
openLoggerWindow()
},
},
},
{
title: '作者主页',
value: 4,
value: 7,
show: true,
props: {
prependIcon: 'mdi-home-circle-outline',
@@ -275,140 +361,146 @@ const dropdownItems = ref([
},
},
])
// 监听插件状态变化
watch(
() => props.plugin?.has_update,
(newHasUpdate, _) => {
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
if (updateItemIndex !== -1) dropdownItems.value[updateItemIndex].show = newHasUpdate
},
)
// 监听插件窗口状态变化
watch(
() => props.plugin?.page_open,
(newOpenState, _) => {
if (newOpenState) openPluginDetail()
},
)
</script>
<template>
<!-- 插件卡片 -->
<VCard
v-if="isVisible"
:width="props.width"
:height="props.height"
@click="() => {
if (props.plugin?.has_page)
showPluginInfo()
else
showPluginConfig()
}"
>
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
<div class="me-n3 absolute bottom-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
<div
class="relative pa-4 text-center card-cover-blurred"
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div class="me-n3 absolute top-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" class="text-white" />
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
/>
<div class="relative flex-1 min-w-0">
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
<VBadge v-if="props.plugin?.state" dot inline color="success" />
{{ props.plugin?.plugin_name }}
<span class="text-sm mt-1 text-gray-200">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
<VCardText class="px-2 py-1 text-white text-shadow line-clamp-3">
{{ props.plugin?.plugin_desc }}
</VCardText>
</div>
<div class="relative flex-shrink-0 self-center">
<VAvatar size="64">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</div>
<VAvatar
size="8rem"
>
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</div>
<VCardItem class="py-2">
<VCardTitle class="flex items-center flex-row">
<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>
</VCardTitle>
</VCardItem>
<VCardText>
{{ props.plugin?.plugin_desc }}
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
<span>
<VIcon icon="mdi-github" class="me-1" />
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
{{ 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>
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
<VIcon icon="mdi-new-box" class="text-white" />
</div>
</VCard>
<!-- 插件配置页面 -->
<VDialog
v-model="pluginConfigDialog"
scrollable
max-width="60rem"
>
<VCard
:title="`${props.plugin?.plugin_name} - 配置`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginConfigDialog = false" />
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="pluginConfigDialog" />
<VDivider />
<VCardText>
<FormRender
v-for="(item, index) in pluginFormItems"
:key="index"
:config="item"
:form="pluginConfigForm"
/>
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
</VCardText>
<VCardActions>
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
<VCardActions class="pt-3">
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
查看数据
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="savePluginConf"
>
保存
</VBtn>
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 插件数据页面 -->
<VDialog
v-model="pluginInfoDialog"
scrollable
max-width="80rem"
>
<VCard
:title="`${props.plugin?.plugin_name}`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginInfoDialog = false" />
<VCardText>
<PageRender
v-for="(item, index) in pluginPageItems"
:key="index"
:config="item"
/>
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
<DialogCloseBtn v-model="pluginInfoDialog" />
<VCardText class="min-h-40">
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
</VCardText>
<VCardActions>
<VBtn
@click="showPluginConfig"
>
配置
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
</VCard>
</VDialog>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
<!-- 更新日志 -->
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
<DialogCloseBtn @click="releaseDialog = false" />
<VDivider />
<VersionHistory :history="props.plugin?.history" />
<VDivider />
<VCardText>
<VBtn @click="updatePlugin" block>
<template #prepend>
<VIcon icon="mdi-arrow-up-circle-outline" />
</template>
更新到最新版本
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="pluginInfoDialog = false"
>
关闭
</VBtn>
</VCardActions>
</VCardText>
</VCard>
</VDialog>
</template>
@@ -420,7 +512,7 @@ const dropdownItems = ref([
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background: rgba(29, 39, 59, 48%);
content: "";
content: '';
inset: 0;
}
</style>

View File

@@ -0,0 +1,97 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import type { MediaServerPlayItem } from '@/api/types'
import noImage from '@images/no-image.jpeg'
// 输入参数
const props = defineProps({
media: Object as PropType<MediaServerPlayItem>,
width: String,
height: String,
})
// 图片加载状态
const isImageLoaded = ref(false)
// 图片加载失败
const imageLoadError = ref(false)
// 角标颜色
function getChipColor(type: string) {
if (type === '电影') return 'border-blue-500 bg-blue-600'
else if (type === '电视剧') return ' bg-indigo-500 border-indigo-600'
else return 'border-purple-600 bg-purple-600'
}
// 计算图片地址
const getImgUrl = computed(() => {
if (imageLoadError.value) return noImage
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
})
// 跳转播放
function goPlay(isHovering = false) {
if (props.media?.link && isHovering) window.open(props.media?.link, '_blank')
}
</script>
<template>
<VHover v-bind="props">
<template #default="hover">
<VCard
v-bind="hover.props"
:height="props.height"
:width="props.width"
class="outline-none shadow ring-gray-500 rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goPlay(hover.isHovering)"
>
<VImg
aspect-ratio="2/3"
:src="getImgUrl"
class="object-cover aspect-w-2 aspect-h-3"
:class="hover.isHovering ? 'on-hover' : ''"
cover
@load="isImageLoaded = true"
@error="imageLoadError = true"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</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>
<!-- 详情 -->
<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?.subtitle }}</span>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }}
</h1>
</VCardText>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.on-hover img {
@apply brightness-50;
}
</style>

View File

@@ -1,12 +1,17 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
import { formatFileSize } from '@core/utils/formatters'
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
import SiteTorrentTable from '../table/SiteTorrentTable.vue'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Site, TorrentInfo } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue'
import type { Site, SiteStatistic } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDisplay } from 'vuetify'
import ProgressDialog from '../dialog/ProgressDialog.vue'
// 显示器宽度
const display = useDisplay()
// 输入参数
const cardProps = defineProps({
@@ -51,53 +56,21 @@ const progressDialog = ref(false)
// 进度文本
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({
username: '',
password: '',
code: '',
})
// 打开种子详情页面
function openTorrentDetail(page_url: string) {
window.open(page_url, '_blank')
}
// 下载种子文件
async function downloadTorrentFile(enclosure: string) {
window.open(enclosure, '_blank')
}
// 站点使用统计
const siteStats = ref<SiteStatistic>({})
// 查询站点图标
async function getSiteIcon() {
try {
siteIcon.value = (await api.get(`site/icon/${cardProps.site?.id}`)).data.icon
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -109,15 +82,23 @@ async function testSite() {
testButtonDisable.value = true
const result: { [key: string]: any } = await api.get(`site/test/${cardProps.site?.id}`)
if (result.success)
$toast.success(`${cardProps.site?.name} 连通性测试成功,可正常使用!`)
else
$toast.error(`${cardProps.site?.name} 连通性测试失败:${result.message}`)
if (result.success) $toast.success(`${cardProps.site?.name} 连通性测试成功,可正常使用!`)
else $toast.error(`${cardProps.site?.name} 连通性测试失败:${result.message}`)
testButtonText.value = '测试'
testButtonDisable.value = false
getSiteStats()
} catch (error) {
console.error(error)
}
catch (error) {
}
// 查询站点使用统计
async function getSiteStats() {
try {
siteStats.value = await api.get(`site/statistic/${cardProps.site?.domain}`)
} catch (error) {
console.error(error)
}
}
@@ -130,14 +111,12 @@ async function handleSiteUpdate() {
// 打开资源浏览弹窗
async function handleResourceBrowse() {
resourceDialog.value = true
getResourceList()
}
// 调用API更新站点Cookie UA
async function updateSiteCookie() {
try {
if (!userPwForm.value.username || !userPwForm.value.password)
return
if (!userPwForm.value.username || !userPwForm.value.password) return
// 更新按钮状态
siteCookieDialog.value = false
@@ -146,49 +125,20 @@ async function updateSiteCookie() {
progressDialog.value = true
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
const result: { [key: string]: any } = await api.get(
`site/cookie/${cardProps.site?.id}`,
{
params: {
username: userPwForm.value.username,
password: userPwForm.value.password,
},
const result: { [key: string]: any } = await api.get(`site/cookie/${cardProps.site?.id}`, {
params: {
username: userPwForm.value.username,
password: userPwForm.value.password,
code: userPwForm.value.code,
},
)
})
if (result.success)
$toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`)
else
$toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
if (result.success) $toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`)
else $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
progressDialog.value = false
updateButtonDisable.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'
}
// 调用API查询站点资源
async function getResourceList() {
resourceLoading.value = true
try {
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
resourceLoading.value = false
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -198,322 +148,175 @@ function openSitePage() {
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()
})
// 保存站点
function saveSite() {
siteEditDialog.value = false
emit('update')
}
// 装载时查询站点图标
onMounted(() => {
getSiteIcon()
getSiteStats()
})
</script>
<template>
<VCard
:height="cardProps.height"
:width="cardProps.width"
:flat="!cardProps.site?.is_active"
class="overflow-hidden"
@click="siteEditDialog = true"
>
<template #image>
<VAvatar
class="absolute right-2 bottom-2 rounded"
variant="flat"
rounded="0"
>
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VCardItem>
<VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle>
<VCardSubtitle>
{{ cardProps.site?.url }}
</VCardSubtitle>
</VCardItem>
<ExistIcon v-if="cardProps.site?.is_active" />
<VCardText class="py-2">
<VTooltip
v-if="cardProps.site?.render === 1"
text="浏览器仿真"
>
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-apple-safari"
/>
</template>
</VTooltip>
<VTooltip
v-if="cardProps.site?.proxy === 1"
text="代理"
>
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-network-outline"
/>
</template>
</VTooltip>
<VTooltip
v-if="cardProps.site?.limit_interval"
text="流控"
>
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-speedometer"
/>
</template>
</VTooltip>
<VTooltip
v-if="cardProps.site?.filter"
text="过滤"
>
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-filter-cog-outline"
/>
</template>
</VTooltip>
</VCardText>
<VDivider
class="opacity-75"
style="border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));"
/>
<VCardActions>
<VBtn
v-if="!cardProps.site?.public"
:disabled="updateButtonDisable"
@click.stop="handleSiteUpdate"
>
<template #prepend>
<VIcon icon="mdi-refresh" />
</template>
更新
</VBtn>
<VBtn
:disabled="testButtonDisable"
@click.stop="testSite"
>
<template #prepend>
<VIcon icon="mdi-link" />
</template>
{{ testButtonText }}
</VBtn>
<VBtn @click.stop="handleResourceBrowse">
<template #prepend>
<VIcon icon="mdi-web" />
</template>
浏览
</VBtn>
</VCardActions>
</VCard>
<!-- 更新站点Cookie & UA弹窗 -->
<VDialog
v-model="siteCookieDialog"
max-width="50rem"
>
<!-- Dialog Content -->
<VCard title="更新站点Cookie & UA">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userPwForm.username"
label="用户名"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userPwForm.password"
label="密码"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
:rules="[requiredValidator]"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@keydown.enter="updateSiteCookie"
/>
</VCol>
</VRow>
</VForm>
<div>
<VCard
:height="cardProps.height"
:width="cardProps.width"
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
class="overflow-hidden"
@click="siteEditDialog = true"
>
<template #image>
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VCardItem style="padding-block-end: 0;">
<VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle>
<VCardSubtitle>
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
</VCardSubtitle>
</VCardItem>
<VCardText class="py-2" style="block-size: 36px;">
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
</template>
</VTooltip>
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
</template>
</VTooltip>
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
</template>
</VTooltip>
<VTooltip v-if="cardProps.site?.filter" text="过滤">
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
</template>
</VTooltip>
</VCardText>
<VDivider />
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="updateSiteCookie"
>
开始更新
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
<template #prepend>
<VIcon icon="mdi-refresh" />
</template>
更新
</VBtn>
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
<template #prepend>
<VIcon icon="mdi-link" />
</template>
{{ testButtonText }}
</VBtn>
<VBtn @click.stop="handleResourceBrowse">
<template #prepend>
<VIcon icon="mdi-web" />
</template>
浏览
</VBtn>
</VCardActions>
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
<span class="absolute top-1 right-8">
<VIcon class="cursor-move">mdi-drag</VIcon>
</span>
</VCard>
</VDialog>
<SiteAddEditForm
v-model="siteEditDialog"
:siteid="cardProps.site?.id"
@save="siteEditDialog = false; emit('update')"
@remove="emit('remove')"
@close="siteEditDialog = false"
/>
<!-- 站点资源弹窗 -->
<VDialog
v-model="resourceDialog"
max-width="80rem"
scrollable
>
<!-- Dialog Content -->
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
<DialogCloseBtn @click="resourceDialog = false" />
<VCardText class="pt-2">
<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
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-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"
<!-- 更新站点Cookie & UA弹窗 -->
<VDialog v-model="siteCookieDialog" max-width="50rem">
<!-- Dialog Content -->
<VCard title="更新站点Cookie & UA">
<DialogCloseBtn @click="siteCookieDialog = false" />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="4">
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="userPwForm.password"
label="密码"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
:rules="[requiredValidator]"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@keydown.enter="updateSiteCookie"
/>
<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>
</VCard>
</VDialog>
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
</VCol>
<VCol cols="12" md="4">
<VTextField v-model="userPwForm.code" label="两步验证" />
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="updateSiteCookie" prepend-icon="mdi-refresh" class="px-5"> 开始更新 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 站点编辑弹窗 -->
<SiteAddEditDialog
v-if="siteEditDialog"
v-model="siteEditDialog"
:siteid="cardProps.site?.id"
@save="saveSite"
@remove="emit('remove')"
@close="siteEditDialog = false"
/>
<!-- 站点资源弹窗 -->
<VDialog
v-if="resourceDialog"
v-model="resourceDialog"
max-width="80rem"
scrollable
z-index="1010"
:fullscreen="!display.mdAndUp.value"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
<DialogCloseBtn @click="resourceDialog = false" />
<VDivider />
<VCardText class="pt-2">
<SiteTorrentTable :site="cardProps.site?.id" />
</VCardText>
</VCard>
</VDialog>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</div>
</template>
<style lang="scss">
<style lang="scss" scoped>
.v-table th {
white-space: nowrap;
}

View File

@@ -1,10 +1,12 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { calculateTimeDifference } from '@/@core/utils'
import { useConfirm } from 'vuetify-use-dialog'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatDateDifference } from '@/@core/utils/formatters'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import type { Subscribe } from '@/api/types'
import router from '@/router'
// 输入参数
const props = defineProps({
@@ -14,6 +16,9 @@ const props = defineProps({
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save'])
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
@@ -24,13 +29,7 @@ const imageLoaded = ref(false)
const subscribeEditDialog = ref(false)
// 上一次更新时间
const lastUpdateText = ref(
`${
props.media?.last_update
? `${calculateTimeDifference(props.media?.last_update || '')}`
: ''
}`,
)
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
// 图片加载完成响应
function imageLoadHandler() {
@@ -39,49 +38,30 @@ function imageLoadHandler() {
// 根据 type 返回不同的图标
function getIcon() {
if (props.media?.type === '电影')
return 'mdi-movie'
else if (props.media?.type === '电视剧')
return 'mdi-television-classic'
else
return 'mdi-help-circle'
if (props.media?.type === '电影') return 'mdi-movie-open'
else if (props.media?.type === '电视剧') return 'mdi-television-play'
else return 'mdi-help-circle'
}
// 计算百分比
function getPercentage() {
if (props.media?.total_episode === 0)
return 0
if (props.media?.total_episode === 0) return 0
return Math.round(
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0))
/ (props.media?.total_episode ?? 1))
* 100,
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0)) / (props.media?.total_episode ?? 1)) * 100,
)
}
// 计算文本颜色
function getTextColor() {
return imageLoaded.value ? 'white' : ''
}
// 计算文本类
function getTextClass() {
return imageLoaded.value ? 'text-white' : ''
}
// 删除订阅
async function removeSubscribe() {
try {
const result: { [key: string]: any } = await api.delete(
`subscribe/${props.media?.id}`,
)
const result: { [key: string]: any } = await api.delete(`subscribe/${props.media?.id}`)
if (result.success) {
// 通知父组件刷新
emit('remove')
}
}
catch (e) {
} catch (e) {
console.log(e)
}
}
@@ -89,15 +69,32 @@ async function removeSubscribe() {
// 搜索订阅
async function searchSubscribe() {
try {
const result: { [key: string]: any } = await api.get(
`subscribe/search/${props.media?.id}`,
)
const result: { [key: string]: any } = await api.get(`subscribe/search/${props.media?.id}`)
// 提示
if (result.success)
$toast.success(`${props.media?.name} 提交搜索请求成功!`)
if (result.success) $toast.success(`${props.media?.name} 提交搜索请求成功!`)
} catch (e) {
console.log(e)
}
catch (e) {
}
// 重置订阅
async function resetSubscribe() {
// 确认
try {
const isConfirmed = await createConfirm({
title: '确认',
content: `重置后 ${props.media?.name} 已下载记录将被清除,未入库的剧集将会重新下载,是否确认?`,
})
if (!isConfirmed) return
// 重置
const result: { [key: string]: any } = await api.get(`subscribe/reset/${props.media?.id}`)
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 重置成功!`)
emit('save')
} else $toast.error(`${props.media?.name} 重置失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
@@ -107,6 +104,17 @@ async function editSubscribeDialog() {
subscribeEditDialog.value = true
}
// 查看详情
async function viewMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
type: props.media?.type,
},
})
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -126,8 +134,26 @@ const dropdownItems = ref([
},
},
{
title: '取消订阅',
title: '查看详情',
value: 3,
props: {
prependIcon: 'mdi-open-in-new',
click: viewMediaDetail,
},
},
{
title: '重置',
value: 4,
props: {
prependIcon: 'mdi-restore-alert',
click: resetSubscribe,
color: 'warning',
},
show: props.media?.type === '电视剧',
},
{
title: '取消订阅',
value: 5,
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
@@ -135,138 +161,144 @@ const dropdownItems = ref([
},
},
])
// 监听插件窗口状态变化
watch(
() => props.media?.page_open,
(newOpenState, _) => {
if (newOpenState) editSubscribeDialog()
},
)
</script>
<template>
<VCard
:key="props.media?.id"
:class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
@click="editSubscribeDialog"
>
<template #image>
<VImg
:src="props.media?.backdrop || props.media?.poster"
aspect-ratio="2/3"
cover
class="brightness-50"
@load="imageLoadHandler"
/>
</template>
<VCardItem>
<template #prepend>
<VIcon
size="1.9rem"
:color="getTextColor()"
:icon="getIcon()"
/>
</template>
<VCardTitle :class="getTextClass()">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : "") }}
</VCardTitle>
<template #append>
<div class="me-n3">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col rounded-lg"
:class="{
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
}"
min-height="170"
@click="editSubscribeDialog"
>
<div class="me-n3 absolute top-1 right-2">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
:color="getTextColor()"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VIcon icon="mdi-dots-vertical" color="white" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
<template v-for="(item, i) in dropdownItems" :key="i">
<VListItem
v-if="item.show !== false"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</template>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VCardItem>
<VCardText>
<p
class="clamp-text mb-0"
:class="getTextClass()"
>
{{ props.media?.description }}
</p>
</VCardText>
<VCardText class="d-flex justify-space-between align-center flex-wrap">
<div class="d-flex align-center">
<IconBtn
icon="mdi-star"
:color="getTextColor()"
class="me-1"
/>
<span
class="text-subtitle-2 me-4"
:class="getTextClass()"
>{{
props.media?.vote
}}</span>
<IconBtn
v-if="props.media?.total_episode"
v-bind="props"
icon="mdi-progress-clock"
:color="getTextColor()"
class="me-1"
/>
<span
v-if="props.media?.season"
class="text-subtitle-2 me-4"
:class="getTextClass()"
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}</span>
<IconBtn
v-if="props.media?.username"
icon="mdi-account"
:color="getTextColor()"
class="me-1"
/>
<span
v-if="props.media?.username"
class="text-subtitle-2 me-4"
:class="getTextClass()"
>
{{ props.media?.username }}
</span>
</div>
</VCardText>
<VCardText
v-if="lastUpdateText"
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
>
<VIcon
icon="mdi-download"
class="me-1"
/> {{ lastUpdateText }}
</VCardText>
<VProgressLinear
v-if="getPercentage() > 0"
:model-value="getPercentage()"
bg-color="success"
color="success"
/>
</VCard>
<template #image>
<VImg
:src="props.media?.backdrop || props.media?.poster"
aspect-ratio="3/2"
cover
@load="imageLoadHandler"
position="top"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
</div>
</template>
<div class="absolute inset-0 subscribe-card-background"></div>
</VImg>
</template>
<div>
<VCardText class="flex items-center">
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
<VImg :src="props.media?.poster" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</div>
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
<div class="mr-2 min-w-0 text-lg font-bold text-white">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
</div>
</div>
</VCardText>
<VCardText class="flex justify-space-between align-center flex-wrap">
<div class="flex align-center">
<IconBtn
v-if="props.media?.total_episode"
v-bind="props"
icon="mdi-progress-download"
color="white"
class="me-1"
/>
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}
</div>
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
{{ props.media?.username }}
</span>
</div>
</VCardText>
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VIcon icon="mdi-download" class="me-1" />
{{ lastUpdateText }}
</VCardText>
<div class="w-full absolute bottom-0">
<VProgressLinear
v-if="getPercentage() > 0"
:model-value="getPercentage()"
bg-color="success"
color="success"
/>
</div>
</div>
</VCard>
</template>
</VHover>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="props.media?.id"
@remove="() => { emit('remove');subscribeEditDialog = false; }"
@save="() => { emit('save');subscribeEditDialog = false; }"
@remove="
() => {
emit('remove')
subscribeEditDialog = false
}
"
@save="
() => {
emit('save')
subscribeEditDialog = false
}
"
@close="subscribeEditDialog = false"
/>
</template>
<style lang="scss">
.subscribe-card-background {
background-image: linear-gradient(90deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
}
</style>

View File

@@ -5,7 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { Context } from '@/api/types'
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
// 输入参数
const props = defineProps({
@@ -36,20 +36,20 @@ const meta = ref(props.torrent?.meta_info)
// 站点图标
const siteIcon = ref('')
// 存储是否已经下载过的记录
const downloaded = ref<String[]>([])
// 查询站点图标
async function getSiteIcon() {
try {
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 询问并添加下载
async function handleAddDownload(_site: any = undefined,
_media: any = undefined,
_torrent: any = undefined) {
async function handleAddDownload(_site: any = undefined, _media: any = undefined, _torrent: any = undefined) {
if (!_media || !_torrent || !_site) {
_site = torrent.value?.site_name
_media = media.value
@@ -59,41 +59,37 @@ async function handleAddDownload(_site: any = undefined,
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认下载【${_site}${_torrent?.title} ?`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
return
if (!isConfirmed) return
addDownload(_media, _torrent)
}
// 添加下载
async function addDownload(_media: any, _torrent: any) {
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
let result: { [key: string]: any }
if (result.success) {
if (_media) {
result = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
} else {
result = await api.post('download/add', _torrent)
}
if (result && result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
}
else {
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
downloaded.value.push(_torrent?.enclosure || '')
} else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败${result?.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
doneNProgress()
@@ -111,14 +107,10 @@ async function downloadTorrentFile() {
// 促销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'
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'
}
// 装载时查询站点图标
@@ -131,41 +123,27 @@ onMounted(() => {
<VCard
:width="props.width"
:height="props.height"
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'elevated'"
@click="handleAddDownload"
>
<template
v-if="!showMoreTorrents"
#image
>
<VAvatar
class="absolute right-2 bottom-2 rounded"
variant="flat"
rounded="0"
>
<template v-if="!showMoreTorrents" #image>
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VCardItem class="py-1">
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
{{ media?.title }} {{ meta?.season_episode }}
{{ media?.title ?? meta?.name }} {{ meta?.season_episode }}
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VCardTitle>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
variant="plain"
@click="openTorrentDetail()"
>
<VListItem variant="plain" @click="openTorrentDetail()">
<template #prepend>
<VIcon icon="mdi-information" />
</template>
@@ -191,10 +169,13 @@ onMounted(() => {
{{ torrent?.title }}
</VCardText>
<VCardText>{{ torrent?.description }}</VCardText>
<VCardItem
v-if="torrent?.labels"
class="pb-3 pt-0 pe-12"
>
<VCardItem v-if="torrent?.labels" class="pb-3 pt-0 pe-12">
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
H&R
</VChip>
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
{{ torrent?.freedate_diff }}
</VChip>
<VChip
v-for="(label, index) in torrent?.labels"
:key="index"
@@ -205,51 +186,24 @@ onMounted(() => {
>
{{ label }}
</VChip>
<VChip
v-if="meta?.edition"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
{{ meta?.edition }}
</VChip>
<VChip
v-if="meta?.resource_pix"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
{{ meta?.resource_pix }}
</VChip>
<VChip
v-if="meta?.video_encode"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-orange-500"
>
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
{{ meta?.video_encode }}
</VChip>
<VChip
v-if="torrent?.size"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-yellow-500"
>
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
{{ formatFileSize(torrent?.size) }}
</VChip>
<VChip
v-if="meta?.resource_team"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-cyan-500"
>
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
{{ meta?.resource_team }}
</VChip>
<VChip
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
:class="
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
"
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
variant="elevated"
size="small"
class="me-1 mb-1"
@@ -258,10 +212,7 @@ onMounted(() => {
</VChip>
</VCardItem>
<VCardActions>
<VBtn
v-if="props.more && props.more.length > 0"
@click.stop="showMoreTorrents = !showMoreTorrents"
>
<VBtn v-if="props.more && props.more.length > 0" @click.stop="showMoreTorrents = !showMoreTorrents">
<template #append>
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
</template>
@@ -271,30 +222,16 @@ onMounted(() => {
<VExpandTransition>
<div v-show="showMoreTorrents">
<VDivider />
<VChipGroup class="p-3">
<VChipGroup class="p-3" column>
<VChip
v-for="(item, index) in props.more"
:key="index"
@click.stop="
handleAddDownload(
item.torrent_info?.site_name,
item.media_info,
item.torrent_info,
)
"
@click.stop="handleAddDownload(item.torrent_info?.site_name, item.media_info, item.torrent_info)"
>
<template #append>
<VBadge color="primary" :content="`↑${item.torrent_info?.seeders}`" inline size="small" />
<VBadge
color="primary"
:content="`↑${item.torrent_info?.seeders}`"
inline
size="small"
/>
<VBadge
v-if="
item.torrent_info?.downloadvolumefactor !== 1
|| item.torrent_info?.uploadvolumefactor !== 1
"
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
:content="item.torrent_info?.volume_factor"
inline
size="small"

View File

@@ -5,7 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { Context } from '@/api/types'
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
// 输入参数
const props = defineProps({
@@ -33,20 +33,20 @@ const meta = ref(props.torrent?.meta_info)
// 站点图标
const siteIcon = ref('')
// 存储是否已经下载过的记录
const downloaded = ref<String[]>([])
// 查询站点图标
async function getSiteIcon() {
try {
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 询问并添加下载
async function handleAddDownload(_site: any = undefined,
_media: any = undefined,
_torrent: any = undefined) {
async function handleAddDownload(_site: any = undefined, _media: any = undefined, _torrent: any = undefined) {
if (!_media || !_torrent || !_site) {
_site = torrent.value?.site_name
_media = media.value
@@ -56,41 +56,37 @@ async function handleAddDownload(_site: any = undefined,
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认下载【${_site}${_torrent?.title} ?`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
return
if (!isConfirmed) return
addDownload(_media, _torrent)
}
// 添加下载
async function addDownload(_media: any, _torrent: any) {
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
let result: { [key: string]: any }
if (result.success) {
if (_media) {
result = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
} else {
result = await api.post('download/add', _torrent)
}
if (result && result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
}
else {
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
downloaded.value.push(_torrent?.enclosure || '')
} else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败${result?.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
doneNProgress()
@@ -108,14 +104,10 @@ async function downloadTorrentFile() {
// 促销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'
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'
}
// 装载时查询站点图标
@@ -125,16 +117,9 @@ onMounted(() => {
</script>
<template>
<VListItem @click="handleAddDownload">
<template
v-if="!showMoreTorrents"
#prepend
>
<VAvatar
class="rounded"
variant="flat"
@click.stop="openTorrentDetail"
>
<VListItem @click="handleAddDownload" :variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'">
<template v-if="!showMoreTorrents" #prepend>
<VAvatar class="rounded" variant="flat" @click.stop="openTorrentDetail">
<VImg :src="siteIcon" />
</VAvatar>
</template>
@@ -146,10 +131,13 @@ onMounted(() => {
<VListItemSubtitle>
{{ torrent?.description }}
</VListItemSubtitle>
<div
v-if="torrent?.labels"
class="pt-2"
>
<div v-if="torrent?.labels" class="pt-2">
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
H&R
</VChip>
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
{{ torrent?.freedate_diff }}
</VChip>
<VChip
v-for="(label, index) in torrent?.labels"
:key="index"
@@ -160,51 +148,24 @@ onMounted(() => {
>
{{ label }}
</VChip>
<VChip
v-if="meta?.edition"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
{{ meta?.edition }}
</VChip>
<VChip
v-if="meta?.resource_pix"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
{{ meta?.resource_pix }}
</VChip>
<VChip
v-if="meta?.video_encode"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-orange-500"
>
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
{{ meta?.video_encode }}
</VChip>
<VChip
v-if="torrent?.size"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-yellow-500"
>
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
{{ formatFileSize(torrent?.size) }}
</VChip>
<VChip
v-if="meta?.resource_team"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-cyan-500"
>
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
{{ meta?.resource_team }}
</VChip>
<VChip
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
:class="
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
"
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
variant="elevated"
size="small"
class="me-1 mb-1"
@@ -215,18 +176,10 @@ onMounted(() => {
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
variant="plain"
@click="openTorrentDetail()"
>
<VListItem variant="plain" @click="openTorrentDetail()">
<template #prepend>
<VIcon icon="mdi-information" />
</template>

View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
import QrcodeVue from 'qrcode.vue'
import api from '@/api'
// 定义事件
const emit = defineEmits(['done', 'close'])
// 二维码内容
const qrCodeContent = ref('')
// ck参数
const ck = ref('')
// t参数
const t = ref('')
// 下方的提示信息
const text = ref('请用阿里云盘 App 扫码')
// 提醒类型
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
// timeout定时器
let timeoutTimer: NodeJS.Timeout | undefined = undefined
// 完成
async function handleDone() {
emit('done')
}
// 调用/aliyun/qrcode api生成二维码
async function getQrcode() {
try {
const result: { [key: string]: any } = await api.get('/aliyun/qrcode')
if (result.success && result.data) {
qrCodeContent.value = result.data.codeContent
ck.value = result.data.ck
t.value = result.data.t
} else {
text.value = result.message
}
} catch (e) {
console.error(e)
}
}
// 调用/aliyun/check api验证二维码
async function checkQrcode() {
try {
const result: { [key: string]: any } = await api.get('/aliyun/check', {
params: {
ck: ck.value,
t: t.value,
},
})
if (result.success && result.data) {
const qrCodeStatus = result.data.qrCodeStatus
text.value = result.data.tip
if (qrCodeStatus == 'CONFIRMED') {
// 已确认完成
alertType.value = 'success'
handleDone()
} else if (qrCodeStatus == 'NEW' || qrCodeStatus == 'SCANED') {
alertType.value = 'info'
// 新建、待扫码
clearTimeout(timeoutTimer)
timeoutTimer = setTimeout(checkQrcode, 3000)
} else {
// 过期或者已取消
alertType.value = 'error'
}
} else {
alertType.value = 'error'
text.value = result.message
}
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
await getQrcode()
timeoutTimer = setTimeout(checkQrcode, 3000)
})
onUnmounted(() => {
if (timeoutTimer) clearTimeout(timeoutTimer)
})
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard title="阿里云盘登录" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 shadow-lg rounded text-center p-3 border">
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,33 @@
<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="elevated" @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3"> 导入 </VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
const props = defineProps({
value: Number,
text: String,
})
</script>
<template>
<!-- 手动整理进度框 -->
<VDialog :scrim="false" width="25rem">
<VCard color="primary">
<VCardText class="text-center">
{{ props.text }}
<VProgressLinear color="white" class="mb-0 mt-1" :model-value="props.value" indeterminate />
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,387 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
import store from '@/store'
import api from '@/api'
import { numberValidator } from '@/@validators'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
import { FileItem, MediaDirectory } from '@/api/types'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
storage: {
type: String,
default: () => 'local',
},
logids: Array<number>,
items: Array<FileItem>,
target: String,
})
// 定义事件
const emit = defineEmits(['done', 'close'])
// 生成1到100季的下拉框选项
const seasonItems = ref(
Array.from({ length: 101 }, (_, i) => i).map(item => ({
title: `${item}`,
value: item,
})),
)
// 当前识别类型
const mediaSource = ref('themoviedb')
// 提示框
const $toast = useToast()
// TMDB选择对话框
const mediaSelectorDialog = ref(false)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 标题
const dialogTitle = computed(() => {
if (props.items) {
if (props.items.length > 1) return `整理 - 共 ${props.items.length}`
return `整理 - ${props.items[0].path}`
} else if (props.logids) {
return `整理 - 共 ${props.logids.length}`
}
return '手动整理'
})
// 表单
const transferForm = reactive({
storage: props.storage,
logid: 0,
path: '',
drive_id: '',
fileid: '',
filetype: '',
target: props.target ?? null,
tmdbid: null,
doubanid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
min_filesize: 0,
scrape: false,
})
// 所有媒体库目录
const libraryDirectories = ref<MediaDirectory[]>([])
// 目的目录下拉框
const targetDirectories = computed(() => {
const directories = libraryDirectories.value.map(item => item.path)
return [...new Set(directories)]
})
// 监听目的路径变化,自动查询目录的刮削配置
watch(transferForm, async () => {
if (transferForm.target) {
const directory = libraryDirectories.value.find(item => item.path === transferForm.target)
if (directory) {
transferForm.scrape = directory.scrape ?? false
}
}
})
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = event => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 整理文件
async function transfer() {
if (!props.logids && !props.items) return
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
// 文件整理
if (props.items) {
for (const item of props.items) {
await handleTransfer(item)
}
}
// 日志整理
if (props.logids) {
for (const logid of props.logids) {
await handleTransferLog(logid)
}
}
// 停止监听进度
stopLoadingProgress()
// 关闭进度条
progressDialog.value = false
// 重新加载
emit('done')
}
// 整理文件
async function handleTransfer(item: FileItem) {
transferForm.path = item.path
transferForm.fileid = item.fileid || ''
transferForm.drive_id = item.drive_id || ''
transferForm.filetype = item.type || 'dir'
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
if (!result.success) $toast.error(`文件 ${item.path} 整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
// 整理日志
async function handleTransferLog(logid: number) {
transferForm.logid = logid
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
// 调用API加载当前系统环境设置
async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result) mediaSource.value = result.data?.RECOGNIZE_SOURCE || 'themoviedb'
} catch (e) {
console.error(e)
}
}
// 查询媒体库目录
async function loadLibraryDirectories() {
try {
const result: { [key: string]: any } = await api.get('system/setting/LibraryDirectories')
if (result.success && result.data?.value) {
libraryDirectories.value = result.data.value
}
} catch (error) {
console.log(error)
}
}
onMounted(() => {
loadSystemSettings()
loadLibraryDirectories()
})
</script>
<template>
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="dialogTitle" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol v-if="props.storage == 'local'" cols="12" md="8">
<VCombobox
v-model="transferForm.target"
:items="targetDirectories"
label="目的路径"
placeholder="留空自动"
hint="整理目的路径,留空将自动匹配"
persistent-hint
/>
</VCol>
<VCol v-if="props.storage == 'local'" cols="12" md="4">
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
{ title: 'Rclone复制', value: 'rclone_copy' },
{ title: 'Rclone移动', value: 'rclone_move' },
]"
hint="文件操作整理方式"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="4">
<VSelect
v-model="transferForm.type_name"
label="类型"
:items="[
{ title: '自动', value: '' },
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
]"
hint="文件的媒体类型"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-if="mediaSource === 'themoviedb'"
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
label="TheMovieDb编号"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
hint="按名称查询媒体编号,留空自动识别"
persistent-hint
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
v-else
v-model="transferForm.doubanid"
:disabled="transferForm.type_name === ''"
label="豆瓣编号"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
hint="按名称查询媒体编号,留空自动识别"
persistent-hint
@click:append-inner="mediaSelectorDialog = true"
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
hint="指定季数"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
hint="使用{ep}定位文件名中的集数部分以辅助识别"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
hint="指定集数或范围如1或1,2"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
hint="指定Part如part1"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
hint="集数偏移运算,如-10或EP*2"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.min_filesize"
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
hint="只整理大于最小文件大小的文件"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="transferForm.scrape"
label="刮削元数据"
hint="整理完成后自动刮削元数据"
persistent-hint
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn variant="elevated" @click="transfer" prepend-icon="mdi-arrow-right-bold" class="px-5"> 开始整理 </VBtn>
</VCardActions>
</VCard>
<!-- 手动整理进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
<!-- TMDB ID搜索框 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector
v-if="mediaSource === 'themoviedb'"
v-model="transferForm.tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector
v-else
v-model="transferForm.doubanid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
</VDialog>
</VDialog>
</template>

View File

@@ -4,6 +4,14 @@ import type { Site } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
import { useDisplay } from 'vuetify'
import { useConfirm } from 'vuetify-use-dialog'
//
const display = useDisplay()
//
const createConfirm = useConfirm()
//
const props = defineProps({
@@ -40,7 +48,7 @@ const statusItems = [
// 150
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
Array.from({ length: 100 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
@@ -48,8 +56,7 @@ const priorityItems = ref(
//
watchEffect(async () => {
if (props.siteid)
fetchSiteInfo()
if (props.siteid) fetchSiteInfo()
})
//
@@ -58,27 +65,24 @@ async function fetchSiteInfo() {
siteForm.value = await api.get(`site/${props.siteid}`)
siteForm.value.proxy = siteForm.value.proxy === 1
siteForm.value.render = siteForm.value.render === 1
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// API
async function addSite() {
if (!siteForm.value?.url)
return
if (!siteForm.value?.url) return
startNProgress()
try {
const result: { [key: string]: string } = await api.post('site/', siteForm.value)
if (result.success) {
$toast.success('新增站点成功')
emit('save')
} else {
$toast.error(`新增站点失败:${result.message}`)
}
else { $toast.error(`新增站点失败:${result.message}`) }
}
catch (error) {
} catch (error) {
console.error(error)
}
doneNProgress()
@@ -86,14 +90,18 @@ async function addSite() {
// API
async function deleteSiteInfo() {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认删除站点?`,
})
if (!isConfirmed) return
try {
const result: { [key: string]: any } = await api.delete(`site/${siteForm.value?.id}`)
if (result.success)
emit('remove')
if (result.success) emit('remove')
else $toast.error(`${siteForm.value?.name} 删除失败:${result.message}`)
}
catch (error) {
} catch (error) {
$toast.error(`${siteForm.value?.name} 删除失败!`)
console.error(error)
}
@@ -107,10 +115,10 @@ async function updateSiteInfo() {
if (result.success) {
$toast.success(`${siteForm.value?.name} 更新成功!`)
emit('save')
} else {
$toast.error(`${siteForm.value?.name} 更新失败:${result.message}`)
}
else { $toast.error(`${siteForm.value?.name} 更新失败:${result.message}`) }
}
catch (error) {
} catch (error) {
$toast.error(`${siteForm.value?.name} 更新失败!`)
console.error(error)
}
@@ -119,152 +127,146 @@ async function updateSiteInfo() {
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VDialog scrollable :close-on-back="false" persistent eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VCol cols="12" md="6">
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
hint="格式http://www.example.com/"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VCol cols="6" md="3">
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
hint="优先级越小越优先"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VCol cols="6" md="3">
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
hint="站点启用/停用"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCol cols="12" md="9">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
hint="订阅模式为`站点RSS`时使用的订阅链接,如未自动获取需手动补充"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
<VTextarea v-model="siteForm.cookie" label="站点Cookie" hint="站点请求头中的Cookie信息" persistent-hint />
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteForm.token"
label="请求头Authorization"
hint="站点请求头中的Authorization信息特殊站点需要"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteForm.apikey"
label="令牌API Key"
hint="站点的访问API Key特殊站点需要"
persistent-hint
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
hint="获取Cookie的浏览器对应的User-Agent"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
hint="限流控制的单位周期时长"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="siteForm.limit_count"
label="访问次数"
label="周期内访问次数"
:rules="[numberValidator]"
hint="单位周期内允许的访问次数"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
hint="每次访问需要间隔的最小时间"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
<VCol cols="12" md="6">
<VSwitch v-model="siteForm.proxy" label="代理" hint="使用代理服务器访问该站点" persistent-hint />
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
<VCol cols="12" md="6">
<VSwitch v-model="siteForm.render" label="仿真" hint="使用浏览器模拟真实访问该站点" persistent-hint />
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn
v-if="props.oper === 'add'"
@click="emit('close')"
>
取消
</VBtn>
<VBtn
v-else
color="error"
@click="deleteSiteInfo"
>
<VCardActions class="pt-3">
<VBtn v-if="props.oper !== 'add'" color="error" @click="deleteSiteInfo" variant="outlined" class="me-3">
删除
</VBtn>
<VSpacer />
<VBtn
v-if="props.oper === 'add'"
color="primary"
variant="tonal"
variant="elevated"
@click="addSite"
prepend-icon="mdi-plus"
class="px-5"
>
新增
</VBtn>
<VBtn
v-else
color="primary"
variant="tonal"
variant="elevated"
@click="updateSiteInfo"
prepend-icon="mdi-content-save"
class="px-5"
>
保存
</VBtn>

View File

@@ -2,11 +2,21 @@
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { Site, Subscribe } from '@/api/types'
import type { MediaDirectory, Site, Subscribe } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useConfirm } from 'vuetify-use-dialog'
//
const display = useDisplay()
//
const createConfirm = useConfirm()
//
const props = defineProps({
subid: Number,
default: Boolean,
type: String,
})
//
@@ -15,6 +25,9 @@ const emit = defineEmits(['remove', 'save', 'close'])
//
const siteList = ref<Site[]>([])
//
const downloadDirectories = ref<MediaDirectory[]>([])
//
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
@@ -30,6 +43,7 @@ const subscribeForm = ref<Subscribe>({
total_episode: 0,
start_episode: 0,
best_version: 0,
search_imdbid: 0,
sites: [],
type: '',
name: '',
@@ -39,7 +53,9 @@ const subscribeForm = ref<Subscribe>({
last_update: '',
username: '',
current_priority: 0,
save_path: '',
save_path: undefined,
date: '',
show_edit_dialog: false,
})
//
@@ -54,14 +70,47 @@ async function updateSubscribeInfo() {
$toast.success(`${subscribeForm.value.name} 更新成功!`)
//
emit('save')
} else {
$toast.error(`${subscribeForm.value.name} 更新失败:${result.message}`)
}
else { $toast.error(`${subscribeForm.value.name} 更新失败:${result.message}`) }
}
catch (e) {
} catch (e) {
console.log(e)
}
}
//
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() {
try {
@@ -69,8 +118,7 @@ async function loadSites() {
//
siteList.value = data.filter(item => item.is_active)
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -78,10 +126,9 @@ async function loadSites() {
//
async function getSiteList() {
//
if (!siteList.value.length)
await loadSites()
if (!siteList.value.length) await loadSites()
const maps = siteList.value.map((item) => {
const maps = siteList.value.map(item => {
return {
title: item.name,
value: item.id,
@@ -94,34 +141,54 @@ async function getSiteList() {
//
async function getSubscribeInfo() {
try {
const result: Subscribe = await api.get(
`subscribe/${props.subid}`,
)
const result: Subscribe = await api.get(`subscribe/${props.subid}`)
subscribeForm.value = result
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
}
catch (e) {
subscribeForm.value.search_imdbid = subscribeForm.value.search_imdbid === 1
} catch (e) {
console.log(e)
}
}
//
async function removeSubscribe() {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认取消订阅?`,
})
if (!isConfirmed) return
try {
const result: { [key: string]: any } = await api.delete(
`subscribe/${props.subid}`,
)
const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`)
if (result.success) {
//
emit('remove')
}
}
catch (e) {
} catch (e) {
console.log(e)
}
}
//
async function loadDownloadDirectories() {
try {
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
if (result.success && result.data?.value) {
downloadDirectories.value = result.data.value
}
} catch (error) {
console.log(error)
}
}
//
const targetDirectories = computed(() => {
//
const directories = downloadDirectories.value.map(item => item.path)
return [...new Set(directories)]
})
//
const qualityOptions = ref([
{
@@ -206,156 +273,164 @@ const effectOptions = ref([
},
])
watchEffect(() => {
if (props.subid) {
getSiteList()
getSubscribeInfo()
}
onMounted(() => {
loadDownloadDirectories()
getSiteList()
if (props.subid) getSubscribeInfo()
if (props.default) queryDefaultSubscribeConfig()
})
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`"
:title="`${
props.default
? `${props.type}默认订阅规则`
: `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `${subscribeForm.season}` : ''}`
}`"
class="rounded-t"
>
<VCardText class="pt-2">
<VDivider />
<VCardText>
<DialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VCol cols="12" md="8">
<VTextField
v-if="!props.default"
v-model="subscribeForm.keyword"
label="搜索关键词"
hint="指定搜索站点时使用的关键词"
persistent-hint
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
:rules="[numberValidator]"
hint="剧集总集数"
persistent-hint
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
:rules="[numberValidator]"
hint="开始订阅集数"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.quality"
label="质量"
:items="qualityOptions"
hint="订阅资源质量"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.resolution"
label="分辨率"
:items="resolutionOptions"
hint="订阅资源分辨率"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.effect"
label="特效"
:items="effectOptions"
hint="订阅资源特效"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
hint="包含规则,支持正则表达式"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
hint="排除规则,支持正则表达式"
persistent-hint
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.sites"
:items="selectSitesOptions"
chips
label="订阅站点"
multiple
hint="订阅的站点范围,不选使用系统设置"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
<VCol cols="12">
<VCombobox
v-model="subscribeForm.save_path"
:items="targetDirectories"
label="保存路径"
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
hint="根据洗版优先级进行洗版订阅"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="subscribeForm.search_imdbid"
label="使用 ImdbID 搜索"
hint="开使用 ImdbID 精确搜索资源"
persistent-hint
/>
</VCol>
<VCol v-if="props.default" cols="12" md="4">
<VSwitch
v-model="subscribeForm.show_edit_dialog"
label="订阅时编辑更多规则"
hint="添加订阅时显示此编辑订阅对话框"
persistent-hint
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn color="error" @click="removeSubscribe">
<VCardActions class="pt-3">
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" variant="outlined" class="me-3">
取消订阅
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="updateSubscribeInfo"
variant="elevated"
@click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
prepend-icon="mdi-content-save"
class="px-5"
>
保存
</VBtn>

View File

@@ -0,0 +1,211 @@
<script lang="ts" setup>
import api from '@/api'
import { Subscribe } from '@/api/types'
import { formatDateDifference } from '@core/utils/formatters'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
// 显示器宽度
const display = useDisplay()
// 输入参数
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="!display.mdAndUp.value">
<VCard class="mx-auto" width="100%">
<VCardItem>
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
</VCardItem>
<VDivider />
<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>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</VDialog>
</template>

View File

@@ -0,0 +1,98 @@
<script lang="ts" setup>
import QrcodeVue from 'qrcode.vue'
import api from '@/api'
// 定义事件
const emit = defineEmits(['done', 'close'])
// 二维码内容
const qrCodeContent = ref('')
// 下方的提示信息
const text = ref('请使用微信或115客户端扫码')
// 提醒类型
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
// timeout定时器
let timeoutTimer: NodeJS.Timeout | undefined = undefined
// 完成
async function handleDone() {
emit('done')
}
// 调用/aliyun/qrcode api生成二维码
async function getQrcode() {
try {
const result: { [key: string]: any } = await api.get('/u115/qrcode')
if (result.success && result.data) {
qrCodeContent.value = result.data.codeContent
} else {
text.value = result.message
}
} catch (e) {
console.error(e)
}
}
// 调用/aliyun/check api验证二维码
async function checkQrcode() {
try {
const result: { [key: string]: any } = await api.get('/u115/check')
if (result.success && result.data) {
const status = result.data.status
text.value = result.data.tip
if (status == 1) {
// 已确认完成
alertType.value = 'success'
handleDone()
} else if (status == 0) {
alertType.value = 'info'
// 新建、待扫码
clearTimeout(timeoutTimer)
timeoutTimer = setTimeout(checkQrcode, 3000)
} else {
// 过期或者已取消
alertType.value = 'error'
}
} else {
alertType.value = 'error'
text.value = result.message
}
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
await getQrcode()
timeoutTimer = setTimeout(checkQrcode, 3000)
})
onUnmounted(() => {
if (timeoutTimer) clearTimeout(timeoutTimer)
})
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard title="115网盘登录" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 shadow-lg rounded border">
<VImg class="mx-auto" :src="qrCodeContent" style="block-size: 200px; inline-size: 200px">
<VSkeletonLoader v-if="!qrCodeContent" class="w-full h-full" />
</VImg>
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,738 @@
<script lang="ts" setup>
import type { Axios, AxiosRequestConfig } from 'axios'
import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import { useToast } from 'vue-toast-notification'
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// APP
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 输入参数
const inProps = defineProps({
icons: Object,
storage: String,
endpoints: Object as PropType<EndPoints>,
axios: {
type: Object as PropType<Axios>,
required: true,
},
refreshpending: Boolean,
item: {
type: Object as PropType<FileItem>,
required: true,
},
sort: String,
})
// 对外事件
const emit = defineEmits(['loading', 'pathchanged', 'refreshed', 'filedeleted', 'renamed'])
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
// 是否选择模式
const selectMode = ref(false)
// 是否正在加载
const loading = ref(true)
// 重命名loading
const renameLoading = ref(false)
// 识别进度条
const progressDialog = ref(false)
// 识别进度文本
const progressText = ref('请稍候 ...')
// 识别进度
const progressValue = ref(0)
// 内容列表
const items = ref<FileItem[]>([])
// 过滤条件
const filter = ref('')
// 重命名弹窗
const renamePopper = ref(false)
// 整理弹窗
const transferPopper = ref(false)
// 新名称
const newName = ref('')
// 处理目录内所有文件
const renameAll = ref(false)
// 当前操作项
const currentItem = ref<FileItem>()
// 选中的项目
const selected = ref<FileItem[]>([])
// 识别结果
const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// 弹出菜单
const dropdownItems = ref<{ [key: string]: any }[]>([])
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 目录过滤
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.name.includes(filter.value)))
// 文件过滤
const files = computed(() => items.value.filter(item => item.type === 'file' && item.name.includes(filter.value)))
// 是否目录
const isDir = computed(() => inProps.item.path?.endsWith('/'))
// 是否文件
const isFile = computed(() => !isDir.value)
// 需要整理的文件项
const transferItems = ref<FileItem[]>([])
// 大小控制
const scrollStyle = computed(() => {
return appMode.value
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
})
// 是否为图片文件
const isImage = computed(() => {
const ext = inProps.item.path?.split('.').pop()?.toLowerCase()
return ['png', 'jpg', 'jpeg', 'gif', 'bmp'].includes(ext ?? '')
})
// 调整选择模式
function changeSelectMode() {
selectMode.value = !selectMode.value
if (!selectMode.value) selected.value = []
}
// 调API加载文件夹内的内容
async function list_files() {
loading.value = true
emit('loading', true)
// 参数
const url = inProps.endpoints?.list.url
.replace(/{storage}/g, inProps.storage)
.replace(/{sort}/g, inProps.sort || 'name')
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.list.method || 'get',
data: inProps.item,
}
// 加载数据
items.value = (await inProps.axios.request(config)) ?? []
emit('loading', false)
loading.value = false
}
// 删除项目
async function deleteItem(item: FileItem, confirm: boolean = true) {
if (confirm) {
const confirmed = await createConfirm({
title: '确认',
content: `是否确认删除${item.type === 'dir' ? '目录' : '文件'} ${item.name}`,
})
if (!confirmed) return
}
// 加载中
emit('loading', true)
// 请求API
const url = inProps.endpoints?.delete.url.replace(/{storage}/g, inProps.storage)
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.delete.method || 'post',
data: item,
}
await inProps.axios.request(config)
// 删除完成
emit('loading', false)
emit('filedeleted')
// 重新加载
list_files()
}
// 批量删除
async function batchDelete() {
const confirmed = await createConfirm({
title: '确认',
content: `是否确认删除选中的 ${selected.value.length} 个项目?`,
})
if (!confirmed) return
// 显示进度条
progressDialog.value = true
progressValue.value = 0
// 删除选中的项目
selected.value.every(async item => {
progressText.value = `正在删除 ${item.name} ...`
await deleteItem(item, false)
})
// 关闭进度条
progressDialog.value = false
// 重新加载
list_files()
}
// 切换路径
function changePath(item: FileItem) {
item.path = inProps.item.path + item.name + (item.type === 'dir' ? '/' : '')
emit('pathchanged', item)
}
// 点击列表项
function listItemClick(item: FileItem) {
if (selectMode.value) {
if (selected.value.includes(item)) {
selected.value = selected.value.filter(i => i !== item)
} else {
selected.value.push(item)
}
// 去重
selected.value = Array.from(new Set(selected.value))
return false
}
changePath(item)
}
// 新窗口中下载文件
async function download(item: FileItem) {
const url = inProps.endpoints?.download.url.replace(/{storage}/g, inProps.storage)
const filterEntries = Object.entries(item).filter(([key, value]) => !['children', 'thumbnail'].includes(key) && value)
const queryParams = filterEntries.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')
window.open(
`${import.meta.env.VITE_API_BASE_URL}${url.slice(1)}?${queryParams}&token=${store.state.auth.token}`,
'_blank',
)
}
// 获取图片地址
function getImgLink(item: FileItem) {
let url = inProps.endpoints?.image.url.replace(/{storage}/g, inProps.storage)
const filterEntries = Object.entries(item).filter(([key, value]) => !['children', 'thumbnail'].includes(key) && value)
const queryParams = filterEntries.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')
return `${import.meta.env.VITE_API_BASE_URL}${url.slice(1)}?${queryParams}&token=${store.state.auth.token}`
}
// 显示重命名弹窗
function showRenmae(item: FileItem) {
currentItem.value = item
newName.value = item.name
renameAll.value = false
renamePopper.value = true
}
// 调用API获取新名称
async function get_recommend_name() {
renameLoading.value = true
try {
const result: { [key: string]: any } = await api.get('transfer/name', {
params: {
path: `${inProps.item.path}${currentItem.value?.name}`,
filetype: currentItem.value?.type ?? 'file',
},
})
if (result.success && result.data) {
newName.value = result.data.name
} else {
$toast.error(result.message)
}
} catch (error) {
console.error(error)
}
renameLoading.value = false
}
// 重命名
async function rename() {
emit('loading', true)
// 关闭弹窗
renamePopper.value = false
// 显示进度条
progressDialog.value = true
progressValue.value = 0
if (renameAll.value) {
progressText.value = `正在重命名 ${currentItem.value?.path} 及目录内所有文件 ...`
} else {
progressText.value = `正在重命名 ${currentItem.value?.name} ...`
}
if (renameAll.value) {
startLoadingProgress()
}
// 调API
let url = inProps.endpoints?.rename.url
.replace(/{storage}/g, inProps.storage)
.replace(/{newname}/g, encodeURIComponent(newName.value))
if (renameAll.value) {
url += '&recursive=true'
}
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.rename.method || 'post',
data: currentItem.value,
}
const result: { [key: string]: any } = await inProps.axios?.request(config)
if (!result.success) {
$toast.error(result.message)
}
// 关闭进度条
if (renameAll.value) {
stopLoadingProgress()
}
progressDialog.value = false
// 通知重新加载
newName.value = ''
renameAll.value = false
emit('loading', false)
emit('renamed')
}
// 显示整理对话框
function showTransfer(item: FileItem) {
transferItems.value = [item]
transferPopper.value = true
}
// 显示批量整理对话框
function showBatchTransfer() {
transferItems.value = selected.value
transferPopper.value = true
}
// 整理完成
function transferDone() {
transferPopper.value = false
list_files()
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
}
// 监听refreshPending变化
watch(
() => inProps.refreshpending,
async () => {
if (inProps.refreshpending) {
await list_files()
emit('refreshed')
}
},
)
// 监听item变化或者storage变化
watch(
[() => inProps.item, () => inProps.storage],
async () => {
// 清空列表
items.value = []
// 关闭弹窗
nameTestResult.value = undefined
nameTestDialog.value = false
// 重置菜单
dropdownItems.value = [
{
title: '识别',
value: 1,
show: true,
props: {
prependIcon: 'mdi-text-recognition',
click: (_item: FileItem) => {
recognize(_item.path || '')
},
},
},
{
title: '刮削',
value: 2,
show: true,
props: {
prependIcon: 'mdi-auto-fix',
click: (_item: FileItem) => {
scrape(_item)
},
},
},
{
title: '重命名',
value: 3,
show: true,
props: {
prependIcon: 'mdi-rename',
click: showRenmae,
},
},
{
title: '整理',
value: 4,
show: true,
props: {
prependIcon: 'mdi-folder-arrow-right',
click: showTransfer,
},
},
{
title: '删除',
value: 5,
show: true,
props: {
prependIcon: 'mdi-delete-outline',
color: 'error',
click: deleteItem,
},
},
]
await list_files()
},
{ immediate: true },
)
// 调用API识别
async function recognize(path: string) {
try {
// 显示进度条
progressDialog.value = true
progressText.value = `正在识别 ${path} ...`
progressValue.value = 0
nameTestResult.value = await api.get('media/recognize_file', {
params: {
path,
},
})
// 关闭进度条
progressDialog.value = false
if (!nameTestResult.value) $toast.error(`${path} 识别失败!`)
nameTestDialog.value = !!nameTestResult.value?.meta_info?.name
} catch (error) {
console.error(error)
}
}
// 调用API刮削
async function scrape(item: FileItem, confirm: boolean = true) {
try {
if (confirm) {
// 确认
const confirmed = await createConfirm({
title: '确认',
content: `是否确认刮削 ${item.path}`,
})
if (!confirmed) return
}
// 显示进度条
progressDialog.value = true
progressText.value = `正在刮削 ${item.path} ...`
const result: { [key: string]: any } = await api.post(`media/scrape/${inProps.storage}`, item)
// 关闭进度条
progressDialog.value = false
if (!result.success) $toast.error(result.message)
else $toast.success(`${item.path} 削刮完成!`)
} catch (error) {
console.error(error)
}
}
// 批量刮削
async function batchScrape() {
// 确认
const confirmed = await createConfirm({
title: '确认',
content: `是否确认刮削选中的 ${selected.value.length} 项?`,
})
if (!confirmed) return
selected.value.map(item => {
scrape(item, false)
})
}
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/batchrename?token=${token}`,
)
progressEventSource.value.onmessage = event => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
onMounted(() => {
list_files()
})
</script>
<template>
<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="changeSelectMode">
<VIcon color="primary" v-if="selectMode"> mdi-selection-remove </VIcon>
<VIcon color="primary" v-else>mdi-select</VIcon>
</IconBtn>
<IconBtn v-if="isFile" @click="recognize(inProps.item.path || '')">
<VIcon color="primary"> mdi-text-recognition </VIcon>
</IconBtn>
<IconBtn v-if="isFile && items.length > 0" @click="download(items[0])">
<VIcon color="primary"> mdi-download </VIcon>
</IconBtn>
<IconBtn v-if="!isFile" @click="list_files">
<VIcon color="primary"> mdi-refresh </VIcon>
</IconBtn>
<!-- 批量操作按钮 -->
<span v-if="selected.length > 0">
<IconBtn @click.stop="batchScrape">
<VIcon color="primary" icon="mdi-auto-fix" />
</IconBtn>
<IconBtn @click.stop="showBatchTransfer">
<VIcon color="primary" icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn @click.stop="batchDelete">
<VIcon icon="mdi-delete-outline" color="error" />
</IconBtn>
</span>
</VToolbar>
<VCardText v-if="loading" class="text-center flex flex-col items-center">
<VProgressCircular size="48" indeterminate color="primary" />
</VCardText>
<!-- 文件详情 -->
<VCardText v-else-if="isFile && !isImage && items.length > 0" class="text-center break-all">
<div v-if="items[0]?.thumbnail" class="flex justify-center">
<VImg max-width="15rem" cover :src="items[0]?.thumbnail" class="rounded border shadow-lg">
<template #placeholder>
<VSkeletonLoader class="object-cover w-full h-full" />
</template>
</VImg>
</div>
<div class="text-xl text-high-emphasis mt-3">{{ items[0]?.name }}</div>
<p class="mt-2" v-if="items[0]?.size && items[0].modify_time">
大小{{ formatBytes(items[0]?.size || 0) }}<br />
修改时间{{ formatTime(items[0]?.modify_time || 0) }}
</p>
</VCardText>
<!-- 图片 -->
<VCardText v-else-if="isFile && isImage && items.length > 0" class="grow d-flex justify-center align-center">
<VImg :src="getImgLink(items[0])" max-width="100%" max-height="100%" />
</VCardText>
<!-- 目录和文件列表 -->
<VCardText v-else-if="dirs.length || files.length" class="p-0">
<VList subheader>
<VVirtualScroll :items="[...dirs, ...files]" :style="scrollStyle">
<template #default="{ item }">
<VHover>
<template #default="hover">
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="listItemClick(item)">
<template #prepend>
<VListItemAction v-if="selectMode">
<VCheckbox v-model="selected" :value="item" />
</VListItemAction>
<template v-else>
<VIcon
v-if="inProps.icons && item.extension"
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
/>
<VIcon v-else icon="mdi-folder-outline" />
</template>
</template>
<VListItemTitle v-text="item.name" />
<VListItemSubtitle v-if="item.size">
{{ formatBytes(item.size) }}
</VListItemSubtitle>
<template #append>
<IconBtn v-if="display.smAndDown.value && !selectMode">
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<template v-for="(menu, i) in dropdownItems" :key="i">
<VListItem
v-if="menu.show"
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>
</template>
</VList>
</VMenu>
</IconBtn>
<span v-if="hover.isHovering && display.mdAndUp.value && !selectMode" class="flex">
<VTooltip text="识别">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="刮削">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="scrape(item)">
<VIcon icon="mdi-auto-fix" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="重命名">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="整理">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="删除">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" color="error" />
</IconBtn>
</template>
</VTooltip>
</span>
</template>
</VListItem>
</template>
</VHover>
</template>
</VVirtualScroll>
</VList>
</VCardText>
<VCardText v-else-if="filter" class="grow d-flex justify-center align-center grey--text py-5">
没有目录或文件
</VCardText>
<VCardText v-else-if="!loading" class="grow d-flex justify-center align-center grey--text py-5"> 空目录 </VCardText>
</VCard>
<!-- 重命名弹窗 -->
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="50rem">
<VCard title="重命名">
<DialogCloseBtn @click="renamePopper = false" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="newName" label="新名称" :loading="renameLoading" />
</VCol>
<VCol cols="12" md="6" v-if="currentItem && currentItem.type == 'dir'">
<VSwitch v-model="renameAll" label="自动重命名目录内所有媒体文件" />
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VBtn color="success" variant="elevated" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
自动识别名称
</VBtn>
<VBtn :disabled="!newName" variant="elevated" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 文件整理弹窗 -->
<ReorganizeDialog
v-if="transferPopper"
v-model="transferPopper"
:storage="inProps.storage"
:items="transferItems"
@done="transferDone"
@close="transferPopper = false"
/>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
<!-- 识别结果对话框 -->
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" width="50rem">
<VCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<MediaInfoCard :context="nameTestResult" />
</VCardItem>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.v-card {
block-size: 100%;
}
.v-toolbar {
background: rgb(var(--v-table-header-background));
}
</style>

View File

@@ -1,14 +1,28 @@
<script lang="ts" setup>
import type { Axios } from 'axios'
import type { EndPoints } from '@/api/types'
import type { Axios, AxiosRequestConfig } from 'axios'
import type { EndPoints, FileItem } from '@/api/types'
import { useDisplay } from 'vuetify'
//
const display = useDisplay()
//
const inProps = defineProps({
storages: Array as PropType<any[]>,
storage: String,
path: String,
item: {
type: Object as PropType<FileItem>,
required: true,
},
itemstack: {
type: Array as PropType<FileItem[]>,
required: true,
},
endpoints: Object as PropType<EndPoints>,
axios: Object as PropType<Axios>,
axios: {
type: Object as PropType<Axios>,
required: true,
},
})
//
@@ -25,10 +39,8 @@ const sort = ref('name')
//
function changeSort() {
if (sort.value === 'name')
sort.value = 'time'
else
sort.value = 'name'
if (sort.value === 'name') sort.value = 'time'
else sort.value = 'name'
emit('sortchanged', sort.value)
}
@@ -36,18 +48,20 @@ function changeSort() {
// PATH
const pathSegments = computed(() => {
let path_str = ''
const isFolder = inProps.path?.endsWith('/')
const segments = inProps.path?.split('/').filter(item => item)
return segments?.map((item, index) => {
path_str += item + ((index < segments.length - 1 || isFolder) ? '/' : '')
return {
name: item,
path: path_str,
}
}) ?? []
const isFolder = inProps.item.path?.endsWith('/')
const segments = inProps.item.path?.split('/').filter(item => item)
return (
segments?.map((item, index) => {
path_str += item + (index < segments.length - 1 || isFolder ? '/' : '')
return {
name: item,
path: path_str,
}
}) ?? []
)
})
//
const storageObject = computed(() => {
return inProps.storages?.find(item => item.code === inProps.storage)
})
@@ -56,20 +70,19 @@ const storageObject = computed(() => {
function changeStorage(code: string) {
if (inProps.storage !== code) {
emit('storagechanged', code)
emit('pathchanged', '')
}
}
//
function changePath(_path: string) {
emit('pathchanged', _path)
function changePath(item: FileItem) {
emit('pathchanged', item)
}
//
function goUp() {
const segments = pathSegments.value ?? []
const path = segments?.length === 1 ? '/' : segments[segments.length - 2].path
changePath(path)
const fileitem = inProps.itemstack[segments.length - 1]
changePath(fileitem)
}
//
@@ -77,15 +90,16 @@ async function mkdir() {
emit('loading', true)
const url = inProps.endpoints?.mkdir.url
.replace(/{storage}/g, inProps.storage)
.replace(/{path}/g, encodeURIComponent(inProps.path + newFolderName.value))
.replace(/{name}/g, newFolderName.value)
const config = {
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.mkdir.method || 'post',
data: inProps.item,
}
// API
await inProps.axios?.request(config)
await inProps.axios.request(config)
newFolderPopper.value = false
newFolderName.value = ''
@@ -97,10 +111,8 @@ async function mkdir() {
//
const sortIcon = computed(() => {
if (sort.value === 'time')
return 'mdi-sort-clock-ascending-outline'
else
return 'mdi-sort-alphabetical-ascending'
if (sort.value === 'time') return 'mdi-sort-clock-ascending-outline'
else return 'mdi-sort-alphabetical-ascending'
})
</script>
@@ -127,16 +139,17 @@ const sortIcon = computed(() => {
</VListItem>
</VList>
</VMenu>
<VBtn variant="text" :input-value="path === '/'" class="px-1" @click="changePath('/')">
<VBtn variant="text" :input-value="item.path === '/'" class="px-1" @click="changePath(inProps.itemstack[0])">
<VIcon :icon="storageObject?.icon" class="mr-2" />
{{ storageObject?.name }}
</VBtn>
<template v-for="(segment, index) in pathSegments" :key="index">
<VBtn
v-if="display.mdAndUp.value"
variant="text"
:input-value="index === pathSegments.length - 1"
class="px-1 d-none d-md-block"
@click="changePath(segment.path)"
class="px-1"
@click="changePath(inProps.itemstack[index + 1])"
>
<VIcon icon=" mdi-chevron-right" />
{{ segment.name }}
@@ -144,36 +157,39 @@ const sortIcon = computed(() => {
</template>
</VToolbarItems>
<div class="flex-grow-1" />
<IconBtn @click="changeSort">
<VIcon :icon="sortIcon" />
</IconBtn>
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
<VDialog
v-model="newFolderPopper"
max-width="50rem"
>
<VTooltip text="调整排序">
<template #activator="{ props }">
<IconBtn title="新建文件夹" v-bind="props">
<VIcon icon="mdi-folder-plus-outline" />
<IconBtn v-bind="props" @click="changeSort">
<VIcon :icon="sortIcon" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="返回上一级" v-if="pathSegments.length > 0">
<template #activator="{ props }">
<IconBtn v-bind="props" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
</template>
</VTooltip>
<VDialog v-model="newFolderPopper" max-width="50rem">
<template #activator="{ props }">
<IconBtn v-bind="props">
<VTooltip text="新建文件夹">
<template #activator="{ props: _props }">
<VIcon v-bind="_props" icon="mdi-folder-plus-outline" />
</template>
</VTooltip>
</IconBtn>
</template>
<VCard title="新建文件夹">
<DialogCloseBtn @click="newFolderPopper = false" />
<VDivider />
<VCardText>
<VTextField v-model="newFolderName" label="名称" />
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn depressed @click="newFolderPopper = false">
取消
</VBtn>
<VBtn
:disabled="!newFolderName"
depressed
variant="tonal"
@click="mkdir"
>
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
新建
</VBtn>
</VCardActions>

View File

@@ -1,571 +0,0 @@
<script lang="ts" setup>
import type { Axios } from 'axios'
import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios'
import { useToast } from 'vue-toast-notification'
import ReorganizeForm from '../form/ReorganizeForm.vue'
import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
// 输入参数
const inProps = defineProps({
icons: Object,
storage: String,
path: String,
endpoints: Object as PropType<EndPoints>,
axios: Object as PropType<Axios>,
refreshpending: Boolean,
sort: String,
})
// 对外事件
const emit = defineEmits(['loading', 'pathchanged', 'refreshed', 'filedeleted', 'renamed'])
// 提示框
const $toast = useToast()
// 是否正在加载
const loading = ref(true)
// 识别进度条
const progressDialog = ref(false)
// 识别进度文本
const progressText = ref('请稍候 ...')
// 识别进度
const progressValue = ref(0)
// 确认框
const createConfirm = useConfirm()
// 存储空间类型
const storage = ref(inProps.storage ?? '')
// axios实例
const axiosInstance = ref<Axios>(inProps.axios ?? axios)
// 内容列表
const items = ref<FileItem[]>([])
// 过滤条件
const filter = ref('')
// 重命名弹窗
const renamePopper = ref(false)
// 整理弹窗
const transferPopper = ref(false)
// 新名称
const newName = ref('')
// 当前名称
const currentItem = ref<FileItem>()
// 识别结果
const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// 目录过滤
const dirs = computed(() =>
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
)
// 文件过滤
const files = computed(() =>
items.value.filter(item => item.type === 'file' && item.basename.includes(filter.value)),
)
// 是否目录
const isDir = computed(() => inProps.path?.endsWith('/'))
// 是否文件
const isFile = computed(() => !isDir.value)
// 是否为图片文件
const isImage = computed(() => {
const ext = inProps.path?.split('.').pop()?.toLowerCase()
return ['png', 'jpg', 'jpeg', 'gif', 'bmp'].includes(ext ?? '')
})
// 调API加载内容
async function load() {
loading.value = true
emit('loading', true)
// 参数
const url = inProps.endpoints?.list.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
.replace(/{sort}/g, inProps.sort || 'name')
const config = {
url,
method: inProps.endpoints?.list.method || 'get',
}
// 加载数据
items.value = await axiosInstance.value.request(config) ?? []
emit('loading', false)
loading.value = false
}
// 删除项目
async function deleteItem(item: FileItem) {
const confirmed = await createConfirm({
title: '确认',
content: `是否确认删除${
item.type === 'dir' ? '目录' : '文件'
} ${item.basename}`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
cancellationButtonProps: {
variant: 'tonal',
},
})
if (confirmed) {
emit('loading', true)
const url = inProps.endpoints?.delete.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(item.path))
const config = {
url,
method: inProps.endpoints?.delete.method || 'post',
}
await axiosInstance.value.request(config)
emit('filedeleted')
emit('loading', false)
// 重新加载
load()
}
}
// 切换路径
function changePath(_path: string) {
emit('pathchanged', _path)
}
// 新窗口中下载文件
function download(path: string) {
if (!path)
return
const token = store.state.auth.token
const url_path = inProps.endpoints?.download.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(path))
const url = `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
// 下载文件
window.open(url, '_blank')
}
// 显示图片
function getImgLink(path: string) {
if (!path)
return ''
const token = store.state.auth.token
const url_path = inProps.endpoints?.image.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(path))
return `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
}
// 显示重命名弹窗
function showRenmae(item: FileItem) {
currentItem.value = item
newName.value = item.name
renamePopper.value = true
}
// 重命名
async function rename() {
emit('loading', true)
const url = inProps.endpoints?.rename.url
.replace(/{storage}/g, inProps.storage)
.replace(/{path}/g, encodeURIComponent(currentItem.value?.path || ''))
.replace(/{newname}/g, encodeURIComponent(newName.value))
const config = {
url,
method: inProps.endpoints?.mkdir.method || 'post',
}
// 调API
await inProps.axios?.request(config)
renamePopper.value = false
newName.value = ''
emit('loading', false)
// 通知重新加载
emit('renamed')
}
// 显示整理对话框
function showTransfer(item: FileItem) {
currentItem.value = item
transferPopper.value = true
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
}
// 监听path变化
watch(
() => inProps.path,
async () => {
items.value = []
nameTestResult.value = undefined
nameTestDialog.value = false
await load()
},
)
// 监听refreshPending变化
watch(
() => inProps.refreshpending,
async () => {
if (inProps.refreshpending) {
await load()
emit('refreshed')
}
},
)
// 调用API识别
async function recognize(path: string) {
try {
// 显示进度条
progressDialog.value = true
progressText.value = `正在识别 ${path} ...`
progressValue.value = 0
nameTestResult.value = await api.get('media/recognize_file', {
params: {
path,
},
})
// 关闭进度条
progressDialog.value = false
if (!nameTestResult.value)
$toast.error(`${path} 识别失败!`)
nameTestDialog.value = !!nameTestResult.value?.meta_info?.name
}
catch (error) {
console.error(error)
}
}
// 弹出菜单
const dropdownItems = ref([
{
title: '识别',
value: 1,
props: {
prependIcon: 'mdi-text-recognition',
click: (_item: FileItem) => {
recognize(_item.path || '')
},
},
}, {
title: '重命名',
value: 2,
props: {
prependIcon: 'mdi-rename',
click: showRenmae,
},
},
{
title: '整理',
value: 3,
props: {
prependIcon: 'mdi-folder-arrow-right',
click: showTransfer,
},
},
{
title: '删除',
value: 4,
props: {
prependIcon: 'mdi-delete-outline',
color: 'error',
click: deleteItem,
},
},
])
onMounted(() => {
load()
})
</script>
<template>
<VCard class="d-flex flex-column">
<VCardText
v-if="loading"
class="text-center flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</VCardText>
<VCardText
v-if="!path"
class="grow d-flex justify-center align-center grey--text"
>
选择目录或文件
</VCardText>
<VCardText
v-else-if="isFile && !isImage"
class="text-center break-all"
>
<strong>{{ items[0]?.name }}</strong><br>
大小{{ formatBytes(items[0]?.size || 0) }}<br>
修改时间{{ formatTime(items[0]?.modify_time || 0) }}
</VCardText>
<VCardText
v-else-if="isFile && isImage"
class="grow d-flex justify-center align-center"
>
<VImg :src="getImgLink(path)" max-width="100%" max-height="100%" />
</VCardText>
<VCardText v-else-if="dirs.length || files.length" class="p-0">
<VList v-if="dirs.length" subheader>
<VListSubheader>目录</VListSubheader>
<VListItem
v-for="(item, index) in dirs"
:key="index"
class="px-3 pe-1"
@click="changePath(item.path)"
>
<template #prepend>
<VIcon icon="mdi-folder-outline" />
</template>
<VListItemTitle v-text="item.name" />
<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>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" />
</IconBtn>
</template>
</VListItem>
</VList>
<VDivider v-if="dirs.length && files.length" />
<VList v-if="files.length" subheader>
<VListSubheader>文件</VListSubheader>
<VListItem
v-for="(item, index) in files"
:key="index"
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>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" />
</IconBtn>
</template>
</VListItem>
</VList>
</VCardText>
<VCardText
v-else-if="filter"
class="grow d-flex justify-center align-center grey--text py-5"
>
没有目录或文件
</VCardText>
<VCardText
v-else-if="!loading"
class="grow d-flex justify-center align-center grey--text py-5"
>
空目录
</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>
<!-- 重命名弹窗 -->
<VDialog
v-model="renamePopper"
max-width="50rem"
>
<VCard title="重命名">
<VCardText>
<VTextField v-model="newName" label="名称" />
</VCardText>
<VCardActions>
<VBtn depressed @click="renamePopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
:disabled="!newName"
depressed
variant="tonal"
@click="rename"
>
重命名
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 文件整理弹窗 -->
<ReorganizeForm
v-model="transferPopper"
:path="currentItem?.path"
@done="transferPopper = false; load()"
@close="transferPopper = false"
/>
<!-- 手动整理进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</VCardText>
</VCard>
</VDialog>
<!-- 识别结果对话框 -->
<VDialog
v-model="nameTestDialog"
width="50rem"
>
<VCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<MediaInfoCard :context="nameTestResult" />
</VCardItem>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.v-card {
height: 100%;
}
.v-toolbar{
background: rgb(var(--v-table-header-background));
}
</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

@@ -1,307 +0,0 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
import store from '@/store'
import api from '@/api'
import { numberValidator } from '@/@validators'
// 输入参数
const props = defineProps({
path: String,
target: String,
logids: Array<number>,
})
// 定义事件
const emit = defineEmits(['done', 'close'])
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
title: `${item}`,
value: item,
})),
)
// 提示框
const $toast = useToast()
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 文件转移表单
const transferForm = reactive({
logid: 0,
path: '',
target: props.target ?? '',
tmdbid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
min_filesize: 0,
})
watchEffect(() => {
transferForm.path = props.path ?? ''
transferForm.target = props.target ?? ''
})
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 整理文件
// eslint-disable-next-line sonarjs/cognitive-complexity
async function transfer() {
if (!props.logids && !props.path)
return
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
if (props.path) {
// 文件整理
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
// 显示结果
if (result.success)
$toast.success(`${props.path} 整理完成!`)
else
$toast.error(`${props.path} 整理失败:${result.message}`)
}
catch (e) {
console.log(e)
}
}
else if (props.logids) {
// 日志整理
for (const logid of props.logids) {
transferForm.logid = logid
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
if (!result.success)
$toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
}
catch (e) {
console.log(e)
}
}
}
// 停止监听进度
stopLoadingProgress()
// 关闭进度条
progressDialog.value = false
// 重新加载
emit('done')
}
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VCard
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="transferForm.target"
label="目的路径"
placeholder="留空自动"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
{ title: 'Rclone复制', value: 'rclone_copy' },
{ title: 'Rclone移动', value: 'rclone_move' },
]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.type_name"
label="类型"
:items="[{ title: '自动', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.min_filesize"
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn depressed @click="emit('close')">
取消
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="transfer"
>
开始整理
</VBtn>
</VCardActions>
</VCard>
<!-- 手动整理进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</VCardText>
</VCard>
</VDialog>
<!-- TMDB ID搜索框 -->
<VDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</VDialog>
</VDialog>
</template>

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import api from '@/api'
import { FileItem } from '@/api/types'
import { VTreeview } from 'vuetify/labs/VTreeview'
// 输入变量为默认路径
const props = defineProps({
root: {
type: String,
default: '/',
},
})
// update:modelValue 事件
const emit = defineEmits(['update:modelValue'])
// 激活的目录
const activedDirs = ref<string[]>([])
// 打开的目录
const openedDirs = ref<string[]>([])
// 目录列表
const treeItems = ref<FileItem[]>([
{
name: '/',
path: props.root,
children: [],
type: '',
basename: props.root,
extension: '',
size: 0,
modify_time: 0,
fileid: '',
parent_fileid: '',
},
])
// 拉取子目录
async function fetchDirs(item: any) {
return api
.get('/local/listdir?path=' + item.path)
.then((data: any) => {
item.children.push(...data)
})
.catch(err => console.warn(err))
}
// 获取选择的目录路径
const selectedPath = computed(() => {
if (activedDirs.value.length > 0) {
return activedDirs.value[0]
}
return ''
})
// 监听目录变化
watch(activedDirs, newVal => {
if (!newVal.length) return
emit('update:modelValue', selectedPath)
})
onMounted(() => {
fetchDirs(treeItems.value[0])
})
</script>
<template>
<VMenu :close-on-content-click="false" content-class="cursor-default">
<template v-slot:activator="{ props }">
<slot name="activator" :menuprops="props" />
</template>
<VTreeview
v-model:activated="activedDirs"
v-model:opened="openedDirs"
:items="treeItems"
:load-children="fetchDirs"
item-key="path"
item-title="name"
item-value="path"
item-type="unknown"
activatable
return-object
max-height="20rem"
expand-icon="mdi-folder"
collapse-icon="mdi-folder-open"
>
</VTreeview>
</VMenu>
</template>

View File

@@ -0,0 +1,79 @@
<script setup lang="ts">
import { DashboardItem } from '@/api/types'
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import DashboardRender from '@/components/render/DashboardRender.vue'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数
const props = defineProps({
// 仪表板配置
config: Object as PropType<DashboardItem>,
// 刷新状态
refreshStatus: Boolean,
// 是否允许刷新数据
allowRefresh: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['update:refreshStatus'])
onUnmounted(() => {
// 组件卸载时禁用刷新状态
emit('update:refreshStatus', false)
})
</script>
<template>
<!-- 系统内置的仪表板 -->
<AnalyticsStorage v-if="config?.id === 'storage'" />
<AnalyticsMediaStatistic v-else-if="config?.id === 'mediaStatistic'" />
<AnalyticsWeeklyOverview v-else-if="config?.id === 'weeklyOverview'" />
<AnalyticsSpeed v-else-if="config?.id === 'speed'" :allowRefresh="props.allowRefresh" />
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" :allowRefresh="props.allowRefresh" />
<AnalyticsCpu v-else-if="config?.id === 'cpu'" :allowRefresh="props.allowRefresh" />
<AnalyticsMemory v-else-if="config?.id === 'memory'" :allowRefresh="props.allowRefresh" />
<MediaServerLibrary v-else-if="config?.id === 'library'" />
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
<MediaServerLatest v-else-if="config?.id === 'latest'" />
<!-- 插件仪表板 -->
<VHover v-else-if="!isNullOrEmptyObject(props.config)">
<template #default="hover">
<!-- 无边框 -->
<div v-if="props.config?.attrs.border === false">
<VCard v-bind="hover.props">
<VCardText class="p-0">
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
</VCardText>
<div v-if="hover.isHovering" class="absolute right-5 top-5">
<VIcon class="cursor-move">mdi-drag</VIcon>
</div>
</VCard>
</div>
<!-- 有边框 -->
<VCard v-else v-bind="hover.props">
<VCardItem v-if="props.config?.attrs.border !== false">
<template #append>
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
</template>
<VCardTitle>
{{ props.config?.attrs?.title ?? props.config?.name }}
</VCardTitle>
<VCardSubtitle v-if="props.config?.attrs?.subtitle"> {{ props.config?.attrs?.subtitle }}</VCardSubtitle>
</VCardItem>
<VCardText>
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
</VCardText>
</VCard>
</template>
</VHover>
</template>

View File

@@ -2,10 +2,16 @@
import api from '@/api'
import type { MediaInfo } from '@/api/types'
//
const props = defineProps({
type: String, // themoviedb | douban
})
interface TmdbItem {
title: string
overview: string
tmdbid: number
doubanid: string
poster: string
}
@@ -21,25 +27,23 @@ const keyword = ref('')
const loading = ref(false)
// ref
const tmdbKeyword = ref<HTMLElement | null>(null)
const inputKeyword = ref<HTMLElement | null>(null)
//
function selectMedia(item: TmdbItem) {
emit('update:modelValue', item.tmdbid)
emit('update:modelValue', item.tmdbid || item.doubanid)
emit('close')
}
// TMDBw500
function getW500Image(url = '') {
if (!url)
return ''
if (!url) return ''
return url.replace('original', 'w500')
}
//
async function searchMedias() {
if (!keyword)
return
if (!keyword) return
// API
try {
@@ -57,16 +61,17 @@ async function searchMedias() {
//
for (const item of result) {
if (props.type && props.type !== item.source) continue
items.value.push({
tmdbid: item.tmdb_id || 0,
doubanid: item.douban_id || '',
poster: getW500Image(item.poster_path),
title: `${item.title}${item.year}`,
overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
})
}
loading.value = false
}
catch (e) {
} catch (e) {
console.error(e)
}
}
@@ -75,25 +80,22 @@ async function searchMedias() {
onMounted(() => {
// 500ms
setTimeout(() => {
tmdbKeyword.value?.focus()
inputKeyword.value?.focus()
}, 500)
})
</script>
<template>
<VCard
class="mx-auto"
width="100%"
>
<VCard class="mx-auto" width="100%">
<VToolbar flat class="p-0">
<VTextField
ref="tmdbKeyword"
ref="inputKeyword"
v-model="keyword"
label="输入名称搜索"
single-line
placeholder="电影或电视剧名称"
variant="solo"
append-inner-icon="mdi-magnify"
prepend-inner-icon="mdi-magnify"
flat
class="mx-1"
:loading="loading"
@@ -101,15 +103,17 @@ onMounted(() => {
@keydown.enter="searchMedias"
/>
</VToolbar>
<VList
v-if="items.length > 0"
lines="three"
>
<DialogCloseBtn
@click="
() => {
emit('close')
}
"
/>
<VDivider />
<VList v-if="items.length > 0" lines="three">
<template v-for="(item, i) in items" :key="i">
<VListItem
@click="selectMedia(item)"
>
<VListItem @click="selectMedia(item)">
<template #prepend>
<VImg
height="75"
@@ -131,7 +135,6 @@ onMounted(() => {
</VListItemTitle>
<VListItemSubtitle class="mt-2" v-html="item.overview" />
</VListItem>
<VDivider v-if="i < items.length - 1" class="mt-1" inset />
</template>
</VList>
</VCard>

View File

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

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { RenderProps } from '@/api/types'
import { type PropType } from 'vue'
// 输入参数
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
})
</script>
<template>
<Component :is="elementProps.config?.component" v-if="!elementProps.config?.html" v-bind="elementProps.config?.props">
{{ elementProps.config?.text }}
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
<slot :name="name" v-bind="_props">
<DashboardRender v-for="(slotItem, slotIndex) in content || []" :key="slotIndex" :config="slotItem" />
</slot>
</template>
<DashboardRender
v-for="(innerItem, innerIndex) in elementProps.config?.content || []"
:key="innerIndex"
:config="innerItem"
/>
</Component>
<Component
:is="elementProps.config?.component"
v-if="elementProps.config?.html"
v-bind="elementProps.config?.props"
v-html="elementProps.config?.html"
/>
</template>

View File

@@ -1,15 +1,7 @@
<script lang="ts" setup>
import { RenderProps } from '@/api/types'
import { type PropType, ref } from 'vue'
// 组件接口
interface RenderProps {
component: string
text: string
html: string
content?: any
props?: any
}
// 输入参数
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
@@ -17,13 +9,15 @@ const elementProps = defineProps({
})
// 配置元素
const formItem = ref<RenderProps>(elementProps.config ?? {
component: 'div',
text: '',
html: '',
props: {},
content: [],
})
const formItem = ref<RenderProps>(
elementProps.config ?? {
component: 'div',
text: '',
html: '',
props: {},
content: [],
},
)
// 配置数据
const formData = ref<any>(elementProps.form || {})
@@ -32,23 +26,32 @@ const formData = ref<any>(elementProps.form || {})
<template>
<Component
:is="formItem.component"
v-if="!formItem.html"
v-if="!formItem.html && !!formItem.props?.modelvalue"
v-bind="formItem.props"
v-model="formData[formItem.props?.model || '']"
v-model:value="formData[formItem.props?.modelvalue]"
>
{{ formItem.text }}
<FormRender
v-for="(innerItem, innerIndex) in (formItem.content || [])"
:key="innerIndex"
v-model="formData[innerItem.props?.model || '']"
:config="innerItem"
:form="formData"
/>
<template v-for="(innerItem, innerIndex) in formItem.content || []" :key="innerIndex">
<FormRender
v-if="!!innerItem.props?.modelvalue"
v-model:value="formData[innerItem.props?.modelvalue]"
:config="innerItem"
:form="formData"
/>
<FormRender v-else v-model="formData[innerItem.props?.model]" :config="innerItem" :form="formData" />
</template>
</Component>
<Component :is="formItem.component" v-else-if="formItem.html" v-bind="formItem.props" v-html="formItem.html" />
<Component :is="formItem.component" v-else v-bind="formItem.props" v-model="formData[formItem.props?.model]">
{{ formItem.text }}
<template v-for="(innerItem, innerIndex) in formItem.content || []" :key="innerIndex">
<FormRender
v-if="!!innerItem.props?.modelvalue"
v-model:value="formData[innerItem.props?.modelvalue]"
:config="innerItem"
:form="formData"
/>
<FormRender v-else v-model="formData[innerItem.props?.model]" :config="innerItem" :form="formData" />
</template>
</Component>
<Component
:is="formItem.component"
v-if="formItem.html"
v-bind="formItem.props"
v-html="formItem.html"
/>
</template>

View File

@@ -1,47 +1,90 @@
<script lang="ts" setup>
import { type PropType, ref } from 'vue'
import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { type PropType } from 'vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import { RenderProps } from '@/api/types'
// 组件接口
interface RenderProps {
component: string
text: string
html: string
content?: any
props?: any
}
// 定议外部事件
const emit = defineEmits(['action'])
// 输入参数
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
})
// 配置元素
const formItem = ref<RenderProps>(elementProps.config ?? {
component: 'div',
text: '',
html: '',
props: {},
content: [],
// 进度框
const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在处理...')
// 元素API事件响应
async function commonAction(api_path: string, method: string, params = {}) {
if (!api_path || !method) return
progressDialog.value = true
try {
if (method.toUpperCase() === 'GET') {
await api.get(api_path, {
params: params,
})
} else {
await api.post(api_path, params)
}
emit('action')
} catch (error) {
console.error(error)
}
progressDialog.value = false
}
// 组装事件
let componentEvents = reactive<{ [key: string]: any }>({})
watchEffect(() => {
if (!isNullOrEmptyObject(elementProps.config?.events)) {
for (const key in elementProps.config?.events) {
const attr = elementProps.config?.events[key]
const func = async () => {
await commonAction(attr['api'], attr['method'], attr['params'])
}
componentEvents[key] = func
}
}
})
</script>
<template>
<Component
:is="formItem.component"
v-if="!formItem.html"
v-bind="formItem.props"
:is="elementProps.config?.component"
v-if="!elementProps.config?.html"
v-bind="elementProps.config?.props"
v-on="componentEvents"
>
{{ formItem.text }}
{{ elementProps.config?.text }}
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
<slot :name="name" v-bind="_props">
<PageRender
v-for="(slotItem, slotIndex) in content || []"
:key="slotIndex"
:config="slotItem"
@action="emit('action')"
/>
</slot>
</template>
<PageRender
v-for="(innerItem, innerIndex) in (formItem.content || [])"
v-for="(innerItem, innerIndex) in elementProps.config?.content || []"
:key="innerIndex"
:config="innerItem"
@action="emit('action')"
/>
</Component>
<Component
:is="formItem.component"
v-if="formItem.html"
v-bind="formItem.props"
v-html="formItem.html"
:is="elementProps.config?.component"
v-if="elementProps.config?.html"
v-bind="elementProps.config?.props"
v-html="elementProps.config?.html"
v-on="componentEvents"
/>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</template>

View File

@@ -1,11 +1,9 @@
<script lang="ts" setup>
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
import { useDisplay } from 'vuetify'
// 输入参数
const props = defineProps({
linkurl: String,
title: String,
})
// 显示器宽度
const display = useDisplay()
// 元素
const slideview_content = ref()
@@ -33,12 +31,10 @@ function slideNext(next: boolean) {
if (run_to_left_px >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth)
run_to_left_px = slideview_content.value.scrollWidth - slideview_content.value.clientWidth
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
}
else {
} else {
const card_index = card_current - card_max
run_to_left_px = card_index * card_width
if (run_to_left_px <= 0)
run_to_left_px = 0
if (run_to_left_px <= 0) run_to_left_px = 0
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
}
slideview_content.value.scrollTo({
@@ -52,7 +48,7 @@ function slideNext(next: boolean) {
function countMaxNumber() {
slide_card_length = slideview_content.value.children.length
card_width = slideview_content.value.firstElementChild.getBoundingClientRect().width
slide_gap_px = (slideview_content.value.scrollWidth / slide_card_length) - card_width
slide_gap_px = slideview_content.value.scrollWidth / slide_card_length - card_width
card_width += slide_gap_px
card_max = Math.trunc(slideview_content.value.clientWidth / card_width)
countDisabled()
@@ -61,16 +57,18 @@ function countMaxNumber() {
// 修改分页切换按钮状态
function countDisabled() {
slideview_scrollLeft.value = slideview_content.value.scrollLeft
card_current = slideview_content.value.scrollLeft === 0 ? 0 : Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
if (slide_card_length * card_width <= slideview_content.value.clientWidth)
disabled.value = 3
else if (slideview_content.value.scrollLeft === 0)
disabled.value = 0
else if (slideview_content.value.scrollLeft >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth - 2)
card_current =
slideview_content.value.scrollLeft === 0
? 0
: Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
if (slide_card_length * card_width <= slideview_content.value.clientWidth) disabled.value = 3
else if (slideview_content.value.scrollLeft === 0) disabled.value = 0
else if (
slideview_content.value.scrollLeft >=
slideview_content.value.scrollWidth - slideview_content.value.clientWidth - 2
)
disabled.value = 2
else
disabled.value = 1
else disabled.value = 1
}
// 组件加载完成
@@ -95,9 +93,9 @@ onActivated(() => {
<template>
<div class="flex justify-between mt-3">
<slot name="title">
<SlideViewTitle v-bind="props" />
<SlideViewTitle />
</slot>
<div v-if="disabled !== 3" class="me-1 d-none d-md-flex">
<div v-if="disabled !== 3 && display.mdAndUp.value" class="me-1 d-flex">
<VBtn
class="rounded-circle"
variant="text"
@@ -128,9 +126,8 @@ onActivated(() => {
<style lang="scss" scoped>
.slideview_content {
overflow: scroll hidden !important;
-ms-overflow-style: none !important;
overflow-x: scroll !important;
overflow-y: hidden !important;
overscroll-behavior-x: contain !important;
scrollbar-width: none !important;
}

View File

@@ -1,24 +1,13 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
linkurl: String,
title: String,
})
const props: any = inject('rankingPropsKey')
</script>
<template>
<div
class="ms-1"
>
<RouterLink
:to="props.linkurl ? props.linkurl : ''"
class="slider-title"
>
<span>{{ props.title }}</span>
<VIcon
icon="mdi-arrow-right-circle-outline"
class="ms-1"
/>
<div class="ms-1">
<RouterLink :to="props?.linkurl ? props?.linkurl : ''" class="slider-title">
<span>{{ props?.title }}</span>
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
</RouterLink>
</div>
</template>

View File

@@ -0,0 +1,206 @@
<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} ?`,
})
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

@@ -2,17 +2,34 @@
import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue'
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import UserNofification from '@/layouts/components/UserNotification.vue'
import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import store from '@/store'
import { SystemNavMenus } from '@/router/menu'
import { NavMenu } from '@/@layouts/types'
import { useDisplay } from 'vuetify'
const display = useDisplay()
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
// 根据分类获取菜单列表
const getMenuList = (header: string) => {
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (!item.admin || superUser))
}
// 返回上一页
function goBack() {
history.back()
}
</script>
<template>
@@ -21,126 +38,51 @@ const superUser = store.state.auth.superUser
<template #navbar="{ toggleVerticalOverlayNavActive }">
<div class="d-flex h-100 align-center mx-1">
<!-- 👉 Vertical Nav Toggle -->
<IconBtn
class="ms-n2 d-lg-none"
@click="toggleVerticalOverlayNavActive(true)"
>
<IconBtn v-if="!appMode && display.mdAndDown.value" class="ms-n2" @click="toggleVerticalOverlayNavActive(true)">
<VIcon icon="mdi-menu" />
</IconBtn>
<!-- 👉 Back Button -->
<IconBtn v-if="appMode" class="ms-n2" @click="goBack">
<VIcon icon="mdi-arrow-left" size="32" />
</IconBtn>
<!-- 👉 Search Bar -->
<SearchBar />
<!-- 👉 Spacer -->
<VSpacer />
<!-- 👉 Github -->
<IconBtn
class="me-2"
href="https://github.com/jxxghp/MoviePilot"
target="_blank"
rel="noopener noreferrer"
>
<VIcon icon="mdi-github" />
</IconBtn>
<!-- 👉 Shortcuts -->
<ShortcutBar />
<ShortcutBar v-if="superUser" />
<!-- 👉 Theme -->
<NavbarThemeSwitcher class="me-2" />
<NavbarThemeSwitcher />
<!-- 👉 Notification -->
<UserNofification />
<!-- 👉 UserProfile -->
<UserProfile />
</div>
</template>
<template #vertical-nav-content>
<VerticalNavLink
:item="{
title: '仪表板',
icon: 'mdi-home-outline',
to: '/dashboard',
}"
/>
<VerticalNavLink v-for="item in getMenuList('开始')" :item="item" />
<!-- 👉 发现 -->
<VerticalNavSectionTitle
:item="{
heading: '发现',
}"
/>
<VerticalNavLink
:item="{
title: '推荐',
icon: 'mdi-table-star',
to: '/ranking',
}"
/>
<VerticalNavLink
:item="{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
}"
/>
<VerticalNavLink v-for="item in getMenuList('发现')" :item="item" />
<!-- 👉 订阅 -->
<VerticalNavSectionTitle
:item="{
heading: '订阅',
}"
/>
<VerticalNavLink
:item="{
title: '电影',
icon: 'mdi-movie-check-outline',
to: '/subscribe-movie',
}"
/>
<VerticalNavLink
:item="{
title: '电视剧',
icon: 'mdi-television-classic',
to: '/subscribe-tv',
}"
/>
<VerticalNavLink
:item="{
title: '日历',
icon: 'mdi-calendar',
to: '/calendar',
}"
/>
<VerticalNavLink v-for="item in getMenuList('订阅')" :item="item" />
<!-- 👉 整理 -->
<VerticalNavSectionTitle
:item="{
heading: '整理',
}"
/>
<VerticalNavLink
:item="{
title: '正在下载',
icon: 'mdi-download-outline',
to: '/downloading',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '历史记录',
icon: 'mdi-history',
to: '/history',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
to: '/filemanager',
}"
/>
<VerticalNavLink v-for="item in getMenuList('整理')" :item="item" />
<!-- 👉 系统 -->
<VerticalNavSectionTitle
v-if="superUser"
@@ -148,37 +90,12 @@ const superUser = store.state.auth.superUser
heading: '系统',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '插件',
icon: 'mdi-apps',
to: '/plugins',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '站点管理',
icon: 'mdi-web',
to: '/site',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '设定',
icon: 'mdi-cog',
to: '/setting',
}"
/>
<VerticalNavLink v-for="item in getMenuList('系统')" :item="item" />
</template>
<template #after-vertical-nav-items />
<!-- 👉 Pages -->
<slot />
<!-- 👉 Footer -->
<template #footer>
<Footer />

View File

@@ -1,3 +1,69 @@
<script setup lang="ts">
import { useDisplay } from 'vuetify'
const display = useDisplay()
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
const route = useRoute()
// 各按钮活动状态
const activeState = computed(() => {
return {
home: route.path === '/dashboard',
ranking: route.path === '/ranking',
movie: route.path === '/subscribe/movie',
tv: route.path === '/subscribe/tv',
apps: route.path === '/apps',
}
})
</script>
<template>
<div class="h-100 d-flex align-center justify-space-between" />
<div v-if="appMode" class="w-100" style="block-size: calc(3.5rem + env(safe-area-inset-bottom))">
<VBottomNavigation
grow
horizontal
color="primary"
class="footer-nav border-t"
style="block-size: calc(3.5rem + env(safe-area-inset-bottom))"
>
<VBtn to="/dashboard" :ripple="false">
<VIcon v-if="activeState.home" size="28">mdi-home</VIcon>
<VIcon v-else size="28">mdi-home-outline</VIcon>
</VBtn>
<VBtn to="/ranking" :ripple="false">
<VIcon v-if="activeState.ranking" size="28">mdi-star</VIcon>
<VIcon v-else size="28">mdi-star-outline</VIcon>
</VBtn>
<VBtn to="/subscribe/movie" :ripple="false">
<VIcon v-if="activeState.movie" size="28">mdi-movie-open</VIcon>
<VIcon v-else size="28">mdi-movie-open-outline</VIcon>
</VBtn>
<VBtn to="/subscribe/tv" :ripple="false">
<VIcon v-if="activeState.tv" size="28">mdi-television-play</VIcon>
<VIcon v-else size="28">mdi-television</VIcon>
</VBtn>
<VBtn to="/apps" :ripple="false">
<VIcon v-if="activeState.apps" size="28">mdi-dots-horizontal-circle</VIcon>
<VIcon v-else size="28">mdi-dots-horizontal</VIcon>
</VBtn>
</VBottomNavigation>
</div>
</template>
<style lang="scss">
.footer-nav {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.8);
padding-block-end: env(safe-area-inset-bottom);
}
.footer-nav .v-btn--variant-text .v-btn__overlay {
background-color: transparent !important;
}
</style>
}

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