Compare commits

...

394 Commits

Author SHA1 Message Date
jxxghp
4f238dc1a3 fix: 移除 AliyunAuthDialog 和 U115AuthDialog 中的 refreshToken 相关代码 2025-03-25 12:57:42 +08:00
jxxghp
d4777fde70 fix: 移除新建文件夹对话框的 v-if 条件 2025-03-24 13:32:30 +08:00
jxxghp
b6c823c386 chore: 更新版本号至2.3.5 2025-03-23 16:39:56 +08:00
jxxghp
b7488214fc feat: 添加全选/全不选功能及按钮文本更新 2025-03-23 12:28:30 +08:00
jxxghp
06b6c3f3cb fix: 调整对话框最大宽度至45rem 2025-03-22 14:11:27 +08:00
jxxghp
abfaf926c4 fix #305 2025-03-22 10:03:11 +08:00
jxxghp
6eabeb09c9 fix #293 2025-03-22 09:53:21 +08:00
jxxghp
a15afabfa7 fix #310 2025-03-22 09:27:27 +08:00
jxxghp
30276d5022 fixhttps://github.com/jxxghp/MoviePilot/issues/4002 2025-03-22 08:08:50 +08:00
jxxghp
683ddc3fce fix: 更新二维码获取逻辑,修复定时器设置位置并优化提示信息 2025-03-21 20:32:05 +08:00
jxxghp
f00f79279b Merge pull request #309 from Aqr-K/fix/settings 2025-03-21 09:55:11 +08:00
Aqr-K
7989965b1a fix: VTextarea no longer displays all rows 2025-03-16 22:19:52 +08:00
jxxghp
5b84ce307b fix: 移除对话框的 persistent 属性 2025-03-15 11:52:16 +08:00
jxxghp
d13264b10e Merge pull request #307 from Aqr-K/fix/types 2025-03-10 19:13:34 +08:00
Aqr-K
29a1c4ae35 fix: 增加 @types/mousetrap 修复 mousetrap 类型缺失警告 2025-03-10 18:59:16 +08:00
jxxghp
9ac15e530a feat: 更新工作流操作对话框,移除节点和连接线的删除逻辑,改为使用删除键处理 2025-03-10 11:03:02 +08:00
jxxghp
d4b446280a feat: 优化媒体服务器播放列表的添加逻辑,避免重复项 2025-03-10 10:43:53 +08:00
jxxghp
4593898549 feat: 优化媒体服务器库和播放列表的添加逻辑,避免重复项 2025-03-10 10:42:12 +08:00
jxxghp
c030d1a309 feat: 在保存通知发送时间后添加系统重载功能 2025-03-10 09:02:16 +08:00
jxxghp
fd71e471b2 feat: 优化发现页面标签页的激活逻辑并初始化选中标签 2025-03-10 08:55:10 +08:00
jxxghp
bc245e0a7a feat: 在发现页面激活时添加排序订阅顺序功能 2025-03-10 08:22:28 +08:00
jxxghp
8236461c37 feat: 根据路由 meta 动态调整 footer 高度 2025-03-10 08:15:24 +08:00
jxxghp
e1e8344764 feat: 注册 Pinia 状态管理并提供全局设置 2025-03-10 08:08:52 +08:00
jxxghp
14398e083e 更新 PathField.vue 2025-03-10 07:48:40 +08:00
jxxghp
f36fe075ce chore: 更新版本号至 2.3.4 2025-03-09 21:44:15 +08:00
jxxghp
25cf9d7fce feat: 根据路由 meta 决定是否显示 footer 2025-03-09 21:43:52 +08:00
jxxghp
9355788221 feat: 添加组件激活时的数据加载功能 2025-03-09 19:43:34 +08:00
jxxghp
64042b51e9 feat: 通和发送时间设置 2025-03-09 18:52:05 +08:00
jxxghp
7145af48ad fix: 优化发现标签页的拖拽图标显示 2025-03-08 18:37:19 +08:00
jxxghp
ddb5468656 feat: 添加可拖拽的发现标签页并实现顺序保存功能 2025-03-08 16:12:21 +08:00
jxxghp
793cdd8f4c feat: 添加进度框以显示系统配置重载状态 2025-03-08 08:07:04 +08:00
jxxghp
faafbb59c6 Merge pull request #304 from Aqr-K/fix/workflow 2025-03-05 06:43:46 +08:00
Aqr-K
cd0ea07c2f fix: 修复创建同类型的节点时,数据未隔离的问题 2025-03-05 04:28:50 +08:00
Aqr-K
f6e3807a3d fix: 完善连接 workflow 节点时的合法性校验 2025-03-05 04:24:54 +08:00
jxxghp
fc36496aee chore: 更新版本号至 2.3.3 2025-03-02 12:32:15 +08:00
jxxghp
1c8881d7a4 feat: 添加重置任务功能 2025-03-02 12:27:58 +08:00
jxxghp
f6e8aacd0f feat: 优化任务执行功能,添加继续执行和重新开始选项;移除媒体过滤器中的类别选择 2025-03-02 11:17:28 +08:00
jxxghp
79ddc39492 feat: 添加标签输入框,优化下载器和 RSS 获取操作的界面布局 2025-03-02 09:45:44 +08:00
jxxghp
e63c5fb8e5 feat: 更新发送事件和发送消息的副标题,明确任务执行内容 2025-03-01 18:26:32 +08:00
jxxghp
695f4827fd chore: 更新版本号至 2.3.2 2025-03-01 15:38:39 +08:00
jxxghp
5a8b183c0f feat: 添加来源类型下拉框,优化媒体获取操作界面 2025-03-01 14:07:07 +08:00
jxxghp
2845a889ed feat: 修复导入工作流代码时的 JSON 解析问题 2025-02-28 22:08:49 +08:00
jxxghp
6333103050 feat: 为工作流组件添加外层 div 包裹,优化布局结构 2025-02-28 22:05:48 +08:00
jxxghp
cb6be91538 feat: 添加扫描目录功能,支持将目录文件扫描到队列 2025-02-28 21:11:21 +08:00
jxxghp
8cdd4b4af5 feat: 修改任务执行成功提示信息,增加延迟刷新 2025-02-28 19:03:00 +08:00
jxxghp
f4ec2029d9 feat: 移除工作流任务卡片的禁用状态 2025-02-28 18:18:24 +08:00
jxxghp
b84b0f229f feat: 添加搜索方式下拉框,优化工作流操作对话框布局 2025-02-28 18:13:13 +08:00
jxxghp
ef6a01a32f feat: 调整导航栏底部高度,禁用卡片点击涟漪效果 2025-02-28 13:02:38 +08:00
jxxghp
b451b8066a feat: 添加仅下载缺失资源的开关选项 2025-02-28 12:15:38 +08:00
jxxghp
57efd516c5 feat: 优化工作流任务卡片和列表视图的布局 2025-02-28 11:19:07 +08:00
jxxghp
d5979e6bf3 feat: 修复进度计算逻辑,添加加载状态和禁用功能 2025-02-27 20:39:22 +08:00
jxxghp
d75970cb2a feat: 更新工作流任务卡片 2025-02-27 20:15:24 +08:00
jxxghp
ad4bb07cd7 feat: 更新工作流组件,优化界面布局,添加消息和媒体过滤功能 2025-02-27 18:56:05 +08:00
jxxghp
9c558c3625 feat: 添加工作流组件的边缘处理和循环执行功能,优化订阅和RSS获取操作 2025-02-27 17:09:01 +08:00
jxxghp
b467bb6c56 feat: 重构工作流组件,动态加载节点类型,移除旧的侧边栏和背景组件 2025-02-27 13:55:06 +08:00
jxxghp
5cd021ea85 feat: 优化插件弹窗加载速度 2025-02-27 12:44:39 +08:00
jxxghp
3d64382c9b feat: 更新拖放功能,重构状态管理,优化工作流组件,添加节点和边的确认删除功能 2025-02-26 21:11:24 +08:00
jxxghp
6d5d4354d9 feat: 重构工作流对话框,合并添加和编辑功能,新增流程操作对话框 2025-02-26 19:07:00 +08:00
jxxghp
1b43446b5c feat: 添加自动刷新功能,每30秒更新工作流数据 2025-02-26 18:24:38 +08:00
jxxghp
7a9984f392 feat: 添加已完成动作数计算和优化工作流列表视图 2025-02-26 18:09:11 +08:00
jxxghp
3c6fbfb106 feat: 添加工作流任务卡片组件,支持编辑、删除和执行功能 2025-02-26 13:58:52 +08:00
jxxghp
bab46964ff feat: 优化用户界面和交互提示 2025-02-25 21:06:43 +08:00
jxxghp
661919f27a feat: 优化用户界面和交互提示 2025-02-25 20:52:43 +08:00
jxxghp
f3a03349b4 feat: 添加工作流新增对话框和编辑功能,优化工作流列表视图 2025-02-25 17:28:09 +08:00
jxxghp
29791bf986 fix #303 2025-02-25 08:35:28 +08:00
jxxghp
a06f0f29c6 Merge pull request #303 from Aqr-K/build/reduce-size 2025-02-25 06:57:48 +08:00
Aqr-K
b426d94180 fix: relevant settings of pinia and lodash-es 2025-02-24 19:50:47 +08:00
Aqr-K
5618d87e58 refactor: replace lodash with lodash-es 2025-02-24 19:49:10 +08:00
Aqr-K
721d4f7685 refactor: replace Vuex with Pinia 2025-02-24 19:26:56 +08:00
jxxghp
7a025bcd38 feat(Workflow): add modules 2025-02-23 13:16:01 +08:00
jxxghp
24a8125621 feat(package): 添加 @vue-flow/core 依赖及相关更新 2025-02-23 12:04:20 +08:00
jxxghp
468584a906 Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2025-02-22 12:11:42 +08:00
jxxghp
c056ec9377 feat(Workflow): 添加工作流功能,包含工作流列表和相关接口定义 2025-02-22 12:11:38 +08:00
jxxghp
87239994ae feat(SiteCard): 优化按钮位置 2025-02-21 13:10:48 +08:00
jxxghp
da09860a53 fix(AccountSettingRule): 更新删除按钮图标为mdi-delete-empty-outline 2025-02-21 13:02:12 +08:00
jxxghp
195ee5b2a6 feat(AccountSetting): 重构规则验证逻辑 2025-02-21 12:54:32 +08:00
jxxghp
32621ee299 feat(Search): 添加站点搜索功能,支持选择和过滤 2025-02-21 10:12:16 +08:00
jxxghp
40645180a0 fix(styles): 修复滚动阻止时的样式问题 2025-02-21 09:29:10 +08:00
jxxghp
59d4b1e544 feat(SearchBar): 添加站点搜索功能,支持多选和过滤 2025-02-20 13:03:37 +08:00
jxxghp
8962a2c4ac fix 2025-02-20 11:23:46 +08:00
jxxghp
6955f35ad1 chore(package): 更新版本号至 2.3.0 2025-02-18 16:49:37 +08:00
jxxghp
1f722e7d7f fix(styles): 移除数据表页脚的上边距 2025-02-18 16:44:30 +08:00
jxxghp
5e587dfd88 feat(SiteResource): 添加站点资源分类功能并优化资源查询逻辑 2025-02-18 16:27:31 +08:00
jxxghp
2c687e5648 feat(UserProfile): 添加功能视图菜单 2025-02-18 13:39:54 +08:00
jxxghp
fdb0f63283 fix(AppCenter): 修改菜单列表过滤逻辑以优化用户权限管理 2025-02-18 12:57:43 +08:00
jxxghp
002e675b47 fix(SiteCard): 修改站点卡片的点击事件和菜单项文本以优化用户体验 2025-02-18 08:34:54 +08:00
jxxghp
114f2a2dd0 fix(Footer): 优化更多菜单的激活状态计算逻辑 2025-02-18 08:12:16 +08:00
jxxghp
c314d49e11 feat(Footer): 使用 VMenu 组件替代 VBottomSheet 以优化更多菜单的展示 2025-02-18 07:56:11 +08:00
jxxghp
f5d0556808 fix(Footer): 修改对话框内容类以调整样式;添加隐藏滚动条的样式混合 2025-02-17 20:54:54 +08:00
jxxghp
27bc2a488f feat(SiteResourceDialog): 改进对话框布局 2025-02-17 20:41:52 +08:00
jxxghp
3a5999c341 fix(SubscribeShareCard): 添加 h-full 类以优化卡片布局 2025-02-16 16:15:25 +08:00
jxxghp
a80a099ee7 fix(Footer): 移除底部导航的 active 属性 2025-02-15 13:04:33 +08:00
jxxghp
68f458738a feat(Footer): 优化底部导航样式 2025-02-15 12:58:13 +08:00
jxxghp
0f08f69738 fix(MediaCard): 修改按钮大小并优化底部填充样式 2025-02-15 08:50:05 +08:00
jxxghp
a664066465 Merge pull request #301 from InfinityPacer/v2 2025-02-15 08:11:01 +08:00
jxxghp
e6c11665a5 更新 package.json 版本号至 2.2.9 2025-02-14 19:33:52 +08:00
InfinityPacer
c119384c22 fix(settings): remove invalid githubMirrorsItems 2025-02-14 14:19:48 +08:00
jxxghp
787cccb89f feat:在多个组件中添加onActivated钩子以优化数据加载逻辑 2025-02-11 17:15:55 +08:00
jxxghp
3df5d75c46 更新 package.json 2025-02-10 22:26:12 +08:00
jxxghp
de6ad2479e feat:为DiscoverSource接口添加依赖关系字典,优化过滤参数的watch逻辑 2025-02-10 22:05:33 +08:00
jxxghp
632dfbaf10 feat:优化TheMovieDbView组件的watch逻辑 2025-02-10 16:46:43 +08:00
jxxghp
68c14c24b8 feat:为TMDB排序和风格字典添加类型定义,优化过滤参数的逻辑,确保参数有效性 2025-02-09 22:21:43 +08:00
jxxghp
d343d6d54d feat:优化TheMovieDbView组件的watch逻辑,分离类型和过滤参数的监听,确保列表刷新更高效 2025-02-09 22:00:38 +08:00
jxxghp
391a160f97 更新 package.json 2025-02-09 11:59:53 +08:00
jxxghp
2d95110f75 feat:优化推荐页面,修复MediaCardSlideView的key绑定,使用电影标题作为唯一标识 2025-02-09 11:57:46 +08:00
jxxghp
e2ced8d36d feat:更新TheMovieDbView组件,添加电影和电视剧风格字典,优化类型过滤逻辑 2025-02-09 11:55:40 +08:00
jxxghp
a2b4511602 feat:优化FormRender组件的属性解析逻辑,支持动态表达式绑定 2025-02-09 11:41:35 +08:00
jxxghp
bdccc71b64 feat:优化FormRender组件的事件处理逻辑,支持动态函数绑定 2025-02-09 11:28:28 +08:00
jxxghp
d7038a7d18 feat:优化FormRender组件,增强v-model和v-show支持,改进属性绑定逻辑;在TheMovieDbView中添加儿童类别 2025-02-09 11:21:46 +08:00
jxxghp
3998e1f685 Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2025-02-08 21:48:03 +08:00
jxxghp
5def9d5f81 feat:重构推荐页面,添加推荐数据源接口并更新路由和视图 2025-02-08 21:47:57 +08:00
jxxghp
c62937371e 更新 package.json 2025-02-08 20:19:12 +08:00
jxxghp
52843dcf97 feat:在排名页面中添加TMDB和豆瓣的热门电影及电视剧链接 2025-02-08 20:14:45 +08:00
jxxghp
ef5680d5ad feat:在ExtraSourceView中添加默认过滤参数支持,确保过滤条件的完整性 2025-02-08 12:53:26 +08:00
jxxghp
bd3f24c84b feat:添加媒体季信息接口,更新相关组件以支持季信息 2025-02-08 12:46:36 +08:00
jxxghp
399f85c52e chore:更新版本号至2.2.7-1 2025-02-07 18:21:13 +08:00
jxxghp
14430e5c89 feat:为选中的v-chip添加自定义颜色样式 2025-02-07 18:09:40 +08:00
jxxghp
b703757d28 feat:添加评分格式化功能,优化媒体卡片中的评分显示 2025-02-07 17:00:35 +08:00
jxxghp
b642eabbb3 feat:在媒体相关组件中添加媒体ID、标题和年份的支持 2025-02-06 20:33:14 +08:00
jxxghp
673596d8f9 feat:在媒体信息中添加媒体ID前缀和媒体ID 2025-02-06 19:21:02 +08:00
jxxghp
b14e927e6c feat:支持探索扩展 2025-02-06 18:04:49 +08:00
jxxghp
b03ae41ac7 feat:在index.html中添加初始加载背景样式 2025-02-06 16:26:10 +08:00
jxxghp
92a0a9fe2f feat:重构主题存储逻辑,优化加载背景和颜色设置 2025-02-06 16:00:32 +08:00
jxxghp
2511acfea1 feat:优化加载背景样式 2025-02-06 13:48:03 +08:00
jxxghp
361a4e0414 feat:优化分组逻辑,使用元信息增强分组键 2025-02-06 11:42:32 +08:00
jxxghp
7e310236fe feat:在转移历史视图中添加分组功能 2025-02-06 10:38:28 +08:00
jxxghp
8705606c70 更新 package.json 2025-02-06 08:39:54 +08:00
jxxghp
1f812a5258 feat:重构过滤选项逻辑,优化过滤表单和排序功能 2025-02-06 08:30:27 +08:00
jxxghp
e9264fa472 feat:小屏搜索结果列表模式增加过滤按钮 2025-02-05 17:40:57 +08:00
jxxghp
9164a1aefc fix #295 2025-02-04 09:59:05 +08:00
jxxghp
30351a02ee 升级版本号 2025-01-31 08:00:53 +08:00
jxxghp
7f918408a6 优化加载界面的样式,调整HTML和CSS以改善用户体验 2025-01-31 08:00:30 +08:00
jxxghp
82f69bcad0 修复在小屏幕下的返回按钮显示逻辑 2025-01-30 21:18:02 +08:00
jxxghp
83b25eabbb 优化对话框组件的样式和属性设置 2025-01-29 19:21:57 +08:00
jxxghp
47da6db51a 为filterParams添加默认排序选项 2025-01-29 19:07:43 +08:00
jxxghp
eee092a7fd fix #292 2025-01-29 18:57:22 +08:00
jxxghp
4c0f65fcbc fix https://github.com/jxxghp/MoviePilot/issues/3823 2025-01-29 18:51:06 +08:00
jxxghp
acbd979569 bangumi添加年份过滤选项 2025-01-28 09:23:45 +08:00
jxxghp
52b68c18bf 优化标签显示效果 2025-01-28 09:11:52 +08:00
jxxghp
c6a74a75da build 2025-01-28 08:23:28 +08:00
jxxghp
e39eb62f52 调整资源页面导入路径,修正TorrentCardListView和TorrentRowListView组件的引用 2025-01-28 08:22:44 +08:00
jxxghp
4ecec4865d 优化过滤选项,简化组件结构,添加评分滑块功能 2025-01-28 08:20:41 +08:00
jxxghp
589007a22a 优化主题设置逻辑,简化代码结构 2025-01-28 07:32:45 +08:00
jxxghp
4d4c9516c6 重构发现页面,添加豆瓣和TheMovieDb过滤选项,优化媒体卡组件 2025-01-27 21:08:52 +08:00
jxxghp
8491f26617 更新菜单项 2025-01-27 18:37:58 +08:00
jxxghp
fcb3768a76 更新菜单项图标,添加豆瓣和TheMovieDb的SVG和PNG图标 2025-01-27 18:16:57 +08:00
jxxghp
966bb769df 更新浏览和发现页面,重构相关组件,调整路由和菜单项 2025-01-27 18:05:02 +08:00
jxxghp
dc8f7caab0 更新 menu.ts 2025-01-27 12:25:42 +08:00
jxxghp
683346d652 添加发现页面及相关路由和菜单项 2025-01-27 11:25:43 +08:00
jxxghp
f5fe39b2d2 更新 App.vue 2025-01-26 22:28:30 +08:00
jxxghp
51beb53f51 更新 index.html 2025-01-26 22:27:41 +08:00
jxxghp
9d3f03c83a 更新 index.html 2025-01-26 22:14:10 +08:00
jxxghp
3eda1e4ef7 更新 index.html 2025-01-26 22:12:42 +08:00
jxxghp
7181f83d66 更新 Footer.vue 2025-01-26 22:10:10 +08:00
jxxghp
fffad6e1b8 更新 UserListView.vue 2025-01-26 22:08:32 +08:00
jxxghp
7f3906e5cb 更新 loader.css 2025-01-26 08:59:31 +08:00
jxxghp
f836d175f0 fix(App.vue): 优化页面加载时的背景移除逻辑,增加延迟以确保渲染完成 2025-01-26 08:48:22 +08:00
jxxghp
f49cafc0cc feat: 添加确保渲染完成的函数并优化加载背景移除逻辑 2025-01-26 08:42:09 +08:00
jxxghp
a3ecad3436 feat: 添加刷新状态控制,优化多个视图的显示逻辑 2025-01-25 19:34:39 +08:00
jxxghp
a019dbd44e refactor(SubscribeListView): 移除不必要的 VPullToRefresh 组件,简化订阅列表渲染逻辑 2025-01-25 19:15:02 +08:00
jxxghp
b316f960a1 Merge pull request #291 from InfinityPacer/v2 2025-01-25 07:43:42 +08:00
InfinityPacer
d049b26825 fix(LibraryCard): handle image loading errors with gradient 2025-01-25 03:07:14 +08:00
jxxghp
852579c6ee 更新 ForkSubscribeDialog.vue,调整 VCardSubtitle 的行数限制以改善文本显示 2025-01-23 17:24:17 +08:00
jxxghp
5adcfa1877 更新 ForkSubscribeDialog.vue 2025-01-22 19:10:27 +08:00
jxxghp
f74458629e 为 DashboardRender 组件添加 key 属性以优化渲染性能 2025-01-22 18:58:06 +08:00
jxxghp
798f9249f8 更新 package.json 版本号至 2.2.4 2025-01-22 18:48:28 +08:00
jxxghp
6b4383643f 为 ForkSubscribeDialog 组件添加用户关注功能,并在 DashboardRender 组件中实现组件重渲染 2025-01-22 13:19:09 +08:00
jxxghp
256e8d0452 为 ForkSubscribeDialog 组件的 VDialog 添加 scrollable 属性 2025-01-21 08:28:59 +08:00
jxxghp
4112214c1f 添加豆瓣用户字段并更新账号绑定标题 2025-01-20 18:25:28 +08:00
jxxghp
c183158ffe 更新 package.json 2025-01-20 13:25:49 +08:00
jxxghp
d523790c0f fix ApexCharts 2025-01-19 14:31:51 +08:00
jxxghp
615ce34a72 Merge pull request #288 from wikrin/v2 2025-01-18 07:08:48 +08:00
Attente
1d59b3566c fix: jxxghp/MoviePilot#3747 2025-01-18 02:54:27 +08:00
jxxghp
8071b90a2b 修复滚动阻塞时的样式问题 2025-01-17 19:50:56 +08:00
jxxghp
8966584ca0 优化海报卡片样式和背景渐变 2025-01-17 19:31:15 +08:00
jxxghp
822711a530 refactor:重构路径输入组件 2025-01-17 13:28:31 +08:00
jxxghp
1fe8aeb9e1 优化日志解析性能 2025-01-16 19:51:58 +08:00
jxxghp
f021ba8a98 优化媒体卡片和海报卡片的点击事件处理,改进路由滚动行为,注册中止控制器以管理异步请求 2025-01-16 19:25:51 +08:00
jxxghp
e4af05cd56 添加媒体信息中的合集类型和ID,优化媒体卡片的跳转逻辑,增加搜索系列合集的功能 2025-01-16 17:52:16 +08:00
jxxghp
43d1cdb91c fix 订阅历史对话框 2025-01-16 15:23:37 +08:00
jxxghp
ed3f66681f 更新依赖版本,优化组件和服务工作者的导入,调整 SCSS 混合宏,修复 Vite 配置中的文件缓存大小限制 2025-01-16 15:14:58 +08:00
jxxghp
c718d57e77 优化重启确认对话框的布局 2025-01-16 08:16:51 +08:00
jxxghp
ce2e88a532 优化日志视图,动态设置日志内容的文本颜色 2025-01-15 22:20:58 +08:00
jxxghp
e60015a477 优化日志解析逻辑,添加倒序插入和日志数量限制 2025-01-15 22:03:28 +08:00
jxxghp
761e3ac76d 更新依赖版本,优化日志处理逻辑,添加日志解析功能 2025-01-15 21:43:39 +08:00
jxxghp
2cf5535376 更新 vuetify 和 vite-plugin-vuetify 版本,优化 PathInput 组件的状态管理 2025-01-15 19:24:36 +08:00
jxxghp
1a3d76d7b9 添加重启确认对话框并优化用户配置对话框的布局 2025-01-15 18:39:26 +08:00
jxxghp
942a536289 修改 PluginCard.vue,替换 VCardText 为 VCardItem,以优化组件结构 2025-01-15 17:07:16 +08:00
jxxghp
fb1f6abf2e 更新 main.ts 2025-01-15 16:42:12 +08:00
jxxghp
61ecb421e6 修改加载背景元素的移除逻辑,确保正确清除加载指示器 2025-01-15 15:37:59 +08:00
jxxghp
0098f9db2f 调整 main.ts 文件的导入顺序 2025-01-15 15:28:20 +08:00
jxxghp
2a348a7f18 优化 main.ts 文件的导入顺序,调整样式文件和核心插件的导入位置 2025-01-15 14:58:48 +08:00
jxxghp
838dff4758 优化 Vite 配置,添加运行时缓存策略,确保静态资源和图像的高效加载 2025-01-15 14:25:17 +08:00
jxxghp
7fb78a86ba 优化 main.ts 文件的导入顺序,提升代码可读性;调整 styles.scss 中的样式定位 2025-01-15 14:10:51 +08:00
jxxghp
07c815e943 Merge pull request #287 from wikrin/v2 2025-01-15 13:40:30 +08:00
jxxghp
9a4392eceb 添加分享人唯一ID,支持删除订阅分享功能,并优化相关组件的事件处理 2025-01-15 13:31:50 +08:00
Attente
dc25e457eb fiix(setting): 保存目录后重载模块 2025-01-14 17:31:19 +08:00
jxxghp
d65ed9725c 移除 CronInput 组件中未使用的 api 导入和 FileItem 类型 2025-01-13 13:07:22 +08:00
jxxghp
41ce095505 更新 SubscribeShareCard 和 ForkSubscribeDialog 组件,调整图标颜色,添加搜索词显示,优化识别词的显示行数,并在 ForkSubscribeDialog 中添加复用次数提示 2025-01-13 08:56:51 +08:00
jxxghp
0e2290ce8a 更新 SubscribeShareDialog 组件,添加 share_title 字段的格式化,设置标题为只读,并优化说明提示文本;在确认分享按钮中添加加载状态 2025-01-13 08:16:27 +08:00
jxxghp
1b8db5b7f1 优化 FormRender 组件的渲染逻辑,增强对插槽和内容的支持,简化模板结构 2025-01-12 18:11:39 +08:00
jxxghp
0cb42c1117 更新 FormRender 组件,使用 RenderProps 类型替代原有的 config 类型,并增强渲染逻辑以支持 html 和 text 属性 2025-01-12 18:05:37 +08:00
jxxghp
a289fe3da5 更新 package.json,版本号从 2.2.0 升级至 2.2.1 2025-01-12 17:03:04 +08:00
jxxghp
f53192cfa2 更新 MessageCard 组件,添加对 props.message.action 的检查以优化条件渲染逻辑 2025-01-12 17:02:29 +08:00
jxxghp
235e014542 重构 PluginCard 组件,替换 DynamicRender 为 FormRender;在 ForkSubscribeDialog 组件中添加处理状态以优化用户体验;删除不再使用的 DynamicRender 组件 2025-01-12 16:40:33 +08:00
jxxghp
211b05c643 更新 DynamicRender 组件,添加对 config.text 的支持以增强渲染功能 2025-01-12 16:24:05 +08:00
jxxghp
3e1bd687f1 Merge pull request #286 from InfinityPacer/v2 2025-01-11 21:31:51 +08:00
jxxghp
072fb01a04 更新 CronInput 组件,添加 persistent 属性以优化 VMenu 行为 2025-01-11 20:48:45 +08:00
jxxghp
81fbf4f5ba 更新 CronInput 组件,修改当前 CRON 值的绑定方式,以支持 v-model 绑定 2025-01-11 20:24:06 +08:00
jxxghp
88c86f49bf 重构 DirectoryCard 组件,替换 VPathField 为 PathInput;删除不再使用的 PathField 组件;更新 CronInput 组件以支持 v-model 绑定;添加 CronField 组件以简化 CRON 表达式输入 2025-01-11 20:20:05 +08:00
jxxghp
3023214072 重构 PluginCard 组件,替换 FormRender 为 DynamicRender,优化动态渲染逻辑;删除不再使用的 FormRender 组件 2025-01-11 16:24:16 +08:00
jxxghp
6ea6f89ab2 FIXME 2025-01-11 15:00:23 +08:00
jxxghp
43c6672ab1 fixme 2025-01-11 14:15:52 +08:00
InfinityPacer
5cb56127d5 feat(login): add autocomplete attributes for browser auto-fill 2025-01-11 13:56:01 +08:00
jxxghp
afa333243f 添加 VCronInput 公共组件,用于快速录入CRON表达式 2025-01-11 13:28:46 +08:00
jxxghp
047e99e27c 更新 SubscribeShareDialog.vue:添加分享处理状态,禁用分享按钮以防止重复提交 2025-01-09 16:19:28 +08:00
jxxghp
eef6f37ace 更新 MessageCard.vue:调整卡片宽度,优化文本处理逻辑以支持换行 2025-01-09 12:44:07 +08:00
jxxghp
e8ede6e606 更新设置相关组件:优化错误提示信息,增强用户反馈 2025-01-09 12:29:45 +08:00
jxxghp
bfb4ea4123 更新 package.json 2025-01-09 08:24:57 +08:00
jxxghp
51b0403f64 更新 MessageCard.vue:优化图片显示和文本处理逻辑 2025-01-09 08:22:12 +08:00
jxxghp
a5cd396de6 更新 DirectoryCard.vue 2025-01-07 20:50:12 +08:00
jxxghp
754bc3d3c9 fix(login): improve error messages and update error display component 2025-01-07 09:56:14 +08:00
jxxghp
07a2bcfb97 更新 package.json 2025-01-06 18:01:02 +08:00
jxxghp
20222201ae Merge pull request #284 from InfinityPacer/v2 2025-01-06 17:59:21 +08:00
jxxghp
a2a5ddd66c 升级版本号至 2.1.9 2025-01-06 11:55:29 +08:00
InfinityPacer
7bfc7602a7 fix(log): add LOG_FILE_FORMAT 2025-01-06 02:38:58 +08:00
InfinityPacer
b52b2cedad fix(log): update hint 2025-01-06 02:37:40 +08:00
jxxghp
e93df6ba2c 为通知设置添加“操作用户和管理员”选项 2025-01-05 13:17:46 +08:00
jxxghp
f9f29ccc3c 调整样式以考虑安全区域的上下内边距,优化布局和溢出处理 2025-01-04 14:12:07 +08:00
jxxghp
3bd63ab7c8 为遮罩层添加最小高度并调整溢出样式 2025-01-04 12:23:03 +08:00
jxxghp
301ea445bb 优化样式,合并对话框的边距设置,并为遮罩层添加最小高度 2025-01-04 12:13:03 +08:00
jxxghp
475bee28c6 优化用户头像上传界面,调整布局和样式 2025-01-04 11:52:45 +08:00
jxxghp
cd69920b41 更新 UserAddEditDialog.vue 2025-01-04 11:08:11 +08:00
jxxghp
83aab4e47d 更新 styles.scss 2025-01-04 10:58:45 +08:00
jxxghp
e12093c966 更新 UserAddEditDialog.vue 2025-01-04 10:52:42 +08:00
jxxghp
f21d546d18 更新 UserAddEditDialog.vue 2025-01-04 10:39:34 +08:00
jxxghp
26c8a6ba43 Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2025-01-04 10:23:20 +08:00
jxxghp
827bb8ba69 feat(AccountSettingSystem): 添加备用TMDB API域名选项 2025-01-04 10:23:16 +08:00
jxxghp
d1d2ef37d2 更新 package.json 2025-01-04 10:16:20 +08:00
jxxghp
659594898b refactor(SiteCard): Rename test button text and simplify action button structure 2025-01-04 10:07:01 +08:00
jxxghp
7569401fe0 style(SiteCard): Update card layout and remove inline styles 2025-01-04 09:43:24 +08:00
jxxghp
dc9c86273d Merge pull request #281 from wintsa123/v2 2025-01-03 22:19:26 +08:00
jxxghp
0e816e678a Merge pull request #283 from Aqr-K/feature/log 2025-01-03 13:38:52 +08:00
wintsa
ff1c2a890c remove console 2025-01-03 09:56:06 +08:00
Aqr-K
b802ad8a75 feat(SystemSettings): Add the log setting UI 2025-01-03 06:23:44 +08:00
wintsa
c11fb54b0b remove console 2025-01-03 00:03:45 +08:00
wintsa
856dec3991 忘记把注释去掉了 2025-01-02 15:42:25 +08:00
wintsa
1d8c71da3f 添加取消请求 2025-01-02 15:39:36 +08:00
jxxghp
4152d0f715 更新 package.json 2024-12-31 07:06:18 +08:00
jxxghp
0ead8cc052 fix(PluginCard): 调整插件卡片的 CSS 类以确保高度填充 2024-12-30 19:18:26 +08:00
jxxghp
2b5ecf3f8a fix(PluginCard): 移除悬停显示插件版本和描述的过渡效果以简化组件结构 2024-12-30 19:10:49 +08:00
jxxghp
de7aeeaeb3 fix(UserAuthDialog): 移除 VDialog 的可滚动属性以优化用户体验 2024-12-30 18:21:29 +08:00
jxxghp
994c52f6aa feat(PluginCard): 在插件卡片中添加悬停显示插件版本信息 2024-12-29 20:21:13 +08:00
jxxghp
c6eb744257 feat(PluginAppCard, PluginCard): 添加 VSlideYTransition 以增强插件描述的显示效果 2024-12-29 20:18:30 +08:00
jxxghp
4f462c5cfd feat(PluginAppCard, PluginCard): 添加插件详情弹窗并优化样式 2024-12-29 19:59:14 +08:00
jxxghp
60850970a8 fix(ForkSubscribeDialog): 更新样式,优化文本对齐和可读性 2024-12-29 14:34:17 +08:00
jxxghp
3b2d5e45bb feat: 添加 MediaInfoDialog 组件并更新相关引用 2024-12-29 14:22:27 +08:00
jxxghp
a604d3223a Merge pull request #279 from Aqr-K/fix/search 2024-12-29 07:54:40 +08:00
Aqr-K
00bd1c45a1 fix: bug
- 增加防抖,解决输入打断
2024-12-28 22:34:32 +08:00
jxxghp
4bc6dc7af7 Merge pull request #278 from Aqr-K/fix/search
fix(search): Input method optimization
2024-12-28 14:38:59 +08:00
jxxghp
3a8effd01f fix(FilterRuleGroupCard, TransferQueueDialog): 更新组件结构,替换 VCardText 为 VCardItem,优化显示效果 2024-12-28 12:16:31 +08:00
Aqr-K
da67088e9c 移除无用值 2024-12-28 11:54:50 +08:00
jxxghp
bacd4d23a3 fix(TransferQueueDialog): 更新加载进度逻辑 2024-12-27 18:42:57 +08:00
jxxghp
020f667749 fix(TransferQueueDialog): 更新加载进度处理逻辑 2024-12-27 17:11:00 +08:00
Aqr-K
84652e8c82 更新 SearchBarView.vue 2024-12-27 16:01:30 +08:00
Aqr-K
565ebd936e 更新 TransferHistoryView.vue 2024-12-27 14:30:37 +08:00
Aqr-K
3af127c66f fix(search): Input method optimization
- 合成文字输入法的支持,允许输入到中途进行暂停等操作,而不打断输入触发高频事件
2024-12-27 14:29:21 +08:00
jxxghp
849bb04249 fix(styles): 调整 grid-plugin-card 的列宽,从 18rem 更新为 20rem 2024-12-27 08:01:12 +08:00
jxxghp
09d647877f fix(ReorganizeDialog, TransferHistoryView, AccountSettingSystem, SearchBarView): 更新提示信息,将“历史记录”替换为“整理记录” 2024-12-27 07:56:20 +08:00
jxxghp
c868afbcbf feat(ReorganizeDialog, TransferQueueDialog): 优化错误提示,添加媒体状态颜色和任务计数功能 2024-12-27 07:53:51 +08:00
jxxghp
3f033bfdec 更新 menu.ts 2024-12-26 22:35:24 +08:00
jxxghp
eca2f43e0e 更新 TransferQueueDialog.vue 2024-12-26 22:30:47 +08:00
jxxghp
eeb17040f7 更新 menu.ts 2024-12-26 22:30:00 +08:00
jxxghp
11dee1ed62 更新 package.json 2024-12-26 21:26:28 +08:00
jxxghp
11a6232f83 Merge pull request #277 from Aqr-K/style/site 2024-12-26 17:32:44 +08:00
jxxghp
9eded24e0e Merge pull request #276 from InfinityPacer/v2 2024-12-26 17:31:55 +08:00
Aqr-K
7548882148 style(site): 样式调整 2024-12-26 16:25:32 +08:00
InfinityPacer
4ad89955d4 feat(config): add TOKENIZED_SEARCH 2024-12-26 13:56:27 +08:00
jxxghp
a53553d658 feat(TransferQueueDialog): 添加状态标签以显示任务状态,优化进度条显示逻辑 2024-12-26 13:42:59 +08:00
jxxghp
2602cb0998 feat(TransferQueueDialog): 优化任务队列显示,添加媒体信息和任务管理功能 2024-12-26 13:30:22 +08:00
jxxghp
e402de29d5 Merge pull request #275 from wikrin/v2 2024-12-26 09:28:46 +08:00
Attente
a4cc1cc615 feat(dialog): 仅在指定条件下启用指定集数 2024-12-26 08:24:11 +08:00
jxxghp
2d900baad1 feat(ReorganizeDialog): 添加SSE支持以监听文件整理进度 2024-12-25 21:55:20 +08:00
jxxghp
17d6f6db05 feat(ReorganizeDialog): 重构整理功能,支持后台处理和日志整理 2024-12-25 20:39:53 +08:00
jxxghp
7f3ba543b7 feat(TransferQueue): 添加整理队列对话框及相关功能 2024-12-25 18:12:50 +08:00
jxxghp
b33cb8a12c refactor(Menu): update menu titles for clarity and consistency 2024-12-25 13:25:03 +08:00
jxxghp
cfa8b78c2e Merge pull request #274 from Aqr-K/style/site 2024-12-24 22:05:40 +08:00
Aqr-K
4024daf189 style(site): 统一对齐高度 2024-12-24 21:49:03 +08:00
jxxghp
77d7c3bb61 refactor(ReorganizeDialog): update progress text and remove success messages 2024-12-24 14:17:56 +08:00
jxxghp
868ad57e12 refactor(ReorganizeDialog): remove SSE progress handling and improve toast messages 2024-12-24 13:49:47 +08:00
jxxghp
eaf9724295 Merge pull request #272 from InfinityPacer/v2 2024-12-23 12:11:21 +08:00
InfinityPacer
30e98de38a Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-12-23 02:47:52 +08:00
InfinityPacer
79c606370c feat(MediaCard): implement lazy loading for API calls 2024-12-23 02:47:37 +08:00
jxxghp
b70597b5f5 Merge pull request #271 from InfinityPacer/v2 2024-12-21 07:59:41 +08:00
InfinityPacer
a469282730 fix(subscribe): reactive update for subscribeState and lastUpdateText 2024-12-20 14:14:23 +08:00
jxxghp
c3708360fa Merge pull request #270 from libashanxi/v2 2024-12-20 12:05:40 +08:00
libashanxi
80f0560e0f Update AccountSettingAbout.vue
修改文档链接
2024-12-20 09:23:10 +08:00
jxxghp
84951cdc44 feat(subscribe): update display list based on user role and improve sorting logic 2024-12-20 08:04:18 +08:00
jxxghp
a72cb797ab fix plugin order 2024-12-18 08:10:04 +08:00
jxxghp
6898e6b816 Merge pull request #269 from Aqr-K/patch-1 2024-12-18 06:59:39 +08:00
Aqr-K
adb0b966ff fix: bug
- 少了个e,导致 `size_range` 导入没生效
2024-12-18 00:16:40 +08:00
jxxghp
81284b8d21 Merge pull request #267 from InfinityPacer/v2 2024-12-12 17:29:52 +08:00
InfinityPacer
1a2b112e64 feat(subscribe): add support for reset movie reset subscribe 2024-12-11 20:15:47 +08:00
InfinityPacer
442c484dc9 feat(subscribe): add state reset to 'R' on subscription reset 2024-12-11 20:02:43 +08:00
InfinityPacer
2368c2f25f Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-12-11 19:58:51 +08:00
InfinityPacer
2320c58254 fix(subscribe): update reset confirmation message 2024-12-11 19:58:33 +08:00
jxxghp
c9a4f36414 Merge pull request #266 from wikrin/v2 2024-12-11 06:55:38 +08:00
Attente
1df9a981b2 发布时间规则支持区间 2024-12-10 23:39:51 +08:00
jxxghp
cbc917b834 Merge pull request #265 from Aqr-K/style-rule 2024-12-10 07:14:07 +08:00
Aqr-K
240a568d16 style(rule): Swap the display of id and name in customRuleCard 2024-12-09 22:09:57 +08:00
jxxghp
eb1a847faa Merge pull request #264 from Aqr-K/fix-copy 2024-12-09 17:33:36 +08:00
Aqr-K
e09e57879b Update yarn.lock 2024-12-09 14:36:10 +08:00
Aqr-K
ddd2982971 fix(copy): add copy-to-clipboard 2024-12-09 14:34:53 +08:00
Aqr-K
621da7e4ef fix(copy): Mobile compatibility issues. 2024-12-09 14:33:12 +08:00
Aqr-K
420827c389 Update AccountSettingSystem.vue 2024-12-09 14:31:45 +08:00
Aqr-K
ce9399b894 Update AccountSettingRule.vue 2024-12-09 14:31:19 +08:00
Aqr-K
1bdd08c59a fix(copy): Mobile compatibility issues. 2024-12-09 14:28:41 +08:00
Aqr-K
52d62dda81 fix(copy): Mobile compatibility issues.
- 增加 `type` 细分来源
- 适配新的复制方法
2024-12-09 14:26:43 +08:00
Aqr-K
d69e3cedae fix(copy): Mobile compatibility issues 2024-12-09 14:22:44 +08:00
jxxghp
648bfcdd0d 更新 package.json 2024-12-09 11:32:01 +08:00
jxxghp
e4b8ff0a64 feat:订阅和插件支持手动排序 2024-12-09 10:43:00 +08:00
jxxghp
4576ef854d Merge pull request #263 from Aqr-K/fix-rule 2024-12-08 15:46:00 +08:00
Aqr-K
7323668db5 Update AccountSettingRule.vue 2024-12-08 13:25:13 +08:00
Aqr-K
b11d709070 Update AccountSettingRule.vue 2024-12-08 13:24:53 +08:00
Aqr-K
e25ac006c2 feat(rule): add deleteAllRules Btn. 2024-12-08 13:20:21 +08:00
Aqr-K
9e85e7edce fix(rule): 移除创建新卡片时,对可选参数的赋值
- 解决导出时,空白值内容过多的问题。
2024-12-07 20:46:00 +08:00
Aqr-K
804bcd440c fix(rule): 修复自定义规导入时,部分参数被抛弃的bug 2024-12-07 18:57:49 +08:00
jxxghp
6e4c896cb7 更新 package.json 2024-12-07 07:40:43 +08:00
jxxghp
0cae89f8e3 Merge pull request #262 from Aqr-K/fix-rule 2024-12-07 07:40:12 +08:00
Aqr-K
59a7607c07 Update AccountSettingRule.vue 2024-12-07 05:28:09 +08:00
Aqr-K
6dca0c157f fix(rule): bug
- 替换错误的规则校验方法。
2024-12-06 23:44:04 +08:00
jxxghp
d2aa5a64aa chore(package): bump version to 2.1.2 2024-12-06 15:23:56 +08:00
jxxghp
2620a55c5a refactor(DirectoryCard): update storage options and labels for clarity 2024-12-06 12:09:24 +08:00
jxxghp
14e33215f8 Merge pull request #261 from InfinityPacer/v2 2024-12-05 23:03:14 +08:00
InfinityPacer
6862c2a744 Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-12-05 19:37:44 +08:00
InfinityPacer
fb215e8d87 feat(Subscribe): support update subscription status 2024-12-05 19:36:41 +08:00
InfinityPacer
f52ad2151b style(SubscribeCard): support different styles for subscription states 2024-12-05 19:35:23 +08:00
jxxghp
1a47b7d09d Merge pull request #260 from InfinityPacer/v2 2024-12-03 20:53:27 +08:00
InfinityPacer
f292071a34 fix(saveSiteSetting): update success toast message for consistency 2024-12-03 19:10:01 +08:00
InfinityPacer
dd616d29e8 fix(saveSiteSetting): add error toast for failed settings save 2024-12-03 19:07:58 +08:00
jxxghp
0509f18d66 Merge pull request #257 from Aqr-K/v2-rulessettings 2024-11-29 18:32:28 +08:00
Aqr-K
f59fb119e4 Update ImportCodeDialog.vue 2024-11-29 17:33:00 +08:00
Aqr-K
46127cac1f Update FilterRuleGroupCard.vue 2024-11-29 17:31:35 +08:00
Aqr-K
c1abf76211 Update AccountSettingRule.vue 2024-11-29 17:30:11 +08:00
jxxghp
fe5b45d48d Merge pull request #256 from InfinityPacer/v2 2024-11-29 15:14:49 +08:00
InfinityPacer
10ac1ebf7b Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-11-29 13:06:50 +08:00
InfinityPacer
e5d8144510 fix(api): update subscribe endpoint URL to include trailing slash 2024-11-29 13:06:32 +08:00
jxxghp
f9a65fba7a 更新 package.json 2024-11-29 07:09:54 +08:00
jxxghp
9b4138349b Merge pull request #255 from wikrin/manual_transfer 2024-11-28 07:24:19 +08:00
Attente
db9c9db5a9 改进手动整理类型/类别可选逻辑 2024-11-28 05:23:20 +08:00
jxxghp
24e992339f Merge pull request #254 from InfinityPacer/v2 2024-11-27 16:26:12 +08:00
InfinityPacer
f26d1babf7 Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-11-27 15:32:30 +08:00
InfinityPacer
de3347cea1 feat(encoding): add detection performance mode 2024-11-27 15:32:09 +08:00
jxxghp
e900fac4bd Merge pull request #253 from Aqr-K/v2-main 2024-11-27 07:02:51 +08:00
jxxghp
396218a467 Merge pull request #251 from wikrin/add 2024-11-27 07:02:20 +08:00
Attente
d3a66ffa8c 去除自动选项
- 仅当`目的路径`为空时,才会设为`自动`
- 已选择`整理方式`后, 再选择配置的`媒体库目录`整理方式不再改变
- `自定义路径`时:
- - `整理方式`为自动时,会修改为`复制`,请注意
- - `整理方式`不为自动时, 不会改变
2024-11-27 05:53:31 +08:00
Aqr-K
1e7ffb4c2e Update main.ts 2024-11-26 22:41:22 +08:00
Aqr-K
3df5d4c690 Update main.ts 2024-11-25 16:42:12 +08:00
Aqr-K
02a8331996 fix: 移除多余import 2024-11-25 16:32:01 +08:00
Aqr-K
a29ad6a091 fix: 调整注册顺序,解决重复注册的警告 2024-11-25 16:30:57 +08:00
Attente
3ef1e65412 手动整理中整理方式增加自动 2024-11-25 13:24:43 +08:00
jxxghp
2deaec1fc6 Merge pull request #250 from InfinityPacer/v2 2024-11-24 17:59:22 +08:00
InfinityPacer
c9b0b23d36 fix(downloader): remove redundant checks and prompts 2024-11-24 17:57:25 +08:00
jxxghp
f06cca4ead Merge pull request #249 from wikrin/v2 2024-11-24 16:58:58 +08:00
Attente
a1990ce3e4 fix(dialog): correct storage option selection in Reorganize Dialog 2024-11-24 16:47:44 +08:00
jxxghp
cbbf023030 更新 package.json 2024-11-24 07:35:25 +08:00
jxxghp
307aa724eb Merge pull request #248 from wikrin/rfc-247 2024-11-23 23:07:13 +08:00
Attente
cd6f37d80f Merge branch 'v2' into rfc-247 2024-11-23 23:05:47 +08:00
jxxghp
b903134770 feat(ReorganizeDialog): 添加文件操作整理方式选项 2024-11-23 23:02:50 +08:00
Attente
11effdd297 Revert "refactor: 优化目标目录下拉框和路径变化监听逻辑"
This reverts commit 01f63a4b6b.
2024-11-23 22:42:48 +08:00
jxxghp
8873d8372d feat(ReorganizeDialog): 添加文件操作整理方式选项 2024-11-23 21:32:27 +08:00
jxxghp
964aa29d12 Merge pull request #245 from wikrin/dev 2024-11-23 19:21:03 +08:00
Attente
b45a3c6539 qb下载器添加用户名输入限制 2024-11-23 17:21:11 +08:00
jxxghp
b72b7ad0fb Merge pull request #246 from DDS-Derek/issue_rfc 2024-11-23 12:38:43 +08:00
DDSRem
0e3106d8c1 chore(issue): add rfc template 2024-11-23 12:34:59 +08:00
jxxghp
71a6626fa9 feat(ReorganizeDialog): 添加按类型和类别分类的选项 2024-11-23 11:21:29 +08:00
jxxghp
68006bac88 chore(yarn.lock): 更新terser到版本5.36.0 2024-11-22 13:02:05 +08:00
jxxghp
34cbcc38a6 feat(NetTestView): 添加对api.github.com和raw.githubusercontent.com的支持 2024-11-22 12:40:05 +08:00
jxxghp
f4daee85c7 Merge pull request #243 from Aqr-K/v2-terser 2024-11-21 18:18:20 +08:00
Aqr-K
dd347039b5 Update vite.config.ts 2024-11-21 16:50:48 +08:00
Aqr-K
0c9367d58a Update package.json 2024-11-21 16:50:18 +08:00
jxxghp
af10c4f1c3 feat(TransferHistoryView): 增加重做目标存储的响应式支持 2024-11-21 16:02:28 +08:00
jxxghp
52fbeda941 Merge pull request #242 from wikrin/v2 2024-11-21 14:00:17 +08:00
jxxghp
ace23af363 chore(package): 更新版本号至2.0.9 2024-11-21 12:14:15 +08:00
jxxghp
a097d89d68 feat(DownloadSettings): 更新下载器设置API,优化下载器加载逻辑 2024-11-21 10:26:12 +08:00
jxxghp
77cb817523 feat(AliyunAuthDialog): 添加配置支持,允许自定义refreshToken和保存设置 2024-11-20 20:25:32 +08:00
jxxghp
c956e271a2 refactor(ReorganizeDialog): 重构目标路径输入组件,移除不必要的目录加载逻辑 2024-11-20 19:24:10 +08:00
jxxghp
6413f30d18 chore(package): 更新版本号至2.0.8 2024-11-20 13:16:16 +08:00
jxxghp
789e748df0 fix(U115AuthDialog): 添加配置支持和自定义Cookie输入 2024-11-20 13:15:31 +08:00
Attente
c89edae375 downloader属性不再为可选, 使组件具有初始值 2024-11-20 11:40:24 +08:00
Attente
f4dca4922b Update SiteAddEditDialog.vue 2024-11-20 10:43:47 +08:00
Attente
73b9ef5ee7 fix(dialog): 站点下载器字段修改默认值 2024-11-20 10:38:04 +08:00
jxxghp
462742961a fix(SubscribeEditDialog): 下载器字段更新默认值 2024-11-20 08:26:07 +08:00
jxxghp
5a647fabfa fix(UserProfile): 添加超级用户条件以控制站点认证对话框的显示 2024-11-20 08:16:27 +08:00
167 changed files with 12566 additions and 7110 deletions

45
.github/ISSUE_TEMPLATE/rfc.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: 功能提案
description: Request for Comments
title: '[RFC]'
labels: ['RFC']
body:
- type: markdown
attributes:
value: |
一份提案(RFC)定位为 **「在某功能/重构的具体开发前,用于开发者间 review 技术设计/方案的文档」**
目的是让协作的开发者间清晰的知道「要做什么」和「具体会怎么做」,以及所有的开发者都能公开透明的参与讨论;
以便评估和讨论产生的影响 (遗漏的考虑、向后兼容性、与现有功能的冲突)
因此提案侧重在对解决问题的 **方案、设计、步骤** 的描述上。
如果仅希望讨论是否添加或改进某功能本身,请使用 -> [Issue: 功能改进](https://github.com/jxxghp/MoviePilot/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml&title=%5BFeature+Request%5D%3A+)
- type: textarea
id: background
attributes:
label: 背景 or 问题
description: 简单描述遇到的什么问题或需要改动什么。可以引用其他 issue、讨论、文档等。
validations:
required: true
- type: textarea
id: goal
attributes:
label: '目标 & 方案简述'
description: 简单描述提案此提案实现后,**预期的目标效果**,以及简单大致描述会采取的方案/步骤,可能会/不会产生什么影响。
validations:
required: true
- type: textarea
id: design
attributes:
label: '方案设计 & 实现步骤'
description: |
详细描述你设计的具体方案,可以考虑拆分列表或要点,一步步描述具体打算如何实现的步骤和相关细节。
这部份不需要一次性写完整,即使在创建完此提案 issue 后,依旧可以再次编辑修改。
validations:
required: false
- type: textarea
id: alternative
attributes:
label: '替代方案 & 对比'
description: |
[可选] 为来实现目标效果,还考虑过什么其他方案,有什么对比?
validations:
required: false

369
auto-imports.d.ts vendored
View File

@@ -3,9 +3,11 @@
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
@@ -20,13 +22,11 @@ declare global {
const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createLogger: typeof import('vuex')['createLogger']
const createNamespacedHelpers: typeof import('vuex')['createNamespacedHelpers']
const createPinia: typeof import('pinia')['createPinia']
const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createStore: typeof import('vuex')['createStore']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
@@ -34,9 +34,11 @@ declare global {
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
@@ -52,10 +54,11 @@ declare global {
const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('vuex')['mapActions']
const mapGetters: typeof import('vuex')['mapGetters']
const mapMutations: typeof import('vuex')['mapMutations']
const mapState: typeof import('vuex')['mapState']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
@@ -66,6 +69,7 @@ declare global {
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
@@ -77,6 +81,7 @@ declare global {
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
@@ -96,9 +101,12 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
@@ -190,6 +198,7 @@ declare global {
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
@@ -209,6 +218,7 @@ declare global {
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMin: typeof import('@vueuse/math')['useMin']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
@@ -234,6 +244,7 @@ declare global {
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProjection: typeof import('@vueuse/math')['useProjection']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
@@ -242,6 +253,7 @@ declare global {
const useRound: typeof import('@vueuse/math')['useRound']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
@@ -256,11 +268,11 @@ declare global {
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStore: typeof import('vuex')['useStore']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSum: typeof import('@vueuse/math')['useSum']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
@@ -313,15 +325,17 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, 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 acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
@@ -336,13 +350,11 @@ declare module 'vue' {
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 createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
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']>
@@ -350,9 +362,11 @@ declare module 'vue' {
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
@@ -368,10 +382,11 @@ declare module 'vue' {
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 mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
@@ -382,6 +397,7 @@ declare module 'vue' {
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
@@ -393,6 +409,7 @@ declare module 'vue' {
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
@@ -412,9 +429,12 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
@@ -506,6 +526,7 @@ declare module 'vue' {
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 useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@@ -525,6 +546,7 @@ declare module 'vue' {
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 useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
@@ -550,6 +572,7 @@ declare module 'vue' {
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 usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
@@ -558,6 +581,7 @@ declare module 'vue' {
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 useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
@@ -572,11 +596,11 @@ declare module 'vue' {
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 useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
@@ -626,313 +650,4 @@ declare module 'vue' {
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']>
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']>
}
}
}

2
components.d.ts vendored
View File

@@ -1,10 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']

View File

@@ -1,161 +1,221 @@
<!DOCTYPE html>
<html lang="en">
<html
lang="en"
style="
overflow: hidden auto;
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
background: var(--initial-loader-bg, #fff);
"
>
<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" />
<script>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
</script>
</head>
<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" />
</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">
<body style="margin: 0">
<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="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)">
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="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)" />
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>
</g>
</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>
<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>
</div>
<div class="loading">
<div class="effect-1 effects"></div>
<div class="effect-2 effects"></div>
<div class="effect-3 effects"></div>
</div>
</div>
<div 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 (primaryColor)
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
</script>
</body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.0.7",
"version": "2.3.5",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -19,86 +19,93 @@
]
},
"dependencies": {
"@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",
"@fullcalendar/core": "^6.1.15",
"@fullcalendar/daygrid": "^6.1.15",
"@fullcalendar/interaction": "^6.1.15",
"@fullcalendar/list": "^6.1.15",
"@fullcalendar/timegrid": "^6.1.15",
"@fullcalendar/vue3": "^6.1.15",
"@iconify/utils": "^2.2.1",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.42.1",
"@vue-flow/minimap": "^1.5.2",
"@vue-flow/node-resizer": "^1.4.0",
"@vue-flow/node-toolbar": "^1.1.0",
"@vue-js-cron/vuetify": "^5.0.9",
"@vueuse/core": "^12.4.0",
"@vueuse/math": "^12.4.0",
"ace-builds": "^1.37.4",
"apexcharts": "^4.0.0",
"axios": "^1.7.9",
"colorthief": "^2.6.0",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13",
"express": "^4.21.2",
"express-http-proxy": "^2.1.1",
"lodash-es": "^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",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"qrcode.vue": "^3.6.0",
"sass": "^1.83.4",
"tailwindcss": "^ 3.4.17",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-toast-notification": "^3.1.3",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.4.1",
"vue3-apexcharts": "^1.8.0",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "3.6.8",
"vuetify": "3.7.3",
"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": "^4.0.4",
"@iconify/vue": "4.1.1",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@iconify/vue": "^4.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@types/lodash": "^4.14.197",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^20.1.4",
"@types/webfontloader": "^1.6.34",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"autoprefixer": "^10.4.14",
"eslint": "^9.0.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint": "^9.18.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.0.1",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-regex": "^1.10.0",
"eslint-plugin-sonarjs": "^0.25.1",
"eslint-plugin-unicorn": "^52.0.0",
"eslint-plugin-sonarjs": "^3.0.1",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-vue": "^9.12.0",
"postcss": "8",
"postcss": "^8.5.1",
"postcss-html": "^1.5.0",
"stylelint": "16.3.1",
"stylelint-config-idiomatic-order": "10.0.0",
"stylelint-config-standard-scss": "13.1.0",
"stylelint": "^16.13.2",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-use-logical-spec": "5.0.1",
"terser": "^5.36.0",
"type-fest": "^4.15.0",
"typescript": "^5.0.4",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.8",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"unplugin-vue-define-options": "^1.5.3",
"vite": "^5.4.11",
"vite-plugin-pages": "^0.32.1",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "2.0.3",
"vue-shepherd": "^3.0.0",
"vue-tsc": "^2.0.10"
"vite-plugin-vuetify": "2.0.4",
"vue-shepherd": "^4.1.0",
"vue-tsc": "^2.0.10",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0"
},
"packageManager": "yarn@1.22.18",
"resolutions": {
"postcss": "8"
}
}
"packageManager": "yarn@1.22.18"
}

View File

@@ -1,16 +1,6 @@
body {
margin: 0;
}
html {
overflow: hidden auto;
background: var(--initial-loader-bg, #fff);
min-block-size: calc(100% + env(safe-area-inset-top));
}
#loading-bg {
position: absolute;
z-index: 999;
position: fixed;
z-index: 9999;
display: block;
background: var(--initial-loader-bg, #fff);
block-size: 100vh;

View File

@@ -5,7 +5,7 @@ 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'
import { saveLocalTheme } from '../utils/theme'
// 显示器宽度
const display = useDisplay()
@@ -103,8 +103,7 @@ function updateTheme() {
savedTheme.value = theme
themeTransition()
// 保存主题到本地
localStorage.setItem('theme', theme)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
saveLocalTheme(theme, globalTheme)
}
// 切换主题
@@ -115,7 +114,7 @@ function changeTheme(theme: string) {
// 保存主题到服务端
try {
api.post('/user/config/Layout', {
theme: nextTheme
theme: nextTheme,
})
} catch (e) {
console.error('保存主题到服务端失败')
@@ -176,7 +175,7 @@ async function saveCustomCSS() {
},
})
if (result.success) $toast.success('自定义CSS保存成功')
if (result.success) $toast.success('自定义CSS保存成功,请刷新页面生效')
} catch (e) {
console.error('保存自定义 CSS 到服务端失败')
}
@@ -210,7 +209,7 @@ onMounted(() => {
</VList>
</VMenu>
<!-- 自定义 CSS -- -->
<VDialog v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard title="自定义主题风格">
<DialogCloseBtn @click="cssDialog = false" />
<VDivider />

View File

@@ -212,10 +212,6 @@ h6,
}
}
.v-data-table-footer {
margin-block-start: 1rem;
}
// 👉 v-field
.v-field:hover .v-field__outline {
--v-field-border-opacity: var(--v-medium-emphasis-opacity);

View File

@@ -1,5 +1,5 @@
.auth-wrapper {
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top));
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top) + env(safe-area-inset-bottom));
}
.auth-footer-mask {

View File

@@ -176,10 +176,6 @@
th {
background: rgb(var(--v-table-header-background)) !important;
}
.v-data-table-footer {
margin-block-start: 1rem;
}
}
// 👉 Pagination

View File

@@ -1,9 +1,10 @@
@use "sass:map";
@use "vuetify/lib/styles/settings" as vuetify_settings;
@mixin avatar-font-sizes($map: $avatar-sizes) {
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
/* stylelint-disable-next-line scss/no-global-function-names */
$size: map-get($map, $sizeName);
$size: map.get($map, $sizeName);
&.v-avatar--size-#{$sizeName} {
font-size: #{$size}px;

View File

@@ -92,8 +92,7 @@
.fc-header-toolbar {
flex-wrap: wrap;
margin: 1.25rem;
column-gap: 0.5rem;
row-gap: 1rem;
gap: 1rem 0.5rem;
}
.fc-toolbar-chunk {
@@ -238,7 +237,7 @@
inline-size: 1.5625rem;
margin-inline-end: 0.25rem;
@media (max-width: 1264px) {
@media (width <= 1264px) {
display: block !important;
}

View File

@@ -10,8 +10,7 @@ export function useDefer(maxFrameCount = 1) {
const refreshFrameCount = () => {
requestAnimationFrame(() => {
frameCount.value++
if (frameCount.value < maxFrameCount)
refreshFrameCount()
if (frameCount.value < maxFrameCount) refreshFrameCount()
})
}
refreshFrameCount()
@@ -19,3 +18,9 @@ export function useDefer(maxFrameCount = 1) {
return frameCount.value >= showInFrameCount
}
}
export function ensureRenderComplete(callback: () => void) {
requestAnimationFrame(() => {
requestAnimationFrame(callback)
})
}

View File

@@ -153,3 +153,12 @@ export function formatDateDifference(dateString: string): string {
if (!dateString) return ''
return dayjs(dateString).fromNow()
}
// 格式化评份如为10及以下的数按原值显示否则格式化为xxM、xxK显示
export function formatRating(rating: number): string {
if (!rating) return ''
if (rating <= 10) return rating.toString()
if (rating < 1000) return rating.toLocaleString()
if (rating < 1000 * 1000) return `${(rating / 1000).toFixed(1)}K`
return `${(rating / 1000 / 1000).toFixed(1)}M`
}

View File

@@ -1,3 +1,5 @@
import copy from 'copy-to-clipboard'
// 请求和获取剪贴板内容
export async function getClipboardContent() {
if (navigator.clipboard && window.isSecureContext) {
@@ -13,20 +15,10 @@ export async function getClipboardContent() {
}
}
// 将内容复制到剪贴板,兼容非安全域场景
// 将内容复制到剪贴板
export async function copyToClipboard(content: string) {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(content)
} else {
const input = document.createElement('textarea')
input.value = content
document.body.appendChild(input)
// 阻止事件冒泡到其他元素,确保 focusin 事件只在 textarea 元素上处理,不会影响其他元素
input.addEventListener('focusin', e => e.stopPropagation())
input.select()
document.execCommand('copy')
document.body.removeChild(input)
}
const success = copy(content)
return success
}
// VAPID公钥转Uint8Array

6
src/@core/utils/theme.ts Normal file
View File

@@ -0,0 +1,6 @@
export function saveLocalTheme(name: string, theme: any) {
// 存储主题到本地
localStorage.setItem('theme', name)
localStorage.setItem('materio-initial-loader-bg', theme.current.value.colors.background)
localStorage.setItem('materio-initial-loader-color', theme.current.value.colors.primary)
}

122
src/@core/utils/workflow.ts Normal file
View File

@@ -0,0 +1,122 @@
import { useVueFlow } from '@vue-flow/core'
import { ref, watch } from 'vue'
import { cloneDeep } from 'lodash-es'
/**
* @returns {string} - A unique id.
*/
function getId() {
// 生成以act_开头的唯一id
return 'act_' + Math.random().toString(36).substr(2, 9)
}
/**
* In a real world scenario you'd want to avoid creating refs in a global scope like this as they might not be cleaned up properly.
* @type {{draggedData: Ref<any>, isDragOver: Ref<boolean>, isDragging: Ref<boolean>}}
*/
const state = {
/**
* The type of the node being dragged.
*/
draggedData: ref<any | null>({}),
isDragOver: ref(false),
isDragging: ref(false),
}
export default function useDragAndDrop() {
const { draggedData, isDragOver, isDragging } = state
const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()
watch(isDragging, dragging => {
document.body.style.userSelect = dragging ? 'none' : ''
})
function onDragStart(event: any, data: any) {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', data)
event.dataTransfer.effectAllowed = 'move'
}
draggedData.value = data
isDragging.value = true
document.addEventListener('drop', onDragEnd)
}
/**
* Handles the drag over event.
*
* @param {DragEvent} event
*/
function onDragOver(event: any) {
event.preventDefault()
if (draggedData.value) {
isDragOver.value = true
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'
}
}
}
function onDragLeave() {
isDragOver.value = false
}
function onDragEnd() {
isDragging.value = false
isDragOver.value = false
draggedData.value = null
document.removeEventListener('drop', onDragEnd)
}
/**
* Handles the drop event.
*
* @param {DragEvent} event
*/
function onDrop(event: any) {
const position = screenToFlowCoordinate({
x: event.clientX,
y: event.clientY,
})
const nodeId = getId()
const newNode = {
id: nodeId,
type: draggedData.value?.type,
name: draggedData.value?.name,
description: draggedData.value?.description,
position,
data: draggedData.value?.data ? cloneDeep(draggedData.value.data) : {},
}
/**
* Align node position after drop, so it's centered to the mouse
*
* We can hook into events even in a callback, and we can remove the event listener after it's been called.
*/
const { off } = onNodesInitialized(() => {
updateNode(nodeId, node => ({
position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 },
}))
off()
})
addNodes(newNode)
}
return {
draggedData,
isDragOver,
isDragging,
onDragStart,
onDragLeave,
onDragOver,
onDrop,
}
}

View File

@@ -0,0 +1 @@
{"root":["./build-icons.ts"],"version":"5.7.3"}

View File

@@ -51,14 +51,23 @@ export default defineComponent({
const main = h(
'main',
{ class: 'layout-page-content' },
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true },
() => h('section', { class: 'page-content-container' }, slots.default?.()),
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
h('section', { class: 'page-content-container' }, slots.default?.()),
),
)
// 👉 根据路由 meta 决定 footer 高度
const shouldShowFooter = !route.meta.hideFooter
// 👉 Footer
const footer = h('footer', { class: 'layout-footer' }, [
h('div', { class: 'footer-content-container' }, slots.footer?.()),
h(
'div',
{
class: ['footer-content-container', !shouldShowFooter && 'footer-content-container-noheight'],
},
slots.footer?.(),
),
])
// 👉 Overlay
@@ -80,11 +89,7 @@ export default defineComponent({
scrollDistance.value && 'window-scrolled',
],
},
[
verticalNav,
h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]),
layoutOverlay,
],
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
)
}
},
@@ -92,9 +97,9 @@ export default defineComponent({
</script>
<style lang="scss">
@use "@configured-variables" as variables;
@use "@layouts/styles/placeholders";
@use "@layouts/styles/mixins";
@use '@configured-variables' as variables;
@use '@layouts/styles/placeholders';
@use '@layouts/styles/mixins';
.layout-wrapper.layout-nav-type-vertical {
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
@@ -116,9 +121,7 @@ export default defineComponent({
inset-block-start: 0;
.navbar-content-container {
block-size: calc(
env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height
);
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
}
@at-root {

View File

@@ -5,7 +5,7 @@
@use "@configured-variables" as variables;
html {
min-height: calc(100% + env(safe-area-inset-top));
min-height: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
background: rgb(var(--v-theme-background));
overflow-y: overlay;
}
@@ -65,6 +65,10 @@ body,
block-size: variables.$layout-vertical-nav-footer-height;
}
.footer-content-container-noheight {
block-size: 0px !important;
}
.layout-footer-sticky & {
position: sticky;
inset-block-end: 0;

View File

@@ -7,5 +7,5 @@
html {
box-sizing: border-box;
min-height: calc(100% + env(safe-area-inset-top))
min-height: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom))
}

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: 0rem !default;
$layout-vertical-nav-footer-height: 3.5rem !default;
// 👉 Layout overlay
$layout-overlay-z-index: 11 !default;

View File

@@ -1,15 +1,16 @@
<script lang="ts" setup>
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
const { global: globalTheme } = useTheme()
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
// 生效主题
async function setTheme() {
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
}
const { global: globalTheme } = useTheme()
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
// 显示状态
const show = ref(false)
// ApexCharts 全局配置
declare global {
@@ -41,14 +42,24 @@ if (window.Apex) {
}
}
// 页面加载时,加载当前用户数据
onBeforeMount(async () => {
setTheme()
onMounted(() => {
ensureRenderComplete(() => {
nextTick(() => {
setTimeout(() => {
// 移除加载动画
removeEl('#loading-bg')
// 将background属性从html的style中移除
document.documentElement.style.removeProperty('background')
// 显示页面
show.value = true
}, 1500)
})
})
})
</script>
<template>
<VApp>
<VApp v-show="show">
<RouterView />
</VApp>
</template>

View File

@@ -3,26 +3,31 @@ export const storageOptions = [
title: '本地',
value: 'local',
icon: 'mdi-folder-multiple-outline',
remote: false,
},
{
title: '阿里云盘',
value: 'alipan',
icon: 'mdi-cloud-outline',
remote: true,
},
{
title: '115网盘',
value: 'u115',
icon: 'mdi-cloud-outline',
remote: true,
},
{
title: 'RClone',
value: 'rclone',
icon: 'mdi-cloud-outline',
remote: true,
},
{
title: 'AList',
value: 'alist',
icon: 'mdi-cloud-outline',
remote: true,
},
]
@@ -71,3 +76,10 @@ export const storageDict = storageOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
export const transferTypeOptions = [
{ title: '复制', value: 'copy' },
{ title: '移动', value: 'move' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
]

View File

@@ -1,6 +1,6 @@
import axios from 'axios'
import router from '@/router'
import store from '@/store'
import { useAuthStore } from '@/stores'
// 创建axios实例
const api = axios.create({
@@ -9,10 +9,12 @@ const api = axios.create({
// 添加请求拦截器
api.interceptors.request.use(config => {
// 认证 Store
const authStore = useAuthStore()
// 在请求头中添加token
const token = store.state.auth.token
if (token) config.headers.Authorization = `Bearer ${token}`
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
@@ -26,8 +28,10 @@ api.interceptors.response.use(
// 请求超时
return Promise.reject(new Error(error))
} else if (error.response.status === 403) {
// 认证 Store
const authStore = useAuthStore()
// 清除登录状态信息
store.dispatch('auth/logout')
authStore.logout()
// token验证失败跳转到登录页面
router.push('/login')
}

View File

@@ -14,6 +14,10 @@ export interface Subscribe {
tmdbid: number
// 豆瓣ID
doubanid?: string
// Bangumi ID
bangumiid?: string
// 其它媒体ID
mediaid?: string
// 季号
season?: number
// 海报
@@ -44,7 +48,7 @@ export interface Subscribe {
lack_episode?: number
// 附加信息
note?: string
// 状态N-新建 R-订阅中
// 状态N-新建 R-订阅中 P-待定 S-暂停
state: string
// 最后更新时间
last_update: string
@@ -73,7 +77,7 @@ export interface Subscribe {
// 过滤规则组
filter_groups?: string[]
// 下载器
downloader?: string
downloader: string
}
// 订阅分享
@@ -88,6 +92,8 @@ export interface SubscribeShare {
share_comment?: string
// 分享人
share_user?: string
// 分享人唯一ID
share_uid?: string
// 订阅名称
name?: string
// 订阅年份
@@ -184,7 +190,7 @@ export interface TransferHistory {
export interface MediaInfo {
// 来源themoviedb、douban、bangumi
source?: string
// 类型 电影、电视剧
// 类型 电影、电视剧、合集
type?: string
// 媒体标题
title?: string
@@ -204,6 +210,12 @@ export interface MediaInfo {
douban_id?: string
// Bangumi ID
bangumi_id?: string
// 合集ID
collection_id?: number
// 其它媒体ID前缀
mediaid_prefix?: string
// 其它媒体ID值
media_id?: string
// 媒体原语种
original_language?: string
// 媒体原发行标题
@@ -276,6 +288,24 @@ export interface MediaInfo {
names?: string[]
}
// 季信息
export interface MediaSeason {
// 上映日期
air_date?: string
// 总集数
episode_count?: number
// 季名称
name?: string
// 描述
overview?: string
// 海报
poster_path?: string
// 季号
season_number?: number
// 评分
vote_average?: number
}
// TMDB季信息
export interface TmdbSeason {
// 上映日期
@@ -390,7 +420,7 @@ export interface Site {
// RSS地址
rss?: string
// 下载器
downloader?: string
downloader: string
// Cookie
cookie?: string
// ApiKey
@@ -1051,7 +1081,7 @@ export interface TransferDirectoryConf {
// 监控模式 fast/compatibility
monitor_mode?: string
// 整理方式 move/copy/link/softlink
transfer_type?: string
transfer_type: string
// 文件覆盖模式 always/size/never/latest
overwrite_mode?: string
// 整理到媒体库目录
@@ -1100,6 +1130,7 @@ export interface FilterRuleGroup {
category?: string
}
// 订阅下载文件详情
export interface SubscribeDownloadFileInfo {
// 种子名称
torrent_title?: string
@@ -1113,6 +1144,7 @@ export interface SubscribeDownloadFileInfo {
file_path?: string
}
// 订阅媒体库文件详情
export interface SubscribeLibraryFileInfo {
// 存储
storage?: string
@@ -1120,6 +1152,7 @@ export interface SubscribeLibraryFileInfo {
file_path?: string
}
// 订阅集详情
export interface SubscribeEpisodeInfo {
// 标题
title?: string
@@ -1133,9 +1166,126 @@ export interface SubscribeEpisodeInfo {
library?: SubscribeLibraryFileInfo[]
}
// 订阅详情
export interface SubscrbieInfo {
// 订阅信息
subscribe: Subscribe
// 集信息 {集号: {download: 文件路径library: 文件路径, backdrop: url, title: 标题, description: 描述}}
episodes: Record<number, SubscribeEpisodeInfo>
}
// 整理表单
export interface TransferForm {
// 文件项
fileitem: FileItem
// 历史ID
logid: number
// 目标存储
target_storage: string
// 目标路径
target_path: string
// TMDB ID
tmdbid?: number
// 豆瓣 ID
doubanid?: string
// 季号
season?: number
// 类型
type_name?: string
// 整理方式
transfer_type: string
// 自定义格式
episode_format?: string
// 指定集数
episode_detail?: string
// 指定PART
episode_part?: string
// 集数偏移
episode_offset?: string
// 最小文件大小
min_filesize: number
// 刮削
scrape: boolean
// 复用历史识别信息
from_history: boolean
// 媒体库类型子目录
library_type_folder?: boolean
// 媒体库类别子目录
library_category_folder?: boolean
}
// 整理队列
export interface TransferQueue {
// 媒体信息
media: MediaInfo
// 季
season?: number
// 任务列表
tasks: {
// 文件项
fileitem: FileItem
// 元数据
meta: MetaInfo
// 状态
state: string
}[]
}
// 探索的数据源
export interface DiscoverSource {
// 数据源名称
name: string
// 媒体ID的前缀不含:
mediaid_prefix: string
// 媒体数据源API地址
api_path: string
// 过滤参数
filter_params: { [key: string]: any }
// 过滤参数UI配置
filter_ui: RenderProps[]
// UI依赖关系字典
depends?: { [key: string]: string[] }
}
// 推荐的数据源
export interface RecommendSource {
// 数据源名称
name: string
// 媒体数据源API地址
api_path: string
}
// 站点资源分类
export interface SiteCategory {
id: number
cat: string
desc: string
}
// 工作流
export interface Workflow {
// 工作流ID
id?: string
// 工作流名称
name?: string
// 工作流描述
description?: string
// 定时器
timer?: string
// 状态
state?: string
// 当前执行动作
current_action?: string
// 任务执行结果
result?: string
// 已执行次数
run_count?: number
// 动作列表
actions?: any[]
// 动作流
flows?: any[]
// 创建时间
add_time?: string
// 最后执行时间
last_time?: string
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 273.42 35.52"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="17.76" x2="273.42" y2="17.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 3</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M191.85,35.37h63.9A17.67,17.67,0,0,0,273.42,17.7h0A17.67,17.67,0,0,0,255.75,0h-63.9A17.67,17.67,0,0,0,174.18,17.7h0A17.67,17.67,0,0,0,191.85,35.37ZM10.1,35.42h7.8V6.92H28V0H0v6.9H10.1Zm28.1,0H46V8.25h.1L55.05,35.4h6L70.3,8.25h.1V35.4h7.8V0H66.45l-8.2,23.1h-.1L50,0H38.2ZM89.14.12h11.7a33.56,33.56,0,0,1,8.08,1,18.52,18.52,0,0,1,6.67,3.08,15.09,15.09,0,0,1,4.53,5.52,18.5,18.5,0,0,1,1.67,8.25,16.91,16.91,0,0,1-1.62,7.58,16.3,16.3,0,0,1-4.38,5.5,19.24,19.24,0,0,1-6.35,3.37,24.53,24.53,0,0,1-7.55,1.15H89.14Zm7.8,28.2h4a21.66,21.66,0,0,0,5-.55A10.58,10.58,0,0,0,110,26a8.73,8.73,0,0,0,2.68-3.35,11.9,11.9,0,0,0,1-5.08,9.87,9.87,0,0,0-1-4.52,9.17,9.17,0,0,0-2.63-3.18A11.61,11.61,0,0,0,106.22,8a17.06,17.06,0,0,0-4.68-.63h-4.6ZM133.09.12h13.2a32.87,32.87,0,0,1,4.63.33,12.66,12.66,0,0,1,4.17,1.3,7.94,7.94,0,0,1,3,2.72,8.34,8.34,0,0,1,1.15,4.65,7.48,7.48,0,0,1-1.67,5,9.13,9.13,0,0,1-4.43,2.82V17a10.28,10.28,0,0,1,3.18,1,8.51,8.51,0,0,1,2.45,1.85,7.79,7.79,0,0,1,1.57,2.62,9.16,9.16,0,0,1,.55,3.2,8.52,8.52,0,0,1-1.2,4.68,9.32,9.32,0,0,1-3.1,3A13.38,13.38,0,0,1,152.32,35a22.5,22.5,0,0,1-4.73.5h-14.5Zm7.8,14.15h5.65a7.65,7.65,0,0,0,1.78-.2,4.78,4.78,0,0,0,1.57-.65,3.43,3.43,0,0,0,1.13-1.2,3.63,3.63,0,0,0,.42-1.8A3.3,3.3,0,0,0,151,8.6a3.42,3.42,0,0,0-1.23-1.13A6.07,6.07,0,0,0,148,6.9a9.9,9.9,0,0,0-1.85-.18h-5.3Zm0,14.65h7a8.27,8.27,0,0,0,1.83-.2,4.67,4.67,0,0,0,1.67-.7,3.93,3.93,0,0,0,1.23-1.3,3.8,3.8,0,0,0,.47-1.95,3.16,3.16,0,0,0-.62-2,4,4,0,0,0-1.58-1.18,8.23,8.23,0,0,0-2-.55,15.12,15.12,0,0,0-2.05-.15h-5.9Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -2,7 +2,7 @@
import { CustomRule } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import filter_svg from '@images/svg/filter.svg'
import { cloneDeep } from 'lodash'
import { cloneDeep } from 'lodash-es'
import { innerFilterRules } from '@/api/constants'
// 输入参数
@@ -98,13 +98,13 @@ function onClose() {
<DialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start">
<h5 class="text-h6 mb-1">{{ props.rule.id }}</h5>
<div class="text-body-1 mb-3">{{ props.rule.name }}</div>
<h5 class="text-h6 mb-1">{{ props.rule.name }}</h5>
<div class="text-body-1 mb-3">{{ props.rule.id }}</div>
</div>
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="ruleInfoDialog" scrollable max-width="40rem" persistent>
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem" persistent>
<VCard :title="`${props.rule.id} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="ruleInfoDialog" />
<VDivider />
@@ -174,9 +174,9 @@ function onClose() {
<VCol cols="6">
<VTextField
v-model="ruleInfo.publish_time"
placeholder="0"
placeholder="0/1-10"
label="发布时间(分钟)"
hint="距离资源发布的最小时间间隔"
hint="距离资源发布的最小时间间隔或时间区间"
persistent-hint
active
/>

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup>
import type { TransferDirectoryConf } from '@/api/types'
import { VDivider, VSpacer, VTextField } from 'vuetify/lib/components/index.mjs'
import api from '@/api'
import { nextTick } from 'vue'
import { storageOptions } from '@/api/constants'
@@ -20,12 +19,6 @@ const props = defineProps({
height: String,
})
// 下载路径
const downloadPath = ref<string>('')
// 媒体库路径
const libraryPath = ref<string>('')
// 卡版是否折叠状态
const isCollapsed = ref(true)
@@ -36,6 +29,11 @@ const typeItems = [
{ title: '电视剧', value: '电视剧' },
]
// 计算资源存储字典(整理方式为下载器时不能为远程存储)
const resourceStorageOptions = computed(() => {
return storageOptions.filter(item => !item.remote || props.directory.monitor_type !== 'downloader')
})
// 自动整理方式下拉字典
const transferSourceItems = [
{ title: '不整理', value: '' },
@@ -105,13 +103,13 @@ async function loadTransferTypeItems() {
// 整理方式无数据提示
const computedNoDataText = computed(() => {
if (!props.directory.library_storage && !props.directory.storage) {
return '无可用整理方式!请先选择下载器储存与媒体库储存!'
return '请选择储存'
} else if (!props.directory.library_storage) {
return '无可用整理方式!请先选择媒体库储存'
return '选择媒体库储存'
} else if (!props.directory.storage) {
return '无可用整理方式!请先选择下载器储存'
return '选择下载器储存'
} else {
return '选择的存储没有支持的整理方法!'
return '选择的存储类型没有支持的整理方'
}
})
@@ -131,24 +129,6 @@ function onClose() {
emit('close')
}
// 下载路径更新
function updateDownloadPath(value: string) {
downloadPath.value = value
emit('update:modelValue', {
download: downloadPath.value,
library: libraryPath.value,
})
}
// 媒体库路径更新
function updateLibraryPath(value: string) {
libraryPath.value = value
emit('update:modelValue', {
download: downloadPath.value,
library: libraryPath.value,
})
}
// 根据选中的媒体类型,获取对应的媒体类别
const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }]
@@ -156,7 +136,7 @@ const getCategories = computed(() => {
return default_value.concat(props.categories[props.directory.media_type ?? ''])
})
// 监听 下载储存与媒体库储存 变化,重新加载整理方式下拉字典
// 监听 资源存储与媒体库储存 变化,重新加载整理方式下拉字典
watch(
[() => props.directory.library_storage, () => props.directory.storage],
([newLibraryStorage, newStorage], [oldLibraryStorage, oldStorage]) => {
@@ -181,6 +161,16 @@ watch(
}
},
)
// 监听monitor_type变化如果为downloader则设置为本地
watch(
() => props.directory.monitor_type,
newMonitorType => {
if (newMonitorType === 'downloader') {
props.directory.storage = 'local'
}
},
)
</script>
<template>
@@ -220,19 +210,20 @@ watch(
/>
</VCol>
<VCol cols="4">
<VSelect v-model="props.directory.storage" variant="underlined" :items="storageOptions" label="下载存储" />
<VSelect
v-model="props.directory.storage"
variant="underlined"
:items="resourceStorageOptions"
label="资源存储"
/>
</VCol>
<VCol cols="8">
<VPathField @update:modelValue="updateDownloadPath" :storage="props.directory.storage">
<template #activator="{ menuprops }">
<VTextField
v-model="props.directory.download_path"
v-bind="menuprops"
variant="underlined"
label="下载目录"
/>
</template>
</VPathField>
<VPathField
v-model="props.directory.download_path"
:storage="props.directory.storage"
variant="underlined"
label="资源目录"
/>
</VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
<VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch>
@@ -270,16 +261,12 @@ watch(
/>
</VCol>
<VCol cols="8">
<VPathField @update:modelValue="updateLibraryPath" :storage="props.directory.library_storage">
<template #activator="{ menuprops }">
<VTextField
v-model="props.directory.library_path"
v-bind="menuprops"
variant="underlined"
label="媒体库目录"
/>
</template>
</VPathField>
<VPathField
v-model="props.directory.library_path"
:storage="props.directory.library_storage"
variant="underlined"
label="媒体库目录"
/>
</VCol>
<VCol cols="4">
<VSelect

View File

@@ -6,7 +6,7 @@ import { useToast } from 'vue-toast-notification'
import type { DownloaderInfo } from '@/api/types'
import qbittorrent_image from '@images/logos/qbittorrent.png'
import transmission_image from '@images/logos/transmission.png'
import {cloneDeep} from "lodash";
import { cloneDeep } from 'lodash-es'
// 定义输入
const props = defineProps({
@@ -104,7 +104,7 @@ function saveDownloaderInfo() {
props.downloaders.forEach(item => {
if (item.default && item !== props.downloader) {
item.default = false
$toast.info(`${item.name}存在默认下载器,已替换成【${downloaderInfo.value.name}`)
$toast.info(`存在默认下载器${item.name}】,已替换成【${downloaderInfo.value.name}`)
}
})
}
@@ -172,7 +172,7 @@ onUnmounted(() => {
</div>
</VCardText>
</VCard>
<VDialog v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
<VCard :title="`${props.downloader.name} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="downloaderInfoDialog" />
<VDivider />
@@ -211,7 +211,6 @@ onUnmounted(() => {
<VTextField
v-model="downloaderInfo.config.username"
label="用户名"
placeholder="admin"
hint="登录使用的用户名"
persistent-hint
active
@@ -289,7 +288,6 @@ onUnmounted(() => {
<VTextField
v-model="downloaderInfo.config.username"
label="用户名"
placeholder="admin"
hint="登录使用的用户名"
persistent-hint
active

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { innerFilterRules } from '@/api/constants'
import { CustomRule } from '@/api/types'
import { cloneDeep } from 'lodash'
import { cloneDeep } from 'lodash-es'
// 输入参数
const props = defineProps({

View File

@@ -6,7 +6,7 @@ import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import { useToast } from 'vue-toast-notification'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import filter_group_svg from '@images/svg/filter-group.svg'
import { cloneDeep } from 'lodash'
import { cloneDeep } from 'lodash-es'
// 输入参数
const props = defineProps({
@@ -72,12 +72,17 @@ const getCategories = computed(() => {
// 规则组规则卡片列表
const filterRuleCards = ref<FilterCard[]>([])
// 规则组类型,仅用于导入判断
const filterRuleCardsType = ref<FilterCard>({
pri: '',
rules: [],
})
// 导入代码弹窗
const importCodeDialog = ref(false)
// 导入代码
const importCodeString = ref('')
// 导入代码类型
const importCodeType = ref('')
// 更新规则卡片的值
function updateFilterCardValue(pri: string, rules: string[]) {
@@ -96,7 +101,7 @@ function filterCardClose(pri: string) {
}
// 分享规则
function shareRules() {
async function shareRules() {
if (filterRuleCards.value.length === 0) return
const value = filterRuleCards.value
@@ -105,32 +110,43 @@ function shareRules() {
.join('>')
try {
copyToClipboard(value)
$toast.success('优先级规则已复制到剪贴板')
let success
success = copyToClipboard(value)
if (await success) $toast.success('优先级规则已复制到剪贴板!')
else $toast.error('优先级规则复制失败:可能是浏览器不支持或被用户阻止!')
} catch (error) {
$toast.error('优先级规则复制失败!')
console.error(error)
}
}
// 导入规则
async function importRules() {
importCodeString.value = ''
async function importRules(ruleType: string) {
importCodeType.value = ruleType
importCodeDialog.value = true
}
// 监听导入代码变化
watchEffect(() => {
if (!importCodeString.value) return
if (!importCodeString.value.startsWith(' ')) importCodeString.value = ` ${importCodeString.value}`
if (!importCodeString.value.endsWith(' ')) importCodeString.value = `${importCodeString.value} `
const groups = importCodeString.value.split('>')
filterRuleCards.value = groups.map((group: string, index: number) => ({
pri: (index + 1).toString(),
rules: group.split('&').filter(rule => rule),
}))
})
// 保存导入代码,直接覆盖原有值
function saveCodeString(type: string, code: any) {
try {
code = code.value
if (type === 'priority') {
// 解析值
if (!code) return
// 首尾增加空格
if (!code.startsWith(' ')) code = ` ${code}`
if (!code.endsWith(' ')) code = `${code} `
const groups = code.split('>')
filterRuleCards.value = groups.map((group: string, index: number) => ({
pri: (index + 1).toString(),
rules: group.split('&').filter(rule => rule),
}))
}
} catch (error) {
$toast.error('导入失败!')
console.error(error)
}
}
// 增加卡片
function addFilterCard() {
@@ -204,12 +220,12 @@ function onClose() {
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
<VCard :title="`${props.group.name} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="groupInfoDialog" />
<VDivider />
<VCardText>
<VRow>
<VCardItem class="pt-1">
<VRow class="mt-1">
<VCol cols="12" md="6">
<VTextField
v-model="groupInfo.name"
@@ -241,7 +257,7 @@ function onClose() {
/>
</VCol>
</VRow>
</VCardText>
</VCardItem>
<VCardText>
<draggable
v-model="filterRuleCards"
@@ -268,7 +284,7 @@ function onClose() {
<VBtn color="primary" variant="tonal" @click="addFilterCard">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="success" variant="tonal" @click="importRules">
<VBtn color="success" variant="tonal" @click="importRules('priority')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules">
@@ -279,8 +295,13 @@ function onClose() {
</VCardActions>
</VCard>
</VDialog>
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
</VDialog>
<ImportCodeDialog
v-if="importCodeDialog"
v-model="importCodeDialog"
title="导入规则优先级"
:dataType="importCodeType"
@close="importCodeDialog = false"
@save="saveCodeString"
/>
</div>
</template>

View File

@@ -91,7 +91,17 @@ async function drawImages(imageList: string[]) {
const img = new Image()
img.setAttribute('crossorigin', 'anonymous')
img.src = imgSrc
await new Promise(resolve => (img.onload = resolve))
try {
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve()
img.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`))
})
} catch (error) {
console.error(error)
ctx.fillStyle = '#e5e7eb'
ctx.fillRect(MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1), MARGIN_HEIGHT, POSTER_WIDTH, POSTER_HEIGHT)
return
}
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
const y = MARGIN_HEIGHT

View File

@@ -2,15 +2,16 @@
import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatSeason } from '@/@core/utils/formatters'
import { formatSeason, formatRating } 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 type { MediaInfo, NotExistMediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
import router, { registerAbortController } 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'
import { useUserStore } from '@/stores'
// 输入参数
const props = defineProps({
@@ -22,7 +23,8 @@ const props = defineProps({
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
const store = useStore()
// 用户 Store
const userStore = useUserStore()
// 提示框
const $toast = useToast()
@@ -55,10 +57,10 @@ const subscribeEditDialog = ref(false)
const subscribeId = ref<number>()
// 季详情
const seasonInfos = ref<TmdbSeason[]>([])
const seasonInfos = ref<MediaSeason[]>([])
// 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([])
const seasonsSelected = ref<MediaSeason[]>([])
// 来源角标字典
const sourceIconDict: { [key: string]: any } = {
@@ -67,11 +69,62 @@ const sourceIconDict: { [key: string]: any } = {
bangumi: bangumiImage,
}
// 绑定MediaCard元素
const mediaCardRef = ref<HTMLElement | null>(null)
// 创建Intersection Observer实例
const observer = ref<IntersectionObserver | null>(null)
// 所有站点
const allSites = ref<Site[]>([])
// 选中的站点
const selectedSites = ref<number[]>([])
// 搜索菜单显示状态
const searchMenuShow = ref(false)
// 全选/全不选按钮文字
const checkAllText = computed(() => (selectedSites.value.length === allSites.value.length ? '全不选' : '全选'))
// 全选/全不选
function checkAllSitesorNot() {
if (selectedSites.value.length === allSites.value.length) {
selectedSites.value = []
} else {
selectedSites.value = allSites.value.map(item => item.id)
}
}
// 查询所有站点
async function querySites() {
try {
const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
} catch (error) {
console.log(error)
}
}
// 查询用户选中的站点
async function querySelectedSites() {
try {
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
selectedSites.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 获得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}`
else if (props.media?.bangumi_id) return `bangumi:${props.media?.bangumi_id}`
else return `${props.media?.mediaid_prefix}:${props.media?.media_id}`
}
// 订阅弹窗选择的多季
@@ -90,13 +143,11 @@ function getChipColor(type: string) {
}
// 添加订阅处理
async function handleAddSubscribe() {
if (props.media?.type === '电视剧' && props.media?.tmdb_id) {
// TMDB电视剧
// 查询TMDB所有季信息
if (props.media?.type === '电视剧') {
// 查询所有季信息
await getMediaSeasons()
if (!seasonInfos.value) {
if (!seasonInfos.value || seasonInfos.value.length === 0) {
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
return
}
@@ -112,11 +163,6 @@ async function handleAddSubscribe() {
seasonsSelected.value = []
subscribeSeasonDialog.value = true
}
} else if (props.media?.type === '电视剧') {
// 豆瓣电视剧,只会有一季
const season = props.media?.season ?? 1
// 添加订阅
addSubscribe(season)
} else {
// 电影
addSubscribe()
@@ -141,6 +187,7 @@ async function addSubscribe(season = 0) {
tmdbid: props.media?.tmdb_id,
doubanid: props.media?.douban_id,
bangumiid: props.media?.bangumi_id,
mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '',
season,
best_version,
})
@@ -219,6 +266,9 @@ async function handleCheckSubscribe() {
// 查询当前媒体是否已入库
async function handleCheckExists() {
try {
const abortController = new AbortController()
registerAbortController(abortController)
const { signal } = abortController
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
params: {
tmdbid: props.media?.tmdb_id,
@@ -227,6 +277,7 @@ async function handleCheckExists() {
season: props.media?.season,
mtype: props.media?.type,
},
signal,
})
if (result.success) isExists.value = true
@@ -238,13 +289,16 @@ async function handleCheckExists() {
// 调用API检查是否已订阅电视剧需要指定季
async function checkSubscribe(season = 0) {
try {
const abortController = new AbortController()
registerAbortController(abortController)
const { signal } = abortController
const mediaid = getMediaId()
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: {
season,
title: props.media?.title,
},
signal,
})
return result.id || null
@@ -267,7 +321,6 @@ async function checkSeasonsNotExists() {
let state = 0
if (item.episodes.length === 0) state = 2
else if (item.episodes.length < item.total_episode) state = 1
seasonsNotExisted.value[item.season] = state
})
}
@@ -282,17 +335,26 @@ async function checkSeasonsNotExists() {
// 查询TMDB的所有季信息
async function getMediaSeasons() {
startNProgress()
try {
seasonInfos.value = await api.get(`tmdb/seasons/${props.media?.tmdb_id}`)
seasonInfos.value = await api.get('media/seasons', {
params: {
mediaid: getMediaId(),
title: props.media?.title,
year: props.media?.year,
season: props.media?.season,
},
})
} catch (error) {
console.error(error)
}
doneNProgress()
}
// 查询订阅弹窗规则
async function queryDefaultSubscribeConfig() {
// 非管理员不显示
if (!store.state.auth.superUser) return false
if (!userStore.superUser) return false
try {
let subscribe_config_url = ''
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
@@ -336,16 +398,36 @@ function getExistText(season: number) {
// 打开详情页
function goMediaDetail(isHovering = false) {
if (isHovering) {
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
type: props.media?.type,
},
})
if (props.media?.collection_id) {
// 跳转到合集列表
router.push({
path: `/browse/tmdb/collection/${props.media?.collection_id}`,
query: {
title: props.media?.title,
},
})
} else {
// 跳转到媒体详情页
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
title: props.media?.title,
year: props.media?.year,
type: props.media?.type,
},
})
}
}
}
// 点击搜索
async function clickSearch() {
if (allSites.value?.length > 0) return
querySites()
querySelectedSites()
}
// 开始搜索
function handleSearch() {
router.push({
@@ -354,15 +436,51 @@ function handleSearch() {
keyword: getMediaId(),
type: props.media?.type,
area: 'title',
title: props.media?.title,
year: props.media?.year,
season: props.media?.season,
sites: selectedSites.value.join(','),
},
})
}
// 装载时检查是否已订阅
onBeforeMount(() => {
// 懒加载检查
function handleCheckLazy() {
if (props.media?.collection_id) {
return
}
handleCheckSubscribe()
handleCheckExists()
}
// 在元素进入视窗时触发懒加载函数
function setupIntersectionObserver() {
if (mediaCardRef.value) {
observer.value = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 只要MediaCard进入视窗就调用懒加载的操作
handleCheckLazy()
// 加载后销毁观察者实例
observer.value?.disconnect()
observer.value = null
}
})
},
{ threshold: 0.1 },
)
observer.value.observe(mediaCardRef.value)
}
}
onMounted(() => {
setupIntersectionObserver()
})
onBeforeUnmount(() => {
observer.value?.disconnect()
observer.value = null
})
// 计算图片地址
@@ -407,82 +525,114 @@ function onRemoveSubscribe() {
<template>
<VHover>
<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="goMediaDetail(hover.isHovering ?? false)"
>
<VImg
aspect-ratio="2/3"
:src="getImgUrl"
class="object-cover aspect-w-2 aspect-h-3"
cover
@load="isImageLoaded = true"
@error="imageLoadError = true"
<div ref="mediaCardRef">
<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="goMediaDetail(hover.isHovering ?? false)"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
<VImg
aspect-ratio="2/3"
:src="getImgUrl"
class="object-cover aspect-w-2 aspect-h-3"
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>
<!-- 详情 -->
<VCardText
v-show="hover.isHovering || imageLoadError || searchMenuShow"
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
>
<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 v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
<div v-else class="flex align-center justify-between">
<VMenu close-on-content-click v-model="searchMenuShow" max-width="450">
<template v-slot:activator="{ props }">
<IconBtn v-bind="props" icon="mdi-magnify" color="white" @click.stop="clickSearch" />
</template>
<VList>
<VListItem>
<VChipGroup v-model="selectedSites" column multiple @click.stop>
<VChip
v-for="site in allSites"
:key="site.id"
:color="selectedSites.includes(site.id) ? 'primary' : ''"
filter
variant="outlined"
:value="site.id"
size="small"
>
{{ site.name }}
</VChip>
</VChipGroup>
<div>
<VBtn size="small" variant="text" @click.stop="checkAllSitesorNot">
{{ checkAllText }}
</VBtn>
</div>
</VListItem>
<VListItem>
<VBtn @click="handleSearch" block>搜索</VBtn>
</VListItem>
</VList>
</VMenu>
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
</div>
</template>
</VImg>
<!-- 详情 -->
<VCardText
v-show="hover.isHovering || imageLoadError"
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
>
<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>
<!-- 类型角标 -->
<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>
<!--来源图标-->
<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>
</VCardText>
<!-- 类型角标 -->
<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"
>
{{ formatRating(props.media?.vote_average) }}
</VChip>
<!--来源图标-->
<VAvatar
size="24"
density="compact"
class="absolute bottom-1 right-1"
tile
v-if="!hover.isHovering && isImageLoaded && props.media?.source && !imageLoadError"
>
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
</VAvatar>
</VCard>
</div>
</template>
</VHover>
<!-- 订阅季弹窗 -->
@@ -536,7 +686,7 @@ function onRemoveSubscribe() {
</VList>
</VCardText>
<div class="my-2 text-center">
<VBtn :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
<VBtn size="large" :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
{{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
</VBtn>
</div>

View File

@@ -4,7 +4,7 @@ import type { Context } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数
const props = defineProps({
defineProps({
context: Object as PropType<Context>,
})
@@ -45,8 +45,7 @@ function openTmdbPage(type: string, tmdbId: number) {
</template>
</VImg>
</div>
<div>
<div class="flex-grow">
<VCardItem class="pb-1">
<VCardTitle class="text-center text-md-left">
{{ context?.media_info?.title || context?.meta_info?.name }}

View File

@@ -5,7 +5,7 @@ import emby_image from '@images/logos/emby.png'
import jellyfin_image from '@images/logos/jellyfin.png'
import plex_image from '@images/logos/plex.png'
import api from '@/api'
import { cloneDeep } from 'lodash'
import { cloneDeep } from 'lodash-es'
// 定义输入
const props = defineProps({
@@ -185,7 +185,7 @@ onMounted(() => {
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
<VCard :title="`${props.mediaserver.name} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="mediaServerInfoDialog" />
<VDivider />

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
import { isNullOrEmptyObject } from '@/@core/utils'
import type { Message } from '@/api/types'
import { formatDateDifference } from '@core/utils/formatters'
@@ -45,24 +46,31 @@ function replaceNewLine(value: string) {
</script>
<template>
<VCard variant="tonal" :width="props.width" :height="props.height" @click="openLink">
<VCard variant="tonal" :width="props.width" :height="props.height" @click="openLink" max-width="23rem">
<div v-if="props.message?.image" class="relative text-center card-cover-blurred">
<VImg
:src="props.message?.image"
aspect-ratio="4/3"
aspect-ratio="3/2"
cover
position="top"
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</div>
<div
v-if="props.message?.title && !props.message?.image && !props.message?.note"
v-if="
props.message?.title &&
!props.message?.text &&
!props.message?.image &&
isNullOrEmptyObject(props.message?.note) &&
props.message?.action === 0
"
class="rounded-md text-body-1 py-2 px-4 elevation-2 bg-primary text-white chat-right mb-1"
>
<p class="mb-0">{{ props.message?.title }}</p>
</div>
<VCardTitle v-else-if="props.message?.title">
<VCardTitle v-else-if="props.message?.title" class="break-words whitespace-break-spaces">
{{ props.message?.title }}
</VCardTitle>
<div
@@ -72,13 +80,13 @@ function replaceNewLine(value: string) {
<p class="mb-0">{{ props.message?.text }}</p>
</div>
<VCardText v-if="props.message?.text && props.message?.action === 1" v-html="replaceNewLine(props.message?.text)" />
<VCardText v-if="props.message?.note">
<VCardText v-if="!isNullOrEmptyObject(props.message?.note)">
<VList>
<VListItem v-for="(value, key) in noteToJson()" :key="key" two-line>
<VListItemTitle v-if="value.title_year" class="font-bold">
<VListItemTitle v-if="value.title_year" class="font-bold break-words whitespace-break-spaces">
{{ key + 1 }}. {{ value.title_year }}
</VListItemTitle>
<VListItemTitle v-if="value.enclosure" class="font-bold whitespace-break-spaces">
<VListItemTitle v-if="value.enclosure" class="font-bold break-words whitespace-break-spaces">
{{ key + 1 }}. {{ value.title }} {{ value.volume_factor }} ↑{{ value.seeders }}
</VListItemTitle>
<VListItemSubtitle v-if="value.type">

View File

@@ -7,7 +7,7 @@ import synologychat_image from '@images/logos/synologychat.png'
import slack_image from '@images/logos/slack.webp'
import chrome_image from '@images/logos/chrome.png'
import { useToast } from 'vue-toast-notification'
import { cloneDeep } from "lodash"
import { cloneDeep } from 'lodash-es'
// 定义输入
const props = defineProps({
@@ -132,7 +132,7 @@ function onClose() {
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="notificationInfoDialog" scrollable max-width="40rem" persistent>
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem" persistent>
<VCard :title="`${props.notification.name} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="notificationInfoDialog" />
<VDivider />

View File

@@ -43,11 +43,8 @@ const imageLoadError = ref(false)
// 更新日志弹窗
const releaseDialog = ref(false)
// 计算插件标签
const pluginLabels = computed(() => {
if (!props.plugin?.plugin_label) return []
return props.plugin.plugin_label.split(',')
})
// 插件详情弹窗
const detailDialog = ref(false)
// 图片加载完成
async function imageLoaded() {
@@ -76,7 +73,7 @@ async function installPlugin() {
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
detailDialog.value = false
// 通知父组件刷新
emit('install')
} else {
@@ -149,80 +146,141 @@ const dropdownItems = ref([
</script>
<template>
<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 flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div>
<VCard :width="props.width" :height="props.height" @click="detailDialog = true" class="flex flex-col h-full">
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
></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>
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
></div>
<div class="relative flex-1 min-w-0">
<VCardTitle class="text-white text-lg 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 text-sm px-2 py-0 text-shadow overflow-hidden 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>
</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>
</div>
<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>
<!-- 安装插件进度框 -->
<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" />
<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>
<div class="me-n3 absolute bottom-1 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>
</VCardText>
</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" />
</VCard>
</VDialog>
<!-- 插件详情-->
<VDialog v-if="detailDialog" v-model="detailDialog" max-width="30rem">
<VCard>
<DialogCloseBtn @click="detailDialog = false" />
<VCardText>
<VCol>
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
<div class="mx-auto mt-5">
<VAvatar size="64">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</div>
<div class="flex-grow">
<VCardItem>
<VCardTitle class="text-center text-md-left">
{{ props.plugin?.plugin_name }}
</VCardTitle>
<VCardSubtitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis ..."
>
{{ props.plugin?.plugin_desc }}
</VCardSubtitle>
<VList lines="one">
<VListItem class="ps-0">
<VListItemTitle class="text-center text-md-left">
<span class="font-weight-medium">版本</span>
<span class="text-body-1"> v{{ props.plugin?.plugin_version }}</span>
</VListItemTitle>
</VListItem>
<VListItem class="ps-0">
<VListItemTitle class="text-center text-md-left">
<span class="font-weight-medium">作者</span>
<span class="text-body-1 cursor-pointer" @click="visitPluginPage">
{{ props.plugin?.plugin_author }}
</span>
</VListItemTitle>
</VListItem>
</VList>
<div class="text-center text-md-left">
<VBtn color="primary" @click="installPlugin" prepend-icon="mdi-download"> 安装到本地 </VBtn>
<div class="text-xs mt-2" v-if="props.count">
<VIcon icon="mdi-fire" /> {{ props.count?.toLocaleString() }} 次下载
</div>
</div>
</VCardItem>
</div>
</div>
</VCol>
</VCardText>
</VCard>
</VDialog>
</div>
</template>

View File

@@ -1,20 +1,15 @@
<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 { useDisplay } from 'vuetify'
import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
// 显示器宽度
const display = useDisplay()
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
import PluginDataDialog from '../dialog/PluginDataDialog.vue'
// 输入参数
const props = defineProps({
@@ -46,15 +41,12 @@ const isVisible = ref(true)
// 插件配置页面
const pluginConfigDialog = ref(false)
// 插件配置表单数据
const pluginConfigForm = ref({})
// 菜单显示状态
const menuVisible = ref(false)
// 进度框
const progressDialog = ref(false)
// 插件表单配置项
let pluginFormItems = reactive([])
// 插件数据页面
const pluginInfoDialog = ref(false)
@@ -64,9 +56,6 @@ const progressText = ref('正在更新插件...')
// 用户头像是否加载完成
const isAvatarLoaded = ref(false)
// 插件数据页面配置项
let pluginPageItems = ref([])
// 图片是否加载完成
const isImageLoaded = ref(false)
@@ -134,75 +123,14 @@ async function uninstallPlugin() {
}
}
// 调用API读取表单页面
async function loadPluginForm() {
try {
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
}
} catch (error) {
console.error(error)
}
}
// 调用API读取数据页面
async function loadPluginPage() {
try {
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
if (result) pluginPageItems.value = result
} catch (error) {
console.error(error)
}
}
// 调用API读取配置数据
async function loadPluginConf() {
try {
const result: { [key: string]: any } = await api.get(`plugin/${props.plugin?.id}`)
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) {
progressDialog.value = false
pluginConfigDialog.value = false
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
// 通知父组件刷新
emit('save')
} else {
progressDialog.value = false
$toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`)
}
} catch (error) {
console.error(error)
}
}
// 显示插件数据
async function showPluginInfo() {
// 加载数据
await loadPluginPage()
pluginConfigDialog.value = false
pluginInfoDialog.value = true
}
// 显示插件配置
async function showPluginConfig() {
// 加载表单
await loadPluginForm()
// 加载配置
await loadPluginConf()
// 显示对话框
pluginInfoDialog.value = false
pluginConfigDialog.value = true
@@ -299,6 +227,12 @@ function openPluginDetail() {
else showPluginConfig()
}
// 配置完成
function configDone() {
pluginConfigDialog.value = false
emit('save')
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -390,130 +324,136 @@ watch(
</script>
<template>
<!-- 插件卡片 -->
<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 flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
/>
<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>
</div>
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
<span class="author-info">
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
</VImg>
<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>
<div>
<!-- 插件卡片 -->
<VHover>
<template #default="hover">
<VCard
v-if="isVisible"
v-bind="hover.props"
:width="props.width"
:height="props.height"
@click="openPluginDetail"
class="flex flex-col h-full"
>
<div
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
/>
<div class="relative flex-1 min-w-0">
<VCardTitle class="text-white text-lg 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-0 text-white text-sm text-shadow overflow-hidden 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>
</div>
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
<span class="author-info">
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
</VImg>
<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>
<div class="me-n3 absolute bottom-1 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu v-model="menuVisible" 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>
</VCardText>
<div v-if="hover.isHovering" class="me-n3 absolute top-0 right-5">
<VIcon class="cursor-move text-white">mdi-drag</VIcon>
</div>
<div v-else-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
<VIcon icon="mdi-new-box" class="text-white" />
</div>
</VCard>
</template>
</VHover>
<!-- 插件配置页面 -->
<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" />
</VCardText>
<VCardActions class="pt-3">
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
查看数据
</VBtn>
<VSpacer />
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 插件配置页面 -->
<PluginConfigDialog
v-if="pluginConfigDialog"
v-model="pluginConfigDialog"
:plugin="props.plugin"
@save="configDone"
@close="pluginConfigDialog = false"
@switch="showPluginInfo"
/>
<!-- 插件数据页面 -->
<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>
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
</VCard>
</VDialog>
<!-- 插件数据页面 -->
<PluginDataDialog
v-if="pluginInfoDialog"
v-model="pluginInfoDialog"
:plugin="props.plugin"
@close="pluginInfoDialog = false"
@switch="showPluginConfig"
/>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
<!-- 进度框 -->
<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>
</VCardText>
</VCard>
</VDialog>
<!-- 更新日志 -->
<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 />
<VCardItem>
<VBtn @click="updatePlugin" block>
<template #prepend>
<VIcon icon="mdi-arrow-up-circle-outline" />
</template>
更新到最新版本
</VBtn>
</VCardItem>
</VCard>
</VDialog>
</div>
</template>
<style lang="scss" scoped>

View File

@@ -31,7 +31,7 @@ const getImgUrl = computed(() => {
})
// 跳转播放
function goPlay(isHovering = false) {
function goPlay(isHovering: boolean | null = false) {
if (props.media?.link && isHovering) window.open(props.media?.link, '_blank')
}
</script>
@@ -48,13 +48,11 @@ function goPlay(isHovering = false) {
'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"
@@ -78,7 +76,9 @@ function goPlay(isHovering = false) {
<!-- 详情 -->
<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"
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2 pb-5"
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
@click.stop="goPlay(hover.isHovering)"
>
<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 ...">
@@ -89,9 +89,3 @@ function goPlay(isHovering = false) {
</template>
</VHover>
</template>
<style lang="scss">
.on-hover img {
@apply brightness-50;
}
</style>

View File

@@ -8,7 +8,6 @@ import SiteCookieUpdateDialog from '../dialog/SiteCookieUpdateDialog.vue'
import api from '@/api'
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
import { VCardActions, VExpandTransition, VProgressLinear, VSpacer } from 'vuetify/lib/components/index.mjs'
import { formatFileSize } from '@/@core/utils/formatters'
// 输入参数
@@ -27,7 +26,7 @@ const siteIcon = ref<string>('')
const $toast = useToast()
// 测试按钮文字
const testButtonText = ref('测试')
const testButtonText = ref('测试连通性')
// 测试按钮可用性
const testButtonDisable = ref(false)
@@ -44,9 +43,6 @@ const resourceDialog = ref(false)
// 用户数据弹窗
const siteUserDataDialog = ref(false)
// 站点操作显示
const siteActionShow = ref(false)
// 站点使用统计
const siteStats = ref<SiteStatistic>({})
@@ -69,7 +65,7 @@ async function testSite() {
if (result.success) $toast.success(`${cardProps.site?.name} 连通性测试成功,可正常使用!`)
else $toast.error(`${cardProps.site?.name} 连通性测试失败:${result.message}`)
testButtonText.value = '测试'
testButtonText.value = '测试连通性'
testButtonDisable.value = false
getSiteStats()
@@ -156,8 +152,9 @@ onMounted(() => {
<div>
<VCard
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
class="overflow-hidden"
@click="siteEditDialog = true"
class="overflow-hidden h-full flex flex-col"
@click="handleResourceBrowse"
:ripple="false"
>
<template #image>
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
@@ -166,10 +163,10 @@ onMounted(() => {
</template>
<VCardItem style="padding-block-end: 0">
<VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
{{ cardProps.site?.name }}
</VCardTitle>
<VCardSubtitle>
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
{{ cardProps.site?.url }}
</VCardSubtitle>
</VCardItem>
<VCardText class="py-1">
@@ -195,44 +192,48 @@ onMounted(() => {
</VTooltip>
</VCardText>
<VCardActions>
<VBtn
:icon="siteActionShow ? 'mdi-chevron-up' : 'mdi-chevron-down'"
@click.stop="siteActionShow = !siteActionShow"
/>
<span class="text-sm">
<div class="text-sm">
{{ formatFileSize(cardProps.data?.upload || 0) }} / {{ formatFileSize(cardProps.data?.download || 0) }}
</span>
</div>
<IconBtn>
<VIcon icon="mdi-chevron-down" color="primary" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="siteEditDialog = true" base-color="info">
<template #prepend>
<VIcon icon="mdi-file-edit-outline" />
</template>
<VListItemTitle>编辑站点</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="handleSiteUserData">
<template #prepend>
<VIcon icon="mdi-chart-bell-curve" />
</template>
<VListItemTitle>查看站点数据</VListItemTitle>
</VListItem>
<VListItem variant="plain" :disabled="testButtonDisable" @click.stop="testSite">
<template #prepend>
<VIcon icon="mdi-link" />
</template>
<VListItemTitle>{{ testButtonText }}</VListItemTitle>
</VListItem>
<VListItem variant="plain" v-if="!cardProps.site?.public" @click="handleSiteUpdate">
<template #prepend>
<VIcon icon="mdi-refresh" />
</template>
<VListItemTitle>更新 Cookie & UA</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="openSitePage">
<template #prepend>
<VIcon icon="mdi-open-in-new" />
</template>
<VListItemTitle>访问站点</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
<VSpacer />
</VCardActions>
<VDivider class="mb-1" v-if="siteActionShow" />
<VExpandTransition>
<div v-show="siteActionShow" class="py-1 pe-12">
<VBtn v-if="!cardProps.site?.public" @click.stop="handleSiteUpdate" variant="text">
<template #prepend>
<VIcon icon="mdi-refresh" />
</template>
更新
</VBtn>
<VBtn :disabled="testButtonDisable" @click.stop="testSite" variant="text">
<template #prepend>
<VIcon icon="mdi-link" />
</template>
{{ testButtonText }}
</VBtn>
<VBtn @click.stop="handleResourceBrowse" variant="text">
<template #prepend>
<VIcon icon="mdi-web" />
</template>
浏览
</VBtn>
<VBtn @click.stop="handleSiteUserData" variant="text">
<template #prepend>
<VIcon icon="mdi-chart-bell-curve" />
</template>
数据
</VBtn>
</div>
</VExpandTransition>
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
<span class="absolute top-1 right-8">
<VIcon class="cursor-move">mdi-drag</VIcon>

View File

@@ -144,10 +144,17 @@ onMounted(() => {
<AliyunAuthDialog
v-if="aliyunAuthDialog"
v-model="aliyunAuthDialog"
:conf="props.storage.config || {}"
@close="aliyunAuthDialog = false"
@done="handleDone"
/>
<U115AuthDialog v-if="u115AuthDialog" v-model="u115AuthDialog" @close="u115AuthDialog = false" @done="handleDone" />
<U115AuthDialog
v-if="u115AuthDialog"
v-model="u115AuthDialog"
:conf="props.storage.config || {}"
@close="u115AuthDialog = false"
@done="handleDone"
/>
<RcloneConfigDialog
v-if="rcloneConfigDialog"
v-model="rcloneConfigDialog"

View File

@@ -38,8 +38,11 @@ const subscribeFilesDialog = ref(false)
// 分享订阅弹窗
const subscribeShareDialog = ref(false)
// 当前的订阅状态
const subscribeState = ref<string>(props.media?.state ?? 'P')
// 上一次更新时间
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
const lastUpdateText = computed(() => (props.media?.last_update ? formatDateDifference(props.media.last_update) : ''))
// 图片加载完成响应
function imageLoadHandler() {
@@ -81,13 +84,39 @@ async function searchSubscribe() {
}
}
// 切换订阅状态
async function toggleSubscribeStatus(state: 'R' | 'S') {
try {
// 根据传入的 state 判断对应的操作文字
const action = state === 'S' ? '暂停' : '启用'
// 弹出确认框
const isConfirmed = await createConfirm({
title: `确认${action}`,
content: `是否${action}订阅 ${props.media?.name}`,
})
if (!isConfirmed) return
// 调用 API 更新订阅状态
const result: { [key: string]: any } = await api.put(`subscribe/status/${props.media?.id}?state=${state}`)
// 提示
if (result.success) {
$toast.success(`${props.media?.name}${action}`)
subscribeState.value = state
emit('save')
} else {
$toast.error(`${action}失败:${result.message}`)
}
} catch (e) {
console.log(e)
}
}
// 重置订阅
async function resetSubscribe() {
// 确认
try {
const isConfirmed = await createConfirm({
title: '确认',
content: `重置后 ${props.media?.name} 已下载记录将被清除,未入库的剧集将会重新下载,是否确认?`,
content: `重置后 ${props.media?.name} 将恢复初始状态,已下载记录将被清除,未入库的内容将会重新下载,是否确认?`,
})
if (!isConfirmed) return
// 重置
@@ -95,6 +124,7 @@ async function resetSubscribe() {
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 重置成功!`)
subscribeState.value = 'R'
emit('save')
} else $toast.error(`${props.media?.name} 重置失败:${result.message}`)
} catch (e) {
@@ -112,12 +142,22 @@ async function editSubscribeDialog() {
subscribeEditDialog.value = true
}
// 获得mediaid
function getMediaId() {
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
else if (props.media?.bangumiid) return `bangumi:${props.media?.bangumiid}`
else return props.media?.mediaid
}
// 查看媒体详情
async function viewMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
mediaid: getMediaId(),
title: props.media?.name,
year: props.media?.year,
type: props.media?.type,
},
})
@@ -129,7 +169,7 @@ async function viewSubscribeFiles() {
}
// 弹出菜单
const dropdownItems = ref([
const dropdownItems = computed(() => [
{
title: '编辑',
value: 1,
@@ -163,18 +203,26 @@ const dropdownItems = ref([
},
},
{
title: '重置',
title: subscribeState.value === 'S' ? '启用' : '暂停',
value: 5,
props: {
prependIcon: subscribeState.value === 'S' ? 'mdi-play' : 'mdi-pause',
click: () => toggleSubscribeStatus(subscribeState.value === 'S' ? 'R' : 'S'),
color: subscribeState.value === 'S' ? 'success' : 'info',
},
},
{
title: '重置',
value: 6,
props: {
prependIcon: 'mdi-restore-alert',
click: resetSubscribe,
color: 'warning',
},
show: props.media?.type === '电视剧',
},
{
title: '分享',
value: 6,
value: 7,
props: {
prependIcon: 'mdi-share',
click: shareSubscribe,
@@ -184,7 +232,7 @@ const dropdownItems = ref([
},
{
title: '取消订阅',
value: 7,
value: 8,
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
@@ -201,6 +249,14 @@ watch(
},
)
// 监听订阅状态
watch(
() => props.media?.state,
newState => {
subscribeState.value = newState ?? 'P'
},
)
// 计算backdrop图片地址
const backdropUrl = computed(() => {
const url = props.media?.backdrop || props.media?.poster
@@ -233,129 +289,137 @@ function onSubscribeEditRemove() {
</script>
<template>
<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="white" />
<VMenu activator="parent" close-on-content-click>
<VList>
<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 #image>
<VImg :src="backdropUrl || posterUrl" 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="posterUrl" 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>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col rounded-lg h-full"
:class="{
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'opacity-70': subscribeState === 'S',
}"
min-height="170"
@click="editSubscribeDialog"
:ripple="false"
>
<div class="me-n3 absolute top-1 right-2">
<IconBtn>
<VIcon icon="mdi-dots-vertical" color="white" />
<VMenu activator="parent" close-on-content-click>
<VList>
<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>
</div>
</VCard>
</template>
</VHover>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="props.media?.id"
@remove="onSubscribeEditRemove"
@save="onSubscribeEditSave"
@close="subscribeEditDialog = false"
/>
<template #image>
<VImg :src="backdropUrl || posterUrl" 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>
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
</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="posterUrl" 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 v-if="hover.isHovering" class="me-n3 absolute top-1 right-10">
<IconBtn><VIcon class="cursor-move text-white">mdi-drag</VIcon></IconBtn>
</div>
</div>
</VCard>
</template>
</VHover>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="props.media?.id"
@remove="onSubscribeEditRemove"
@save="onSubscribeEditSave"
@close="subscribeEditDialog = false"
/>
<!-- 订阅文件信息弹窗 -->
<SubscribeFilesDialog
v-if="subscribeFilesDialog"
v-model="subscribeFilesDialog"
:subid="props.media?.id"
@close="subscribeFilesDialog = false"
/>
<!-- 分享订阅弹窗 -->
<SubscribeShareDialog
v-if="subscribeShareDialog"
v-model="subscribeShareDialog"
:sub="props.media"
@close="subscribeShareDialog = false"
/>
<!-- 订阅文件信息弹窗 -->
<SubscribeFilesDialog
v-if="subscribeFilesDialog"
v-model="subscribeFilesDialog"
:subid="props.media?.id"
@close="subscribeFilesDialog = false"
/>
<!-- 分享订阅弹窗 -->
<SubscribeShareDialog
v-if="subscribeShareDialog"
v-model="subscribeShareDialog"
:sub="props.media"
@close="subscribeShareDialog = false"
/>
</div>
</template>
<style lang="scss">
.subscribe-card-background {

View File

@@ -1,23 +1,17 @@
<script lang="ts" setup>
import { formatDateDifference } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { SubscribeShare } from '@/api/types'
import router from '@/router'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import ForkSubscribeDialog from '../dialog/ForkSubscribeDialog.vue'
// 输入参数
const props = defineProps({
media: Object as PropType<SubscribeShare>,
})
// 提示框
const $toast = useToast()
// 确认框
const createConfirm = useConfirm()
// 定义删除事件
const emit = defineEmits(['delete'])
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
@@ -28,6 +22,9 @@ const imageLoaded = ref(false)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 复用订阅弹窗
const forkSubscribeDialog = ref(false)
// 订阅ID
const subscribeId = ref<number>()
@@ -57,121 +54,128 @@ const posterUrl = computed(() => {
return url
})
// 获得mediaid
function getMediaId() {
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
}
// 查看媒体详情
async function viewMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
mediaid: getMediaId(),
title: props.media?.name,
year: props.media?.year,
type: props.media?.type,
},
})
}
// 复用订阅
async function forkSubscribe() {
// 开始处理
startNProgress()
try {
// 确认
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认添加来自 ${props.media?.share_user} 分享的订阅:${props.media?.share_title}`,
})
if (!isConfirmed) return
function showForkSubscribe() {
forkSubscribeDialog.value = true
}
// 请求API
const result: { [key: string]: any } = await api.post('subscribe/fork', props.media)
// 完成复用订阅
function finishForkSubscribe(subid: number) {
subscribeId.value = subid
forkSubscribeDialog.value = false
subscribeEditDialog.value = true
}
// 订阅状态
if (result.success) {
$toast.success(`${props.media?.share_title} 添加订阅成功!`)
// 弹出订阅编辑弹窗
subscribeId.value = result.data.id
subscribeEditDialog.value = true
} else {
$toast.error(`${props.media?.share_title} 添加订阅失败:${result.message}`)
}
} catch (error) {
console.error(error)
} finally {
doneNProgress()
}
// 删除订阅分享时处理
function doDelete() {
forkSubscribeDialog.value = false
// 通知父组件刷新
emit('delete')
}
</script>
<template>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
}"
min-height="170"
@click="forkSubscribe"
>
<template #image>
<VImg :src="backdropUrl || posterUrl" 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 class="h-full">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col rounded-lg h-full"
:class="{
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
}"
min-height="170"
@click="showForkSubscribe"
>
<template #image>
<VImg :src="backdropUrl || posterUrl" 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 class="h-full flex flex-col">
<VCardText class="flex items-center pb-1 grow">
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
<VImg :src="posterUrl" 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>
</template>
<div class="absolute inset-0 subscribe-card-background"></div>
</VImg>
</template>
<div>
<VCardText class="flex items-center pb-1">
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
<VImg :src="posterUrl" 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 pl-2 xl:pl-4">
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.share_title }}
<div class="flex flex-col justify-center pl-2 xl:pl-4">
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.share_title }}
</div>
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
{{ props.media?.share_comment }}
</div>
</div>
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
{{ props.media?.share_comment }}
</VCardText>
<VCardText class="flex justify-space-between align-center flex-wrap">
<div class="flex align-center">
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
<div class="text-subtitle-2 me-4 text-white">
{{ props.media?.share_user }}
</div>
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
{{ props.media?.count.toLocaleString() }}
</span>
</div>
</div>
</VCardText>
<VCardText class="flex justify-space-between align-center flex-wrap">
<div class="flex align-center">
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
<div class="text-subtitle-2 me-4 text-white">
{{ props.media?.share_user }}
</div>
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="error" class="me-1" />
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
{{ props.media?.count.toLocaleString() }}
</span>
</div>
</VCardText>
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VIcon icon="mdi-calcdar" class="me-1" />
{{ dateText }}
</VCardText>
</div>
</VCard>
</template>
</VHover>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="subscribeEditDialog = false"
/>
</VCardText>
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VIcon icon="mdi-calcdar" class="me-1" />
{{ dateText }}
</VCardText>
</div>
</VCard>
</template>
</VHover>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="subscribeEditDialog = false"
/>
<!-- 复用订阅弹窗 -->
<ForkSubscribeDialog
v-if="forkSubscribeDialog"
v-model="forkSubscribeDialog"
:media="props.media"
@close="forkSubscribeDialog = false"
@fork="finishForkSubscribe"
@delete="doDelete"
/>
</div>
</template>
<style lang="scss">
.subscribe-card-background {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import api from '@/api'
import { Subscribe, User } from '@/api/types'
import store from '@/store'
import { useUserStore } from '@/stores'
import avatar1 from '@images/avatars/avatar-1.png'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
@@ -22,10 +22,10 @@ const props = defineProps({
})
// 当前用户的ID
const currentLoginUserId = computed(() => store.state.auth.userID)
const currentLoginUserId = computed(() => useUserStore().userID)
// 当前用户是否是管理员
const currentUserIsSuperuser = computed(() => store.state.auth.superUser)
const currentUserIsSuperuser = computed(() => useUserStore().superUser)
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save'])
@@ -161,14 +161,7 @@ onMounted(() => {
</VList>
</VCardText>
<VCardText class="flex flex-row justify-center">
<VBtn
v-if="currentUserIsSuperuser"
color="primary"
class="me-4"
@click="editUser"
>
编辑
</VBtn>
<VBtn v-if="currentUserIsSuperuser" color="primary" class="me-4" @click="editUser"> 编辑 </VBtn>
<VBtn
v-if="currentUserIsSuperuser && props.user.id != currentLoginUserId"
color="error"

View File

@@ -0,0 +1,318 @@
<script lang="ts" setup>
import { Workflow } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
import WorkflowActionsDialog from '@/components/dialog/WorkflowActionsDialog.vue'
import api from '@/api'
// 定义输入参数
const props = defineProps({
workflow: {
required: true,
type: Object as PropType<Workflow>,
},
})
// 定义事件
const emit = defineEmits(['refresh'])
// 提示框
const $toast = useToast()
// 确认框
const createConfirm = useConfirm()
// 编辑对话框
const editDialog = ref(false)
// 流程对话框
const flowDialog = ref(false)
// 加载中
const loading = ref(false)
// 编辑任务
function handleEdit(item: Workflow) {
editDialog.value = true
}
// 编辑流程
function handleFlow(item: Workflow) {
flowDialog.value = true
}
// 计算已完成的动作数
function resolveDoneActions(item: Workflow) {
return item.current_action?.split(',').length || 0
}
// 编辑完成
function editDone() {
editDialog.value = false
flowDialog.value = false
emit('refresh')
}
// 删除任务
async function handleDelete(item: Workflow) {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认删除任务 ${item.name} ?`,
})
if (!isConfirmed) return
try {
const result: { [key: string]: string } = await api.delete(`workflow/${item.id}`)
if (result.success) {
$toast.success('删除任务成功!')
emit('refresh')
} else {
$toast.error(`删除任务失败:${result.message}`)
}
} catch (error) {
console.error(error)
}
}
// 开始任务
async function handleEnable(item: Workflow) {
loading.value = true
try {
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/start`)
if (result.success) {
$toast.success('启用任务成功!')
emit('refresh')
} else {
$toast.error(`启用任务失败:${result.message}`)
}
} catch (error) {
console.error(error)
}
loading.value = false
}
// 停用任务
async function handlePause(item: Workflow) {
loading.value = true
try {
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/pause`)
if (result.success) {
$toast.success('停用任务成功!')
emit('refresh')
} else {
$toast.error(`停用任务失败:${result.message}`)
}
} catch (error) {
console.error(error)
}
loading.value = false
}
// 立即执行任务
async function handleRun(item: Workflow, from_begin: boolean) {
loading.value = true
try {
setTimeout(() => {
emit('refresh')
}, 500)
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/run?from_begin=${from_begin}`, {
from_begin,
})
if (result.success) {
$toast.success('任务执行完成!')
emit('refresh')
} else {
$toast.error(`任务执行失败:${result.message}`)
emit('refresh')
}
} catch (error) {
console.error(error)
}
loading.value = false
}
// 重置任务
async function handleReset(item: Workflow) {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认重置任务 ${item.name} ?`,
})
if (!isConfirmed) return
try {
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/reset`)
if (result.success) {
$toast.success('重置任务成功!')
emit('refresh')
} else {
$toast.error(`重置任务失败:${result.message}`)
}
} catch (error) {
console.error(error)
}
}
// 计算状态颜色
const resolveStatusVariant = (status: string | undefined) => {
if (status === 'S') return { color: 'success', text: '成功' }
else if (status === 'R') return { color: 'primary', text: '运行中' }
else if (status === 'F') return { color: 'error', text: '失败' }
else if (status === 'P') return { color: 'secondary', text: '暂停' }
else return { color: 'info', text: '等待' }
}
// 计算当前动作占比
const resolveProgress = (item: Workflow) => {
const current_action_length = item.current_action?.split(',').length || 0
return item.actions?.length ? Math.round((current_action_length / (item.actions.length || 1)) * 100) : 0
}
</script>
<template>
<div class="h-full">
<VCard class="mx-auto h-full" @click="handleFlow(workflow)" :ripple="false" :loading="loading">
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
<template #prepend>
<VAvatar variant="text" class="me-2">
<VIcon
v-if="workflow?.state === 'P'"
color="success"
size="x-large"
icon="mdi-play"
@click.stop="handleEnable(workflow)"
/>
<VIcon v-else color="warning" icon="mdi-pause" size="x-large" @click.stop="handlePause(workflow)" />
</VAvatar>
</template>
<VCardTitle class="text-white">
{{ workflow?.name }}
</VCardTitle>
<VCardSubtitle class="text-white">{{ workflow?.description }}</VCardSubtitle>
<template #append>
<IconBtn>
<VIcon icon="mdi-vector-polyline-edit" @click.stop="handleFlow(workflow)" />
</IconBtn>
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" base-color="primary" @click="handleEdit(workflow)">
<template #prepend>
<VIcon icon="mdi-note-edit" />
</template>
<VListItemTitle>编辑任务</VListItemTitle>
</VListItem>
<VListItem
v-if="workflow.current_action"
variant="plain"
base-color="info"
@click="handleRun(workflow, false)"
>
<template #prepend>
<VIcon icon="mdi-play-speed" />
</template>
<VListItemTitle>继续执行</VListItemTitle>
</VListItem>
<VListItem
v-if="workflow.current_action"
variant="plain"
base-color="info"
@click="handleRun(workflow, true)"
>
<template #prepend>
<VIcon icon="mdi-replay" />
</template>
<VListItemTitle>重新执行</VListItemTitle>
</VListItem>
<VListItem v-else variant="plain" base-color="info" @click="handleRun(workflow, true)">
<template #prepend>
<VIcon icon="mdi-run" />
</template>
<VListItemTitle>立即执行</VListItemTitle>
</VListItem>
<VListItem variant="plain" base-color="warning" @click="handleReset(workflow)">
<template #prepend>
<VIcon icon="mdi-restore-alert" />
</template>
<VListItemTitle>重置任务</VListItemTitle>
</VListItem>
<VListItem variant="plain" base-color="error" @click="handleDelete(workflow)">
<template #prepend>
<VIcon icon="mdi-delete" />
</template>
<VListItemTitle>删除任务</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>
</VCardItem>
<VDivider />
<VCardText>
<div class="d-flex flex-column gap-y-4">
<div class="d-flex flex-wrap gap-x-6">
<div class="flex-1">
<div class="mb-1">定时</div>
<h5 class="text-h6">{{ workflow?.timer }}</h5>
</div>
<div class="flex-1">
<div class="mb-1">状态</div>
<h5 class="text-h6" :class="`text-${resolveStatusVariant(workflow?.state).color}`">
{{ resolveStatusVariant(workflow?.state).text }}
</h5>
</div>
</div>
<div class="d-flex flex-wrap gap-x-6">
<div class="flex-1">
<div class="mb-1">动作数</div>
<div>
<VAvatar size="32" color="primary" variant="tonal">
<span class="text-sm">{{ workflow?.actions?.length }}</span>
</VAvatar>
</div>
</div>
<div class="flex-1">
<div class="mb-1">已执行次数</div>
<h5 class="text-h6">{{ workflow?.run_count }}</h5>
</div>
</div>
<div class="d-flex flex-wrap gap-x-6">
<div class="flex-1">
<div class="mb-1">进度</div>
<div class="d-flex align-center gap-5">
<div class="flex-grow-1">
<VProgressLinear color="info" rounded :model-value="resolveProgress(workflow)" />
</div>
<span> {{ resolveProgress(workflow) }}% </span>
</div>
</div>
</div>
<div class="d-flex flex-wrap gap-x-6" v-if="workflow?.result">
<div class="flex-1">
<div class="mb-1">错误信息</div>
<div class="text-error">{{ workflow?.result }}</div>
</div>
</div>
</div>
</VCardText>
</VCard>
<!-- 流程对话框 -->
<WorkflowActionsDialog
v-if="flowDialog"
v-model="flowDialog"
@close="flowDialog = false"
@save="editDone"
:workflow="workflow"
/>
<!-- 编辑对话框 -->
<WorkflowAddEditDialog
v-if="editDialog"
v-model="editDialog"
@close="editDialog = false"
@save="editDone"
:workflow="workflow"
/>
</div>
</template>

View File

@@ -13,6 +13,9 @@ const props = defineProps({
torrent: Object as PropType<TorrentInfo>,
})
// 定义成功和失败事件
const emit = defineEmits(['done', 'error', 'close'])
// 提示框
const $toast = useToast()
@@ -22,8 +25,8 @@ const selectedDownloader = ref<string | null>(null)
// 选择的保存目录
const selectedDirectory = ref<string | null>(null)
// 定义成功和失败事件
const emit = defineEmits(['done', 'error', 'close'])
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 所有目录设置
const directories = ref<TransferDirectoryConf[]>([])
@@ -53,14 +56,10 @@ const targetDirectories = computed(() => {
return [...new Set(downloadDirectories)]
})
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
downloaders.value = result.data?.value ?? []
downloaders.value = await api.get('download/clients')
} catch (error) {
console.log(error)
}
@@ -139,7 +138,7 @@ onMounted(() => {
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItem v-if="torrent?.description">
<template #prepend>
<VIcon icon="mdi-subtitles-outline"></VIcon>
</template>
@@ -147,7 +146,7 @@ onMounted(() => {
<span class="text-body-1 whitespace-break-spaces">{{ torrent?.description }}</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItem v-if="torrent?.size">
<template #prepend>
<VIcon icon="mdi-database"></VIcon>
</template>
@@ -165,7 +164,7 @@ onMounted(() => {
<VSelect
v-model="selectedDownloader"
:items="downloaderOptions"
label="指定下载器"
label="下载器(默认)"
variant="underlined"
placeholder="留空默认"
/>
@@ -174,7 +173,7 @@ onMounted(() => {
<VCombobox
v-model="selectedDirectory"
:items="targetDirectories"
label="指定保存目录"
label="保存目录(自动)"
placeholder="留空自动匹配"
variant="underlined"
/>

View File

@@ -2,6 +2,14 @@
import QrcodeVue from 'qrcode.vue'
import api from '@/api'
// 定义输入
const props = defineProps({
conf: {
type: Object as PropType<{ [key: string]: any }>,
required: true,
},
})
// 定义事件
const emit = defineEmits(['done', 'close'])
@@ -25,6 +33,7 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
// 完成
async function handleDone() {
clearTimeout(timeoutTimer)
emit('done')
}
@@ -36,6 +45,7 @@ async function getQrcode() {
qrCodeContent.value = result.data.codeContent
ck.value = result.data.ck
t.value = result.data.t
timeoutTimer = setTimeout(checkQrcode, 3000)
} else {
text.value = result.message
}
@@ -77,7 +87,6 @@ async function checkQrcode() {
onMounted(async () => {
await getQrcode()
timeoutTimer = setTimeout(checkQrcode, 3000)
})
onUnmounted(() => {

View File

@@ -0,0 +1,277 @@
<script setup lang="ts">
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { SubscribeShare } from '@/api/types'
import router from '@/router'
import { useToast } from 'vue-toast-notification'
import { VBtn } from 'vuetify/lib/components/index.mjs'
// 输入参数
const props = defineProps({
media: Object as PropType<SubscribeShare>,
})
// 定义事件
const emit = defineEmits(['fork', 'delete', 'close'])
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 提示框
const $toast = useToast()
// 处理中
const processing = ref(false)
// 删除中
const deleting = ref(false)
// 是否折叠
const isExpanded = ref(false)
// follow用户列表
const followUsers = ref<string[]>([])
// 当前用户是否已follow
const isFollowed = computed(() => followUsers.value.includes(props.media?.share_uid || ''))
// 折叠展开
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
// 加载follow用户列表
async function queryFollowUsers() {
try {
const result: { [key: string]: any } = await api.get('system/setting/FollowSubscribers')
followUsers.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// follow用户
async function followUser() {
try {
const result: { [key: string]: any } = await api.post(`subscribe/follow?share_uid=${props.media?.share_uid}`)
if (result.success) {
queryFollowUsers()
}
} catch (error) {
console.log(error)
}
}
// unfollow用户
async function unfollowUser() {
try {
const result: { [key: string]: any } = await api.delete('subscribe/follow', {
params: {
share_uid: props.media?.share_uid,
},
})
if (result.success) {
queryFollowUsers()
}
} catch (error) {
console.log(error)
}
}
// 计算海报图片地址
const posterUrl = computed(() => {
const url = props.media?.poster
// 使用图片缓存
if (globalSettings.GLOBAL_IMAGE_CACHE && url)
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
return url
})
// 获得mediaid
function getMediaId() {
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
}
// 查看媒体详情
async function viewMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
title: props.media?.name,
year: props.media?.year,
type: props.media?.type,
},
})
}
// 复用订阅
async function doFork() {
// 开始处理
startNProgress()
try {
processing.value = true
// 请求API
const result: { [key: string]: any } = await api.post('subscribe/fork', props.media)
// 订阅状态
if (result.success) {
$toast.success(`${props.media?.share_title} 添加订阅成功!`)
// 完成
emit('fork', result.data.id)
} else {
$toast.error(`${props.media?.share_title} 添加订阅失败:${result.message}`)
}
} catch (error) {
console.error(error)
} finally {
processing.value = false
doneNProgress()
}
}
// 删除订阅分享
async function doDelete() {
// 开始处理
startNProgress()
try {
deleting.value = true
// 请求API
const result: { [key: string]: any } = await api.delete(`subscribe/share/${props.media?.id}`, {
params: {
share_uid: globalSettings.USER_UNIQUE_ID,
},
})
// 订阅状态
if (result.success) {
$toast.success(`${props.media?.share_title} 取消分享成功!`)
// 完成
emit('delete', result.data.id)
} else {
$toast.error(`${props.media?.share_title} 取消分享失败:${result.message}`)
}
} catch (error) {
console.error(error)
} finally {
deleting.value = false
doneNProgress()
}
}
onMounted(() => {
queryFollowUsers()
})
</script>
<template>
<VDialog max-width="40rem" scrollable>
<VCard>
<DialogCloseBtn @click="emit('close')" />
<VCardText>
<VCol>
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
<div class="ma-auto">
<VImg
width="10rem"
aspect-ratio="2/3"
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
:src="posterUrl"
@click="viewMediaDetail"
cover
>
<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-grow">
<VCardItem>
<VCardTitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-2 overflow-hidden text-ellipsis"
>
{{ props.media?.share_title }}
</VCardTitle>
<VCardSubtitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis"
>
{{ props.media?.share_comment }}
</VCardSubtitle>
<VList lines="one">
<VListItem class="ps-0">
<VListItemTitle class="text-center text-md-left">
<span class="font-weight-medium">分享人</span>
<span class="text-body-1"> {{ media?.share_user }}</span>
</VListItemTitle>
</VListItem>
<VListItem class="ps-0" v-if="media?.keyword">
<VListItemTitle class="text-center text-md-left">
<span class="font-weight-medium">搜索词</span>
<span class="text-body-1"> {{ media?.keyword }}</span>
</VListItemTitle>
</VListItem>
<VListItem class="ps-0" v-if="media?.custom_words" @click.stop="toggleExpand">
<VListItemTitle
class="text-center text-md-left break-words whitespace-break-spaces"
:class="{
'line-clamp-4 overflow-hidden text-ellipsis': !isExpanded,
}"
>
<span class="font-weight-medium">识别词</span>
<span class="text-body-1"> {{ media?.custom_words }}</span>
</VListItemTitle>
</VListItem>
</VList>
<div class="text-center text-md-left">
<div>
<VBtn
color="primary"
:disabled="processing"
@click="doFork"
prepend-icon="mdi-heart"
:loading="processing"
>
订阅
</VBtn>
<VBtn
v-if="props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID"
color="error"
:disabled="deleting"
@click="doDelete"
prepend-icon="mdi-delete"
:loading="deleting"
class="ms-2"
>
取消分享
</VBtn>
<VBtn
v-else-if="isFollowed && props.media?.share_uid"
color="warning"
@click="unfollowUser"
prepend-icon="mdi-account-remove"
class="ms-2"
>
取消关注
</VBtn>
<VBtn
v-else-if="props.media?.share_uid"
@click="followUser"
color="info"
prepend-icon="mdi-account-plus"
class="ms-2"
>
关注
</VBtn>
</div>
<div class="text-xs mt-2" v-if="props.media?.count">
<VIcon icon="mdi-fire" /> {{ props.media?.count?.toLocaleString() }} 次复用
</div>
</div>
</VCardItem>
</div>
</div>
</VCol>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -2,17 +2,18 @@
// 输入参数
const props = defineProps({
title: String,
dataType: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 定义事件
const emit = defineEmits(['close', 'save'])
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('save', props.dataType, codeString)
emit('close')
}
</script>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import { Context } from '@/api/types'
import MediaInfoCard from '../cards/MediaInfoCard.vue'
// 输入参数
defineProps({
context: Object as PropType<Context>,
})
// 定义事件
const emit = defineEmits(['close'])
</script>
<template>
<VDialog max-width="50rem">
<VCard>
<DialogCloseBtn @click="emit('close')" />
<VCardItem>
<MediaInfoCard :context="context" />
</VCardItem>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import { useDisplay } from 'vuetify'
import type { Plugin } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import FormRender from '../render/FormRender.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
// 输入参数
const props = defineProps({
plugin: {
type: Object as PropType<Plugin>,
},
})
// 定义事件
const emit = defineEmits(['close', 'save', 'switch'])
// 显示器宽度
const display = useDisplay()
// 插件配置表单数据
const pluginConfigForm = ref({})
// 插件表单配置项
let pluginFormItems = reactive([])
// 进度框
const progressDialog = ref(false)
// 进度文字
const progressText = ref('')
// 提示框
const $toast = useToast()
// 是否刷新
const isRefreshed = ref(false)
// 调用API读取表单页面
async function loadPluginForm() {
try {
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
}
} catch (error) {
console.error(error)
}
isRefreshed.value = true
}
// 调用API读取配置数据
async function loadPluginConf() {
try {
const result: { [key: string]: any } = await api.get(`plugin/${props.plugin?.id}`)
if (!isNullOrEmptyObject(result)) pluginConfigForm.value = result
} catch (error) {
console.error(error)
}
isRefreshed.value = true
}
// 调用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) {
progressDialog.value = false
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
// 通知父组件刷新
emit('save')
} else {
progressDialog.value = false
$toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`)
}
} catch (error) {
console.error(error)
}
}
onBeforeMount(async () => {
await loadPluginForm()
await loadPluginConf()
})
</script>
<template>
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText v-if="isRefreshed">
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :model="pluginConfigForm" />
</VCardText>
<VCardActions class="pt-3">
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" variant="outlined" color="info"> 查看数据 </VBtn>
<VSpacer />
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
</VCardActions>
</VCard>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</VDialog>
</template>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { useDisplay } from 'vuetify'
import type { Plugin } from '@/api/types'
import PageRender from '@/components/render/PageRender.vue'
import api from '@/api'
// 输入参数
const props = defineProps({
plugin: {
type: Object as PropType<Plugin>,
},
})
// 定义事件
const emit = defineEmits(['close', 'save', 'switch'])
// 显示器宽度
const display = useDisplay()
// APP
const appMode = inject('pwaMode') && display.mdAndDown.value
// 是否刷新
const isRefreshed = ref(false)
// 插件数据页面配置项
let pluginPageItems = ref([])
// 调用API读取数据页面
async function loadPluginPage() {
try {
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
if (result) pluginPageItems.value = result
} catch (error) {
console.error(error)
}
isRefreshed.value = true
}
onMounted(() => {
loadPluginPage()
})
</script>
<template>
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
<VCardText v-else class="min-h-40">
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
</VCardText>
<VFab
icon="mdi-cog"
location="bottom"
size="x-large"
fixed
app
appear
@click="emit('switch')"
:class="{ 'mb-10': appMode }"
/>
</VCard>
</VDialog>
</template>

View File

@@ -34,7 +34,7 @@ async function saveHandle() {
if (result.success) {
$toast.success('插件仓库保存成功')
emit('save')
} else $toast.error('插件仓库保存失败')
} else $toast.error(`插件仓库保存失败${result?.message}`)
} catch (error) {
console.log(error)
}

View File

@@ -2,11 +2,11 @@
import { useToast } from 'vue-toast-notification'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
import api from '@/api'
import { storageOptions } from '@/api/constants'
import { storageOptions, transferTypeOptions } from '@/api/constants'
import { numberValidator } from '@/@validators'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
import { FileItem, TransferDirectoryConf } from '@/api/types'
import { FileItem, TransferDirectoryConf, TransferForm } from '@/api/types'
// 显示器宽度
const display = useDisplay()
@@ -49,7 +49,7 @@ const progressEventSource = ref<EventSource>()
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
const progressText = ref('正在处理 ...')
// 整理进度
const progressValue = ref(0)
@@ -65,21 +65,21 @@ const dialogTitle = computed(() => {
return '手动整理'
})
// 禁用指定集数
const disableEpisodeDetail = computed(() => {
if (props.items) {
if (transferForm.episode_format) return false
return !(props.items.length === 1 && props.items[0].type !== 'dir')
}
})
// 表单
const transferForm = reactive({
fileitem: {},
const transferForm = reactive<TransferForm>({
fileitem: {} as FileItem,
logid: 0,
target_storage: props.target_storage ?? 'local',
target_path: props.target_path ?? null,
tmdbid: null,
doubanid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
target_path: '',
min_filesize: 0,
scrape: false,
from_history: false,
@@ -87,7 +87,6 @@ const transferForm = reactive({
// 所有媒体库目录
const directories = ref<TransferDirectoryConf[]>([])
// 查询目录
async function loadDirectories() {
try {
@@ -100,7 +99,8 @@ async function loadDirectories() {
// 目的目录下拉框
const targetDirectories = computed(() => {
return directories.value.map(item => item.library_path)
const libraryDirectories = directories.value.map(item => item.library_path)
return [...new Set(libraryDirectories)]
})
// 监听目的路径变化,配置默认值
@@ -110,14 +110,52 @@ watch(
if (newPath) {
const directory = directories.value.find(item => item.library_path === newPath)
if (directory) {
transferForm.target_storage = directory.storage ?? 'local'
transferForm.transfer_type = directory.transfer_type ?? ''
transferForm.target_storage = directory.library_storage ?? 'local'
transferForm.transfer_type = transferForm.transfer_type || directory.transfer_type
transferForm.scrape = directory.scraping ?? false
transferForm.library_category_folder = directory.library_category_folder ?? false
transferForm.library_type_folder = directory.library_type_folder ?? false
} else {
transferForm.transfer_type = transferForm.transfer_type || 'copy'
transferForm.scrape = false
transferForm.library_category_folder = false
transferForm.library_type_folder = false
}
} else {
// 路径为空时, 恢复到`自动`条件
transferForm.transfer_type = ''
transferForm.library_type_folder = undefined
transferForm.library_category_folder = undefined
}
},
)
// 整理文件
async function handleTransfer(item: FileItem, background: boolean = false) {
transferForm.fileitem = item
transferForm.logid = 0
try {
const result: { [key: string]: any } = await api.post(`transfer/manual?background=${background}`, transferForm)
if (!result.success) $toast.error(result.message)
else if (background) $toast.success(`文件 ${item.name} 已加入整理队列!`)
} catch (e) {
console.log(e)
}
}
// 整理日志
async function handleTransferLog(logid: number, background: boolean = false) {
transferForm.logid = logid
transferForm.fileitem = {} as FileItem
try {
const result: { [key: string]: any } = await api.post(`transfer/manual?background=${background}`, transferForm)
if (!result.success) $toast.error(result.message)
else if (background) $toast.success(`历史记录 ${logid} 已加入整理队列!`)
} catch (e) {
console.log(e)
}
}
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
@@ -137,63 +175,49 @@ function stopLoadingProgress() {
}
// 整理文件
async function transfer() {
async function transfer(background: boolean = false) {
if (!props.logids && !props.items) return
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
if (!background) {
// 开始监听进度
startLoadingProgress()
}
// 文件整理
if (props.items) {
for (const item of props.items) {
await handleTransfer(item)
await handleTransfer(item, background)
}
}
// 日志整理
if (props.logids) {
for (const logid of props.logids) {
await handleTransferLog(logid)
await handleTransferLog(logid, background)
}
}
// 停止监听进度
stopLoadingProgress()
if (!background) {
// 停止监听进度
stopLoadingProgress()
}
// 关闭进度条
progressDialog.value = false
// 重新加载
emit('done')
}
// 整理文件
async function handleTransfer(item: FileItem) {
transferForm.fileitem = item
transferForm.logid = 0
try {
const result: { [key: string]: any } = await api.post('transfer/manual', transferForm)
if (!result.success) $toast.error(`文件 ${item.path} 整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
// 整理日志
async function handleTransferLog(logid: number) {
transferForm.logid = logid
transferForm.fileitem = {}
try {
const result: { [key: string]: any } = await api.post('transfer/manual', transferForm)
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
onMounted(() => {
loadDirectories()
})
onUnmounted(() => {
stopLoadingProgress()
})
</script>
<template>
@@ -218,18 +242,16 @@ onMounted(() => {
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
]"
:items="transferTypeOptions"
hint="文件操作整理方式"
persistent-hint
/>
>
<template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? '自动' : item.title }}
</template>
</VSelect>
</VCol>
<VCol cols="12" md="12">
<VCol cols="12">
<VCombobox
v-model="transferForm.target_path"
:items="targetDirectories"
@@ -304,6 +326,7 @@ onMounted(() => {
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
:disabled="disableEpisodeDetail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
hint="指定集数或范围如1或1,2"
@@ -340,6 +363,22 @@ onMounted(() => {
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VSwitch
v-model="transferForm.library_type_folder"
label="按类型分类"
hint="整理时目的路径下按媒体类型添加子目录"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VSwitch
v-model="transferForm.library_category_folder"
label="按类别分类"
hint="整理时在目的路径下按媒体类别添加子目录"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="transferForm.scrape"
@@ -352,7 +391,7 @@ onMounted(() => {
<VSwitch
v-model="transferForm.from_history"
label="复用历史识别信息"
hint="使用历史记录中已识别的媒体信息"
hint="使用历史整理记录中已识别的媒体信息"
persistent-hint
/>
</VCol>
@@ -361,7 +400,12 @@ onMounted(() => {
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn variant="elevated" @click="transfer" prepend-icon="mdi-arrow-right-bold" class="px-5"> 开始整理 </VBtn>
<VBtn variant="elevated" color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
加入整理队列
</VBtn>
<VBtn variant="elevated" @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
立即整理
</VBtn>
</VCardActions>
</VCard>
<!-- 手动整理进度框 -->

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import type { Site } from '@/api/types'
import type { DownloaderConf, Site } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
@@ -35,6 +35,7 @@ const siteForm = ref<Site>({
limit_seconds: 0,
name: '',
domain: '',
downloader: '',
})
// 提示框
@@ -65,10 +66,9 @@ const downloaderOptions = ref<{ title: string; value: string }[]>([])
async function loadDownloaderSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
const downloaders = result.data?.value ?? []
const downloaders: DownloaderConf[] = await api.get('download/clients')
downloaderOptions.value = [
{ title: '默认', value: null },
{ title: '默认', value: '' },
...downloaders.map((item: { name: any }) => ({
title: item.name,
value: item.name,
@@ -166,7 +166,7 @@ onMounted(async () => {
</script>
<template>
<VDialog scrollable :close-on-back="false" persistent eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VDialog scrollable :close-on-back="false" eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
class="rounded-t"

View File

@@ -67,17 +67,18 @@ async function updateSiteCookie() {
}
</script>
<template>
<VDialog max-width="50rem">
<VDialog max-width="30rem">
<!-- Dialog Content -->
<VCard title="更新站点Cookie & UA">
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="4">
<VCol cols="12">
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
</VCol>
<VCol cols="12" md="4">
<VCol cols="12">
<VTextField
v-model="userPwForm.password"
label="密码"
@@ -88,19 +89,19 @@ async function updateSiteCookie() {
@keydown.enter="updateSiteCookie"
/>
</VCol>
<VCol cols="12" md="4">
<VCol cols="12">
<VTextField v-model="userPwForm.code" label="两步验证" />
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VSpacer />
<VCardActions class="mx-auto">
<VBtn
size="large"
variant="elevated"
@click="updateSiteCookie"
:disabled="updateButtonDisable"
:loading="updateButtonDisable"
prepend-icon="mdi-refresh"
class="px-5"
>

View File

@@ -1,19 +1,31 @@
<script setup lang="ts">
import { Site } from '@/api/types'
import { useDisplay } from 'vuetify'
import api from '@/api'
import type { TorrentInfo } from '@/api/types'
import type { TorrentInfo, SiteCategory } from '@/api/types'
import { formatFileSize } from '@core/utils/formatters'
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
site: Object as PropType<Site>,
})
// 关键字
const keyword = ref<string>()
// 选择分类
const selectCategory = ref<number[]>([])
// 全部分类
const siteCategoryList = ref<SiteCategory[]>()
// 分类选项
const categoryOptions = computed(() => {
return siteCategoryList.value?.map(item => {
return { title: item.desc, value: item.id }
})
})
// 注册事件
const emit = defineEmits(['close'])
@@ -55,17 +67,6 @@ 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?.id}`)
resourceLoading.value = false
} catch (error) {
console.error(error)
}
}
// 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0) return 'text-white bg-lime-500'
@@ -93,17 +94,75 @@ function addDownloadError(error: string) {
addDownloadDialog.value = false
}
// 调用API查询站点资源
async function getResourceList() {
resourceLoading.value = true
try {
resourceDataList.value = await api.get(`site/resource/${props.site?.id}`, {
params: {
keyword: keyword.value,
cat: selectCategory.value?.join(','),
},
})
} catch (error) {
console.error(error)
}
resourceLoading.value = false
}
// 加载站点分类
async function getSiteCategoryList() {
try {
siteCategoryList.value = await api.get(`site/category/${props.site?.id}`)
} catch (error) {
console.error(error)
}
}
// 装载时查询站点图标
onMounted(() => {
getSiteCategoryList()
getResourceList()
})
</script>
<template>
<VDialog max-width="80rem" scrollable z-index="1010" :fullscreen="!display.mdAndUp.value">
<VCard :title="`浏览 - ${props.site?.name}`">
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText class="pt-2">
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
<VCard>
<!-- Toolbar -->
<div>
<VToolbar color="primary">
<VToolbarTitle>{{ `浏览 - ${props.site?.name}` }}</VToolbarTitle>
<VSpacer />
<VToolbarItems>
<VBtn icon variant="plain" @click="emit('close')" class="me-3">
<VIcon size="large" color="white" icon="ri-close-line" />
</VBtn>
</VToolbarItems>
</VToolbar>
</div>
<div class="p-3">
<VRow>
<VCol cols="6" md="5">
<VTextField v-model="keyword" size="small" density="compact" label="搜索关键字" clearable />
</VCol>
<VCol cols="6" md="5">
<VSelect
v-model="selectCategory"
:items="categoryOptions"
size="small"
density="compact"
chips
label="资源分类"
multiple
clearable
/>
</VCol>
<VCol cols="12" md="2" class="text-center">
<VBtn block prepend-icon="mdi-magnify" @click="getResourceList">搜索</VBtn>
</VCol>
</VRow>
</div>
<VCardText class="px-0 py-0 my-0">
<VDataTable
v-model:items-per-page="resourceItemsPerPage"
:headers="resourceHeaders"
@@ -119,6 +178,7 @@ onMounted(() => {
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
class="h-full"
>
<template #item.title="{ item }">
<a href="javascript:void(0)" @click.stop="addDownload(item)">

View File

@@ -2,9 +2,7 @@
import type { Site, SiteUserData } from '@/api/types'
import api from '@/api'
import { useDisplay, useTheme } from 'vuetify'
import { VAvatar, VCardText, VIcon } from 'vuetify/lib/components/index.mjs'
import { formatFileSize } from '@/@core/utils/formatters'
import VueApexCharts from 'vue3-apexcharts'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
// 显示器宽度
@@ -34,7 +32,6 @@ const siteDatas = ref<SiteUserData[]>([])
// 最新一天的数据
const siteData = computed(() => siteDatas.value[siteDatas.value.length - 1])
// 站点数据列表中的上传量、下载量数据生成图形使用的数据
const historySeries = computed(() => {
return [
@@ -57,6 +54,8 @@ const historyChartOptions = computed(() => {
parentHeightOffset: 0,
toolbar: { show: false },
animations: { enabled: true },
background: currentTheme.value.surface, // 新增背景色同步
foreColor: currentTheme.value.onSurface, // 新增文字颜色同步
dataLabels: {
enabled: true,
},
@@ -64,6 +63,9 @@ const historyChartOptions = computed(() => {
autoScaleYaxis: true,
},
},
theme: {
mode: vuetifyTheme.global.current.value.dark ? 'dark' : 'light', // 同步主题模式
},
tooltip: {
enabled: true,
tooltip: {
@@ -71,6 +73,10 @@ const historyChartOptions = computed(() => {
format: 'dd MMM yyyy',
},
},
style: {
background: currentTheme.value.background, // 提示框背景色同步
color: currentTheme.value.onBackground, // 文字颜色同步
},
},
grid: {
xaxis: {
@@ -143,10 +149,15 @@ const seedingChartOptions = computed(() => {
parentHeightOffset: 0,
toolbar: { show: false },
animations: { enabled: true },
background: currentTheme.value.surface, // 新增背景色同步
foreColor: currentTheme.value.onSurface, // 新增文字颜色同步
zoom: {
autoScaleYaxis: true,
},
},
theme: {
mode: vuetifyTheme.global.current.value.dark ? 'dark' : 'light', // 同步主题模式
},
tooltip: {
enabled: true,
x: {
@@ -154,6 +165,10 @@ const seedingChartOptions = computed(() => {
return '数量:' + val.toLocaleString()
},
},
style: {
background: currentTheme.value.background, // 提示框背景色同步
color: currentTheme.value.onBackground, // 文字颜色同步
},
},
grid: {
xaxis: {
@@ -243,13 +258,12 @@ async function fetchSiteUserData() {
}
}
// 刷新站点数据
async function refreshSiteData(){
async function refreshSiteData() {
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.post(`site/userdata/${props.site?.id}`)
if (result.success){
if (result.success) {
await fetchSiteUserData()
}
} catch (error) {
@@ -267,12 +281,14 @@ onBeforeMount(async () => {
<VDialog scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
<VCard class="rounded-t">
<VCardItem>
<VCardTitle>{{ `数据 - ${props.site?.name}` }}
<IconBtn @click.stop="refreshSiteData" color="info"><VIcon icon="mdi-refresh"</VIcon></IconBtn>
<VCardTitle
>{{ `数据 - ${props.site?.name}` }}
<IconBtn @click.stop="refreshSiteData" color="info"><VIcon icon="mdi-refresh" /></IconBtn>
</VCardTitle>
<DialogCloseBtn @click="emit('close')" />
</VCardItem>
<VCardText>
<VDivider />
<VCardText class="pt-5">
<VRow class="match-height">
<!-- 用户信息 -->
<VCol cols="12" md="3">
@@ -441,7 +457,7 @@ onBeforeMount(async () => {
<VCol>
<VCard title="历史流量">
<VCardText>
<VueApexCharts type="line" :options="historyChartOptions" :series="historySeries" :height="300" />
<VApexChart type="line" :options="historyChartOptions" :series="historySeries" :height="300" />
</VCardText>
</VCard>
</VCol>
@@ -450,7 +466,7 @@ onBeforeMount(async () => {
<VCol>
<VCard title="做种分布">
<VCardText>
<VueApexCharts type="scatter" :options="seedingChartOptions" :series="seedingSeries" :height="300" />
<VApexChart type="scatter" :options="seedingChartOptions" :series="seedingSeries" :height="300" />
</VCardText>
</VCard>
</VCol>

View File

@@ -2,7 +2,7 @@
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useConfirm } from 'vuetify-use-dialog'
import { VTextarea, VTextField } from 'vuetify/lib/components/index.mjs'
@@ -50,6 +50,7 @@ const subscribeForm = ref<Subscribe>({
sites: [],
best_version: undefined,
current_priority: 0,
downloader: '',
date: '',
show_edit_dialog: false,
})
@@ -62,10 +63,9 @@ const downloaderOptions = ref<{ title: string; value: string }[]>([])
async function loadDownloaderSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
const downloaders = result.data?.value ?? []
const downloaders: DownloaderConf[] = await api.get('download/clients')
downloaderOptions.value = [
{ title: '默认', value: null },
{ title: '默认', value: '' },
...downloaders.map((item: { name: any }) => ({
title: item.name,
value: item.name,
@@ -417,7 +417,7 @@ onMounted(() => {
v-model="subscribeForm.downloader"
:items="downloaderOptions"
label="下载器"
hint="指定该订阅使用的下载器,留空自动使用默认下载器"
hint="指定该订阅使用的下载器"
persistent-hint
/>
</VCol>

View File

@@ -132,59 +132,63 @@ onBeforeMount(() => {
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="download">
<transition name="fade-slide" appear>
<VDataTable
items-per-page="50"
:headers="downloadHeaders"
:items="downloadInfos"
:items-length="totalCount"
density="compact"
item-value="title"
return-object
fixed-header
hover
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
>
<template #item.episode_number="{ item }">
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
</template>
<template #item.torrent_title="{ item }">
<div class="text-xs" v-for="file in item.download">
{{ file.site_name }}{{ file.torrent_title }}
</div>
</template>
<template #item.file_path="{ item }">
<div class="text-xs" v-for="file in item.download">{{ file.file_path }}</div>
</template>
<template #no-data> 没有数据 </template>
</VDataTable>
<div>
<VDataTable
items-per-page="50"
:headers="downloadHeaders"
:items="downloadInfos"
:items-length="totalCount"
density="compact"
item-value="title"
return-object
fixed-header
hover
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
>
<template #item.episode_number="{ item }">
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
</template>
<template #item.torrent_title="{ item }">
<div class="text-xs" v-for="file in item.download">
{{ file.site_name }}{{ file.torrent_title }}
</div>
</template>
<template #item.file_path="{ item }">
<div class="text-xs" v-for="file in item.download">{{ file.file_path }}</div>
</template>
<template #no-data> 没有数据 </template>
</VDataTable>
</div>
</transition>
</VWindowItem>
<VWindowItem value="library">
<transition name="fade-slide" appear>
<VDataTable
items-per-page="50"
:headers="libraryHeaders"
:items="libraryInfos"
:items-length="totalCount"
density="compact"
item-value="title"
return-object
fixed-header
hover
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
>
<template #item.episode_number="{ item }">
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
</template>
<template #item.file_path="{ item }">
<div class="text-xs" v-for="file in item.library">{{ file.file_path }}</div>
</template>
<template #no-data> 没有数据 </template>
</VDataTable>
<div>
<VDataTable
items-per-page="50"
:headers="libraryHeaders"
:items="libraryInfos"
:items-length="totalCount"
density="compact"
item-value="title"
return-object
fixed-header
hover
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
>
<template #item.episode_number="{ item }">
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
</template>
<template #item.file_path="{ item }">
<div class="text-xs" v-for="file in item.library">{{ file.file_path }}</div>
</template>
<template #no-data> 没有数据 </template>
</VDataTable>
</div>
</transition>
</VWindowItem>
</VWindow>

View File

@@ -86,7 +86,7 @@ async function reSubscribe(item: Subscribe) {
else progressText.value = `正在重新订阅 ${item.name}${item.season} 季 ...`
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.post('subscribe', item)
const result: { [key: string]: any } = await api.post('subscribe/', item)
if (result.success) {
emit('save')
}
@@ -138,14 +138,7 @@ const dropdownItems = ref([
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
</VCardItem>
<VDivider />
<DialogCloseBtn
@click="
() => {
emit('close')
}
"
/>
<!-- <VList lines="two" v-if="historyList.length > 0"> -->
<DialogCloseBtn @click="emit('close')" />
<VList lines="two">
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="overflow-hidden" @load="loadHistory">
<template #loading>
@@ -207,7 +200,7 @@ const dropdownItems = ref([
</template>
</VInfiniteScroll>
</VList>
<VCardText v-if="historyList.length === 0" class="text-center"> 没有已完成的订阅 </VCardText>
<VCardText v-if="historyList.length === 0 && isRefreshed" class="text-center"> 没有已完成的订阅 </VCardText>
</VCard>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />

View File

@@ -4,6 +4,7 @@ import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Subscribe, SubscribeShare } from '@/api/types'
import { useDisplay } from 'vuetify'
import { formatSeason } from '@/@core/utils/formatters'
// 显示器宽度
const display = useDisplay()
@@ -16,16 +17,22 @@ const props = defineProps({
// 定义触发的自定义事件
const emit = defineEmits(['close'])
// 分享处理状态
const shareDoing = ref(false)
// 订阅编辑表单
const shareForm = ref<SubscribeShare>({
subscribe_id: props.sub?.id ?? 0,
share_title: `${props.sub?.name} ${formatSeason(props.sub?.season ? props.sub?.season.toString() : '')}`,
})
// 分享订阅
async function doShare() {
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
try {
shareDoing.value = true
const result: { [key: string]: any } = await api.post('subscribe/share', shareForm.value)
shareDoing.value = false
// 提示
if (result.success) {
$toast.success(`${props.sub?.name} 分享成功!`)
@@ -56,8 +63,8 @@ const $toast = useToast()
<VCol cols="12">
<VTextField
v-model="shareForm.share_title"
readonly
label="标题"
hint="给分享取一个便于识别的名称"
:rules="[requiredValidator]"
persistent-hint
/>
@@ -67,7 +74,7 @@ const $toast = useToast()
v-model="shareForm.share_comment"
label="说明"
:rules="[requiredValidator]"
hint="关于该订阅的说明"
hint="填写关于该订阅的说明,订阅中的搜索词、识别词等将会默认包含在分享中"
persistent-hint
/>
</VCol>
@@ -85,7 +92,16 @@ const $toast = useToast()
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn variant="elevated" @click="doShare" prepend-icon="mdi-share" class="px-5"> 确认分享 </VBtn>
<VBtn
variant="elevated"
:disabled="shareDoing"
@click="doShare"
prepend-icon="mdi-share"
class="px-5"
:loading="shareDoing"
>
确认分享
</VBtn>
</VCardActions>
</VCard>
</VDialog>

View File

@@ -0,0 +1,187 @@
<script lang="ts" setup>
import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api'
import { FileItem, TransferQueue } from '@/api/types'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 定义触发的自定义事件
const emit = defineEmits(['close'])
// 数据列表
const dataList = ref<TransferQueue[]>([])
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 数据可刷新标志
const refreshFlag = ref(false)
// 活动标签
const activeTab = ref('')
// 状态标签
const stateDict: { [key: string]: string } = {
'waiting': '等待中',
'running': '正在整理',
'completed': '完成',
'failed': '失败',
}
// 获取状态颜色
function getStateColor(state: string) {
if (state === 'waiting') return 'gray'
else if (state === 'running') return 'primary'
else if (state === 'completed') return 'success'
else return 'error'
}
// 从dataList中提取所有的媒体信息
const mediaList = computed(() => {
return dataList.value.map(item => item.media)
})
// 按media计算总数和完成数返回 x/x
function getMediaCount(title_year: string) {
// 按title_year查询出所有media列表
const medias = dataList.value.filter(item => item.media.title_year === title_year)
// 计算media下任务的总数
const total = medias.reduce((acc, cur) => acc + cur.tasks.length, 0)
// 计算media下任务的完成数
const completed = medias.reduce((acc, cur) => acc + cur.tasks.filter(task => task.state === 'completed').length, 0)
return `${completed} / ${total}`
}
// 根据媒体信息获取对应的整理任务
const activeTasks = computed(() => {
return dataList.value.find(item => item.media.title_year === activeTab.value)?.tasks
})
// 调用API获取队列信息
async function get_transfer_queue() {
try {
dataList.value = await api.get('transfer/queue')
if (dataList.value.length > 0) {
if (!activeTab.value || activeTasks.value?.length == 0) activeTab.value = dataList.value[0].media.title_year || ''
}
} catch (error) {
console.error(error)
}
}
// 移除队列任务
async function remove_queue_task(fileitem: FileItem) {
try {
await api.delete(`transfer/queue`, { data: fileitem })
get_transfer_queue()
} catch (error) {
console.error(error)
}
}
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`)
progressEventSource.value.onmessage = event => {
const progress = JSON.parse(event.data)
if (progress) {
if (!progress.enable) {
progressText.value = '请稍候 ...'
progressValue.value = 0
if (refreshFlag.value) {
refreshFlag.value = false
get_transfer_queue()
}
return
}
progressText.value = progress.text
progressValue.value = progress.value
if (progress.value >= 100 && refreshFlag.value) {
refreshFlag.value = false
get_transfer_queue()
} else {
if (progress.value > 0 && refreshFlag.value && progress.text?.includes('整理完成')) {
refreshFlag.value = false
get_transfer_queue()
} else {
refreshFlag.value = true
}
}
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
onMounted(() => {
get_transfer_queue()
startLoadingProgress()
})
onUnmounted(() => {
stopLoadingProgress()
})
</script>
<template>
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard class="mx-auto" width="100%">
<VCardItem>
<VCardTitle>整理队列</VCardTitle>
</VCardItem>
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VProgressLinear
v-if="dataList.length > 0 && progressValue > 0"
:value="progressValue"
color="primary"
indeterminate
/>
<VCardItem v-if="dataList.length > 0 && progressValue > 0" class="text-center pt-2">
<span class="text-sm">{{ progressText }}</span>
</VCardItem>
<VCardText v-if="dataList.length === 0" class="text-center"> 没有正在整理的任务 </VCardText>
<VCardText>
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill" stacked>
<VTab
v-for="media in mediaList"
:value="media.title_year"
selected-class="v-slide-group-item--active v-tab--selected"
>
<div class="font-bold text-lg">{{ media.title }}</div>
<div>({{ getMediaCount(media.title_year || '') }})</div>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem v-for="media in mediaList" :value="media.title_year">
<VList>
<VListItem v-for="task in activeTasks">
<VListItemTitle>{{ task.fileitem.name }}</VListItemTitle>
<VListItemSubtitle>
大小{{ formatFileSize(task.fileitem.size || 0) }}
<VChip size="small" :color="getStateColor(task.state)" class="ms-2">
{{ stateDict[task.state] }}
</VChip>
</VListItemSubtitle>
<template #append>
<IconBtn size="small" icon="mdi-cancel" @click="remove_queue_task(task.fileitem)" />
</template>
</VListItem>
</VList>
</VWindowItem>
</VWindow>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -2,6 +2,14 @@
import api from '@/api'
import QrcodeVue from 'qrcode.vue'
// 定义输入
const props = defineProps({
conf: {
type: Object as PropType<{ [key: string]: any }>,
required: true,
},
})
// 定义事件
const emit = defineEmits(['done', 'close'])
@@ -19,6 +27,7 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
// 完成
async function handleDone() {
clearTimeout(timeoutTimer)
emit('done')
}
@@ -28,6 +37,7 @@ async function getQrcode() {
const result: { [key: string]: any } = await api.get('/storage/qrcode/u115')
if (result.success && result.data) {
qrCodeContent.value = result.data.codeContent
timeoutTimer = setTimeout(checkQrcode, 3000)
} else {
text.value = result.message
}
@@ -73,7 +83,6 @@ async function checkQrcode() {
onMounted(async () => {
await getQrcode()
timeoutTimer = setTimeout(checkQrcode, 3000)
})
onUnmounted(() => {

View File

@@ -5,7 +5,7 @@ import { doneNProgress, startNProgress } from '@/api/nprogress'
import api from '@/api'
import { useDisplay } from 'vuetify'
import avatar1 from '@images/avatars/avatar-1.png'
import store from '@/store'
import { useUserStore } from '@/stores'
// 显示器宽度
const display = useDisplay()
@@ -23,8 +23,11 @@ const props = defineProps({
oper: String,
})
// 用户 Store
const userStore = useUserStore()
// 当前登录用户名称
const currentLoginUser = store.state.auth.userName
const currentLoginUser = userStore.userName
// 用户名
const userName = ref('')
@@ -199,13 +202,15 @@ async function updateUser() {
if (oldUserName !== currentUserName.value) {
$toast.success(`${oldUserName}】更名【${currentUserName.value}】, 更新成功!`)
// 如果是当前登录用户,更新当前用户名称显示
if (isCurrentUser.value) store.commit('auth/setUserName', currentUserName.value)
if (isCurrentUser.value) {
userStore.setUserName(currentUserName.value)
}
} else {
$toast.success(`${userForm.value?.name}】更新成功!`)
}
// 更新本地头像显示
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
store.commit('auth/setAvatar', currentAvatar.value)
userStore.setAvatar(currentAvatar.value)
}
emit('save')
} else {
@@ -262,48 +267,55 @@ onMounted(() => {
</script>
<template>
<VDialog scrollable :close-on-back="false" persistent eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VDialog scrollable :close-on-back="false" eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}用户${props.oper !== 'add' ? ` - ${userName}` : ''}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText class="d-flex">
<VCardItem>
<!-- 👉 Avatar -->
<VAvatar rounded="lg" size="100" class="me-6" :image="currentAvatar" />
<div class="flex flex-row">
<VAvatar rounded="lg" size="100" class="me-5" :image="currentAvatar" />
<!-- 👉 Upload Photo -->
<div class="flex flex-col justify-center gap-5">
<div class="flex flex-wrap gap-2">
<VBtn color="primary" @click="refInputEl?.click()">
<VIcon icon="mdi-cloud-upload-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">上传新头像</span>
</VBtn>
<!-- 👉 Upload Photo -->
<form class="d-flex flex-column justify-center gap-5">
<div class="d-flex flex-wrap gap-2">
<VBtn color="primary" @click="refInputEl?.click()">
<VIcon icon="mdi-cloud-upload-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">上传新头像</span>
</VBtn>
<input
ref="refInputEl"
type="file"
name="file"
accept=".jpeg,.png,.jpg,GIF"
hidden
@input="changeAvatar"
/>
<input ref="refInputEl" type="file" name="file" accept=".jpeg,.png,.jpg,GIF" hidden @input="changeAvatar" />
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar" v-if="props.oper !== 'add'">
<VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
</VBtn>
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar" v-if="props.oper !== 'add'">
<VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
</VBtn>
<VBtn
type="reset"
:color="props.oper === 'add' ? 'info' : 'error'"
variant="tonal"
@click="resetDefaultAvatar"
>
<VIcon icon="mdi-image-sync-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">默认</span>
</VBtn>
<VBtn
type="reset"
:color="props.oper === 'add' ? 'info' : 'error'"
variant="tonal"
@click="resetDefaultAvatar"
>
<VIcon icon="mdi-image-sync-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">默认</span>
</VBtn>
</div>
<p class="text-body-1 mb-0">允许 JPGPNGGIFWEBP 格式 最大尺寸 800KB</p>
</div>
<p class="text-body-1 mb-0">允许 JPGPNGGIFWEBP 格式 最大尺寸 800KB</p>
</form>
</VCardText>
</div>
</VCardItem>
<VCardText>
<VForm @submit.prevent="() => {}" class="mt-3">
<VForm @submit.prevent="() => {}">
<VDivider class="my-10">
<span>用户基础设置</span>
</VDivider>
@@ -355,7 +367,7 @@ onMounted(() => {
</VCol>
</VRow>
<VDivider class="my-10">
<span>消息账号绑定</span>
<span>账号绑定</span>
</VDivider>
<VRow>
<VCol cols="12" md="6">
@@ -388,6 +400,9 @@ onMounted(() => {
label="SynologyChat用户"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="userForm.settings.douban_userid" density="comfortable" clearable label="豆瓣用户" />
</VCol>
</VRow>
</VForm>
</VCardText>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
@@ -61,7 +62,10 @@ async function loadLastAuthParams() {
try {
const result: { [key: string]: any } = await api.get(`system/setting/UserSiteAuthParams`)
if (result.success) {
authForm.value = result.data?.value || { site: null, params: {} }
const ret = result.data?.value
if (ret && !isNullOrEmptyObject(ret.params)) {
authForm.value = ret
}
}
} catch (e) {
console.error(e)
@@ -84,6 +88,22 @@ async function handleDone() {
// 认证处理
async function checkUser() {
if (!authForm.value.site) {
$toast.error('请选择认证站点!')
return
}
if (!authSites.value[authForm.value.site]) {
$toast.error('站点配置不存在!')
return
}
if (formFields.value.length > 0) {
for (const field of formFields.value) {
if (!authForm.value.params[field.site.toUpperCase() + '_' + field.key.toUpperCase()]) {
$toast.error(`请输入${field.name}`)
return
}
}
}
loading.value = true
try {
const result: { [key: string]: any } = await api.post(`site/auth`, authForm.value)
@@ -109,7 +129,7 @@ onMounted(async () => {
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VDialog width="40rem" max-height="85vh">
<VCard title="用户认证" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText>

View File

@@ -0,0 +1,316 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { VueFlow, useVueFlow, type Connection, type GraphNode } from '@vue-flow/core'
import { MiniMap } from '@vue-flow/minimap'
import useDragAndDrop from '@core/utils/workflow'
import { Workflow } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import WorkflowSidebar from '@/layouts/components/WorkflowSidebar.vue'
import DropzoneBackground from '@/layouts/components/DropzoneBackground.vue'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
const { onConnect, addEdges, nodes, edges } = useVueFlow()
const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
// 连接事件
onConnect((connection: Connection) => {
// 双重校验
if (!isValidConnection(connection)) {
$toast.warning('非法连接:不能连接自身或同类型端口!')
return
}
addEdges(connection)
})
// 获取指定节点端口的类型(输入/输出)
const getPortType = (node: GraphNode, handleId: string) => {
// 检查是否是输入端口(对应 handleBounds.target
const isInput = node.handleBounds?.target?.some(h => h.id === handleId)
if (isInput) return 'input'
// 检查是否是输出端口(对应 handleBounds.source
const isOutput = node.handleBounds?.source?.some(h => h.id === handleId)
return isOutput ? 'output' : null
}
// 校验连接是否合法
const isValidConnection = (connection: Connection) => {
// 获取连接的源节点和目标节点
const sourceNode = nodes.value.find(n => n.id === connection.source)
const targetNode = nodes.value.find(n => n.id === connection.target)
if (!sourceNode || !targetNode) return false
// 获取端口类型
const sourcePortType = getPortType(sourceNode, connection.sourceHandle!)
const targetPortType = getPortType(targetNode, connection.targetHandle!)
/* 同时满足三个条件,才允许连接:
* 1. 源端口是输出类型output
* 2. 目标端口是输入类型input
* 3. 不是同一节点的连接
*/
return sourcePortType === 'output' && targetPortType === 'input' && connection.source !== connection.target
}
// 自定义节点类型
const nodeTypes: Record<string, any> = ref({})
// 自动扫描目录下所有的 .vue 文件
const components = import.meta.glob('../workflow/*Action.vue')
// 动态加载某个组件
const loadComponent = async (componentName: string) => {
const component = components[`../workflow/${componentName}.vue`]
if (component) {
return ((await component()) as any).default
}
throw new Error(`组件 ${componentName} 未找到`)
}
// 将所有components中的组件加载到nodeTypes中
for (const path in components) {
const componentName = path.match(/\.\/workflow\/(.*).vue$/)?.[1]
if (!componentName) {
continue
}
loadComponent(componentName).then(component => {
nodeTypes.value[componentName] = markRaw(component)
})
}
// 定义输入参数
const props = defineProps({
workflow: Object as PropType<Workflow>,
})
// 定义事件
const emit = defineEmits(['close', 'save'])
// 站点编辑表单数据
const workflowForm = ref<any>(props.workflow || {})
// 提示框
const $toast = useToast()
// 导入代码对话框
const importCodeDialog = ref(false)
// 调用API 编辑任务
async function updateWorkflow() {
// 更新节点和流程
workflowForm.value.actions = nodes
workflowForm.value.flows = edges
try {
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
if (result.success) {
$toast.success(`保存任务流程成功!`)
emit('save')
} else {
$toast.error(`保存任务流程失败:${result.message}`)
}
} catch (error) {
console.error(error)
}
}
// 保存导入的代码,直接覆盖原有值
function saveCodeString(type: string, code: any) {
try {
if (code) {
const codeObject = JSON.parse(code.value)
if (type === 'workflow') {
nodes.value = codeObject.actions || []
edges.value = codeObject.flows || []
}
importCodeDialog.value = false
$toast.success('导入成功!')
}
} catch (error) {
$toast.error('导入失败!')
console.error(error)
}
}
// 分享工作流程
function shareWorkflow() {
const codeString = JSON.stringify({ actions: nodes.value, flows: edges.value })
navigator.clipboard.writeText(codeString)
$toast.success('任务流程代码已复制到剪贴板!')
}
onMounted(() => {
if (props.workflow) {
nodes.value = props.workflow.actions ?? []
edges.value = props.workflow.flows ?? []
}
})
</script>
<template>
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
<VCard>
<!-- Toolbar -->
<div>
<VToolbar color="primary">
<VToolbarItems>
<VBtn icon @click="emit('close')" class="ms-3">
<VIcon size="large" color="white" icon="mdi-close" />
</VBtn>
</VToolbarItems>
<VToolbarTitle> 编辑流程 - {{ workflow?.name }} </VToolbarTitle>
<VToolbarItems>
<VBtn icon @click="importCodeDialog = true">
<VIcon size="large" color="white" icon="mdi-import" />
</VBtn>
<VBtn icon @click="shareWorkflow">
<VIcon size="large" color="white" icon="mdi-share" />
</VBtn>
<VBtn icon @click="updateWorkflow" class="mx-5">
<VIcon size="large" color="white" icon="mdi-content-save" />
</VBtn>
</VToolbarItems>
</VToolbar>
</div>
<VDivider />
<VCardText class="px-0 py-0">
<div class="dnd-flow" @drop="onDrop">
<VueFlow
:nodes="nodes"
:edges="edges"
:nodeTypes="nodeTypes"
:is-valid-connection="isValidConnection"
:default-edge-options="{ type: 'animation', animated: true }"
:edge-updater-radius="10"
@dragover="onDragOver"
@dragleave="onDragLeave"
delete-key-code="Delete"
auto-connect
>
<MiniMap />
<DropzoneBackground
:style="{
backgroundColor: isDragOver ? '#e7f3ff' : 'transparent',
transition: 'background-color 0.2s ease',
}"
>
</DropzoneBackground>
</VueFlow>
<WorkflowSidebar />
</div>
</VCardText>
</VCard>
<ImportCodeDialog
v-if="importCodeDialog"
v-model="importCodeDialog"
title="导入任务流程"
dataType="workflow"
@close="importCodeDialog = false"
@save="saveCodeString"
/>
</VDialog>
</template>
<style>
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';
@import '@vue-flow/controls/dist/style.css';
@import '@vue-flow/minimap/dist/style.css';
@import '@vue-flow/node-resizer/dist/style.css';
.vue-flow__minimap {
transform: scale(75%);
transform-origin: bottom right;
}
.dnd-flow {
flex-direction: column;
display: flex;
height: 100%;
}
.dnd-flow aside {
color: #fff;
font-weight: 700;
border-right: 1px solid #eee;
padding: 15px 10px;
font-size: 12px;
background: #10b981bf;
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
box-shadow: 0 5px 10px #0000004d;
}
.dnd-flow aside .nodes > * {
margin-bottom: 10px;
cursor: grab;
font-weight: 500;
-webkit-box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, 0.25);
box-shadow: 5px 5px 10px 2px #00000040;
}
.dnd-flow aside .description {
margin-bottom: 10px;
}
.dnd-flow .vue-flow-wrapper {
flex-grow: 1;
height: 100%;
}
@media screen and (min-width: 640px) {
.dnd-flow {
flex-direction: row;
}
.dnd-flow aside {
max-width: 25%;
}
}
@media screen and (max-width: 639px) {
.dnd-flow aside .nodes {
display: flex;
flex-direction: row;
gap: 5px;
}
}
.dropzone-background {
position: relative;
height: 100%;
width: 100%;
}
.dropzone-background .overlay {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
pointer-events: none;
}
.vue-flow__handle {
height: 24px;
width: 8px;
border-radius: 4px;
}
.vue-flow__edge-path,
.vue-flow__connection-path {
stroke-width: 3;
}
.vue-flow__handle-left {
background-color: rgb(var(--v-theme-info));
}
.vue-flow__handle-right {
background-color: rgb(var(--v-theme-error));
}
</style>

View File

@@ -0,0 +1,133 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import type { Workflow } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import { useDisplay } from 'vuetify'
// 输入参数
const props = defineProps({
// 任务信息
workflow: Object as PropType<Workflow>,
})
// 新增或修改字样
const title = computed(() => (props.workflow ? '编辑' : '创建'))
// 显示器宽度
const display = useDisplay()
// 注册事件
const emit = defineEmits(['save', 'remove', 'close'])
// 站点编辑表单数据
const workflowForm = ref<Workflow>(
props.workflow || {
name: undefined,
timer: undefined,
description: undefined,
state: 'P',
run_count: 0,
},
)
// 提示框
const $toast = useToast()
// 调用API 新增任务
async function addWorkflow() {
if (!workflowForm.value.name || !workflowForm.value.timer) {
$toast.error('请填写完整信息!')
return
}
startNProgress()
try {
const result: { [key: string]: string } = await api.post('workflow/', workflowForm.value)
if (result.success) {
$toast.success(`创建任务成功,请编辑流程!`)
emit('save')
} else {
$toast.error(`创建任务失败:${result.message}`)
}
} catch (error) {
console.error(error)
}
doneNProgress()
}
// 调用API 编辑任务
async function editWorkflow() {
if (!workflowForm.value.name || !workflowForm.value.timer) {
$toast.error('请填写完整信息!')
return
}
startNProgress()
try {
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
if (result.success) {
$toast.success(`修改任务成功!`)
emit('save')
} else {
$toast.error(`修改任务失败:${result.message}`)
}
} catch (error) {
console.error(error)
}
doneNProgress()
}
</script>
<template>
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${title}任务`" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12">
<VTextField
v-model="workflowForm.name"
label="别名"
:rules="[requiredValidator]"
persistent-hint
hint="任务名称"
/>
</VCol>
<VCol cols="12">
<VCronField
v-model="workflowForm.timer"
label="定时"
:rules="[requiredValidator]"
placeholder="5位cron表达式"
persistent-hint
hint="任务执行周期"
/>
</VCol>
<VCol cols="12">
<VTextarea v-model="workflowForm.description" label="任务描述" />
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
v-if="workflow"
block
color="primary"
variant="elevated"
@click="editWorkflow"
prepend-icon="mdi-content-save"
class="px-5"
>
保存
</VBtn>
<VBtn v-else block color="primary" variant="elevated" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
创建
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import CronInput from '@/components/input/CronInput.vue'
const attrs = useAttrs()
const props = defineProps({
modelValue: {
type: String,
default: '* * * * *',
},
})
const emit = defineEmits(['update:modelValue'])
const innerValue = ref(props.modelValue)
watch(
() => props.modelValue,
value => {
innerValue.value = value
},
)
const propsWithoutModelValue = computed(() => {
const { modelValue, ...rest } = props
return { ...rest, ...attrs }
})
function updateModelValue(value: string) {
innerValue.value = value
emit('update:modelValue', value)
}
</script>
<template>
<CronInput v-model="innerValue" @update:modelValue="updateModelValue">
<template #activator="{ menuprops }">
<VTextField
:modelValue="innerValue"
@update:modelValue="updateModelValue"
v-bind="{ ...menuprops, ...propsWithoutModelValue }"
clearable
/>
</template>
</CronInput>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import PathInput from '@/components/input/PathInput.vue'
const attrs = useAttrs()
const props = defineProps({
modelValue: {
type: String,
default: '/',
},
storage: {
type: String,
default: 'local',
},
})
const emit = defineEmits(['update:modelValue'])
const innerValue = ref(props.modelValue)
watch(
() => props.modelValue,
value => {
innerValue.value = value
},
)
const propsWithoutModelValue = computed(() => {
const { modelValue, ...rest } = props
return { ...rest, ...attrs }
})
function updateModelValue(value: string) {
innerValue.value = value
emit('update:modelValue', value)
}
</script>
<template>
<PathInput v-model="innerValue" :storage="props.storage" @update:modelValue="updateModelValue">
<template #activator="{ menuprops }">
<VTextField
:modelValue="innerValue"
@update:modelValue="updateModelValue"
v-bind="{ ...menuprops, ...propsWithoutModelValue }"
/>
</template>
</PathInput>
</template>

View File

@@ -7,9 +7,9 @@ import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import { useDisplay } from 'vuetify'
import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
// 显示器宽度
const display = useDisplay()
@@ -735,14 +735,12 @@ onMounted(() => {
<!-- 进度框 -->
<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>
<MediaInfoDialog
v-if="nameTestDialog"
v-model="nameTestDialog"
:context="nameTestResult"
@close="nameTestDialog = false"
/>
</template>
<style lang="scss" scoped>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
const props = defineProps({
modelValue: {
type: String,
default: '* * * * *',
},
})
const emit = defineEmits(['update:modelValue'])
const currentCron = ref(props.modelValue)
watch(currentCron, newVal => {
emit('update:modelValue', newVal)
})
watch(
() => props.modelValue,
value => {
currentCron.value = value
},
)
</script>
<template>
<div>
<VMenu :close-on-content-click="false" content-class="cursor-default" persistent>
<template v-slot:activator="{ props }">
<slot name="activator" :menuprops="props" />
</template>
<VList>
<VListItem>
<VCronVuetify v-model="currentCron" locale="zh-CN" :chip-props="{ color: 'success' }" class="mt-1" />
</VListItem>
</VList>
</VMenu>
</div>
</template>

View File

@@ -1,108 +0,0 @@
<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: '/',
required: true,
},
storage: {
type: String,
default: 'local',
},
})
// 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: 'dir',
basename: props.root,
storage: props.storage,
},
])
// 拉取子目录
async function fetchDirs(item: any) {
return api
.post('/storage/list', item)
.then((data: any) => {
// 只添加目录到子目录
data = data.filter((i: any) => i.type === 'dir')
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)
})
// 监听存储变化
watch(
() => props.storage,
async newVal => {
treeItems.value = [
{
name: '/',
path: props.root,
children: [],
type: 'dir',
basename: props.root,
storage: newVal,
},
]
openedDirs.value = []
activedDirs.value = []
},
)
</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,174 @@
<script setup lang="ts">
import api from '@/api'
import { FileItem } from '@/api/types'
const props = defineProps({
modelValue: {
type: String,
default: '/',
},
root: {
type: String,
default: '/',
required: true,
},
storage: {
type: String,
default: 'local',
},
})
const emit = defineEmits(['update:modelValue'])
const menuVisible = ref(false)
const treeItems = ref<FileItem[]>([
{
name: '/',
path: props.root,
children: [],
type: 'dir',
basename: props.root,
storage: props.storage,
},
])
const activedDirs = ref<FileItem[]>([])
const openedDirs = ref<FileItem[]>([])
// 调用API查询子目录
async function fetchDirs(item: any) {
return api
.post('/storage/list', item)
.then((data: any) => {
data = data.filter((i: any) => i.type === 'dir')
item.children?.push(...data)
})
.catch(err => console.warn(err))
}
// 递归查询路径
function findPath(item: FileItem, path: string): FileItem | null {
if (item.path === path) {
return item
}
if (item.children) {
for (const child of item.children) {
const res: FileItem | null = findPath(child, path)
if (res) {
return res
}
}
}
return null
}
// 根据路径展开所有子目录
async function expandDirs(path: string) {
// 分割路径
const paths = path.split('/').filter(i => i)
// 展开根目录
const root_item = treeItems.value[0]
await fetchDirs(root_item)
openedDirs.value.push(root_item)
// 逐级展开
let currentPath = '/'
for (const p of paths) {
currentPath += `${p}/`
// 查询当前目录
const item = findPath(root_item, currentPath)
if (!item) {
break
}
// 加载子目录
if (item.children?.length === 0) {
await fetchDirs(item)
}
// 打开当前目录
if (!openedDirs.value.includes(item) && path != currentPath) {
openedDirs.value.push(item)
}
// 选中当前目录
if (path == currentPath) {
activedDirs.value = [item]
}
}
}
// 当前选中项
const selectedPath = computed(() => {
if (activedDirs.value.length > 0) {
return activedDirs.value[0].path
}
return ''
})
watch(activedDirs, newVal => {
if (!newVal.length) return
emit('update:modelValue', selectedPath.value)
})
watch(
() => menuVisible.value,
async visible => {
if (visible) {
treeItems.value = [
{
name: '/',
path: props.root,
children: [],
type: 'dir',
basename: props.root,
storage: props.storage,
},
]
openedDirs.value = []
activedDirs.value = []
await expandDirs(props.modelValue)
}
},
)
watch(
() => props.storage,
async newVal => {
treeItems.value = [
{
name: '/',
path: props.root,
children: [],
type: 'dir',
basename: props.root,
storage: newVal,
},
]
activedDirs.value = []
openedDirs.value = []
},
)
</script>
<template>
<div>
<VMenu v-model="menuVisible" :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"
activatable
return-object
max-height="20rem"
expand-icon="mdi-folder"
collapse-icon="mdi-folder-open"
/>
</VMenu>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
defineProps<{ title: string }>()
</script>
<template>
<VListSubheader>{{ title }}</VListSubheader>
<VListItem><slot /></VListItem>
</template>

View File

@@ -6,10 +6,21 @@ import { type PropType } from 'vue'
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
})
// key
const componentKey = ref(0)
onActivated(() => {
componentKey.value++
})
</script>
<template>
<Component :is="elementProps.config?.component" v-if="!elementProps.config?.html" v-bind="elementProps.config?.props">
<Component
:key="componentKey"
: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">
@@ -23,6 +34,7 @@ const elementProps = defineProps({
/>
</Component>
<Component
:key="componentKey"
:is="elementProps.config?.component"
v-if="elementProps.config?.html"
v-bind="elementProps.config?.props"

View File

@@ -1,57 +1,160 @@
<script lang="ts" setup>
<script setup lang="ts">
import { RenderProps } from '@/api/types'
import { type PropType, ref } from 'vue'
// 输入参数
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
form: Object as PropType<any>,
})
// 定义 props
defineProps<{
config: RenderProps // JSON 配置
model: Record<string, any> // 数据模型
}>()
// 配置元素
const formItem = ref<RenderProps>(
elementProps.config ?? {
component: 'div',
text: '',
html: '',
props: {},
content: [],
},
)
/**
* 解析属性,支持 v-model 和动态绑定
* @param rawProps 原始属性
* @param model 数据模型
* @returns 解析后的属性
*/
const parseProps = (rawProps: Record<string, any>, model: Record<string, any>) => {
const parsedProps: Record<string, any> = {}
// 配置数据
const formData = ref<any>(elementProps.form || {})
const isExpression = (value: string) => value.startsWith('{{') && value.endsWith('}}')
const extractExpression = (value: string) => value.slice(2, -2).trim()
for (const [key, value] of Object.entries(rawProps)) {
if (key === 'modelvalue') {
// 将 modelvalue 转换为 v-model:value 的形式
parsedProps['value'] = model[value]
parsedProps['onUpdate:value'] = (newValue: any) => {
model[value] = newValue
}
} else if (['model', 'v-model'].includes(key)) {
// 处理 v-model
parsedProps['modelValue'] = model[value]
parsedProps['onUpdate:modelValue'] = (newValue: any) => {
model[value] = newValue
}
} else if (['show', 'v-show'].includes(key)) {
// 处理 v-show实现显示隐藏
const expression = isExpression(value) ? extractExpression(value) : value
const isVisible = new Function('model', `with(model) { return ${expression} }`)(model)
// 动态设置 style.display
if (!parsedProps.style) {
parsedProps.style = {}
}
parsedProps.style.display = isVisible ? '' : 'none'
} else if (key.startsWith('model:') || key.startsWith('v-model:')) {
// 处理 v-model:<prop>
const propName = key.split(':')[1]
parsedProps[propName] = model[value]
parsedProps[`onUpdate:${propName}`] = (newValue: any) => {
model[value] = newValue
}
} else if (key.startsWith('on')) {
// 处理事件监听,值是函数的代码 function xxx(e) { ... }
if (typeof value === 'string') {
// 创建动态函数并绑定model上下文
const handler = new Function(
'model',
'event',
`
try {
with(model) {
return (${value})(event);
}
} catch(e) {
console.error('事件处理函数执行错误:', e);
}
`,
)
// 包装事件处理器保持vue事件参数传递特性
parsedProps[key] = (...args: any[]) => {
const [event] = args
return handler(model, event)
}
}
} else {
// 如果是表达式,需要绑定
if (typeof value === 'string' && isExpression(value)) {
const expression = extractExpression(value)
parsedProps[key] = new Function('model', `with(model) { return ${expression} }`)(model)
} else if (typeof value === 'string' && value in model) {
// 如果是数据模型的属性,直接绑定
parsedProps[key] = model[value]
} else {
// 其他情况直接赋值
parsedProps[key] = value
}
}
}
return parsedProps
}
/**
* 渲染插槽内容
* @param slotContent 插槽配置
* @param model 数据模型
* @param slotScope 插槽作用域
*/
const renderSlotContent = (slotContent: any, model: any, slotScope: any) => {
if (Array.isArray(slotContent)) {
// 如果插槽内容是数组,递归渲染
return slotContent.map(childConfig => renderComponent(childConfig, model, slotScope))
}
// 如果插槽内容是单个配置,递归渲染
return renderComponent(slotContent, model, slotScope)
}
/**
* 渲染组件函数(递归支持嵌套)
* @param config JSON 配置
* @param model 数据模型
* @param slotScope 插槽作用域
* @returns 渲染的组件 VNode
*/
const renderComponent = (config: any, model: any, slotScope: any = {}) => {
const { component, props: componentProps = {}, content = [], slots = {}, html, text } = config
// 动态解析组件
const Component = resolveComponent(component)
// 解析属性
const parsedProps = parseProps(componentProps, model)
// 动态插槽解析
const slotNodes: Record<string, any> = {}
for (const [slotName, slotContent] of Object.entries(slots)) {
slotNodes[slotName] = (slotScopeData: any) =>
renderSlotContent(slotContent, model, { ...slotScope, ...slotScopeData })
}
// 渲染组件内容
const renderContent = () => {
// 如果配置了 `html`,直接渲染为 HTML 内容
if (html) {
return h(Component, { innerHTML: typeof html === 'string' ? html : model[html] })
}
// 如果配置了 `text`,直接渲染为文本内容
if (text) {
return typeof text === 'string' ? text : model[text]
}
// 如果配置了 `content`,递归渲染子组件
if (Array.isArray(content)) {
return content.map((childConfig: any) => renderComponent(childConfig, model, slotScope))
}
return null
}
// 渲染组件
return h(Component, parsedProps, {
...slotNodes,
default: renderContent,
})
}
</script>
<template>
<Component
:is="formItem.component"
v-if="!formItem.html && !!formItem.props?.modelvalue"
v-bind="formItem.props"
v-model:value="formData[formItem.props?.modelvalue]"
>
{{ 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-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="renderComponent(config, model)" />
</template>

View File

@@ -9,7 +9,7 @@ import { RenderProps } from '@/api/types'
const emit = defineEmits(['action'])
// 输入参数
const elementProps = defineProps({
const props = defineProps({
config: Object as PropType<RenderProps>,
})
@@ -41,9 +41,9 @@ async function commonAction(api_path: string, method: string, params = {}) {
// 组装事件
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]
if (!isNullOrEmptyObject(props.config?.events)) {
for (const key in props.config?.events) {
const attr = props.config?.events[key]
const func = async () => {
await commonAction(attr['api'], attr['method'], attr['params'])
}
@@ -54,35 +54,20 @@ watchEffect(() => {
</script>
<template>
<Component
:is="elementProps.config?.component"
v-if="!elementProps.config?.html"
v-bind="elementProps.config?.props"
v-on="componentEvents"
>
{{ 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>
<Component :is="config?.component" v-if="!config?.html" v-bind="config?.props" v-on="componentEvents">
{{ config?.text }}
<PageRender
v-for="(innerItem, innerIndex) in elementProps.config?.content || []"
v-for="(innerItem, innerIndex) in config?.content || []"
:key="innerIndex"
:config="innerItem"
@action="emit('action')"
/>
</Component>
<Component
:is="elementProps.config?.component"
v-if="elementProps.config?.html"
v-bind="elementProps.config?.props"
v-html="elementProps.config?.html"
:is="config?.component"
v-if="config?.html"
v-bind="config?.props"
v-html="config?.html"
v-on="componentEvents"
/>
<!-- 进度框 -->

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import api from '@/api'
import { DownloaderConf } from '@/api/types'
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
// 下载器选项
const downloaderOptions = ref<{ title: string; value: string }[]>([])
// 加载所有下载器
async function loadDownloaderSetting() {
try {
const downloaders: DownloaderConf[] = await api.get('download/clients')
downloaderOptions.value = [
{ title: '默认', value: '' },
...downloaders.map((item: { name: any }) => ({
title: item.name,
value: item.name,
})),
]
} catch (error) {
console.error('加载下载器设置失败:', error)
}
}
onMounted(() => {
loadDownloaderSetting()
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-download-box-outline" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>添加下载</VCardTitle>
<VCardSubtitle>根据资源列表添加下载任务</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect v-model="data.downloader" :items="downloaderOptions" label="下载器" outlined dense />
</VCol>
<VCol cols="12">
<VTextField v-model="data.labels" label="标签" placeholder="多个使用,分隔" outlined dense />
</VCol>
<VCol cols="12">
<VPathField v-model="data.save_path" storage="local" label="保存路径" clearable placeholder="留空自动" />
</VCol>
<VCol cols="12">
<VSwitch v-model="data.only_lack" label="仅下载缺失的资源" />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
</script>
<template>
<div>
<VCard>
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-star-check" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>添加订阅</VCardTitle>
<VCardSubtitle>根据媒体列表添加订阅</VCardSubtitle>
</VCardItem>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-progress-download" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>获取下载任务</VCardTitle>
<VCardSubtitle>获取下载队列中的任务状态</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSwitch v-model="data.loop" label="循环执行" />
</VCol>
<VCol cols="12">
<VTextField
v-model="data.loop_interval"
:disabled="!data.loop"
type="number"
label="循环间隔 (秒)"
outlined
dense
clearable
/>
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,156 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
import api from '@/api'
import { RecommendSource } from '@/api/types'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
// 内置榜单
const innerList = [
{
'api_path': 'recommend/tmdb_trending',
'name': '流行趋势',
},
{
'api_path': 'recommend/douban_showing',
'name': '正在热映',
},
{
'api_path': 'bangumi/calendar',
'name': 'Bangumi每日放送',
},
{
'api_path': 'recommend/tmdb_movies',
'name': 'TMDB热门电影',
},
{
'api_path': 'recommend/tmdb_tvs?with_original_language=zh|en|ja|ko',
'name': 'TMDB热门电视剧',
},
{
'api_path': 'recommend/douban_movie_hot',
'name': '豆瓣热门电影',
},
{
'api_path': 'recommend/douban_tv_hot',
'name': '豆瓣热门电视剧',
},
{
'api_path': 'recommend/douban_tv_animation',
'name': '豆瓣热门动漫',
},
{
'api_path': 'recommend/douban_movies',
'name': '豆瓣最新电影',
},
{
'api_path': 'recommend/douban_tvs',
'name': '豆瓣最新电视剧',
},
{
'api_path': 'recommend/douban_movie_top250',
'name': '豆瓣电影TOP250',
},
{
'api_path': 'recommend/douban_tv_weekly_chinese',
'name': '豆瓣国产剧集榜',
},
{
'api_path': 'recommend/douban_tv_weekly_global',
'name': '豆瓣全球剧集榜',
},
]
// 额外的数据源
const extraRecommendSources = ref<RecommendSource[]>([])
// 加载额外的发现数据源
async function loadExtraRecommendSources() {
try {
extraRecommendSources.value = await api.get('recommend/source')
if (extraRecommendSources.value.length > 0) {
innerList.push(
...extraRecommendSources.value.map(source => ({
api_path: source.api_path,
name: source.name,
})),
)
}
} catch (error) {
console.log(error)
}
}
// 来源类型下拉框
const sourceTypeOptions = [
{ value: 'ranking', title: '推荐榜单' },
{ value: 'api', title: 'API' },
]
// 计算下拉框
const sourceOptions = computed(() => innerList.map(item => item.name))
onMounted(() => {
loadExtraRecommendSources()
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-multimedia" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>获取媒体数据</VCardTitle>
<VCardSubtitle>获取榜单等媒体数据列表</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect v-model="data.source_type" :items="sourceTypeOptions" label="来源" outlined dense />
</VCol>
</VRow>
<VRow v-if="data.source_type === 'ranking'">
<VCol cols="12">
<VSelect
v-model="data.sources"
:items="sourceOptions"
label="选择榜单"
chips
multiple
outlined
dense
clearable
/>
</VCol>
</VRow>
<VRow v-else>
<VCol cols="12">
<VTextField
v-model="data.api_path"
label="API地址"
placeholder="/api/v1/plugin/xxx/xxxx"
outlined
dense
clearable
/>
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-rss" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>获取RSS资源</VCardTitle>
<VCardSubtitle>订阅RSS地址获取资源</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="data.url" label="RSS地址" outlined dense clearable />
</VCol>
<VCol cols="12">
<VTextField v-model="data.ua" label="User-Agent" outlined dense clearable />
</VCol>
<VCol cols="12">
<VTextField v-model="data.timeout" type="number" label="超时时间" outlined dense clearable />
</VCol>
<VCol cols="6">
<VSwitch v-model="data.match_media" label="匹配媒体信息" />
</VCol>
<VCol cols="6">
<VSwitch v-model="data.proxy" label="使用代理" />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import api from '@/api'
import { Site } from '@/api/types'
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
// 电影/电视剧下拉框
const typeOptions = ref([
{
title: '电影',
value: '电影',
},
{
title: '电视剧',
value: '电视剧',
},
])
// 搜索方式下拉框
const searchOptions = ref([
{
title: '名称',
value: 'keyword',
},
{
title: '媒体列表',
value: 'media',
},
])
// 站点数据列表
const siteList = ref<Site[]>([])
// 获取站点列表数据
async function loadSites() {
try {
const data: Site[] = await api.get('site/rss')
// 过滤站点,只有启用的站点才显示
siteList.value = data.filter(item => item.is_active)
} catch (error) {
console.error(error)
}
}
// 站点选项
const siteOptions = computed(() => {
return siteList.value.map(item => {
return {
title: item.name,
value: item.id,
}
})
})
onMounted(() => {
loadSites()
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-search-web" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>搜索站点资源</VCardTitle>
<VCardSubtitle>搜索站点种子资源列表</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect v-model="data.search_type" label="搜索方式" :items="searchOptions" outlined dense />
</VCol>
</VRow>
<VRow v-if="data.search_type === 'keyword'">
<VCol cols="6">
<VTextField v-model="data.name" label="名称" outlined dense />
</VCol>
<VCol cols="6">
<VTextField v-model="data.year" label="年份" outlined dense />
</VCol>
<VCol cols="6">
<VSelect v-model="data.type" label="类型" :items="typeOptions" outlined dense />
</VCol>
<VCol cols="6">
<VTextField v-model="data.season" type="number" label="季" outlined dense />
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSelect v-model="data.sites" label="站点" :items="siteOptions" chips multiple outlined dense />
</VCol>
</VRow>
<VRow v-if="data.search_type === 'keyword'">
<VCol cols="12">
<VSwitch v-model="data.match_media" label="匹配媒体信息" />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,81 @@
<script setup lang="ts">
import api from '@/api'
import { Handle, Position } from '@vue-flow/core'
const props = defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
// 电影/电视剧下拉框
const typeOptions = ref([
{
title: '电影',
value: '电影',
},
{
title: '电视剧',
value: '电视剧',
},
])
// 二级分类策略
const mediaCategories = ref<{ [key: string]: any }>({})
// 调用API查询自动分类配置
async function loadMediaCategories() {
try {
mediaCategories.value = await api.get('media/category')
} catch (error) {
console.log(error)
}
}
// 根据选中的媒体类型,获取对应的媒体类别
const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }]
if (!mediaCategories.value || !mediaCategories.value[props.data.type ?? '']) return default_value
return default_value.concat(mediaCategories.value[props.data.type ?? ''])
})
onMounted(() => {
loadMediaCategories()
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-filter-check" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>过滤媒体数据</VCardTitle>
<VCardSubtitle>对媒体数据列表进行过滤</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect v-model="data.type" label="类型" :items="typeOptions" outlined dense />
</VCol>
<VCol cols="6">
<VTextField v-model="data.year" label="年份" outlined dense />
</VCol>
<VCol cols="6">
<VTextField v-model="data.vote" type="number" label="评分" outlined dense />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,176 @@
<script setup lang="ts">
import api from '@/api'
import { FilterRuleGroup } from '@/api/types'
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
// 质量选择框数据
const qualityOptions = ref([
{
title: '全部',
value: '',
},
{
title: '蓝光原盘',
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
},
{
title: 'Remux',
value: 'Remux',
},
{
title: 'BluRay',
value: 'Blu-?Ray',
},
{
title: 'UHD',
value: 'UHD|UltraHD',
},
{
title: 'WEB-DL',
value: 'WEB-?DL|WEB-?RIP',
},
{
title: 'HDTV',
value: 'HDTV',
},
{
title: 'H265',
value: '[Hx].?265|HEVC',
},
{
title: 'H264',
value: '[Hx].?264|AVC',
},
])
// 分辨率选择框数据
const resolutionOptions = ref([
{
title: '全部',
value: '',
},
{
title: '4k',
value: '4K|2160p|x2160',
},
{
title: '1080p',
value: '1080[pi]|x1080',
},
{
title: '720p',
value: '720[pi]|x720',
},
])
// 特效选择框数据
const effectOptions = ref([
{
title: '全部',
value: '',
},
{
title: '杜比视界',
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
},
{
title: '杜比全景声',
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
},
{
title: 'HDR',
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
},
{
title: 'SDR',
value: '[\\s.]+SDR[\\s.]+',
},
])
// 所有规则组列表
const filterRuleGroups = ref<FilterRuleGroup[]>([])
// 加载规则组
async function queryFilterRuleGroups() {
try {
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
filterRuleGroups.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 计算过滤规则组选择框数据
const ruleGroupsOptions = computed(() => {
return filterRuleGroups.value.map(group => ({
title: group.name,
value: group.name,
}))
})
onMounted(() => {
queryFilterRuleGroups()
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-filter-multiple" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>过滤资源</VCardTitle>
<VCardSubtitle>对资源列表数据进行过滤</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="6">
<VSelect v-model="data.quality" label="质量" :items="qualityOptions" outlined dense />
</VCol>
<VCol cols="6">
<VSelect v-model="data.resolution" label="分辨率" :items="resolutionOptions" outlined dense />
</VCol>
<VCol cols="6">
<VSelect v-model="data.effect" label="特效" :items="effectOptions" outlined dense />
</VCol>
<VCol cols="6">
<VTextField v-model="data.size" label="大小范围" placeholder="MB" outlined dense />
</VCol>
<VCol cols="12">
<VTextField v-model="data.include" label="包含(关键字、正则式)" outlined dense />
</VCol>
<VCol cols="12">
<VTextField v-model="data.exclude" label="排除(关键字、正则式)" outlined dense />
</VCol>
<VCol cols="12">
<VSelect
v-model="data.rule_groups"
chips
multiple
label="过滤规则组"
:items="ruleGroupsOptions"
outlined
dense
/>
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
import { storageOptions } from '@/api/constants'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-file-move" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>扫描目录</VCardTitle>
<VCardSubtitle>扫描目录文件到队列</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect v-model="data.storage" label="存储" :items="storageOptions" outlined dense />
</VCol>
<VCol cols="12">
<VPathField v-model="data.directory" :storage="data.storage" label="目录" clearable />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-file-find" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>刮削文件</VCardTitle>
<VCardSubtitle>刮削媒体信息和图片</VCardSubtitle>
</VCardItem>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-send-check" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>发送事件</VCardTitle>
<VCardSubtitle>发送任务执行事件</VCardSubtitle>
</VCardItem>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import api from '@/api'
import { NotificationConf } from '@/api/types'
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
// 所有消息渠道
const notifications = ref<NotificationConf[]>([])
// 调用API查询通知渠道设置
async function loadNotificationSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Notifications')
notifications.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 计算消息渠道选项
const sourceOptions = computed(() => {
return notifications.value.map(item => {
return {
title: item.name,
value: item.name,
}
})
})
onMounted(() => {
loadNotificationSetting()
})
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-message-arrow-right" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>发送消息</VCardTitle>
<VCardSubtitle>发送任务执行消息</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect
v-model="data.client"
:items="sourceOptions"
label="消息渠道"
chips
multiple
outlined
dense
clearable
/>
</VCol>
<VCol cols="12">
<VTextField v-model="data.userid" label="用户ID" chips multiple outlined dense clearable />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
// 来源下拉框
const sourceOptions = ref([
{
title: '文件列表',
value: 'files',
},
{
title: '下载任务',
value: 'downloads',
},
])
</script>
<template>
<div>
<VCard max-width="20rem">
<Handle id="edge_in" type="target" :position="Position.Left" />
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-file-move" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>整理文件</VCardTitle>
<VCardSubtitle>整理重命名队列中的文件</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect v-model="data.source" label="来源" :items="sourceOptions" outlined dense />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -8,7 +8,7 @@ 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 { useUserStore } from '@/stores'
import { SystemNavMenus } from '@/router/menu'
import { NavMenu } from '@/@layouts/types'
import { useDisplay } from 'vuetify'
@@ -16,8 +16,11 @@ import { useDisplay } from 'vuetify'
const display = useDisplay()
const appMode = inject('pwaMode')
// 用户 Store
const userStore = useUserStore()
// 是否超级用户
let superUser = store.state.auth.superUser
let superUser = userStore.superUser
// 开始菜单项
const startMenus = ref<NavMenu[]>([])
@@ -64,7 +67,7 @@ onMounted(() => {
<VIcon icon="mdi-menu" />
</IconBtn>
<!-- 👉 Back Button -->
<IconBtn v-if="appMode && display.mdAndDown.value" class="ms-n2" @click="goBack">
<IconBtn v-if="appMode" class="ms-n2" @click="goBack">
<VIcon icon="mdi-arrow-left" size="32" />
</IconBtn>
<!-- 👉 Search Bar -->

View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
import { Background } from '@vue-flow/background'
</script>
<template>
<div class="dropzone-background">
<Background :size="2" :gap="20" pattern-color="#BDBDBD" />
<div class="overlay">
<slot />
</div>
</div>
</template>

View File

@@ -1,38 +1,49 @@
<script setup lang="ts">
import { SystemNavMenus } from '@/router/menu'
import { useDisplay } from 'vuetify'
import { VMenu } from 'vuetify/lib/components/index.mjs'
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
const route = useRoute()
// 各按钮活动状态
const moreMenuDialog = ref(false)
const moreMemus = computed(() => SystemNavMenus.filter(menu => !menu.footer))
const activeState = computed(() => {
return {
home: route.path === '/dashboard',
ranking: route.path === '/ranking',
recommend: route.path === '/recommend',
movie: route.path === '/subscribe/movie',
tv: route.path === '/subscribe/tv',
apps: route.path === '/apps',
}
})
const moreActiveState = computed(() => {
return !Object.values(activeState.value).some(v => v)
})
const currentPath = computed(() => route.path)
</script>
<template>
<div v-if="appMode" class="w-100" style="block-size: calc(3.5rem + env(safe-area-inset-bottom))">
<div v-if="appMode" class="w-100">
<VBottomNavigation
grow
horizontal
color="primary"
class="footer-nav border-t"
style="block-size: calc(3.5rem + env(safe-area-inset-bottom))"
:z-index="9998"
>
<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>
<VBtn to="/recommend" :ripple="false">
<VIcon v-if="activeState.recommend" size="28">mdi-star</VIcon>
<VIcon v-else size="28">mdi-star-outline</VIcon>
</VBtn>
<VBtn to="/subscribe/movie" :ripple="false">
@@ -43,9 +54,31 @@ const activeState = computed(() => {
<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 :ripple="false">
<VIcon
size="28"
:icon="moreMenuDialog ? 'mdi-close' : 'mdi-dots-horizontal'"
:color="moreActiveState ? 'primary' : ''"
/>
<VMenu v-model="moreMenuDialog" close-on-content-click activator="parent">
<VDivider />
<VList class="font-bold" lines="one">
<VListSubheader class="bg-transparent"> 更多 </VListSubheader>
<VListItem
class="pe-20"
v-for="(menu, index) in moreMemus"
:key="index"
:prepend-icon="menu.icon"
nav
:to="menu.to"
:base-color="currentPath === menu.to ? 'primary' : undefined"
>
<VListItemTitle>
<span class="text-lg">{{ menu.title }}</span>
</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</VBottomNavigation>
</div>
@@ -64,4 +97,3 @@ const activeState = computed(() => {
background-color: transparent !important;
}
</style>
}

View File

@@ -262,7 +262,7 @@ onMounted(() => {
<VDialog
v-if="messageDialog"
v-model="messageDialog"
max-width="60rem"
max-width="45rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { useStore } from 'vuex'
import { useConfirm } from 'vuetify-use-dialog'
import { useToast } from 'vue-toast-notification'
import router from '@/router'
@@ -7,9 +6,12 @@ import avatar1 from '@images/avatars/avatar-1.png'
import api from '@/api'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
import { useAuthStore, useUserStore } from '@/stores'
// Vuex Store
const store = useStore()
// 认证 Store
const authStore = useAuthStore()
// 用户 Store
const userStore = useUserStore()
// 确认框
const createConfirm = useConfirm()
@@ -23,23 +25,21 @@ const progressDialog = ref(false)
// 站点认证对话框
const siteAuthDialog = ref(false)
// 重启确认对话框
const restartDialog = ref(false)
// 执行注销操作
function logout() {
// 清除登录状态信息
store.dispatch('auth/logout')
authStore.logout()
// 重定向到登录页面或其他适当的页面
router.push('/login')
}
// 执行重启操作
async function restart() {
// 弹出提示
const confirmed = await createConfirm({
title: '确认',
content: '确认重启系统吗?',
})
if (confirmed) {
{
restartDialog.value = false
// 调用API重启
try {
// 显示等待框
@@ -60,6 +60,11 @@ async function restart() {
}
}
// 显示重启确认对话框
async function showRestartDialog() {
restartDialog.value = true
}
// 显示站点认证对话框
function showSiteAuthDialog() {
siteAuthDialog.value = true
@@ -71,11 +76,11 @@ function siteAuthDone() {
logout()
}
// 从Vuex Store中获取信息
const superUser = computed(() => store.state.auth.superUser)
const userName = computed(() => store.state.auth.userName)
const avatar = computed(() => store.state.auth.avatar || avatar1)
const userLevel = computed(() => store.state.auth.level)
// 从用户 Store中获取信息
const superUser = computed(() => userStore.superUser)
const userName = computed(() => userStore.userName)
const avatar = computed(() => userStore.avatar || avatar1)
const userLevel = computed(() => userStore.level)
</script>
<template>
@@ -110,8 +115,15 @@ const userLevel = computed(() => store.state.auth.level)
<VListItemTitle>个人信息</VListItemTitle>
</VListItem>
<VListItem link @click="router.push('/apps')">
<template #prepend>
<VIcon class="me-2" icon="mdi-view-grid-outline" size="22" />
</template>
<VListItemTitle>功能视图</VListItemTitle>
</VListItem>
<!-- 👉 Site Auth -->
<VListItem v-if="userLevel < 2" link @click="showSiteAuthDialog">
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog">
<template #prepend>
<VIcon class="me-2" icon="mdi-lock-check-outline" size="22" />
</template>
@@ -130,7 +142,7 @@ const userLevel = computed(() => store.state.auth.level)
<VDivider v-if="superUser" class="my-2" />
<!-- 👉 restart -->
<VListItem v-if="superUser" @click="restart">
<VListItem v-if="superUser" @click="showRestartDialog">
<template #prepend>
<VIcon class="me-2" icon="mdi-restart" size="22" />
</template>
@@ -152,4 +164,25 @@ const userLevel = computed(() => store.state.auth.level)
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在重启 ..." />
<!-- 用户认证对话框 -->
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
<!-- 重启确认对话框 -->
<VDialog v-if="restartDialog" v-model="restartDialog" max-width="25rem">
<VCard>
<VCardItem>
<div class="flex items-center justify-center mt-3">
<VAvatar color="warning" variant="text" size="x-large">
<VIcon size="x-large" icon="mdi-alert" />
</VAvatar>
<div class="ms-3">
<p class="font-bold text-xl text-high-emphasis">确认重启系统吗</p>
<p>重启后您将被注销并需要重新登录</p>
</div>
</div>
</VCardItem>
<VCardActions class="mx-auto">
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5"> 确定 </VBtn>
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">取消</VBtn>
</VCardActions>
<DialogCloseBtn @click="restartDialog = false" />
</VCard>
</VDialog>
</template>

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