Compare commits

...

257 Commits

Author SHA1 Message Date
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
147 changed files with 10959 additions and 6482 deletions

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,5 +1,6 @@
<!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">
@@ -29,9 +30,17 @@
<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>
<body>
<body style="margin: 0;">
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
@@ -146,16 +155,6 @@
</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>
</html>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.1.5",
"version": "2.3.2",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -19,88 +19,92 @@
]
},
"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",
"@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.10",
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"lodash": "^4.17.21",
"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/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)
}
// 切换主题
@@ -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" persistent 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`
}

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)
}

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

@@ -0,0 +1,121 @@
import { useVueFlow } from '@vue-flow/core'
import { ref, watch } from 'vue'
/**
* @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,
}
/**
* 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

@@ -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;
}

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

@@ -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
// 海报
@@ -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 {
// 上映日期
@@ -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,6 +1166,7 @@ export interface SubscribeEpisodeInfo {
library?: SubscribeLibraryFileInfo[]
}
// 订阅详情
export interface SubscrbieInfo {
// 订阅信息
subscribe: Subscribe
@@ -1140,6 +1174,7 @@ export interface SubscrbieInfo {
episodes: Record<number, SubscribeEpisodeInfo>
}
// 整理表单
export interface TransferForm {
// 文件项
fileitem: FileItem
@@ -1178,3 +1213,79 @@ export interface TransferForm {
// 媒体库类别子目录
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'
// 输入参数
@@ -104,7 +104,7 @@ function onClose() {
<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 />

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)
@@ -105,13 +98,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 +124,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: '' }]
@@ -228,16 +203,12 @@ watch(
/>
</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>
@@ -275,16 +246,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 />

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({
@@ -220,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"
@@ -257,7 +257,7 @@ function onClose() {
/>
</VCol>
</VRow>
</VCardText>
</VCardItem>
<VCardText>
<draggable
v-model="filterRuleCards"

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,50 @@ 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)
// 查询所有站点
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 +131,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 +151,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 +175,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 +254,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 +265,7 @@ async function handleCheckExists() {
season: props.media?.season,
mtype: props.media?.type,
},
signal,
})
if (result.success) isExists.value = true
@@ -238,13 +277,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 +309,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 +323,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 +386,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 +424,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 +513,109 @@ 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>
</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 +669,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 {
@@ -150,7 +147,7 @@ const dropdownItems = ref([
<template>
<div>
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col h-full">
<VCard :width="props.width" :height="props.height" @click="detailDialog = true" class="flex flex-col h-full">
<div
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
@@ -160,11 +157,11 @@ const dropdownItems = ref([
: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">
<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 px-2 py-1 text-shadow line-clamp-3">
<VCardText class="text-white text-sm px-2 py-0 text-shadow overflow-hidden line-clamp-3 ...">
{{ props.plugin?.plugin_desc }}
</VCardText>
</div>
@@ -226,5 +223,64 @@ const dropdownItems = ref([
<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([
{
@@ -411,12 +345,12 @@ watch(
: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">
<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>
<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">
<VCardText class="px-2 py-0 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
{{ props.plugin?.plugin_desc }}
</VCardText>
</div>
@@ -450,7 +384,7 @@ watch(
<div class="me-n3 absolute bottom-1 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
@@ -481,33 +415,23 @@ watch(
</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" />
@@ -519,14 +443,14 @@ watch(
<VDivider />
<VersionHistory :history="props.plugin?.history" />
<VDivider />
<VCardText>
<VCardItem>
<VBtn @click="updatePlugin" block>
<template #prepend>
<VIcon icon="mdi-arrow-up-circle-outline" />
</template>
更新到最新版本
</VBtn>
</VCardText>
</VCardItem>
</VCard>
</VDialog>
</div>

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

@@ -38,11 +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() {
@@ -142,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,
},
})
@@ -239,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
@@ -285,6 +303,7 @@ function onSubscribeEditRemove() {
}"
min-height="170"
@click="editSubscribeDialog"
:ripple="false"
>
<div class="me-n3 absolute top-1 right-2">
<IconBtn>

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,266 @@
<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) {
loading.value = true
try {
setTimeout(() => {
emit('refresh')
}, 500)
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/run`)
if (result.success) {
$toast.success('任务执行完成!')
emit('refresh')
} else {
$toast.error(`任务执行失败:${result.message}`)
emit('refresh')
}
} catch (error) {
console.error(error)
}
loading.value = false
}
// 计算状态颜色
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 variant="plain" base-color="info" @click="handleRun(workflow)">
<template #prepend>
<VIcon icon="mdi-run" />
</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

@@ -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

@@ -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

@@ -49,7 +49,7 @@ const progressEventSource = ref<EventSource>()
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
const progressText = ref('正在处理 ...')
// 整理进度
const progressValue = ref(0)
@@ -65,6 +65,14 @@ 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<TransferForm>({
fileitem: {} as FileItem,
@@ -119,9 +127,35 @@ watch(
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 = '请稍候 ...'
@@ -141,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 = {} as 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>
@@ -224,7 +244,8 @@ onMounted(() => {
label="整理方式"
:items="transferTypeOptions"
hint="文件操作整理方式"
persistent-hint>
persistent-hint
>
<template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? '自动' : item.title }}
</template>
@@ -305,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"
@@ -369,7 +391,7 @@ onMounted(() => {
<VSwitch
v-model="transferForm.from_history"
label="复用历史识别信息"
hint="使用历史记录中已识别的媒体信息"
hint="使用历史整理记录中已识别的媒体信息"
persistent-hint
/>
</VCol>
@@ -378,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

@@ -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 [
@@ -243,13 +240,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 +263,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 +439,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 +448,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

@@ -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

@@ -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

@@ -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 {
@@ -269,41 +274,48 @@ onMounted(() => {
>
<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

@@ -129,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,303 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { VueFlow, useVueFlow } 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(addEdges)
// 自定义节点类型
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('任务流程代码已复制到剪贴板!')
}
// 删除选中节点或连接线
const deleteSelectedNodeOrEdge = () => {
// 删除选中的节点
const selectedNode = nodes.value.find((node: { selected: any }) => node.selected)
if (selectedNode) {
// 删除节点
nodes.value = nodes.value.filter((node: { id: any }) => node.id !== selectedNode.id)
// 删除与该节点相关的 edges
edges.value = edges.value.filter(
(edge: { source: any; target: any }) => edge.source !== selectedNode.id && edge.target !== selectedNode.id,
)
}
// 删除选中的连接线
const selectedEdge = edges.value.find((edge: { selected: any }) => edge.selected)
if (selectedEdge) {
// 删除连接线
edges.value = edges.value.filter((edge: { id: any }) => edge.id !== selectedEdge.id)
}
}
// 键盘按键事件处理
const handleKeyDown = (event: { key: string }) => {
if (event.key === 'Delete' || event.key === 'Backspace') {
deleteSelectedNodeOrEdge()
}
}
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"
:default-edge-options="{ type: 'animation', animated: true }"
:edge-updater-radius="10"
@dragover="onDragOver"
@dragleave="onDragLeave"
@keydown="handleKeyDown"
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" persistent 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

@@ -169,7 +169,7 @@ const sortIcon = computed(() => {
</IconBtn>
</template>
</VTooltip>
<VDialog v-model="newFolderPopper" max-width="50rem">
<VDialog v-if="newFolderPopper" v-model="newFolderPopper" max-width="50rem">
<template #activator="{ props }">
<IconBtn v-bind="props">
<VTooltip text="新建文件夹">

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,102 +0,0 @@
<script setup lang="ts">
import api from '@/api'
import { FileItem } from '@/api/types'
const props = defineProps({
root: {
type: String,
default: '/',
required: true,
},
storage: {
type: String,
default: 'local',
},
})
const emit = defineEmits(['update:modelValue'])
const activedDirs = ref<string[]>([])
const openedDirs = ref<string[]>([])
const isUserAction = ref(false) // 标志:是否为用户主动操作
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 || !isUserAction.value) return
emit('update:modelValue', selectedPath.value)
isUserAction.value = false
})
watch(
() => props.storage,
async newVal => {
treeItems.value = [
{
name: '/',
path: props.root,
children: [],
type: 'dir',
basename: props.root,
storage: newVal,
},
]
openedDirs.value = []
activedDirs.value = []
},
)
function handleUserSelect() {
isUserAction.value = true
}
</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"
@update:activated="handleUserSelect"
/>
</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,141 @@
<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')) {
// 处理事件监听,值是函数的代码
const eventName = key.replace('on', '').toLowerCase()
parsedProps[eventName] = new Function('model', `with(model) { return ${value} }`)(model)
} 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,70 @@
<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">
<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,48 @@
<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="12">
<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,113 @@
<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>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,84 @@
<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="12">
<VSelect v-model="data.category" label="类别" :items="getCategories" 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

@@ -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,6 +115,13 @@ 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 && superUser" link @click="showSiteAuthDialog">
<template #prepend>
@@ -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>

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import api from '@/api'
import useDragAndDrop from '@core/utils/workflow'
const { onDragStart } = useDragAndDrop()
// 组件列表
const actions = ref([])
// 加载组件列表
async function load_actions() {
try {
actions.value = await api.get('workflow/actions')
} catch (error) {
console.error(error)
}
}
onMounted(() => {
load_actions()
})
</script>
<template>
<aside>
<div class="mb-3"><VLabel>可选动作组件</VLabel></div>
<div class="nodes flex flex-wrap justify-center">
<div
class="vue-flow__node-default cursor-grab mx-1"
v-for="(action, index) in actions"
:key="index"
:draggable="true"
@dragstart="onDragStart($event, action)"
>
{{ action['name'] }}
</div>
</div>
</aside>
</template>

View File

@@ -1,20 +1,31 @@
// 1. 配置与兼容性
import './ace-config'
import '@/@core/utils/compatibility'
import '@/@iconify/icons-bundle'
import '@/plugins/webfontloader'
import App from '@/App.vue'
// 2. 核心插件和 UI 框架
import { createApp } from 'vue'
import vuetify from '@/plugins/vuetify'
import router from '@/router'
import store from '@/store'
import { createApp } from 'vue'
import { removeEl } from './@core/utils/dom'
import { fetchGlobalSettings } from './api'
import { isPWA } from './@core/utils/navigator'
import './ace-config'
import pinia from '@/stores/index'
// 3. 全局组件
import App from '@/App.vue'
import { VAceEditor } from 'vue3-ace-editor'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
import { CronVuetify } from '@vue-js-cron/vuetify'
// 4. 工具函数和其他辅助模块
import { fetchGlobalSettings } from './api'
import { isPWA } from './@core/utils/navigator'
// 5. 其他插件和功能模块
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import VueApexCharts from 'vue3-apexcharts'
// 6. 注册自定义组件
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import MediaCard from './components/cards/MediaCard.vue'
import PosterCard from './components/cards/PosterCard.vue'
@@ -23,12 +34,18 @@ import PersonCard from './components/cards/PersonCard.vue'
import MediaInfoCard from './components/cards/MediaInfoCard.vue'
import TorrentCard from './components/cards/TorrentCard.vue'
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
import PathField from './components/input/PathField.vue'
import CronField from './components/field/CronField.vue'
import PathField from './components/field/PathField.vue'
// 7. 样式文件
import '@core/scss/template/libs/vuetify/index.scss'
import 'vuetify/styles'
import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss'
import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import 'vue3-perfect-scrollbar/style.css'
import '@vue-js-cron/vuetify/dist/vuetify.css'
import '@styles/styles.scss'
// 创建Vue实例
const app = createApp(App)
@@ -38,9 +55,6 @@ async function initializeApp() {
// 是否为PWA
const pwaMode = await isPWA()
app.provide('pwaMode', pwaMode)
// 全局设置
const globalSettings = await fetchGlobalSettings()
app.provide('globalSettings', globalSettings)
} catch (error) {
console.error('Failed to initialize app', error)
}
@@ -48,14 +62,20 @@ async function initializeApp() {
// 注册全局组件
initializeApp().then(() => {
// 优先注册框架
app
.use(vuetify)
// 注册全局组件
// 1. 注册 UI 框架
app.use(vuetify)
// 2. 注册状态管理与路由
app.use(pinia).use(router)
// 3. 全局设置
app.provide('globalSettings', fetchGlobalSettings())
// 4. 注册全局组件
app
.component('VAceEditor', VAceEditor)
.component('VApexChart', VueApexCharts)
.component('VCronVuetify', CronVuetify)
.component('VDialogCloseBtn', DialogCloseBtn)
.component('VMediaCard', MediaCard)
.component('VPosterCard', PosterCard)
@@ -64,19 +84,19 @@ initializeApp().then(() => {
.component('VMediaInfoCard', MediaInfoCard)
.component('VTorrentCard', TorrentCard)
.component('VMediaIdSelector', MediaIdSelector)
.component('VCronField', CronField)
.component('VPathField', PathField)
// 注册插件
// 5. 注册其他插件
app
.use(router)
.use(store)
.use(PerfectScrollbarPlugin)
.use(ToastPlugin, {
position: 'bottom-right',
})
.use(VuetifyUseDialog, {
confirmDialog: {
dialogProps: {
maxWidth: '40rem',
maxWidth: '30rem',
},
confirmationButtonProps: {
variant: 'elevated',
@@ -93,8 +113,5 @@ initializeApp().then(() => {
cancellationText: '取消',
},
})
.use(PerfectScrollbarPlugin)
.use(VueApexCharts)
.mount('#app')
.$nextTick(() => removeEl('#loading-bg'))
})

View File

@@ -1,18 +1,18 @@
<script setup lang="ts">
import { NavMenu } from '@/@layouts/types'
import { SystemNavMenus } from '@/router/menu'
import store from '@/store'
import { useUserStore } from '@/stores'
import draggable from 'vuedraggable'
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
// 从 Store 中获取superuser信息
const superUser = useUserStore().superUser
// APP图标顺序
const appOrder = ref<string[]>([])
// 根据分类获取菜单列表
const getMenuList = () => {
return SystemNavMenus.filter((item: NavMenu) => (!item.admin || superUser) && !item.footer)
return SystemNavMenus.filter((item: NavMenu) => !item.admin || superUser)
}
// APP列表

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import api from '@/api'
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
import PersonCardListView from '@/views/discover/PersonCardListView.vue'

View File

@@ -3,7 +3,7 @@ import draggable from 'vuedraggable'
import api from '@/api'
import { isNullOrEmptyObject } from '@/@core/utils'
import { DashboardItem } from '@/api/types'
import store from '@/store'
import { useUserStore } from '@/stores'
import DashboardElement from '@/components/misc/DashboardElement.vue'
import { useDisplay } from 'vuetify'
@@ -11,8 +11,8 @@ import { useDisplay } from 'vuetify'
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
// 从用户 Store 中获取superuser信息
const superUser = useUserStore().superUser
// 是否拉升高度
const isElevated = ref(true)
@@ -340,7 +340,7 @@ onDeactivated(() => {
/>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-model="dialog" max-width="35rem" scrollable>
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>设置仪表板</VCardTitle>

90
src/pages/discover.vue Normal file
View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { DiscoverTabs } from '@/router/menu'
import router from '@/router'
import TheMovieDbView from '@/views/discover/TheMovieDbView.vue'
import DoubanView from '@/views/discover/DoubanView.vue'
import BangumiView from '@/views/discover/BangumiView.vue'
import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
import { DiscoverSource } from '@/api/types'
import api from '@/api'
const route = useRoute()
const activeTab = ref(route.query.tab)
function jumpTab(tab: string) {
router.push('/subscribe/discover?tab=' + tab)
}
// 额外的数据源
const extraDiscoverSources = ref<DiscoverSource[]>([])
// 加载额外的发现数据源
async function loadExtraDiscoverSources() {
try {
extraDiscoverSources.value = await api.get('discover/source')
} catch (error) {
console.log(error)
}
}
onMounted(async () => {
await loadExtraDiscoverSources()
})
onActivated(async () => {
loadExtraDiscoverSources()
})
</script>
<template>
<div>
<VTabs v-model="activeTab" show-arrows>
<VTab v-for="item in DiscoverTabs" :value="item.tab" @to="jumpTab(item.tab)">
<div class="min-w-24">
{{ item.title }}
</div>
</VTab>
<VTab
v-for="item in extraDiscoverSources"
:key="item.mediaid_prefix"
:value="item.mediaid_prefix"
@to="jumpTab(item.mediaid_prefix)"
>
<div class="min-w-24">
{{ item.name }}
</div>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="themoviedb">
<transition name="fade-slide" appear>
<div>
<TheMovieDbView />
</div>
</transition>
</VWindowItem>
<VWindowItem value="douban">
<transition name="fade-slide" appear>
<div>
<DoubanView />
</div>
</transition>
</VWindowItem>
<VWindowItem value="bangumi">
<transition name="fade-slide" appear>
<div>
<BangumiView />
</div>
</transition>
</VWindowItem>
<VWindowItem v-for="item in extraDiscoverSources" :key="item.mediaid_prefix" :value="item.mediaid_prefix">
<transition name="fade-slide" appear>
<div>
<ExtraSourceView :source="item" />
</div>
</transition>
</VWindowItem>
</VWindow>
</div>
</template>

View File

@@ -29,20 +29,26 @@ function jumpTab(tab: string) {
onMounted(async () => {
await loadDownloaderSetting()
})
onActivated(async () => {
loadDownloaderSetting()
})
</script>
<template>
<div v-if="downloaders.length > 0">
<VTabs v-model="activeTab">
<VTab v-for="item in downloaders" :value="item.name" @to="jumpTab(item.name)">
<span class="mx-5">{{ item.name }}</span>
<span class="min-w-24">{{ item.name }}</span>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem v-for="item in downloaders" :value="item.name">
<transition name="fade-slide" appear>
<DownloadingListView :name="item.name" />
<div>
<DownloadingListView :name="item.name" />
</div>
</transition>
</VWindowItem>
</VWindow>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { debounce } from 'lodash'
import { debounce } from 'lodash-es'
import { VForm } from 'vuetify/components/VForm'
import { useStore } from 'vuex'
import { useAuthStore, useUserStore } from '@/stores'
import { authState, userState } from '@/stores/types'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import router from '@/router'
@@ -9,11 +10,14 @@ import logo from '@images/logo.png'
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
import { saveLocalTheme } from '@/@core/utils/theme'
// 主题
const { global: globalTheme } = useTheme()
// Vuex Store
const store = useStore()
// 认证 Store
const authStore = useAuthStore()
//用户 Store
const userStore = useUserStore()
// 表单
const form = ref({
@@ -85,8 +89,7 @@ async function setTheme() {
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
// 存储主题到本地
localStorage.setItem('theme', themeValue)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
saveLocalTheme(themeValue, globalTheme)
}
// 订阅推送通知
@@ -119,7 +122,7 @@ async function afterLogin(superuser: boolean) {
// 生效主题配置
await setTheme()
// 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/')
router.push(authStore.originalPath ?? '/')
// 订阅推送通知
if (superuser) await subscribeForPushNotifications()
}
@@ -130,7 +133,6 @@ function login() {
// 进行表单校验
if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) {
errorMessage.value = '请输入完整信息'
return
}
// 用户名密码
@@ -148,38 +150,33 @@ function login() {
},
})
.then((response: any) => {
// 获取token
const token = response.access_token
const superUser = response.super_user
const userID = response.user_id
const userName = response.user_name
const avatar = response.avatar
const level = response.level
const remember = form.value.remember
const permissions = response.permissions
const authPayLoad: authState = {
token: response.access_token,
remember: form.value.remember,
}
// 更新token和remember状态到Vuex Store
store.dispatch('auth/login', {
token,
remember,
superUser,
userID,
userName,
avatar,
level,
permissions,
})
const userPayload: userState = {
superUser: response.super_user,
userID: response.user_id,
userName: response.user_name,
avatar: response.avatar,
level: response.level,
permissions: response.permissions,
}
authStore.login(authPayLoad)
userStore.loginUser(userPayload)
// 登录后处理
afterLogin(superUser)
afterLogin(userPayload.superUser)
})
.catch((error: any) => {
// 登录失败,显示错误提示
if (!error.response) errorMessage.value = '登录失败,请检查网络连接'
else if (error.response.status === 401) errorMessage.value = '登录失败,请检查用户名、密码或双重验证是否正确'
else if (error.response.status === 403) errorMessage.value = '登录失败,您没有权限访问'
else if (error.response.status === 500) errorMessage.value = '登录失败,服务器错误'
else errorMessage.value = `登录失败 ${error.response.status},请检查用户名、密码或双重验证码是否正确`
if (!error.response) errorMessage.value = '登录失败,请检查网络连接'
else if (error.response.status === 401) errorMessage.value = '登录失败,请检查用户名、密码或双重验证是否正确'
else if (error.response.status === 403) errorMessage.value = '登录失败,您没有权限访问'
else if (error.response.status === 500) errorMessage.value = '登录失败,服务器错误'
else errorMessage.value = `登录失败 ${error.response.status},请检查用户名、密码或双重验证码是否正确`
})
}
@@ -192,9 +189,9 @@ function startBackgroundRotation() {
// 自动登录
onMounted(async () => {
// 从Vuex Store中获取token和remember状态
const token = store.state.auth.token
const remember = store.state.auth.remember
// 获取token和remember状态
const token = authStore.token
const remember = authStore.remember
// 如果token存在且保持登录状态为true则跳转到首页
if (token && remember) {
@@ -241,7 +238,7 @@ onUnmounted(() => {
<VCardTitle class="font-weight-bold text-2xl text-uppercase"> MoviePilot </VCardTitle>
</VCardItem>
<VCardText>
<VForm ref="refForm" @submit.prevent="() => {}">
<VForm ref="refForm" autocomplete="on" @submit.prevent="() => {}">
<VRow>
<!-- username -->
<VCol cols="12">
@@ -250,6 +247,8 @@ onUnmounted(() => {
v-model="form.username"
label="用户名"
type="text"
name="username"
autocomplete="username"
:rules="[requiredValidator]"
@input="fetchOTP"
/>
@@ -260,6 +259,8 @@ onUnmounted(() => {
v-model="form.password"
label="密码"
:type="isPasswordVisible ? 'text' : 'password'"
name="current-password"
autocomplete="current-password"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
:rules="[requiredValidator]"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@@ -275,9 +276,9 @@ onUnmounted(() => {
<VCol cols="12">
<!-- login button -->
<VBtn block type="submit" @click="login"> 登录 </VBtn>
<div v-if="errorMessage" class="text-error mt-2 text-shadow">
<VAlert v-if="errorMessage" type="error" variant="tonal" class="mt-3">
{{ errorMessage }}
</div>
</VAlert>
</VCol>
</VRow>
</VForm>

View File

@@ -9,13 +9,16 @@ const mediaid = route.query?.mediaid?.toString()
// 类型
const type = route.query?.type?.toString()
// 标题
const title = route.query?.title?.toString()
// 年份
const year = route.query?.year?.toString()
</script>
<template>
<div>
<MediaDetailView
:mediaid="mediaid"
:type="type"
/>
<MediaDetailView :mediaid="mediaid" :type="type" :title="title" :year="year" />
</div>
</template>

View File

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

196
src/pages/recommend.vue Normal file
View File

@@ -0,0 +1,196 @@
<script setup lang="ts">
import api from '@/api'
import { RecommendSource } from '@/api/types'
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
import { useDisplay } from 'vuetify'
// APP
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>([
{
apipath: 'recommend/tmdb_trending',
linkurl: '/browse/recommend/tmdb_trending?title=流行趋势',
title: '流行趋势',
},
{
apipath: 'recommend/douban_showing',
linkurl: '/browse/recommend/douban_showing?title=正在热映',
title: '正在热映',
},
{
apipath: 'bangumi/calendar',
linkurl: '/browse/bangumi/calendar?title=Bangumi每日放送',
title: 'Bangumi每日放送',
},
{
apipath: 'recommend/tmdb_movies',
linkurl: '/browse/recommend/tmdb_movies?title=TMDB热门电影',
title: 'TMDB热门电影',
},
{
apipath: 'recommend/tmdb_tvs?with_original_language=zh|en|ja|ko',
linkurl: '/browse/recommend/tmdb_tvs??with_original_language=zh|en|ja|ko&title=TMDB热门电视剧',
title: 'TMDB热门电视剧',
},
{
apipath: 'recommend/douban_movie_hot',
linkurl: '/browse/recommend/douban_movie_hot?title=豆瓣热门电影',
title: '豆瓣热门电影',
},
{
apipath: 'recommend/douban_tv_hot',
linkurl: '/browse/recommend/douban_tv_hot?title=豆瓣热门电视剧',
title: '豆瓣热门电视剧',
},
{
apipath: 'recommend/douban_tv_animation',
linkurl: '/browse/recommend/douban_tv_animation?title=豆瓣热门动漫',
title: '豆瓣热门动漫',
},
{
apipath: 'recommend/douban_movies',
linkurl: '/browse/recommend/douban_movies?title=豆瓣最新电影',
title: '豆瓣最新电影',
},
{
apipath: 'recommend/douban_tvs',
linkurl: '/browse/recommend/douban_tvs?title=豆瓣最新电视剧',
title: '豆瓣最新电视剧',
},
{
apipath: 'recommend/douban_movie_top250',
linkurl: '/browse/recommend/douban_movie_top250?title=电影TOP250',
title: '豆瓣电影TOP250',
},
{
apipath: 'recommend/douban_tv_weekly_chinese',
linkurl: '/browse/recommend/douban_tv_weekly_chinese?title=豆瓣国产剧集榜',
title: '豆瓣国产剧集榜',
},
{
apipath: 'recommend/douban_tv_weekly_global',
linkurl: '/browse/recommend/douban_tv_weekly_global?title=豆瓣全球剧集榜',
title: '豆瓣全球剧集榜',
},
])
// 计算启用的视图
const enabledViews = computed(() => viewList.filter(item => enableConfig.value[item.title]))
// 榜单启用配置, 以title为key
const enableConfig = ref<{ [key: string]: boolean }>({
...Object.fromEntries(viewList.map(item => [item.title, true])),
})
// 弹窗
const dialog = ref(false)
// 额外的数据源
const extraRecommendSources = ref<RecommendSource[]>([])
// 加载额外的发现数据源
async function loadExtraRecommendSources() {
try {
extraRecommendSources.value = await api.get('recommend/source')
if (extraRecommendSources.value.length > 0) {
viewList.push(
...extraRecommendSources.value.map(source => ({
apipath: source.api_path,
linkurl: `/browse/recommend/${source.api_path}?title=${source.name}`,
title: source.name,
})),
)
}
} catch (error) {
console.log(error)
}
}
// 加载面板配置
async function loadConfig() {
// 显示配置
const local_enable = localStorage.getItem('MP_RECOMMEND')
if (local_enable) {
enableConfig.value = JSON.parse(local_enable)
} else {
const response = await api.get('/user/config/Recommend')
if (response && response.data && response.data.value) {
enableConfig.value = response.data.value
localStorage.setItem('MP_RECOMMEND', JSON.stringify(response.data.value))
}
}
}
// 设置项目
async function saveConfig() {
// 启用配置
const enableString = JSON.stringify(enableConfig.value)
localStorage.setItem('MP_RECOMMEND', enableString)
// 保存到服务端
try {
await api.post('/user/config/Recommend', enableConfig.value)
} catch (error) {
console.error(error)
}
dialog.value = false
}
onBeforeMount(async () => {
await loadConfig()
})
onMounted(async () => {
await loadExtraRecommendSources()
})
onActivated(async () => {
loadExtraRecommendSources()
})
</script>
<template>
<div>
<MediaCardSlideView v-for="item in enabledViews" :key="item.title" v-bind="item" />
<!-- 弹窗根据配置生成选项 -->
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>设置推荐榜单</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol v-for="item in viewList" :key="item.title" cols="6" md="4" sm="4">
<VCheckbox v-model="enableConfig[item.title]" :label="item.title" />
</VCol>
</VRow>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VSpacer />
<VBtn variant="outlined" color="secondary" class="me-4" @click="dialog = false"> 关闭 </VBtn>
<VBtn @click="saveConfig">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardText>
</VCard>
</VDialog>
</div>
<!-- 底部操作按钮 -->
<VFab
icon="mdi-text-box-edit"
location="bottom"
size="x-large"
fixed
app
appear
@click="dialog = true"
:class="{ 'mb-12': appMode }"
/>
</template>

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