Compare commits

...

159 Commits

Author SHA1 Message Date
jxxghp
adc718b751 实现文件浏览器的拖动分隔条功能 2025-06-06 08:44:06 +08:00
jxxghp
df9981d0c9 重构 LoadingBanner 组件 2025-06-06 08:32:07 +08:00
jxxghp
f58b661b1b Merge pull request #344 from cddjr/fix_search_progress 2025-06-05 20:46:50 +08:00
景大侠
ec1926ba60 fix: 优化搜索进度条,避免卡”正在搜索,请稍候...“
1、通过进度有无变化来判定超时,避免误判
2、避免搜索期间误判完成,导致SSE被提前终止
2025-06-05 20:29:51 +08:00
jxxghp
e853851933 修改点击事件和工具栏密度设置 2025-06-05 19:31:34 +08:00
jxxghp
3705ce3b90 更新 UserAuthDialog.vue 2025-06-04 22:45:05 +08:00
jxxghp
7ad73ff251 移除保存设置时的重载系统调用,进一步简化设置保存逻辑 2025-06-04 08:19:16 +08:00
jxxghp
6c23e8892a 移除多个组件中的重载系统生效配置函数,简化保存设置逻辑 2025-06-03 19:53:31 +08:00
jxxghp
58efafac71 Merge pull request #343 from wkeylin/v2 2025-06-03 15:19:53 +08:00
wkeylin
abf2364bf6 fix: 日志日期优化 2025-06-03 14:30:15 +08:00
jxxghp
0650f35dbb Update module-federation-guide.md 2025-06-03 10:39:34 +08:00
jxxghp
cc593634d2 更新模块联邦指南,添加关于上传dist文件夹的注意事项,明确不需要上传的目录和文件类型 2025-06-03 10:37:12 +08:00
jxxghp
79a3b9de8a 更新版本号至 2.5.3 2025-06-03 10:17:19 +08:00
jxxghp
ceb46ec974 Merge pull request #342 from jtcymc/v2 2025-06-03 06:43:07 +08:00
shaw
a7e2893a57 refactor(components): 将 VSelect 组件替换为 VAutocomplete组件
- 在 DirectoryCard.vue 中将 VSelect 替换为VAutocomplete,用于 library_storage 字段
- 在 FilterRuleGroupCard.vue 中将两个 VSelect 组件替换为 VAutocomplete,用于 media_type 和 category 字段
2025-06-02 22:37:36 +08:00
shaw
2efe8efde0 refactor(components): 将 VSelect 组件替换为 VAutocomplete组件,以支持搜索待选项
- 在多个组件中将 VSelect 组件替换为 VAutocomplete 组件,以支持搜索待选项
- 此更改可以提供更丰富的用户交互体验和更好的性能
2025-06-02 21:48:07 +08:00
jxxghp
31047b0d44 优化账户设置缓存页面的筛选条件 2025-05-30 17:01:47 +08:00
jxxghp
7c2b724d10 fix ui 2025-05-30 09:04:15 +08:00
jxxghp
ca5670f06b v2.5.2 2025-05-30 08:48:39 +08:00
jxxghp
427e05871d 调整SubscribeCard组件的样式 2025-05-30 08:32:16 +08:00
jxxghp
bef56bdb56 优化账户设置缓存页面中的输入字段,添加持久提示和图标,提升用户体验 2025-05-30 08:27:10 +08:00
jxxghp
d450d02e18 在账户设置缓存页面中添加固定表头 2025-05-30 08:25:03 +08:00
jxxghp
85a766cc7b 调整多个组件的样式和结构,优化用户界面体验 2025-05-30 08:15:48 +08:00
jxxghp
a473f356c9 优化缓存管理页面 2025-05-29 22:56:40 +08:00
jxxghp
52b5fdf383 添加清空缓存确认提示,优化缓存管理页面的用户体验 2025-05-29 22:37:03 +08:00
jxxghp
b886f02043 缓存管理页面 2025-05-29 20:49:19 +08:00
jxxghp
61963ea497 reset 2025-05-29 20:12:14 +08:00
jxxghp
2f9b27ad9e reset 2025-05-29 20:11:34 +08:00
jxxghp
9334109767 Merge pull request #341 from madrays/v2
增加缓存管理页面
2025-05-29 12:32:51 +08:00
jxxghp
2bc52576d9 更新package.json中的版本号 2025-05-29 08:23:18 +08:00
jxxghp
700d2c4a51 刷新数据时重新加载文件夹配置,以确保插件正确显示。 2025-05-29 08:21:17 +08:00
madrays
103bdb32c8 增加缓存管理页面 2025-05-29 00:45:12 +08:00
jxxghp
92b745e180 优化搜索站点对话框 2025-05-28 21:25:37 +08:00
jxxghp
a2007083b8 更新MoviePilot自动更新设置逻辑,支持'release'和'dev'选项 2025-05-28 21:15:52 +08:00
jxxghp
36a5f7ff29 添加自动更新MoviePilot和站点资源的设置选项 2025-05-28 21:05:46 +08:00
jxxghp
f727aea51d 为多个设置组件的保存按钮添加图标,以提升用户体验和一致性。 2025-05-28 10:09:05 +08:00
jxxghp
936ca24328 优化对话框组件,添加图标以提升用户体验 2025-05-28 08:59:31 +08:00
jxxghp
62f49b6087 优化插件文件夹内插件的筛选逻辑 2025-05-28 08:49:53 +08:00
jxxghp
e9ddbf9962 添加代理服务器设置 2025-05-28 08:24:42 +08:00
jxxghp
196cf522e6 fix 2025-05-27 21:41:06 +08:00
jxxghp
3fce3bf4a7 优化多个组件的输入框,添加图标以提升用户体验,确保提示信息的一致性和可读性。 2025-05-27 21:38:25 +08:00
jxxghp
1cfee25695 优化多个组件的输入框,添加图标以提升用户体验,确保提示信息的一致性和可读性。 2025-05-27 21:23:08 +08:00
jxxghp
5711285a77 更新多个卡片组件,统一标题文本为“配置”,添加图标以提升用户体验,优化输入框提示信息,确保一致性和可读性。 2025-05-27 17:46:51 +08:00
jxxghp
e6f537ca3a 优化多个对话框组件的布局,添加图标以提升用户体验,调整部分文本提示,确保一致性和可读性。 2025-05-27 17:40:20 +08:00
jxxghp
3b5220af57 fix plugin list loading 2025-05-27 14:00:15 +08:00
jxxghp
fa6b4b1d2d 调整插件列表显示行数,从三行改为两行,以优化界面布局。 2025-05-27 13:49:55 +08:00
jxxghp
7968e5374b 优化文件夹内插件的显示顺序,确保按照保存顺序排列插件,提升用户体验。 2025-05-27 13:48:13 +08:00
jxxghp
64997ebe45 重构插件混合排序逻辑,优化全局排序配置,兼容旧格式,提升插件和文件夹的排序体验。 2025-05-27 13:40:55 +08:00
jxxghp
f8592b01e2 优化错误日志输出 2025-05-27 13:29:53 +08:00
jxxghp
087474f514 fix 2025-05-27 13:26:09 +08:00
jxxghp
1725088f05 fix 插件混合排序问题 2025-05-27 13:12:09 +08:00
jxxghp
ec1b756a3d 添加混合排序功能,重构插件列表显示逻辑,移除冗余代码并优化拖拽排序体验。 2025-05-27 13:01:08 +08:00
jxxghp
76a06e0817 移除 AddDownloadDialog 组件中的显示器宽度逻辑,简化对话框全屏显示设置 2025-05-27 07:54:34 +08:00
jxxghp
02fb608d7b 更新 PluginCard.vue 2025-05-26 22:40:48 +08:00
jxxghp
e17fc2fc12 更新 package.json 2025-05-26 21:38:10 +08:00
jxxghp
4f6c317652 修复 PersonDetailView 组件中的 VImg 标签,移除多余的 v-img 指令以简化代码。 2025-05-26 21:30:23 +08:00
jxxghp
46c198be26 重构 credits.vue 和 media.vue 组件,简化 API 路径处理,移除不必要的路由参数,同时优化 PersonCardListView 组件的样式。 2025-05-26 21:28:52 +08:00
jxxghp
8552203d43 PluginCard 组件中的实时日志弹窗代码 2025-05-26 13:26:13 +08:00
jxxghp
139eaa7016 优化 PluginCard 组件 2025-05-26 12:44:08 +08:00
jxxghp
d81120ab8f 为 PluginCard 组件添加实时日志弹窗功能 2025-05-26 12:37:49 +08:00
jxxghp
6353d56beb Merge pull request #339 from madrays/v2 2025-05-26 11:26:26 +08:00
madrays
aa05496b42 插件分身多语言支持 2025-05-26 11:20:10 +08:00
madrays
dc15e537d8 增加插件分身功能 2025-05-26 10:55:55 +08:00
jxxghp
6fbd41f40a 优化 PluginAppCard 和 PluginCard 组件的样式 2025-05-25 20:57:42 +08:00
jxxghp
0181f614e1 为 SiteCard 和 SubscribeCard 组件添加显示器宽度逻辑,优化图标的鼠标移动样式 2025-05-25 19:50:57 +08:00
jxxghp
fded7b0b28 为多个组件的对话框添加全屏显示逻辑 2025-05-25 19:44:04 +08:00
jxxghp
7e637f835a 优化 TorrentCardListView 和 TorrentRowListView 组件的确认按钮样式 2025-05-25 15:51:24 +08:00
jxxghp
deaaf1834d 为 v-table 组件的表头添加背景模糊效果和背景色,提升视觉效果 2025-05-25 15:01:28 +08:00
jxxghp
139c870f99 更新 MediaServerCard.vue 2025-05-25 11:01:26 +08:00
jxxghp
4cc2350bc6 移除 SiteResourceDialog 组件中的分页文本绑定 2025-05-25 09:17:00 +08:00
jxxghp
8b31a118da 为英文和中文语言文件添加分页文本格式,提升用户界面信息展示 2025-05-24 22:28:58 +08:00
jxxghp
cca26acb78 更新 PluginFolderCard 和 PluginCardListView 组件的默认渐变背景颜色,提升视觉效果 2025-05-24 20:09:43 +08:00
jxxghp
245edbd2f6 优化 PluginAppCard 组件的文本显示方式 2025-05-24 20:06:11 +08:00
jxxghp
903d22c622 优化多个组件的样式和结构,调整文本显示方式,提升用户界面体验 2025-05-24 20:01:20 +08:00
jxxghp
8b1805628e 为 PluginFolderCard 组件添加背景图片计算逻辑和背景遮罩样式,优化背景显示效果 2025-05-24 17:36:32 +08:00
jxxghp
11c8c488da 调整 ConfirmDialog 组件的宽度属性 2025-05-24 17:22:49 +08:00
jxxghp
4dd4e0e148 自实现 UseConfirm 组件 2025-05-24 17:19:43 +08:00
jxxghp
21f352aa64 优化 PluginAppCard 组件,添加插件标签显示功能;调整 PluginFolderCard 组件的菜单位置和图标样式;更新 PluginCardListView 组件的文件夹显示逻辑。 2025-05-24 16:38:34 +08:00
jxxghp
6c4beffdb7 优化多个组件的按钮样式 2025-05-24 15:37:40 +08:00
jxxghp
43d3efa838 优化 PluginFolderCard 组件 2025-05-24 14:47:47 +08:00
jxxghp
1c99839ab4 更新版本号至 2.5.0 2025-05-24 14:20:36 +08:00
jxxghp
c9e05ce5b1 调整 PluginFolderCard 组件的最小高度属性,从 9rem 修改为 8.5rem 2025-05-24 14:11:39 +08:00
jxxghp
3fe7ed0e1d 优化多个组件中的按钮样式 2025-05-24 14:06:10 +08:00
jxxghp
b3bff5c6f5 移除 PluginCardListView 组件中的调试日志,优化错误处理逻辑 2025-05-24 14:06:10 +08:00
jxxghp
e357bac70f 为文件夹功能添加国际化支持 2025-05-24 14:06:10 +08:00
jxxghp
ad51d4e4f3 调整 PluginCardListView 组件的样式 2025-05-24 14:06:10 +08:00
jxxghp
912d8ced93 更新 PluginFolderCard 组件,添加国际化支持 2025-05-24 14:06:10 +08:00
jxxghp
8334999e98 优化 PluginAppCard、PluginCard 和 PluginFolderCard 组件的样式,调整布局和响应式设计 2025-05-24 14:06:10 +08:00
jxxghp
5e23ea7809 更新 NotificationChannelCard.vue 2025-05-24 09:43:47 +08:00
jxxghp
b62d291aab Merge pull request #338 from madrays/v2 2025-05-24 06:34:30 +08:00
madrays
a34dd8148f 重构插件页面,增加文件夹功能 2025-05-24 03:58:14 +08:00
jxxghp
ba13e6ac35 fix #337 2025-05-23 22:29:19 +08:00
jxxghp
8efa5f7a28 调整 SubscribeCard 组件中 VCardText 的下边距,从 1 修改为 2,以改善布局效果 2025-05-23 08:04:26 +08:00
jxxghp
f0ef9565e2 更新 SubscribeCard.vue 2025-05-23 07:24:23 +08:00
jxxghp
78688ab63c 优化 SubscribeCard 组件的样式,调整文本和图标的大小,增强可读性 2025-05-23 07:15:30 +08:00
jxxghp
e90b30bf63 调整 SubscribeCard 组件中图像容器的宽度,从 16px 修改为 14px,以优化布局 2025-05-22 15:22:51 +08:00
jxxghp
5312b82ba7 优化 PluginAppCard、PluginCard 和 SubscribeCard 组件的样式,调整布局和间距,增强响应式设计 2025-05-22 15:21:25 +08:00
jxxghp
bc705f2560 更新 SubscribeCard.vue 2025-05-22 06:59:39 +08:00
jxxghp
6477f43de1 更新 SubscribeCard.vue 2025-05-22 06:50:32 +08:00
jxxghp
bdc0fdd076 优化 PluginAppCard 和 PluginCard 组件的样式 2025-05-21 21:29:26 +08:00
jxxghp
1f09e1ff93 优化垂直导航样式,修复边框半径设置,删除不必要的代码,移除 TransitionExpand 组件 2025-05-21 21:06:40 +08:00
jxxghp
4bcc89d9da 优化 PluginAppCard 和 PluginCard 组件的样式 2025-05-21 20:49:52 +08:00
jxxghp
8f93b49dde 优化多个组件的样式,调整卡片布局和间距,更新网格列数以适应不同屏幕尺寸 2025-05-21 20:26:48 +08:00
jxxghp
74eeae900e 调整背景透明度 2025-05-21 19:32:42 +08:00
jxxghp
63424bb134 Merge pull request #336 from Aqr-K/fix/i18n 2025-05-20 19:48:58 +08:00
Aqr-K
1c5e410881 fix(i18n): 修复非支持地区输出null,导致的显示问题 2025-05-20 19:14:31 +08:00
jxxghp
f79cc41f3c 更新 FetchMediasAction 组件,调整下拉框选项格式为包含值和标题的对象 2025-05-19 12:27:07 +08:00
jxxghp
49cccbe69e 更新 package.json 版本号至 2.4.9 2025-05-18 15:36:46 +08:00
jxxghp
c4a02f7497 新增自定义通知类型支持,更新相关提示信息和样式 2025-05-18 13:39:44 +08:00
jxxghp
59e12c5e96 优化 TorrentCard 组件的样式,更新替换词支持格式的描述信息 2025-05-18 12:55:38 +08:00
jxxghp
a347bdc412 将 package.json 版本号降级至 2.4.8 2025-05-16 12:38:34 +08:00
jxxghp
3f3c1ecd02 更新 package.json 版本号至 2.4.9 2025-05-16 12:37:34 +08:00
jxxghp
d5d9c78c91 重构 InvokePluginAction 组件,优化插件和动作选项的加载逻辑 2025-05-15 22:12:57 +08:00
jxxghp
5b0d8d902b 工作流新增调用插件功能组件 2025-05-15 20:53:41 +08:00
jxxghp
2978e46d02 fix ui 2025-05-15 13:03:09 +08:00
jxxghp
54e0633d77 更新 package.json 2025-05-15 12:09:53 +08:00
jxxghp
ab3db66195 增加安全图片域名功能,优化自定义壁纸API相关提示信息 2025-05-15 09:59:51 +08:00
jxxghp
17e19da3d8 Merge pull request #334 from Seed680/v2
背景壁纸增加自定义API
2025-05-15 09:22:36 +08:00
qiaoyun680
f22aca0c5d 背景壁纸增加自定义API,优化输入提示 2025-05-14 20:52:10 +08:00
qiaoyun680
c257e11ee3 背景壁纸增加自定义API 2025-05-14 20:28:01 +08:00
jxxghp
8b23f0bb2e 在登录页面中优化背景模糊效果 2025-05-14 14:35:39 +08:00
jxxghp
a82a89afd3 优化样式,调整背景模糊效果和颜色透明度,以提升用户界面视觉效果。 2025-05-14 14:22:44 +08:00
jxxghp
5c0d0d5a95 更新 DashboardItem 接口,将 render_mode 属性改为可选,并调整 dashboard.vue 中 VDialog 组件的最大高度以改善用户界面体验。 2025-05-14 13:22:31 +08:00
jxxghp
9dbd090482 优化 TorrentItem.vue 中的 VChip 组件,调整属性格式以提升代码可读性。 2025-05-14 11:26:53 +08:00
jxxghp
e25583dff9 v2.4.7 2025-05-14 09:11:44 +08:00
jxxghp
d997dc0394 优化登录页面,添加登录按钮的加载状态管理,确保用户体验流畅。 2025-05-13 23:28:03 +08:00
jxxghp
6b6353ed41 优化 App.vue 中的背景图片加载逻辑,调整异步加载方式并简化图片地址获取逻辑。 2025-05-13 19:25:41 +08:00
jxxghp
e73d906564 fix #333 2025-05-13 19:14:13 +08:00
jxxghp
7e3e850e21 Merge pull request #333 from Seed680/v2 2025-05-13 18:46:23 +08:00
qiaoyun680
56b2dc4ebf 资源搜索结果页面增加排序切换 2025-05-13 16:25:22 +08:00
jxxghp
9444b0e518 优化 App.vue 中的背景图片加载逻辑,添加登录状态变化时清空背景图片数组的处理,并更新图片地址获取逻辑以支持缓存和原始地址的选择。 2025-05-13 13:37:37 +08:00
jxxghp
bcb72118f5 在背景图片加载失败时添加重试机制,3秒后自动重试加载背景图片 2025-05-13 08:18:44 +08:00
jxxghp
c59be8d981 更新 module-federation-guide.md 2025-05-12 18:03:09 +08:00
jxxghp
8466a40455 重构 App.vue 中的背景图片加载逻辑 2025-05-12 13:51:45 +08:00
jxxghp
f435b4fc52 在 fetchBackgroundImages 函数中初始化 activeImageIndex 为 0,以确保背景图片加载时的索引正确。 2025-05-12 11:23:48 +08:00
jxxghp
5686c6fe65 在构建工作流中移除了删除标签的选项,以简化发布流程。 2025-05-12 11:09:49 +08:00
jxxghp
6810112eda 在 App.vue 中添加登录状态变化的监听,确保登录后重新加载背景图片;同时更新 .vscode/settings.json,增加 i18n-ally.localesPaths 配置。 2025-05-12 10:44:01 +08:00
jxxghp
11a2d07935 优化 App.vue 中的国际化代码,调整 LoadingBanner 组件的样式,增加 SubscribeFilesDialog 组件的加载状态管理。 2025-05-12 07:56:52 +08:00
jxxghp
02cd2f1570 在构建工作流中添加了删除标签和发布的选项,并设置在出错时继续执行。 2025-05-11 08:44:30 +08:00
jxxghp
924c1d72ea 优化自定义滚动条样式 2025-05-11 08:41:48 +08:00
jxxghp
5d9b2e1919 更新 AlistConfigDialog.vue 2025-05-11 07:56:50 +08:00
jxxghp
f7fa440f9a 更新 AlistConfigDialog.vue 2025-05-10 23:31:49 +08:00
jxxghp
d4aaa46968 优化 SubscribeCard 和 SubscribeShareCard 组件的结构 2025-05-10 23:18:00 +08:00
jxxghp
93ac5e1b3b 优化 PluginAppCard 和 PluginCard 组件的背景样式,更新渐变效果以增强视觉层次感。 2025-05-10 22:38:19 +08:00
jxxghp
c7a8c68e14 调整 PluginCard 组件的背景样式,优化渐变效果以提升视觉效果。 2025-05-10 22:25:53 +08:00
jxxghp
77afb4d736 优化 PluginAppCard 和 PluginCard 组件的背景样式 2025-05-10 22:10:02 +08:00
jxxghp
141796ab24 更新 AccountSettingSystem.vue 中的 v-model,修改为 TMDB_SCRAP_ORIGINAL_IMAGE,以更准确地反映设置项。 2025-05-10 21:58:13 +08:00
jxxghp
30d733f55d v2.4.6 2025-05-10 21:54:32 +08:00
jxxghp
6a39e65b6b 添加 TMDB 刮削原语种选项。 2025-05-10 21:45:45 +08:00
jxxghp
c27013b7ad Merge pull request #332 from Seed680/v2 2025-05-10 21:24:45 +08:00
jxxghp
582ce496fa 添加 TMDB 刮削图片语言相关 2025-05-10 20:44:06 +08:00
jxxghp
5b4dbb82d5 调整 ShortcutBar 组件中的对话框最大宽度 2025-05-10 19:33:57 +08:00
jxxghp
011a0d16ab 加载远程组件时如未注册则重新注册 2025-05-10 08:40:14 +08:00
jxxghp
ac5539194d 优化 PersonCard 组件,移除多余的样式类以简化结构 2025-05-09 20:29:54 +08:00
Seed680
6b7e1b3c4e Merge branch 'jxxghp:v2' into v2 2025-05-09 09:21:10 +08:00
jxxghp
30c3d00139 移除 Vite 配置中的手动分块选项,简化配置以提升可读性和维护性。 2025-05-09 00:06:58 +08:00
jxxghp
36d460cd74 更新 Vite 配置,启用压缩选项以移除控制台日志和调试器, 2025-05-09 00:04:14 +08:00
Seed680
11cb2eb0f8 Merge branch 'jxxghp:v2' into v2 2025-05-08 23:28:18 +08:00
qiaoyun680
4dce1c94a3 feat(storge): 添加alist存储的登录方式(令牌、访客) 2025-05-08 23:26:54 +08:00
114 changed files with 5379 additions and 1503 deletions

View File

@@ -44,6 +44,7 @@ jobs:
- name: Delete Release
uses: dev-drprasad/delete-tag-and-release@v1.1
continue-on-error: true
with:
tag_name: ${{ env.frontend_version }}
delete_release: true

View File

@@ -106,5 +106,8 @@
}
]
},
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
"i18n-ally.localesPaths": [
"src/locales"
]
}

1
components.d.ts vendored
View File

@@ -8,6 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ConfirmDialog: typeof import('./src/@core/components/ConfirmDialog.vue')['default']
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']

View File

@@ -9,7 +9,7 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
## 2. 技术要求
- Node.js 16+
- Node.js 20+
- Vue 3
- Vite 4+
- TypeScript 5+
@@ -80,13 +80,6 @@ export default defineConfig({
target: 'esnext', // 必须设置为esnext以支持顶层await
minify: false, // 开发阶段建议关闭混淆
cssCodeSplit: true, // 改为true以便能分离样式文件
rollupOptions: {
output: {
manualChunks: {
'vuetify-lib': ['vuetify'] // 将vuetify单独分离出来
}
}
}
},
css: {
preprocessorOptions: {
@@ -271,7 +264,10 @@ const props = defineProps({
yarn build
```
将生成的dist文件夹上传到插件后端目录下默认为`dist/assets`
- 将生成的dist文件夹上传到插件后端目录下默认为`dist/assets`
**注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*``_plugin-vue_export-helper-*``remoteEntry.js`
- 在插件的后端python代码中实现以下方法来集成远程组件

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.4.5",
"version": "2.5.3",
"private": true,
"type": "module",
"bin": "dist/service.js",
@@ -62,7 +62,6 @@
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "3.7.3",
"vuetify-use-dialog": "^0.6.11",
"webfontloader": "^1.6.28"
},
"devDependencies": {
@@ -113,4 +112,4 @@
"workbox-window": "^7.3.0"
},
"packageManager": "yarn@1.22.18"
}
}

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
modelValue: boolean
type?: 'info' | 'warn' | 'error'
title?: string
content?: string
confirmText?: string
cancelText?: string
width?: string | number
}
const props = withDefaults(defineProps<Props>(), {
type: 'info',
title: '',
content: '',
confirmText: '',
cancelText: '',
width: '28rem',
})
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm'): void
(e: 'cancel'): void
}>()
// 对话框类型对应的图标和颜色
const typeConfig = {
info: {
icon: 'mdi-information',
color: 'info',
},
warn: {
icon: 'mdi-alert',
color: 'warning',
},
error: {
icon: 'mdi-alert-circle',
color: 'error',
},
}
// 获取当前类型的配置
const currentType = computed(() => typeConfig[props.type])
// 确认按钮点击
function handleConfirm() {
emit('confirm')
emit('update:modelValue', false)
}
// 取消按钮点击
function handleCancel() {
emit('cancel')
emit('update:modelValue', false)
}
</script>
<template>
<VDialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" :max-width="width">
<VCard>
<VCardItem>
<div class="d-flex align-center justify-start mt-3">
<VAvatar :color="currentType.color" variant="text" size="x-large">
<VIcon size="x-large" :icon="currentType.icon" />
</VAvatar>
<div class="mx-3">
<p class="font-weight-bold text-xl text-high-emphasis">{{ title }}</p>
<p>{{ content }}</p>
</div>
</div>
</VCardItem>
<VCardActions class="mx-auto">
<VBtn variant="tonal" color="secondary" class="px-5" @click="handleCancel">
{{ cancelText }}
</VBtn>
<VBtn variant="elevated" :color="currentType.color" @click="handleConfirm" class="px-5">
{{ confirmText }}
</VBtn>
</VCardActions>
<VDialogCloseBtn @click="handleCancel" />
</VCard>
</VDialog>
</template>

View File

@@ -1,15 +1,88 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
progress: Number,
text: String,
})
</script>
<template>
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="64" indeterminate color="primary" />
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
<span>{{ props.text }}</span>
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center my-5">
<div class="initial-loading-container">
<div class="initial-loading-content">
<div class="wave-loader">
<div class="wave-dot"></div>
<div class="wave-dot"></div>
<div class="wave-dot"></div>
<div class="wave-dot"></div>
</div>
<div class="initial-loading-text" v-if="props.text">{{ props.text }}</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 初始的加载状态 */
.initial-loading-container {
display: flex;
align-items: center;
justify-content: center;
min-block-size: 20vh;
}
.initial-loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.wave-loader {
display: flex;
align-items: center;
block-size: 40px;
gap: 6px;
}
.wave-dot {
border-radius: 50%;
animation: wave 1.5s ease-in-out infinite;
background-color: rgb(var(--v-theme-primary));
block-size: 8px;
inline-size: 8px;
}
.wave-dot:nth-child(1) {
animation-delay: 0s;
}
.wave-dot:nth-child(2) {
animation-delay: 0.2s;
}
.wave-dot:nth-child(3) {
animation-delay: 0.4s;
}
.wave-dot:nth-child(4) {
animation-delay: 0.6s;
}
@keyframes wave {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
}
.initial-loading-text {
color: rgb(var(--v-theme-primary));
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 1px;
}
</style>

View File

@@ -118,11 +118,6 @@
opacity: var(--v-disabled-opacity);
pointer-events: none;
}
}
// 👉 Vertical nav link
.nav-link {
@extend %nav-link;
> .router-link-exact-active {
@extend %nav-link-active;

View File

@@ -22,7 +22,7 @@ $header: ".layout-navbar";
// If navbar is contained => Add border radius to header
@if variables.$layout-vertical-nav-navbar-is-contained {
#{$header} {
// border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
}
}
@@ -31,7 +31,7 @@ $header: ".layout-navbar";
/* This html selector with not selector is required when:
dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and out style broke
*/
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-fixed,
html.v-overlay-scroll-blocked .layout-navbar-fixed,
&.window-scrolled.layout-navbar-fixed {
#{$header} {
@@ -63,7 +63,7 @@ $header: ".layout-navbar";
#{$header} {
@if variables.$layout-vertical-nav-navbar-is-contained {
// border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
}
background-color: rgb(var(--v-theme-surface));

View File

@@ -2,6 +2,13 @@
position: relative;
background: transparent;
box-shadow: none;
.v-theme--light & {
backdrop-filter: blur(16px);
background: rgba(var(--v-theme-surface), 0.9);
box-shadow: 0 0 8px 0 rgba(var(--v-theme-on-surface), 0.1);
}
&::before {
position: absolute;
@@ -11,29 +18,17 @@
inset-block-start: 0;
inset-inline: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out, background 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
// PC端样式 (默认)
.v-theme--light & {
background: linear-gradient(
to bottom,
rgba(var(--v-theme-surface), 0.9) 0%,
rgba(var(--v-theme-surface), 0.7) 20%,
rgba(var(--v-theme-surface), 0.5) 40%,
rgba(var(--v-theme-surface), 0.3) 60%,
rgba(var(--v-theme-surface), 0.1) 80%,
rgba(var(--v-theme-surface), 0.0) 100%
);
}
.v-theme--dark & {
background: linear-gradient(
to bottom,
rgba(var(--v-theme-background), 0.8) 0%,
rgba(var(--v-theme-background), 0.6) 20%,
rgba(var(--v-theme-background), 0.4) 40%,
rgba(var(--v-theme-background), 0.25) 60%,
rgba(var(--v-theme-background), 0.1) 80%,
rgba(var(--v-theme-background), 1) 0%,
rgba(var(--v-theme-background), 0.8) 20%,
rgba(var(--v-theme-background), 0.6) 40%,
rgba(var(--v-theme-background), 0.4) 60%,
rgba(var(--v-theme-background), 0.2) 80%,
rgba(var(--v-theme-background), 0.0) 100%
);
}
@@ -41,11 +36,11 @@
.v-theme--purple & {
background: linear-gradient(
to bottom,
rgba(var(--v-theme-background), 0.8) 0%,
rgba(var(--v-theme-background), 0.6) 20%,
rgba(var(--v-theme-background), 0.4) 40%,
rgba(var(--v-theme-background), 0.25) 60%,
rgba(var(--v-theme-background), 0.1) 80%,
rgba(var(--v-theme-background), 1) 0%,
rgba(var(--v-theme-background), 0.8) 20%,
rgba(var(--v-theme-background), 0.6) 40%,
rgba(var(--v-theme-background), 0.4) 60%,
rgba(var(--v-theme-background), 0.2) 80%,
rgba(var(--v-theme-background), 0.0) 100%
);
}
@@ -53,68 +48,25 @@
.v-theme--transparent & {
background: linear-gradient(
to bottom,
rgba(11, 11, 11, 60%) 0%,
rgba(11, 11, 11, 50%) 20%,
rgba(11, 11, 11, 40%) 40%,
rgba(11, 11, 11, 25%) 60%,
rgba(11, 11, 11, 10%) 80%,
rgba(11, 11, 11, 0%) 100%
rgba(var(--v-theme-background), 0.5) 0%,
rgba(var(--v-theme-background), 0.4) 20%,
rgba(var(--v-theme-background), 0.3) 40%,
rgba(var(--v-theme-background), 0.2) 60%,
rgba(var(--v-theme-background), 0.1) 80%,
rgba(var(--v-theme-background), 0.0) 100%
);
@media (width <= 640px) {
background: linear-gradient(
to bottom,
rgba(var(--v-theme-background), 0.9) 0%,
rgba(var(--v-theme-background), 0.7) 20%,
rgba(var(--v-theme-background), 0.5) 40%,
rgba(var(--v-theme-background), 0.3) 60%,
rgba(var(--v-theme-background), 0.1) 80%,
rgba(var(--v-theme-background), 0.0) 100%
);
}
}
}
}
// 移动端样式
@media (pointer: coarse) {
%blurry-bg {
&::before {
.v-theme--light & {
background: linear-gradient(
to bottom,
rgba(var(--v-theme-surface), 1) 0%,
rgba(var(--v-theme-surface), 0.9) 20%,
rgba(var(--v-theme-surface), 0.7) 40%,
rgba(var(--v-theme-surface), 0.5) 60%,
rgba(var(--v-theme-surface), 0.2) 80%,
rgba(var(--v-theme-surface), 0.0) 100%
);
}
.v-theme--dark & {
background: linear-gradient(
to bottom,
rgba(var(--v-theme-background), 1) 0%,
rgba(var(--v-theme-background), 0.85) 20%,
rgba(var(--v-theme-background), 0.7) 40%,
rgba(var(--v-theme-background), 0.5) 60%,
rgba(var(--v-theme-background), 0.3) 80%,
rgba(var(--v-theme-background), 0.0) 100%
);
}
.v-theme--purple & {
background: linear-gradient(
to bottom,
rgba(var(--v-theme-background), 1) 0%,
rgba(var(--v-theme-background), 0.85) 20%,
rgba(var(--v-theme-background), 0.7) 40%,
rgba(var(--v-theme-background), 0.5) 60%,
rgba(var(--v-theme-background), 0.3) 80%,
rgba(var(--v-theme-background), 0.0) 100%
);
}
.v-theme--transparent & {
background: linear-gradient(
to bottom,
rgba(11, 11, 11, 90%) 0%,
rgba(11, 11, 11, 80%) 20%,
rgba(11, 11, 11, 60%) 40%,
rgba(11, 11, 11, 40%) 60%,
rgba(11, 11, 11, 15%) 80%,
rgba(11, 11, 11, 0%) 100%
);
}
}
}
}

View File

@@ -1,209 +0,0 @@
<script lang="ts">
import { Transition } from 'vue'
import { useDisplay } from 'vuetify'
import VerticalNav from '@layouts/components/VerticalNav.vue'
export default defineComponent({
setup(props, { slots }) {
const isOverlayNavActive = ref(false)
const isLayoutOverlayVisible = ref(false)
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
const route = useRoute()
const { mdAndDown } = useDisplay()
// This is alternative to below two commented watcher
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
const scrollDistance = ref(window.scrollY)
onMounted(() => {
window.addEventListener('scroll', () => {
scrollDistance.value = window.scrollY
})
})
return () => {
// 👉 Vertical nav
const verticalNav = h(
VerticalNav,
{ isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive },
{
'nav-header': () => slots['vertical-nav-header']?.(),
'before-nav-items': () => slots['before-vertical-nav-items']?.(),
'default': () => slots['vertical-nav-content']?.(),
'after-nav-items': () => slots['after-vertical-nav-items']?.(),
},
)
// 👉 Navbar
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
h(
'div',
{ class: 'navbar-content-container' },
slots.navbar?.({
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
}),
),
])
const main = h(
'main',
{ class: 'layout-page-content' },
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
h('section', { class: 'page-content-container' }, slots.default?.()),
),
)
// 👉 根据路由 meta 决定 footer 高度
const shouldShowFooter = !route.meta.hideFooter
// 👉 Footer
const footer = h('footer', { class: 'layout-footer' }, [
h(
'div',
{
class: ['footer-content-container', !shouldShowFooter && 'footer-content-container-noheight'],
},
slots.footer?.(),
),
])
// 👉 Overlay
const layoutOverlay = h('div', {
class: ['layout-overlay', 'touch-none', { visible: isLayoutOverlayVisible.value }],
onClick: () => {
isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value
},
})
return h(
'div',
{
class: [
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
'layout-navbar-fixed',
mdAndDown.value && 'layout-overlay-nav',
route.meta.layoutWrapperClasses,
scrollDistance.value && 'window-scrolled',
],
},
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
)
}
},
})
</script>
<style lang="scss">
@use '@configured-variables' as variables;
@use '@layouts/styles/placeholders';
@use '@layouts/styles/mixins';
.layout-wrapper.layout-nav-type-vertical {
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
block-size: 100%;
.layout-content-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
min-block-size: calc(var(--vh, 1vh) * 100);
transition: padding-inline-start 0.2s ease-in-out;
will-change: padding-inline-start;
}
.layout-navbar {
position: fixed;
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
inset-block-start: 0;
.navbar-content-container {
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
}
@at-root {
.layout-wrapper.layout-nav-type-vertical {
.layout-navbar {
@if variables.$layout-vertical-nav-navbar-is-contained {
@include mixins.boxed-content;
} @else {
.navbar-content-container {
// @include mixins.boxed-content;
}
}
}
}
}
}
&.layout-navbar-fixed .layout-navbar {
@extend %layout-navbar-fixed;
}
&.layout-navbar-hidden .layout-navbar {
@extend %layout-navbar-hidden;
}
// 👉 Footer
.layout-footer {
@include mixins.boxed-content;
}
// 👉 Layout overlay
.layout-overlay {
position: fixed;
z-index: variables.$layout-overlay-z-index;
background-color: rgb(0 0 0 / 60%);
cursor: pointer;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease-in-out;
will-change: transform;
&.visible {
opacity: 1;
pointer-events: auto;
}
}
&:not(.layout-overlay-nav) .layout-content-wrapper {
padding-inline-start: variables.$layout-vertical-nav-width;
}
// Adjust right column pl when vertical nav is collapsed
&.layout-vertical-nav-collapsed .layout-content-wrapper {
padding-inline-start: variables.$layout-vertical-nav-collapsed-width;
}
// 👉 Content height fixed
&.layout-content-height-fixed {
.layout-content-wrapper {
max-block-size: calc(var(--vh) * 100);
}
.layout-page-content {
// display: flex;
overflow: hidden;
.page-content-container {
inline-size: 100%;
> :first-child {
max-block-size: 100%;
overflow-y: auto;
}
}
}
}
}
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
.layout-navbar {
width: 100%;
}
}
</style>

View File

@@ -4,13 +4,9 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
import api from '@/api'
import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n'
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
import { SupportedLocale } from '@/types/i18n'
// 国际化
const { t } = useI18n()
// 生效主题
const { global: globalTheme } = useTheme()
let themeValue = localStorage.getItem('theme') || 'light'
@@ -21,9 +17,6 @@ globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
const localeValue = getBrowserLocale()
setI18nLanguage(localeValue as SupportedLocale)
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 显示状态
const show = ref(false)
@@ -31,6 +24,9 @@ const show = ref(false)
const authStore = useAuthStore()
const isLogin = computed(() => authStore.token)
// 生成背景图片key
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
// 背景图片
const backgroundImages = ref<string[]>([])
const activeImageIndex = ref(0)
@@ -78,6 +74,7 @@ function updateHtmlThemeAttribute(themeName: string) {
async function fetchBackgroundImages() {
try {
backgroundImages.value = await api.get(`/login/wallpapers`)
activeImageIndex.value = 0
} catch (e) {
console.error(e)
}
@@ -85,6 +82,7 @@ async function fetchBackgroundImages() {
// 开始背景图片轮换
function startBackgroundRotation() {
// 清除轮换定时器
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
if (backgroundImages.value.length > 1) {
@@ -106,7 +104,6 @@ function startBackgroundRotation() {
function preloadImage(url: string): Promise<boolean> {
return new Promise(resolve => {
const img = new Image()
const imageUrl = getImgUrl(url)
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
@@ -117,7 +114,7 @@ function preloadImage(url: string): Promise<boolean> {
resolve(false)
}, 5000) // 5秒超时
img.src = imageUrl
img.src = url
// 如果图片已经缓存onload可能不会触发
if (img.complete) {
@@ -127,28 +124,6 @@ function preloadImage(url: string): Promise<boolean> {
})
}
// 计算图片地址
function getImgUrl(url: string) {
// 使用图片缓存
if (globalSettings.GLOBAL_IMAGE_CACHE && isLogin.value)
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
return url
}
// 处理页面可见性变化
function handleVisibilityChange() {
if (document.visibilityState === 'visible') {
// 如果已有背景图片数据,直接重启轮换
if (backgroundImages.value.length > 0) {
startBackgroundRotation()
}
// 如果没有背景图片数据,重新获取
else {
fetchBackgroundImages().then(() => startBackgroundRotation())
}
}
}
// 添加logo动画效果并延迟移除加载界面
function animateAndRemoveLoader() {
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
@@ -167,29 +142,61 @@ function animateAndRemoveLoader() {
}
}
onMounted(() => {
// 加载背景图片
async function loadBackgroundImages() {
await fetchBackgroundImages()
.then(() => {
startBackgroundRotation()
})
.catch(() => {
// 3秒后重试
setTimeout(() => {
loadBackgroundImages()
}, 3000)
})
}
onMounted(async () => {
// 初始化data-theme属性
updateHtmlThemeAttribute(globalTheme.name.value)
// 加载背景图片并开始轮换
fetchBackgroundImages().then(() => startBackgroundRotation())
// 默认隐藏页面
show.value = false
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', handleVisibilityChange)
// 加载背景图片
await loadBackgroundImages()
// 移除加载动画
ensureRenderComplete(() => {
nextTick(() => {
setTimeout(() => {
// 移除加载动画
// 移除加载动画,显示页面
animateAndRemoveLoader()
}, 1500)
})
})
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
loadBackgroundImages()
}
})
// 添加PWA的页面恢复事件监听
window.addEventListener('pageshow', event => {
// persisted属性为true表示页面是从bfcache中恢复的
if (event.persisted) {
loadBackgroundImages()
}
})
})
onUnmounted(() => {
// 移除页面可见性监听
document.removeEventListener('visibilitychange', handleVisibilityChange)
document.removeEventListener('visibilitychange', () => {})
// 移除PWA的页面恢复事件监听
window.removeEventListener('pageshow', () => {})
// 清除轮换定时器
if (backgroundRotationTimer) {
@@ -202,20 +209,18 @@ onUnmounted(() => {
<template>
<div class="app-wrapper">
<!-- 透明主题背景 -->
<template v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)">
<div class="background-container">
<div
v-for="(imageUrl, index) in backgroundImages"
:key="index"
class="background-image"
:class="{ 'active': index === activeImageIndex }"
:style="{ backgroundImage: `url(${getImgUrl(imageUrl)})` }"
></div>
<!-- 全局磨砂层 -->
<div v-if="isLogin" class="global-blur-layer"></div>
</div>
</template>
<div v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)" class="background-container">
<div
v-for="(imageUrl, index) in backgroundImages"
:key="`bg-${index}-${loginStateKey}`"
class="background-image"
:class="{ 'active': index === activeImageIndex }"
:style="{ 'backgroundImage': `url(${imageUrl})` }"
></div>
<!-- 全局磨砂层 -->
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
</div>
<!-- 页面内容 -->
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
<RouterView />
</VApp>

View File

@@ -335,6 +335,10 @@ export const actionStepOptions = [
title: i18n.global.t('actionStep.transferFile'),
value: '整理文件',
},
{
title: i18n.global.t('actionStep.invokePlugin'),
value: '调用插件',
},
]
// 操作步骤字典

View File

@@ -565,9 +565,9 @@ export interface NotExistMediaInfo {
// 插件
export interface Plugin {
id?: string
id: string
// 插件名称
plugin_name?: string
plugin_name: string
// 插件描述
plugin_desc?: string
// 插件图标
@@ -632,7 +632,7 @@ export interface DashboardItem {
// 页面元素
elements: RenderProps[]
// 渲染方式
render_mode: string
render_mode?: string
}
// 种子信息
@@ -1305,3 +1305,49 @@ export interface Workflow {
// 最后执行时间
last_time?: string
}
// 种子缓存项
export interface TorrentCacheItem {
// 种子hash用于操作标识
hash: string
// 站点域名
domain: string
// 种子标题
title: string
// 种子描述
description?: string
// 种子大小
size: number
// 发布时间
pubdate?: string
// 站点名称
site_name?: string
// 识别的媒体名称
media_name?: string
// 识别的媒体年份
media_year?: string
// 识别的媒体类型
media_type?: string
// 季集信息
season_episode?: string
// 资源信息
resource_term?: string
// 种子链接
enclosure?: string
// 详情页面
page_url?: string
// 海报图片
poster_path?: string
// 背景图片
backdrop_path?: string
}
// 种子缓存数据
export interface TorrentCacheData {
// 缓存数量
count: number
// 站点数量
sites: number
// 缓存数据
data: TorrentCacheItem[]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -136,6 +136,12 @@ const sort = ref('name')
// 是否显示目录树
const showDirTree = ref(false)
// 拖动分隔条相关
const navigatorWidth = ref(280) // 初始宽度
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartWidth = ref(0)
// 计算属性
const storagesArray = computed(() => {
return props.storages?.map(item => ({
@@ -181,6 +187,58 @@ function fileListUpdated(items: FileItem[]) {
fileListItems.value = items
}
// 阻止选择事件
function preventSelect(event: Event) {
event.preventDefault()
return false
}
// 拖动分隔条相关方法
function startDrag(event: MouseEvent) {
event.preventDefault() // 阻止默认行为
event.stopPropagation() // 阻止事件冒泡
isDragging.value = true
dragStartX.value = event.clientX
dragStartWidth.value = navigatorWidth.value
document.addEventListener('mousemove', handleDrag, { passive: false })
document.addEventListener('mouseup', stopDrag, { passive: false })
document.addEventListener('selectstart', preventSelect) // 阻止选择开始
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
;(document.body.style as any).webkitUserSelect = 'none' // Safari兼容
;(document.body.style as any).mozUserSelect = 'none' // Firefox兼容
}
function handleDrag(event: MouseEvent) {
if (!isDragging.value) return
event.preventDefault() // 阻止默认行为
const deltaX = event.clientX - dragStartX.value
const newWidth = dragStartWidth.value + deltaX
// 设置最小和最大宽度限制
const minWidth = 200
const maxWidth = window.innerWidth * 0.6
navigatorWidth.value = Math.max(minWidth, Math.min(maxWidth, newWidth))
}
function stopDrag() {
isDragging.value = false
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('selectstart', preventSelect)
document.body.style.cursor = ''
document.body.style.userSelect = ''
;(document.body.style as any).webkitUserSelect = ''
;(document.body.style as any).mozUserSelect = ''
}
// 外层DIV大小控制
const scrollStyle = computed(() => {
return appMode
@@ -219,8 +277,14 @@ const fileListStyle = computed(() => {
:items="fileListItems"
:endpoints="endpoints"
:axios="axios"
:style="{ width: `${navigatorWidth}px`, minWidth: `${navigatorWidth}px` }"
@navigate="pathChanged"
/>
<!-- 拖动分隔条 -->
<div v-if="showDirTree" class="divider" :class="{ 'divider-dragging': isDragging }" @mousedown="startDrag">
<div class="divider-line"></div>
<VIcon class="divider-icon" size="small">mdi-drag-vertical</VIcon>
</div>
<FileList
:item="item"
:storage="activeStorage"
@@ -231,6 +295,7 @@ const fileListStyle = computed(() => {
:sort="sort"
:listStyle="fileListStyle"
:showTree="showDirTree"
:style="{ flex: 1 }"
@pathchanged="pathChanged"
@loading="loadingChanged"
@refreshed="refreshPending = false"
@@ -243,3 +308,64 @@ const fileListStyle = computed(() => {
</div>
</div>
</template>
<style scoped>
.divider {
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
cursor: col-resize;
inline-size: 4px;
transition: background-color 0.2s ease;
user-select: none;
}
.divider:hover {
background-color: rgba(var(--v-theme-on-surface), 0.08);
}
.divider-dragging {
background-color: rgba(var(--v-theme-primary), 0.12) !important;
}
.divider-line {
background-color: rgba(var(--v-theme-outline), 0.3);
block-size: 100%;
inline-size: 1px;
transition: background-color 0.2s ease;
user-select: none;
}
.divider-dragging .divider-line {
background-color: rgb(var(--v-theme-primary)) !important;
}
.divider:hover .divider-line {
background-color: rgba(var(--v-theme-primary), 0.8);
}
.divider-icon {
position: absolute;
z-index: 1;
padding: 2px;
border-radius: 2px;
background-color: rgba(var(--v-theme-surface), 0.9);
color: rgba(var(--v-theme-on-surface-variant), 0.6);
opacity: 0;
pointer-events: none;
transition: all 0.2s ease;
}
.divider-dragging .divider-icon {
background-color: rgba(var(--v-theme-surface), 0.95);
color: rgb(var(--v-theme-primary));
opacity: 1;
}
.divider:hover .divider-icon {
color: rgba(var(--v-theme-primary), 0.9);
opacity: 1;
}
</style>

View File

@@ -5,6 +5,10 @@ import filter_svg from '@images/svg/filter.svg'
import { cloneDeep } from 'lodash-es'
import { innerFilterRules } from '@/api/constants'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
@@ -106,8 +110,20 @@ function onClose() {
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem">
<VCard :title="t('customRule.title', { id: props.rule.id })">
<VDialog
v-if="ruleInfoDialog"
v-model="ruleInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-filter-outline" class="me-2" />
</template>
<VCardTitle>{{ t('customRule.title', { id: props.rule.id }) }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn v-model="ruleInfoDialog" />
<VDivider />
<VCardText>
@@ -121,6 +137,7 @@ function onClose() {
:hint="t('customRule.hint.ruleId')"
persistent-hint
active
prepend-inner-icon="mdi-identifier"
/>
</VCol>
<VCol cols="12" md="6">
@@ -131,6 +148,7 @@ function onClose() {
:hint="t('customRule.hint.ruleName')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12">
@@ -141,6 +159,7 @@ function onClose() {
:hint="t('customRule.hint.include')"
persistent-hint
active
prepend-inner-icon="mdi-plus-circle"
/>
</VCol>
<VCol cols="12">
@@ -151,6 +170,7 @@ function onClose() {
:hint="t('customRule.hint.exclude')"
persistent-hint
active
prepend-inner-icon="mdi-minus-circle"
/>
</VCol>
<VCol cols="6">
@@ -161,6 +181,7 @@ function onClose() {
:hint="t('customRule.hint.sizeRange')"
persistent-hint
active
prepend-inner-icon="mdi-harddisk"
/>
</VCol>
<VCol cols="6">
@@ -171,6 +192,7 @@ function onClose() {
:hint="t('customRule.hint.seeders')"
persistent-hint
active
prepend-inner-icon="mdi-account-group"
/>
</VCol>
<VCol cols="6">
@@ -181,13 +203,14 @@ function onClose() {
:hint="t('customRule.hint.publishTime')"
persistent-hint
active
prepend-inner-icon="mdi-calendar-clock"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveRuleInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">{{
<VBtn @click="saveRuleInfo" prepend-icon="mdi-content-save" class="px-5">{{
t('customRule.action.confirm')
}}</VBtn>
</VCardActions>

View File

@@ -214,7 +214,7 @@ watch(
<VForm>
<VRow>
<VCol cols="6">
<VSelect
<VAutocomplete
v-model="props.directory.media_type"
variant="underlined"
:items="typeItems"
@@ -223,7 +223,7 @@ watch(
/>
</VCol>
<VCol cols="6">
<VSelect
<VAutocomplete
v-model="props.directory.media_category"
variant="underlined"
:items="getCategories"
@@ -231,7 +231,7 @@ watch(
/>
</VCol>
<VCol cols="4">
<VSelect
<VAutocomplete
v-model="props.directory.storage"
variant="underlined"
:items="resourceStorageOptions"
@@ -277,7 +277,7 @@ watch(
/>
</VCol>
<VCol cols="4">
<VSelect
<VAutocomplete
v-model="props.directory.library_storage"
variant="underlined"
:items="libraryStorageOptions"

View File

@@ -10,6 +10,10 @@ import custom_image from '@images/logos/downloader.png'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { downloaderDict } from '@/api/constants'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 获取i18n实例
const { t } = useI18n()
@@ -188,8 +192,22 @@ onUnmounted(() => {
</VCardText>
</VCard>
</VHover>
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`">
<VDialog
v-if="downloaderInfoDialog"
v-model="downloaderInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-download" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.downloader.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn v-model="downloaderInfoDialog" />
<VDivider />
<VCardText>
@@ -215,6 +233,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -225,6 +244,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -234,6 +254,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -244,6 +265,7 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -292,6 +314,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -302,6 +325,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -311,6 +335,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -321,6 +346,7 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
</VRow>
@@ -332,6 +358,7 @@ onUnmounted(() => {
:hint="t('downloader.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
@@ -341,13 +368,14 @@ onUnmounted(() => {
:hint="t('downloader.nameRequired')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveDownloaderInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveDownloaderInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -56,7 +56,7 @@ onMounted(() => {
<VCardTitle>{{ t('filterRule.priority') }} {{ props.pri }}</VCardTitle>
<VRow>
<VCol>
<VSelect
<VAutocomplete
v-model="props.rules"
variant="underlined"
:items="selectFilterOptions"

View File

@@ -8,6 +8,10 @@ import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import filter_group_svg from '@images/svg/filter-group.svg'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 获取i18n实例
const { t } = useI18n()
@@ -219,7 +223,13 @@ function onClose() {
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem">
<VDialog
v-if="groupInfoDialog"
v-model="groupInfoDialog"
scrollable
max-width="80rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`">
<VDialogCloseBtn v-model="groupInfoDialog" />
<VDivider />
@@ -233,26 +243,29 @@ function onClose() {
:hint="t('filterRule.groupName')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="6" md="3">
<VSelect
<VAutocomplete
v-model="groupInfo.media_type"
:label="t('filterRule.mediaType')"
:items="mediaTypeItems"
:hint="t('filterRule.mediaType')"
persistent-hint
active
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="6" md="3">
<VSelect
<VAutocomplete
v-model="groupInfo.category"
:items="getCategories"
:label="t('filterRule.category')"
:hint="t('filterRule.category')"
persistent-hint
active
prepend-inner-icon="mdi-folder-open"
/>
</VCol>
</VRow>
@@ -280,17 +293,17 @@ function onClose() {
<div class="text-center" v-if="filterRuleCards.length == 0">{{ t('filterRule.add') }}</div>
</VCardText>
<VCardActions class="pt-3">
<VBtn color="primary" variant="tonal" @click="addFilterCard">
<VBtn color="primary" @click="addFilterCard">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="success" variant="tonal" @click="importRules('priority')">
<VBtn color="success" @click="importRules('priority')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules">
<VBtn color="info" @click="shareRules">
<VIcon icon="mdi-share" />
</VBtn>
<VSpacer />
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveGroupInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -10,6 +10,10 @@ import api from '@/api'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { mediaServerDict } from '@/api/constants'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 获取i18n实例
const { t } = useI18n()
@@ -199,8 +203,22 @@ onMounted(() => {
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.mediaserver.name} - ${t('common.config')}`">
<VDialog
v-if="mediaServerInfoDialog"
v-model="mediaServerInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-cog" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.mediaserver.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn v-model="mediaServerInfoDialog" />
<VDivider />
<VCardText>
@@ -219,6 +237,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -229,6 +248,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -239,6 +259,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -248,10 +269,11 @@ onMounted(() => {
:hint="t('mediaserver.embyApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
@@ -262,6 +284,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -275,6 +298,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -285,6 +309,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -295,6 +320,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -304,10 +330,11 @@ onMounted(() => {
:hint="t('mediaserver.jellyfinApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
@@ -318,6 +345,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -331,6 +359,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -341,6 +370,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
@@ -351,10 +381,16 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="mediaServerInfo.config.username" :label="t('mediaserver.username')" active />
<VTextField
v-model="mediaServerInfo.config.username"
:label="t('mediaserver.username')"
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
@@ -362,10 +398,11 @@ onMounted(() => {
v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')"
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
@@ -376,6 +413,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -389,6 +427,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -399,6 +438,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -409,6 +449,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -418,25 +459,11 @@ onMounted(() => {
:hint="t('mediaserver.plexTokenHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
chips
multiple
clearable
:hint="t('mediaserver.syncLibrariesHint')"
persistent-hint
active
append-inner-icon="mdi-refresh"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
@@ -447,6 +474,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -458,16 +486,22 @@ onMounted(() => {
:label="t('mediaserver.type')"
:hint="t('mediaserver.customTypeHint')"
persistent-hint
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField :label="t('common.name')" :hint="t('mediaserver.nameRequired')" persistent-hint />
<VTextField
:label="t('common.name')"
:hint="t('mediaserver.nameRequired')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveMediaServerInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveMediaServerInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -6,9 +6,14 @@ import vocechat_image from '@images/logos/vocechat.png'
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 custom_image from '@images/logos/notification.png'
import { useToast } from 'vue-toast-notification'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
const { t } = useI18n()
@@ -51,6 +56,7 @@ const notificationTypeNames: { [key: string]: string } = {
synologychat: t('notification.synologychat.name'),
slack: t('notification.slack.name'),
webpush: t('notification.webpush.name'),
custom: t('setting.notification.custom'),
}
// 消息类型下拉字典
@@ -105,7 +111,7 @@ const getIcon = computed(() => {
case 'webpush':
return chrome_image
default:
return wechat_image
return custom_image
}
})
@@ -131,12 +137,26 @@ function onClose() {
</div>
<div class="text-body-1 mb-3">{{ notificationTypeNames[notification.type] }}</div>
</div>
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" />
<VImg :src="getIcon" cover class="mt-7 me-1" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.notification.name} - ${t('notification.config')}`">
<VDialogCloseBtn v-model="notificationInfoDialog" />
<VDialog
v-if="notificationInfoDialog"
v-model="notificationInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-cog" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.notification.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="notificationInfoDialog = false" />
<VDivider />
<VCardText>
<VForm>
@@ -145,7 +165,7 @@ function onClose() {
<VSwitch v-model="notificationInfo.enabled" :label="t('notification.enabled')" />
</VCol>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="notificationInfo.switchs"
:items="notificationTypes"
:label="t('notification.type')"
@@ -154,6 +174,7 @@ function onClose() {
clearable
chips
persistent-hint
prepend-inner-icon="mdi-bell-outline"
/>
</VCol>
</VRow>
@@ -165,6 +186,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -173,6 +195,7 @@ function onClose() {
:label="t('notification.wechat.corpId')"
:hint="t('notification.wechat.corpIdHint')"
persistent-hint
prepend-inner-icon="mdi-domain"
/>
</VCol>
<VCol cols="12" md="6">
@@ -181,6 +204,7 @@ function onClose() {
:label="t('notification.wechat.appId')"
:hint="t('notification.wechat.appIdHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
@@ -189,6 +213,7 @@ function onClose() {
:label="t('notification.wechat.appSecret')"
:hint="t('notification.wechat.appSecretHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -197,6 +222,7 @@ function onClose() {
:label="t('notification.wechat.proxy')"
:hint="t('notification.wechat.proxyHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -205,6 +231,7 @@ function onClose() {
:label="t('notification.wechat.token')"
:hint="t('notification.wechat.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol cols="12" md="6">
@@ -213,6 +240,7 @@ function onClose() {
:label="t('notification.wechat.encodingAesKey')"
:hint="t('notification.wechat.encodingAesKeyHint')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -222,10 +250,11 @@ function onClose() {
:placeholder="t('notification.wechat.adminsPlaceholder')"
:hint="t('notification.wechat.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
</VRow>
<VRow v-if="notificationInfo.type == 'telegram'">
<VRow v-else-if="notificationInfo.type == 'telegram'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
@@ -233,6 +262,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -241,6 +271,7 @@ function onClose() {
:label="t('notification.telegram.token')"
:hint="t('notification.telegram.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -249,6 +280,7 @@ function onClose() {
:label="t('notification.telegram.chatId')"
:hint="t('notification.telegram.chatIdHint')"
persistent-hint
prepend-inner-icon="mdi-chat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -258,6 +290,7 @@ function onClose() {
:placeholder="t('notification.telegram.usersPlaceholder')"
:hint="t('notification.telegram.usersHint')"
persistent-hint
prepend-inner-icon="mdi-account-group"
/>
</VCol>
<VCol cols="12" md="6">
@@ -267,10 +300,11 @@ function onClose() {
:placeholder="t('notification.telegram.adminsPlaceholder')"
:hint="t('notification.telegram.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
</VRow>
<VRow v-if="notificationInfo.type == 'slack'">
<VRow v-else-if="notificationInfo.type == 'slack'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
@@ -278,6 +312,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -287,6 +322,7 @@ function onClose() {
:placeholder="t('notification.slack.oauthTokenPlaceholder')"
:hint="t('notification.slack.oauthTokenHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -296,6 +332,7 @@ function onClose() {
:placeholder="t('notification.slack.appTokenPlaceholder')"
:hint="t('notification.slack.appTokenHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
@@ -305,10 +342,11 @@ function onClose() {
:placeholder="t('notification.slack.channelPlaceholder')"
:hint="t('notification.slack.channelHint')"
persistent-hint
prepend-inner-icon="mdi-pound"
/>
</VCol>
</VRow>
<VRow v-if="notificationInfo.type == 'synologychat'">
<VRow v-else-if="notificationInfo.type == 'synologychat'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
@@ -316,6 +354,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -324,6 +363,7 @@ function onClose() {
:label="t('notification.synologychat.webhook')"
:hint="t('notification.synologychat.webhookHint')"
persistent-hint
prepend-inner-icon="mdi-webhook"
/>
</VCol>
<VCol cols="12" md="6">
@@ -332,10 +372,11 @@ function onClose() {
:label="t('notification.synologychat.token')"
:hint="t('notification.synologychat.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
</VRow>
<VRow v-if="notificationInfo.type == 'vocechat'">
<VRow v-else-if="notificationInfo.type == 'vocechat'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
@@ -343,6 +384,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -351,6 +393,7 @@ function onClose() {
:label="t('notification.vocechat.host')"
:hint="t('notification.vocechat.hostHint')"
persistent-hint
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -359,6 +402,7 @@ function onClose() {
:label="t('notification.vocechat.apiKey')"
:hint="t('notification.vocechat.apiKeyHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -368,10 +412,11 @@ function onClose() {
:placeholder="t('notification.vocechat.channelIdPlaceholder')"
:hint="t('notification.vocechat.channelIdHint')"
persistent-hint
prepend-inner-icon="mdi-pound"
/>
</VCol>
</VRow>
<VRow v-if="notificationInfo.type == 'webpush'">
<VRow v-else-if="notificationInfo.type == 'webpush'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
@@ -379,6 +424,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -387,13 +433,35 @@ function onClose() {
:label="t('notification.webpush.username')"
:hint="t('notification.webpush.usernameHint')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
</VRow>
<VRow v-else>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.type"
:label="t('notification.type')"
:hint="t('notification.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
:label="t('notification.name')"
:hint="t('notification.nameRequired')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveNotificationInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveNotificationInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -82,9 +82,7 @@ function goPersonDetail() {
}"
@click.stop="goPersonDetail"
>
<div
class="person-card relative transform-gpu cursor-pointer rounded transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div class="person-card relative cursor-pointer ring-gray-700">
<div style="padding-block-end: 150%">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
@@ -100,10 +98,7 @@ function goPersonDetail() {
<div class="w-full truncate text-center font-bold">
{{ getPersonName() }}
</div>
<div
class="overflow-hidden whitespace-normal text-center text-sm"
style="display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2"
>
<div class="overflow-hidden whitespace-normal text-center text-sm text-ellipsis line-clamp-2">
{{ getPersonCharacter() }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />

View File

@@ -36,7 +36,17 @@ const $toast = useToast()
const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在安装插件...')
const progressText = ref('')
// 获取当前插件的标签
const pluginLabels = computed(() => {
if (!props.plugin?.plugin_label) return []
return props.plugin.plugin_label
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0)
})
// 图片是否加载完成
const isImageLoaded = ref(false)
@@ -167,51 +177,77 @@ const dropdownItems = ref([
}"
>
<div
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
class="flex-grow"
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
></div>
<div class="relative flex-1 min-w-0">
<VCardText class="px-2 pt-2 pb-0">
<VCardTitle
class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ..."
class="text-white px-2 pb-0 text-lg 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>
<span class="text-sm mt-1 text-gray-200"> v{{ props.plugin?.plugin_version }} </span>
</VCardTitle>
<VCardText class="text-white text-sm px-2 py-0 text-shadow overflow-hidden line-clamp-3 ...">
{{ props.plugin?.plugin_desc }}
</VCardText>
</div>
<div class="relative flex-shrink-0 self-center">
<VAvatar size="64">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</VCardText>
<div class="relative flex flex-row items-start px-2 justify-between grow">
<div class="relative flex-1 min-w-0">
<div
class="text-white text-sm px-2 py-1 text-shadow overflow-hidden ..."
:class="{ 'line-clamp-3': !props.plugin?.plugin_label, 'line-clamp-2': props.plugin?.plugin_label }"
>
{{ props.plugin?.plugin_desc }}
</div>
<!-- 插件标签 -->
<div v-if="pluginLabels.length > 0" class="plugin-app-card__tags-section px-2">
<VChip
v-for="tag in pluginLabels"
:key="tag"
size="x-small"
variant="tonal"
color="info"
class="me-1 mb-1"
tile
>
{{ tag }}
</VChip>
</div>
</div>
<div class="relative flex-shrink-0 self-center pb-3">
<VAvatar size="48">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</div>
</div>
</div>
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
<span>
<VIcon icon="mdi-github" class="me-1" />
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
{{ props.plugin?.plugin_author }}
</a>
</span>
<span v-if="props.count" class="ms-3">
<VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</span>
<div class="me-n3 absolute bottom-1 right-3">
<VCardText
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
>
<div class="flex flex-nowrap items-center w-full pe-10">
<div class="flex flex-nowrap max-w-40 items-center align-middle">
<VIcon icon="mdi-github" class="me-1" />
<a
class="overflow-hidden text-ellipsis whitespace-nowrap"
:href="props.plugin?.author_url"
target="_blank"
@click.stop
>
{{ props.plugin?.plugin_author }}
</a>
</div>
<div v-if="props.count" class="ms-2 flex-shrink-0 download-count align-middle items-center">
<VIcon size="small" icon="mdi-download" />
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
</div>
</div>
<div class="absolute bottom-0 right-0">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VIcon size="small" icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem v-for="(item, i) in dropdownItems" v-show="item.show" :key="i" @click="item.props.click">

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import api from '@/api'
import type { Plugin } from '@/api/types'
import { isNullOrEmptyObject } from '@core/utils'
@@ -10,7 +10,12 @@ import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
import PluginDataDialog from '../dialog/PluginDataDialog.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
@@ -54,6 +59,9 @@ const progressDialog = ref(false)
// 插件数据页面
const pluginInfoDialog = ref(false)
// 实时日志弹窗
const loggingDialog = ref(false)
// 进度框文本
const progressText = ref('正在更新插件...')
@@ -69,6 +77,18 @@ const imageLoadError = ref(false)
// 更新日志弹窗
const releaseDialog = ref(false)
// 插件分身对话框
const pluginCloneDialog = ref(false)
// 插件分身表单
const cloneForm = ref({
suffix: '',
name: '',
description: '',
version: '',
icon: '',
})
// 监听动作标识如为true则打开详情
watch(
() => props.action,
@@ -120,7 +140,12 @@ async function uninstallPlugin() {
// 通知父组件刷新
emit('remove')
} else {
$toast.error(t('plugin.uninstallFailed', { name: props.plugin?.plugin_name, message: result.message }))
$toast.error(
t('plugin.uninstallFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
}
} catch (error) {
console.error(error)
@@ -174,7 +199,12 @@ async function resetPlugin() {
// 通知父组件刷新
emit('save')
} else {
$toast.error(t('plugin.resetFailed', { name: props.plugin?.plugin_name, message: result.message }))
$toast.error(
t('plugin.resetFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
}
} catch (error) {
console.error(error)
@@ -205,7 +235,12 @@ async function updatePlugin() {
// 通知父组件刷新
emit('save')
} else {
$toast.error(t('plugin.updateFailed', { name: props.plugin?.plugin_name, message: result.message }))
$toast.error(
t('plugin.updateFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
}
} catch (error) {
console.error(error)
@@ -237,6 +272,54 @@ function configDone() {
emit('save')
}
// 显示插件分身对话框
function showPluginClone() {
cloneForm.value = {
suffix: '',
name: t('plugin.cloneDefaultName', { name: props.plugin?.plugin_name }),
description: t('plugin.cloneDefaultDescription', { description: props.plugin?.plugin_desc }),
version: props.plugin?.plugin_version || '1.0',
icon: props.plugin?.plugin_icon || '',
}
pluginCloneDialog.value = true
}
// 执行插件分身
async function executePluginClone() {
if (!cloneForm.value.suffix.trim()) {
$toast.error(t('plugin.suffixRequired'))
return
}
try {
progressDialog.value = true
progressText.value = t('plugin.cloning', { name: props.plugin?.plugin_name })
const result: { [key: string]: any } = await api.post(`plugin/clone/${props.plugin?.id}`, {
suffix: cloneForm.value.suffix.trim(),
name: cloneForm.value.name.trim(),
description: cloneForm.value.description.trim(),
version: cloneForm.value.version.trim(),
icon: cloneForm.value.icon.trim(),
})
progressDialog.value = false
if (result.success) {
$toast.success(t('plugin.cloneSuccess', { name: cloneForm.value.name }))
pluginCloneDialog.value = false
// 通知父组件刷新
emit('remove')
} else {
$toast.error(t('plugin.cloneFailed', { message: result.message }))
}
} catch (error) {
progressDialog.value = false
$toast.error(t('plugin.cloneFailedGeneral'))
console.error(error)
}
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -257,6 +340,16 @@ const dropdownItems = ref([
click: showPluginConfig,
},
},
{
title: t('plugin.clone'),
value: 8,
show: true,
props: {
prependIcon: 'mdi-content-copy',
color: 'info',
click: showPluginClone,
},
},
{
title: t('plugin.update'),
value: 3,
@@ -294,7 +387,7 @@ const dropdownItems = ref([
props: {
prependIcon: 'mdi-file-document-outline',
click: () => {
openLoggerWindow()
loggingDialog.value = true
},
},
},
@@ -328,7 +421,7 @@ watch(
</script>
<template>
<div>
<div class="h-full">
<!-- 插件卡片 -->
<VHover>
<template #default="hover">
@@ -344,50 +437,61 @@ watch(
}"
>
<div
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
class="flex-grow"
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
/>
<div class="relative flex-1 min-w-0">
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
<VBadge v-if="props.plugin?.state" dot inline color="success" />
<VCardText class="px-2 pt-2 pb-0">
<VCardTitle
class="text-white px-2 pb-0 text-lg text-shadow whitespace-nowrap overflow-hidden text-ellipsis"
>
<VBadge dot inline :color="props.plugin?.state ? 'success' : 'secondary'" />
{{ props.plugin?.plugin_name }}
<span class="text-sm mt-1 text-gray-200"> v{{ props.plugin?.plugin_version }} </span>
</VCardTitle>
<VCardText class="px-2 py-0 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
{{ props.plugin?.plugin_desc }}
</VCardText>
</div>
<div class="relative flex-shrink-0 self-center cursor-move">
<VAvatar size="64">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</VCardText>
<div class="relative flex flex-row items-start px-2 justify-between grow">
<div class="relative flex-1 min-w-0">
<div class="px-2 py-1 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
{{ props.plugin?.plugin_desc }}
</div>
</div>
<div class="relative flex-shrink-0 self-center pb-3" :class="{ 'cursor-move': display.mdAndUp.value }">
<VAvatar size="48">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
</div>
</div>
</div>
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
<span class="author-info">
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
</VImg>
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
{{ props.plugin?.plugin_author }}
</a>
</span>
<span v-if="props.count" class="ms-3">
<VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</span>
<div class="me-n3 absolute bottom-1 right-3">
<VCardText
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
>
<div class="flex flex-nowrap items-center w-full pe-10">
<div class="flex flex-nowrap max-w-40 items-center align-middle">
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
<VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" />
</VImg>
<a
:href="props.plugin?.author_url"
target="_blank"
@click.stop
class="overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ props.plugin?.plugin_author }}
</a>
</div>
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count items-center align-middle">
<VIcon size="small" icon="mdi-download" />
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
</span>
</div>
<div class="absolute bottom-0 right-0">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
@@ -439,7 +543,7 @@ watch(
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
<!-- 更新日志 -->
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" max-height="85vh" scrollable>
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable :fullscreen="!display.mdAndUp.value">
<VCard :title="t('plugin.updateHistoryTitle', { name: props.plugin?.plugin_name })">
<VDialogCloseBtn @click="releaseDialog = false" />
<VDivider />
@@ -455,6 +559,144 @@ watch(
</VCardItem>
</VCard>
</VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
<VCardTitle class="d-inline-flex">
<VIcon icon="mdi-file-document" class="me-2" />
{{ t('plugin.logTitle') }}
<a class="mx-2 d-inline-flex align-center cursor-pointer" @click="openLoggerWindow">
<VChip color="grey-darken-1" size="small" class="ml-2">
<VIcon icon="mdi-open-in-new" size="small" start />
{{ t('common.openInNewWindow') }}
</VChip>
</a>
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<LoggingView :logfile="`plugins/${props.plugin?.id?.toLowerCase()}.log`" />
</VCardText>
</VCard>
</VDialog>
<!-- 插件分身对话框 -->
<VDialog
v-if="pluginCloneDialog"
v-model="pluginCloneDialog"
width="600"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-content-copy" class="me-2" />
</template>
<VCardTitle>{{ t('plugin.cloneTitle') }}</VCardTitle>
<VCardSubtitle>{{ t('plugin.cloneSubtitle', { name: props.plugin?.plugin_name }) }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="pluginCloneDialog = false" />
<VDivider />
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.suffix"
:label="t('plugin.suffix') + ' *'"
:placeholder="t('plugin.suffixPlaceholder')"
:hint="t('plugin.suffixHint')"
persistent-hint
:rules="[
v => !!v || t('plugin.suffixRequired'),
v => /^[a-zA-Z0-9]+$/.test(v) || t('plugin.suffixFormatError'),
v => v.length <= 20 || t('plugin.suffixLengthError'),
]"
required
prepend-inner-icon="mdi-tag"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.name"
:label="t('plugin.cloneName')"
:placeholder="t('plugin.cloneNamePlaceholder')"
:hint="t('plugin.cloneNameHint')"
persistent-hint
prepend-inner-icon="mdi-rename-box"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="cloneForm.description"
:label="t('plugin.cloneDescriptionLabel')"
:placeholder="t('plugin.cloneDescriptionPlaceholder')"
:hint="t('plugin.cloneDescriptionHint')"
persistent-hint
prepend-inner-icon="mdi-text"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.version"
:label="t('plugin.cloneVersion')"
:placeholder="t('plugin.cloneVersionPlaceholder')"
:hint="t('plugin.cloneVersionHint')"
persistent-hint
prepend-inner-icon="mdi-numeric"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.icon"
:label="t('plugin.cloneIcon')"
:placeholder="t('plugin.cloneIconPlaceholder')"
:hint="t('plugin.cloneIconHint')"
persistent-hint
prepend-inner-icon="mdi-image"
/>
</VCol>
<!-- 重要提醒 -->
<VCol cols="12">
<VAlert type="warning" variant="tonal" density="compact" class="mt-2" icon="mdi-alert-circle-outline">
<div class="text-body-2">
<strong>{{ t('common.notice') }}</strong
>{{ t('plugin.cloneNotice') }}
</div>
</VAlert>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
color="primary"
@click="executePluginClone"
prepend-icon="mdi-content-copy"
class="px-5"
:disabled="!cloneForm.suffix.trim()"
>
{{ t('plugin.createClone') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
</template>
@@ -469,11 +711,6 @@ watch(
inset: 0;
}
.author-info {
display: flex;
align-items: center;
}
.author-avatar {
border-radius: 50%;
block-size: 24px;

View File

@@ -0,0 +1,663 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from '@/composables/useConfirm'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 文件夹配置接口
interface FolderConfig {
plugins?: string[]
order?: number
background?: string
icon?: string
color?: string
gradient?: string
showIcon?: boolean
}
// 输入参数
const props = defineProps({
folderName: String,
pluginCount: Number,
folderConfig: {
type: Object as PropType<FolderConfig>,
default: () => ({}),
},
width: String,
height: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['open', 'delete', 'rename', 'update-config'])
// 多语言
const { t } = useI18n()
// 响应式显示
const display = useDisplay()
// 提示框
const $toast = useToast()
// 确认框
const createConfirm = useConfirm()
// 菜单显示状态
const menuVisible = ref(false)
// 重命名对话框
const renameDialog = ref(false)
// 设置对话框
const settingDialog = ref(false)
// 新名称
const newFolderName = ref('')
// 默认颜色
const defaultColor = '#2196F3'
// 默认图标
const defaultIcon = 'mdi-folder'
// 默认渐变
const defaultGradient =
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(135deg, rgba(33, 150, 243, 0.7) 0%, rgba(33, 150, 243, 0.8s) 100%)'
// 文件夹设置
const folderSettings = ref<FolderConfig>({
background: '',
icon: defaultIcon,
color: defaultColor,
gradient: defaultGradient,
showIcon: true,
})
// 计算背景图片
const backgroundImage = computed(() => {
return props.folderConfig.background || folderSettings.value.background
})
// 预设图标选项
const iconOptions = [
'mdi-folder',
'mdi-folder-star',
'mdi-folder-heart',
'mdi-folder-cog',
'mdi-folder-music',
'mdi-folder-image',
'mdi-folder-video',
'mdi-folder-download',
'mdi-folder-network',
'mdi-folder-special',
]
// 预设颜色选项
const colorOptions = [
'#2196F3', // 蓝色
'#4CAF50', // 绿色
'#FF9800', // 橙色
'#9C27B0', // 紫色
'#F44336', // 红色
'#607D8B', // 蓝灰色
'#795548', // 棕色
'#E91E63', // 粉色
]
// 预设渐变选项
const gradientOptions = [
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(33, 150, 243, 0.7) 0%, rgba(33, 150, 243, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(76, 175, 80, 0.7) 0%, rgba(76, 175, 80, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(255, 152, 0, 0.7) 0%, rgba(255, 152, 0, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(156, 39, 176, 0.7) 0%, rgba(156, 39, 176, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(244, 67, 54, 0.7) 0%, rgba(244, 67, 54, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(96, 125, 139, 0.7) 0%, rgba(96, 125, 139, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(233, 30, 99, 0.7) 0%, rgba(233, 30, 99, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(63, 81, 181, 0.7) 0%, rgba(156, 39, 176, 0.8) 100%)',
]
// 计算背景渐变
const backgroundGradient = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.gradient || settings.gradient || gradientOptions[0]
})
// 计算图标
const folderIcon = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.icon || settings.icon || defaultIcon
})
// 计算图标颜色
const iconColor = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.color || settings.color || defaultColor
})
// 计算是否显示图标
const shouldShowIcon = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.showIcon !== undefined ? config.showIcon : settings.showIcon !== undefined ? settings.showIcon : true
})
// 监听props变化更新本地设置
watch(
() => props.folderConfig,
newConfig => {
if (newConfig) {
folderSettings.value = {
...folderSettings.value,
...newConfig,
}
}
},
{ deep: true, immediate: true },
)
// 打开文件夹
function openFolder() {
emit('open', props.folderName)
}
// 重命名文件夹
function showRenameDialog() {
newFolderName.value = props.folderName || ''
renameDialog.value = true
}
// 确认重命名
async function confirmRename() {
if (!newFolderName.value.trim()) {
$toast.error(t('folder.folderNameCannotBeEmpty'))
return
}
if (newFolderName.value === props.folderName) {
renameDialog.value = false
return
}
try {
emit('rename', props.folderName, newFolderName.value)
renameDialog.value = false
} catch (error) {
console.error(error)
}
}
// 删除文件夹
async function deleteFolder() {
const isConfirmed = await createConfirm({
title: t('common.confirm'),
content: t('folder.confirmDeleteFolder', { folderName: props.folderName }),
})
if (!isConfirmed) return
try {
emit('delete', props.folderName)
} catch (error) {
console.error(error)
}
}
// 显示设置对话框
function showSettingDialog() {
folderSettings.value = {
background: props.folderConfig?.background || '',
icon: props.folderConfig?.icon || defaultIcon,
color: props.folderConfig?.color || defaultColor,
gradient: props.folderConfig?.gradient || gradientOptions[0],
showIcon: props.folderConfig?.showIcon !== undefined ? props.folderConfig.showIcon : true,
}
settingDialog.value = true
}
// 保存设置
function saveSettings() {
const config = {
...props.folderConfig,
...folderSettings.value,
}
emit('update-config', props.folderName, config)
settingDialog.value = false
$toast.success(t('folder.folderSettingsSaved'))
}
// 弹出菜单
const dropdownItems = ref([
{
title: t('folder.settingAppearance'),
value: 0,
show: true,
props: {
prependIcon: 'mdi-palette',
click: showSettingDialog,
},
},
{
title: t('folder.rename'),
value: 1,
show: true,
props: {
prependIcon: 'mdi-pencil',
click: showRenameDialog,
},
},
{
title: t('folder.deleteFolder'),
value: 2,
show: true,
props: {
prependIcon: 'mdi-delete',
color: 'error',
click: deleteFolder,
},
},
])
</script>
<template>
<div class="h-full">
<!-- 文件夹卡片 -->
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:ripple="false"
:width="props.width"
:height="props.height"
min-height="8.5rem"
@click="openFolder"
class="plugin-folder-card h-full"
:class="{
'plugin-folder-card--mobile': display.mobile,
'plugin-folder-card--hover': hover.isHovering,
}"
>
<template v-if="backgroundImage" #image>
<VImg :src="backgroundImage" cover position="top"> </VImg>
</template>
<!-- 背景遮罩当有背景图片时 -->
<div v-if="backgroundImage" class="plugin-folder-card__overlay" />
<!-- 背景渐变层 -->
<div v-else class="plugin-folder-card__bg" :style="{ background: backgroundGradient }" />
<!-- 卡片内容 -->
<div class="plugin-folder-card__content">
<!-- 主体内容 -->
<div class="plugin-folder-card__body" :class="{ 'plugin-folder-card__body--no-icon': !shouldShowIcon }">
<!-- 文件夹图标 -->
<div v-if="shouldShowIcon" class="plugin-folder-card__icon-container">
<VIcon
:icon="folderIcon"
:size="display.mobile ? 56 : 72"
:color="iconColor"
:class="{ 'cursor-move': display.mdAndUp.value }"
/>
</div>
<!-- 文件夹信息 -->
<div
class="plugin-folder-card__info"
:class="{ 'cursor-move': display.mdAndUp.value, 'plugin-folder-card__info--no-icon': !shouldShowIcon }"
>
<!-- 文件夹名称 -->
<h3 class="plugin-folder-card__name">
{{ props.folderName }}
</h3>
<!-- 插件数量 -->
<p class="plugin-folder-card__count">{{ t('folder.pluginCount', { count: props.pluginCount }) }}</p>
</div>
</div>
<!-- 更多菜单按钮 - 右下角 -->
<div class="absolute top-0 right-0">
<VMenu v-model="menuVisible" location="top end" :close-on-content-click="true">
<template #activator="{ props: menuProps }">
<IconBtn v-bind="menuProps" @click.stop>
<VIcon size="small" icon="mdi-dots-vertical" class="text-white" />
</IconBtn>
</template>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" size="16" />
</template>
<VListItemTitle class="text-body-2">{{ item.title }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</div>
</div>
</VCard>
</template>
</VHover>
<!-- 重命名对话框 -->
<VDialog v-if="renameDialog" v-model="renameDialog" max-width="400">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-pencil" class="me-2" />
</template>
<VCardTitle>{{ t('folder.renameFolder') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="renameDialog = false" />
<VDivider />
<VCardText>
<VTextField
v-model="newFolderName"
:label="t('folder.folderName')"
variant="outlined"
autofocus
@keyup.enter="confirmRename"
/>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" prepend-icon="mdi-check" class="px-5" @click="confirmRename">确认</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 设置对话框 -->
<VDialog
v-if="settingDialog"
v-model="settingDialog"
max-width="600"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="settingDialog = false" />
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-palette" class="mr-2" />
{{ t('folder.folderAppearanceSettings') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<!-- 显示图标开关 -->
<VCol cols="12">
<VSwitch
v-model="folderSettings.showIcon"
:label="t('folder.showFolderIcon')"
color="primary"
hide-details
/>
</VCol>
<!-- 图标选择 -->
<VCol v-if="folderSettings.showIcon" cols="12" md="6">
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.icon') }}</VCardSubtitle>
<div class="icon-grid">
<VBtn
v-for="icon in iconOptions"
icon
:key="icon"
:variant="folderSettings.icon === icon ? 'tonal' : 'text'"
:color="folderSettings.icon === icon ? 'primary' : 'default'"
size="large"
class="ma-1"
@click="folderSettings.icon = icon"
>
<VIcon :icon="icon" size="24" />
</VBtn>
</div>
</VCol>
<!-- 颜色选择 -->
<VCol v-if="folderSettings.showIcon" cols="12" md="6">
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.iconColor') }}</VCardSubtitle>
<div class="color-grid">
<VBtn
v-for="color in colorOptions"
:key="color"
:variant="folderSettings.color === color ? 'tonal' : 'text'"
:color="color"
size="large"
class="ma-1 color-btn"
:style="{ backgroundColor: color }"
@click="folderSettings.color = color"
>
<VIcon v-if="folderSettings.color === color" icon="mdi-check" color="white" />
</VBtn>
</div>
</VCol>
<!-- 渐变背景选择 -->
<VCol cols="12">
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.backgroundGradient') }}</VCardSubtitle>
<div class="gradient-grid">
<VBtn
v-for="(gradient, index) in gradientOptions"
:key="index"
:variant="folderSettings.gradient === gradient ? 'tonal' : 'text'"
class="ma-1 gradient-btn"
:style="{ background: gradient }"
size="large"
@click="folderSettings.gradient = gradient"
>
<VIcon v-if="folderSettings.gradient === gradient" icon="mdi-check" color="white" />
</VBtn>
</div>
</VCol>
<!-- 自定义背景图片 -->
<VCol cols="12">
<VTextField
v-model="folderSettings.background"
:label="t('folder.customBackgroundImageURL')"
placeholder="https://example.com/image.jpg"
variant="outlined"
:hint="t('folder.customBackgroundImageHint')"
persistent-hint
prepend-inner-icon="mdi-image"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="saveSettings">保存</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
</template>
<style lang="scss" scoped>
.plugin-folder-card {
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&--hover {
transform: translateY(-4px);
}
&__bg {
position: absolute;
z-index: 0;
inset: 0;
outline: none;
}
&__overlay {
position: absolute;
z-index: 1;
background: rgba(0, 0, 0, 60%);
inset: 0;
}
&__content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
padding: 16px;
block-size: 100%;
padding-block-end: 12px;
.plugin-folder-card--mobile & {
padding: 12px;
padding-block-end: 10px;
}
}
&__body {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 16px;
padding-block: 0;
padding-inline: 8px;
.plugin-folder-card--mobile & {
gap: 12px;
padding-block: 0;
padding-inline: 4px;
}
&--no-icon {
align-items: flex-start;
justify-content: flex-start;
padding: 16px;
gap: 0;
.plugin-folder-card--mobile & {
padding: 12px;
gap: 0;
}
}
}
&__icon-container {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
}
&__info {
flex: 1;
min-block-size: 0;
text-align: start;
&--no-icon {
flex: none;
text-align: start;
}
}
&__name {
display: -webkit-box;
overflow: hidden;
margin: 0;
-webkit-box-orient: vertical;
color: white;
font-size: 1.1rem;
font-weight: 600;
-webkit-line-clamp: 1;
line-clamp: 1;
line-height: 1.3;
max-inline-size: none;
text-overflow: ellipsis;
text-shadow: 0 2px 4px rgba(0, 0, 0, 50%);
.plugin-folder-card--mobile & {
font-size: 1rem;
}
.plugin-folder-card__info--no-icon & {
font-size: 1.3rem;
font-weight: 700;
-webkit-line-clamp: 2;
line-clamp: 2;
margin-block-end: 4px;
.plugin-folder-card--mobile & {
font-size: 1.2rem;
}
}
}
&__count {
color: white;
font-size: 0.85rem;
margin-block: 2px 0;
margin-inline: 0;
opacity: 0.9;
text-shadow: 0 1px 2px rgba(0, 0, 0, 50%);
.plugin-folder-card--mobile & {
font-size: 0.8rem;
}
.plugin-folder-card__info--no-icon & {
font-size: 0.9rem;
margin-block-start: 0;
.plugin-folder-card--mobile & {
font-size: 0.85rem;
}
}
}
}
// 设置对话框样式
.icon-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
max-block-size: 200px;
overflow-y: auto;
}
.color-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
}
.gradient-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
max-block-size: 200px;
overflow-y: auto;
}
.color-btn {
border-radius: 8px !important;
block-size: 60px !important;
min-inline-size: 60px !important;
}
.gradient-btn {
border-radius: 8px !important;
block-size: 60px !important;
min-inline-size: 120px !important;
}
</style>

View File

@@ -0,0 +1,183 @@
<script lang="ts" setup>
import PluginCard from './PluginCard.vue'
import PluginFolderCard from './PluginFolderCard.vue'
interface MixedSortItem {
type: 'folder' | 'plugin'
id: string
data: any
order: number
}
interface Props {
item: MixedSortItem
pluginStatistics?: { [key: string]: number }
pluginActions?: { [key: string]: boolean }
showRemoveButton?: boolean
}
const props = withDefaults(defineProps<Props>(), {
pluginStatistics: () => ({}),
pluginActions: () => ({}),
showRemoveButton: false,
})
const emit = defineEmits<{
openFolder: [folderName: string]
deleteFolder: [folderName: string]
renameFolder: [oldName: string, newName: string]
updateFolderConfig: [folderName: string, config: any]
refreshData: []
actionDone: [pluginId: string]
removeFromFolder: [pluginId: string]
dropToFolder: [event: DragEvent, folderName: string]
}>()
// 拖拽事件处理
function handleDragOver(event: DragEvent) {
// 只有当拖拽的是插件时才允许放入文件夹
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
event.dataTransfer!.dropEffect = 'move'
const target = event.currentTarget as HTMLElement
target.classList.add('drag-over')
}
}
function handleDragEnter(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
}
}
function handleDragLeave(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
}
}
function handleDropToFolder(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
emit('dropToFolder', event, props.item.id)
}
}
</script>
<template>
<div class="mixed-sort-card-wrapper h-full">
<!-- 文件夹卡片 -->
<div
v-if="item.type === 'folder'"
class="drop-zone h-full"
:data-plugin-id="item.id"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
@drop="handleDropToFolder"
>
<PluginFolderCard
:folder-name="item.data.name"
:plugin-count="item.data.pluginCount"
:folder-config="item.data.config"
@open="$emit('openFolder', item.id)"
@delete="$emit('deleteFolder', item.id)"
@rename="(oldName, newName) => $emit('renameFolder', oldName, newName)"
@update-config="(folderName, config) => $emit('updateFolderConfig', folderName, config)"
/>
</div>
<!-- 插件卡片 -->
<div v-else-if="item.type === 'plugin'" class="plugin-item-wrapper h-full" :data-plugin-id="item.id">
<PluginCard
:count="pluginStatistics[item.id] || 0"
:plugin="item.data"
:action="pluginActions[item.id] || false"
@remove="$emit('refreshData')"
@save="$emit('refreshData')"
@action-done="$emit('actionDone', item.id)"
/>
<!-- 移出文件夹按钮(仅在文件夹内显示) -->
<VBtn
v-if="showRemoveButton"
icon="mdi-folder-remove"
variant="text"
color="warning"
size="small"
class="remove-from-folder-btn"
@click="$emit('removeFromFolder', item.id)"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.mixed-sort-card-wrapper {
block-size: 100%;
inline-size: 100%;
// 确保拖拽时的边界清晰
&.sortable-chosen {
opacity: 0.5;
}
&.sortable-ghost {
border: 2px dashed #2196f3;
border-radius: 16px;
background: rgba(33, 150, 243, 10%);
opacity: 0.3;
}
}
// 拖拽相关样式
.drop-zone {
position: relative;
isolation: isolate; // 创建新的层叠上下文
transition: all 0.3s ease;
&.drag-over {
border: 2px dashed #2196f3;
border-radius: 16px;
box-shadow: 0 0 20px rgba(33, 150, 243, 50%);
transform: scale(1.02);
}
}
.plugin-item-wrapper {
position: relative;
isolation: isolate; // 创建新的层叠上下文
.remove-from-folder-btn {
position: absolute;
z-index: 10;
border-radius: 50%;
backdrop-filter: blur(4px);
background: rgba(255, 255, 255, 10%);
inset-block-start: 4px;
inset-inline-end: 4px;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover .remove-from-folder-btn {
opacity: 1;
}
}
// 拖拽时的样式优化
.mixed-sort-card-wrapper.sortable-drag {
.remove-from-folder-btn {
display: none !important;
}
}
</style>

View File

@@ -11,7 +11,11 @@ import api from '@/api'
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
import { formatFileSize } from '@/@core/utils/formatters'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -214,7 +218,7 @@ onMounted(() => {
elevation="0"
rounded="lg"
hover
@click="siteEditDialog = true"
@click="handleResourceBrowse"
>
<!-- 装饰性状态指示器 -->
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
@@ -224,7 +228,7 @@ onMounted(() => {
<!-- 顶部图标和站点名称 -->
<div class="flex items-center mb-1">
<!-- 站点图标 -->
<VAvatar tile rounded="lg" size="32" class="me-2 cursor-move">
<VAvatar tile rounded="lg" size="32" class="me-2" :class="{ 'cursor-move': display.mdAndUp.value }">
<VImg :src="siteIcon" class="w-full h-full" :alt="cardProps.site?.name" cover>
<template #placeholder>
<div class="w-full h-full">
@@ -335,11 +339,11 @@ onMounted(() => {
<VIcon icon="mdi-dots-vertical" size="20" />
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
<VList>
<VListItem @click="handleResourceBrowse" base-color="info">
<VListItem @click="siteEditDialog = true" base-color="info">
<template #prepend>
<VIcon icon="mdi-web" size="20" />
<VIcon icon="mdi-file-edit-outline" size="20" />
</template>
<VListItemTitle>{{ t('site.browseResources') }}</VListItemTitle>
<VListItemTitle>{{ t('site.actions.edit') }}</VListItemTitle>
</VListItem>
<VListItem @click="deleteSiteInfo">
<template #prepend>

View File

@@ -16,6 +16,10 @@ import { useToast } from 'vue-toast-notification'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useI18n } from 'vue-i18n'
import { storageIconDict } from '@/api/constants'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -200,9 +204,18 @@ function onClose() {
@close="aListConfigDialog = false"
@done="handleDone"
/>
<VDialog v-if="customConfigDialog" v-model="customConfigDialog" scrollable max-width="30rem">
<VDialog
v-if="customConfigDialog"
v-model="customConfigDialog"
scrollable
max-width="30rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog" />
</template>
<VCardTitle>{{ t('storage.custom') }}</VCardTitle>
<VDialogCloseBtn v-model="customConfigDialog" />
</VCardItem>
@@ -215,16 +228,21 @@ function onClose() {
:label="t('storage.type')"
:hint="t('storage.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="customName" :label="t('storage.name')" persistent-hint active />
<VTextField
v-model="customName"
:label="t('storage.name')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="handleDone" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="handleDone" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import SubscribeFilesDialog from '../dialog/SubscribeFilesDialog.vue'
import SubscribeShareDialog from '../dialog/SubscribeShareDialog.vue'
@@ -9,6 +9,10 @@ import api from '@/api'
import type { Subscribe } from '@/api/types'
import router from '@/router'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -296,99 +300,116 @@ function onSubscribeEditRemove() {
<div>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full"
<div
class="w-full h-full rounded-lg overflow-hidden"
:class="{
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
'opacity-70': subscribeState === 'S',
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
}"
min-height="170"
@click="editSubscribeDialog"
:ripple="false"
>
<div class="me-n3 absolute top-1 right-2">
<IconBtn>
<VIcon icon="mdi-dots-vertical" color="white" />
<VMenu activator="parent" close-on-content-click>
<VList>
<template v-for="(item, i) in dropdownItems" :key="i">
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</template>
</VList>
</VMenu>
</IconBtn>
</div>
<template #image>
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
</div>
</template>
<div class="absolute inset-0 subscribe-card-background"></div>
</VImg>
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
</template>
<div>
<VCardText class="flex items-center py-3">
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md cursor-move" v-if="imageLoaded">
<VImg :src="posterUrl" aspect-ratio="2/3" 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 flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
<div class="mr-2 min-w-0 text-lg font-bold text-white">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
</div>
</div>
</VCardText>
<VCardText class="flex justify-space-between align-center flex-wrap py-3">
<div class="flex align-center">
<IconBtn
v-if="props.media?.total_episode"
v-bind="props"
icon="mdi-progress-download"
color="white"
class="me-1"
/>
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}
</div>
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
{{ props.media?.username }}
</span>
</div>
</VCardText>
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VIcon icon="mdi-download" class="me-1" />
{{ lastUpdateText }}
</VCardText>
<div class="w-full absolute bottom-0">
<VProgressLinear
v-if="getPercentage() > 0"
:model-value="getPercentage()"
bg-color="success"
color="success"
/>
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full"
:class="{
'opacity-70': subscribeState === 'S',
}"
rounded="0"
min-height="150"
@click="editSubscribeDialog"
:ripple="false"
>
<div class="me-n3 absolute top-1 right-4">
<IconBtn>
<VIcon icon="mdi-dots-vertical" color="white" />
<VMenu activator="parent" close-on-content-click>
<VList>
<template v-for="(item, i) in dropdownItems" :key="i">
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</template>
</VList>
</VMenu>
</IconBtn>
</div>
</div>
</VCard>
<template #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 outline-none subscribe-card-background"></div>
</VImg>
<div
v-if="subscribeState === 'P'"
class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none"
/>
</template>
<div>
<VCardText class="flex items-center pt-3 pb-2">
<div
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md"
v-if="imageLoaded"
:class="{ 'cursor-move': display.mdAndUp.value }"
>
<VImg :src="posterUrl" aspect-ratio="2/3" 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 flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
<div class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ...">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
</div>
</div>
</VCardText>
<VCardText class="flex justify-space-between align-center flex-wrap px-3">
<div class="flex align-center">
<IconBtn
v-if="props.media?.total_episode"
size="small"
v-bind="props"
icon="mdi-progress-download"
color="white"
/>
<div v-if="props.media?.season" class="text-subtitle-2 me-2 text-white">
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}
</div>
<IconBtn v-if="props.media?.username" icon="mdi-account" size="small" color="white" />
<span v-if="props.media?.username" class="text-subtitle-2 text-white">
{{ props.media?.username }}
</span>
</div>
</VCardText>
<VCardText
v-if="lastUpdateText"
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300 text-xs"
>
<VIcon icon="mdi-download" class="me-1" />
{{ lastUpdateText }}
</VCardText>
<div class="w-full absolute bottom-0">
<VProgressLinear
v-if="getPercentage() > 0"
:model-value="getPercentage()"
bg-color="success"
color="success"
/>
</div>
</div>
</VCard>
</div>
</template>
</VHover>
<!-- 订阅编辑弹窗 -->

View File

@@ -97,64 +97,69 @@ function doDelete() {
<div class="h-full">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full"
<div
class="w-full h-full rounded-lg overflow-hidden"
:class="{
'transition transform-cpu duration-300 -translate-y-1': 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" />
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full"
rounded="0"
min-height="150"
@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 pa-3 pb-1 grow">
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md" 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 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" 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 py-2">
<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="white" 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>
</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>
</div>
</template>
</VHover>
<!-- 订阅编辑弹窗 -->

View File

@@ -283,7 +283,7 @@ onMounted(() => {
v-for="(item, index) in props.more"
:key="index"
@click.stop="handleAddDownload(item)"
class="border-b border-opacity-5 hover:bg-primary-lighten-5"
class="hover:bg-primary-lighten-5"
>
<template v-slot:prepend>
<div class="d-flex align-center gap-1">

View File

@@ -133,7 +133,12 @@ onMounted(() => {
<VListItemTitle>
<div class="d-flex flex-row flex-wrap align-center mb-2">
<span class="text-h6 font-weight-bold me-2">{{ media?.title ?? meta?.name }}</span>
<VChip v-if="meta?.season_episode" class="chip-season rounded-sm font-weight-bold" variant="elevated">
<VChip
v-if="meta?.season_episode"
class="chip-season rounded-sm font-weight-bold"
variant="elevated"
size="small"
>
{{ meta?.season_episode }}
</VChip>
</div>

View File

@@ -4,7 +4,7 @@ import { Subscribe, User } from '@/api/types'
import { useUserStore } from '@/stores'
import avatar1 from '@images/avatars/avatar-1.png'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { Workflow } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
import WorkflowActionsDialog from '@/components/dialog/WorkflowActionsDialog.vue'
import api from '@/api'
@@ -179,7 +179,13 @@ const resolveProgress = (item: Workflow) => {
:loading="loading"
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
>
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
<VCardItem
:class="{
'py-1': workflow?.description,
'py-3': !workflow?.description,
[`bg-${resolveStatusVariant(workflow?.state).color}`]: true,
}"
>
<template #prepend>
<VAvatar variant="text" class="me-2">
<VIcon

View File

@@ -134,69 +134,75 @@ onMounted(() => {
<template>
<VDialog max-width="35rem" scrollable>
<VCard>
<VCardTitle class="py-4 me-12">
<VIcon icon="mdi-download" class="me-2" />
<span v-if="title">{{ torrent?.site_name }} - {{ title }}</span>
<span v-else>{{ t('dialog.addDownload.confirmDownload') }}</span>
</VCardTitle>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-monitor-arrow-down-variant" class="me-2" />
</template>
<VCardTitle>{{ t('dialog.addDownload.confirmDownload') }}</VCardTitle>
<VCardSubtitle>{{ torrent?.site_name }} - {{ title }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VList lines="one">
<VListItem>
<template #prepend>
<VIcon icon="mdi-web"></VIcon>
</template>
<VListItemTitle>
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.description">
<template #prepend>
<VIcon icon="mdi-subtitles-outline"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.size">
<template #prepend>
<VIcon icon="mdi-database"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2">
<VChip variant="tonal" label>
{{ formatFileSize(torrent?.size || 0) }}
</VChip>
</span>
</VListItemTitle>
</VListItem>
</VList>
<VRow class="px-7">
<VCol cols="12" md="4">
<VSelect
v-model="selectedDownloader"
:items="downloaderOptions"
size="small"
:label="t('dialog.addDownload.downloader')"
variant="underlined"
:placeholder="t('dialog.addDownload.defaultPlaceholder')"
density="compact"
/>
</VCol>
<VCol cols="12" md="8">
<VCombobox
v-model="selectedDirectory"
:items="targetDirectories"
:label="t('dialog.addDownload.saveDirectory')"
size="small"
:placeholder="t('dialog.addDownload.autoPlaceholder')"
variant="underlined"
density="compact"
/>
</VCol>
</VRow>
<VCardText>
<VList lines="one">
<VListItem>
<template #prepend>
<VIcon icon="mdi-web"></VIcon>
</template>
<VListItemTitle>
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.description">
<template #prepend>
<VIcon icon="mdi-subtitles-outline"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.size">
<template #prepend>
<VIcon icon="mdi-database"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2">
<VChip variant="tonal" label>
{{ formatFileSize(torrent?.size || 0) }}
</VChip>
</span>
</VListItemTitle>
</VListItem>
</VList>
<VRow class="px-5">
<VCol cols="12" md="6">
<VSelect
v-model="selectedDownloader"
:items="downloaderOptions"
size="small"
:label="t('dialog.addDownload.downloader')"
variant="underlined"
:placeholder="t('dialog.addDownload.defaultPlaceholder')"
density="comfortable"
prepend-inner-icon="mdi-download"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="selectedDirectory"
:items="targetDirectories"
:label="t('dialog.addDownload.saveDirectory')"
size="small"
:placeholder="t('dialog.addDownload.autoPlaceholder')"
variant="underlined"
density="comfortable"
prepend-inner-icon="mdi-folder"
/>
</VCol>
</VRow>
</VCardText>
<VCardText class="text-center">
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
{{ buttonText }}

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -28,17 +32,33 @@ async function handleReset() {
const result: { [key: string]: any } = await api.get('/storage/reset/alist')
if (result.success) {
// 重置成功
alertType.value = 'success'
handleDone()
} else {
alertType.value = 'error'
text.value = result.message
}
} catch (e) {
console.error(e)
}
}
// 登录类型
let loginType = ref('username')
if (props.conf.token) {
loginType = ref('token')
} else if (props.conf.username) {
loginType = ref('username')
} else {
loginType = ref('guest')
}
// 数据源
const sourceItems = [
{
'title': t('dialog.alistConfig.loginTypeOptions.username'),
'value': 'username',
},
{ 'title': t('dialog.alistConfig.loginTypeOptions.token'), 'value': 'token' },
{ 'title': t('dialog.alistConfig.loginTypeOptions.guest'), 'value': 'guest' },
]
// 保存alist设置
async function savaAlistConfig() {
try {
@@ -50,9 +70,18 @@ async function savaAlistConfig() {
</script>
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard :title="t('dialog.alistConfig.title')">
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.alistConfig.title') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
@@ -61,33 +90,55 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.serverUrl')"
:label="t('dialog.alistConfig.serverUrl')"
persistent-hint
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
<VCol cols="12" md="4">
<VSelect
v-model="loginType"
:items="sourceItems"
:label="t('dialog.alistConfig.loginType')"
:hint="t('dialog.alistConfig.loginType')"
persistent-hint
prepend-inner-icon="mdi-login"
/>
</VCol>
<VCol cols="12" md="4" v-if="loginType == 'username'">
<VTextField
v-model="props.conf.username"
:hint="t('dialog.alistConfig.username')"
:label="t('dialog.alistConfig.username')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VCol cols="12" md="4" v-if="loginType == 'username'">
<VTextField
type="password"
v-model="props.conf.password"
:hint="t('dialog.alistConfig.password')"
:label="t('dialog.alistConfig.password')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="8" v-if="loginType == 'token'">
<VTextField
v-model="props.conf.token"
:hint="t('dialog.alistConfig.loginTypeOptions.token')"
:label="t('dialog.alistConfig.loginTypeOptions.token')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.alistConfig.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.alistConfig.complete') }}
</VBtn>
</VCardActions>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -106,11 +110,20 @@ onUnmounted(() => {
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="t('dialog.aliyunAuth.loginTitle')">
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 rounded text-center p-3 border">
<VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.aliyunAuth.loginTitle') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText class="pt-2 flex flex-col items-center justify-center">
<div class="mt-6 rounded text-center p-3 border">
<VImg class="mx-auto" :src="qrCodeUrl" width="200" height="200">
<template #placeholder>
<div class="w-full h-full">
@@ -119,16 +132,18 @@ onUnmounted(() => {
</template>
</VImg>
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
<div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
</div>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.aliyunAuth.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.aliyunAuth.complete') }}
</VBtn>
</VCardActions>

View File

@@ -25,14 +25,20 @@ function handleImport() {
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="props.title">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-code-json" class="me-2" />
</template>
<VCardTitle>{{ props.title }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
<VTextarea v-model="codeString" prepend-inner-icon="mdi-code-json" />
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3">
<VBtn @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3">
{{ t('dialog.importCode.import') }}
</VBtn>
</VCardActions>

View File

@@ -161,18 +161,12 @@ onBeforeMount(async () => {
</div>
</VCardText>
<VCardActions class="pt-3">
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" variant="outlined" color="info">
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" color="info">
{{ t('dialog.pluginConfig.viewData') }}
</VBtn>
<VSpacer />
<!-- 只有Vuetify模式显示默认保存按钮Vue模式由组件内部控制 -->
<VBtn
v-if="renderMode === 'vuetify'"
@click="savePluginConf"
variant="elevated"
prepend-icon="mdi-content-save"
class="px-5"
>
<VBtn v-if="renderMode === 'vuetify'" @click="savePluginConf" prepend-icon="mdi-content-save" class="px-5">
保存
</VBtn>
</VCardActions>

View File

@@ -2,6 +2,11 @@
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -9,6 +14,16 @@ const $toast = useToast()
// 插件仓库设置字符串
const repoString = ref('')
// 用于显示的仓库地址数组
const repoArray = ref<string[]>([])
// 计算属性:在数组和换行符分隔的字符串之间转换
const displayRepos = computed({
get: () => repoArray.value.join('\n'),
set: (value: string) => {
repoArray.value = value.split('\n').filter((repo: string) => repo.trim() !== '')
},
})
// 定义事件
const emit = defineEmits(['save', 'close'])
@@ -17,7 +32,10 @@ const emit = defineEmits(['save', 'close'])
async function queryMarketRepoSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
if (result && result.data && result.data.value) repoString.value = result.data.value
if (result && result.data && result.data.value) {
repoString.value = result.data.value
repoArray.value = result.data.value.split(',').filter((repo: string) => repo.trim() !== '')
}
} catch (error) {
console.log(error)
}
@@ -26,8 +44,9 @@ async function queryMarketRepoSetting() {
// 保存设置
async function saveHandle() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoString.value)
// 将数组转换为逗号分隔的字符串
const repoStringToSave = repoArray.value.join(',')
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)
if (result.success) {
$toast.success(t('dialog.pluginMarketSetting.saveSuccess'))
@@ -44,7 +63,7 @@ onMounted(() => {
</script>
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardTitle>
@@ -53,17 +72,19 @@ onMounted(() => {
</VCardTitle>
<VDialogCloseBtn @click="emit('close')" />
</VCardItem>
<VDivider />
<VCardText class="pt-2">
<VTextarea
v-model="repoString"
v-model="displayRepos"
:placeholder="t('dialog.pluginMarketSetting.repoPlaceholder')"
:hint="t('dialog.pluginMarketSetting.repoHint')"
persistent-hint
auto-grow
/>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="saveHandle" prepend-icon="mdi-content-save-check" class="px-5 me-3">
<VBtn @click="saveHandle" prepend-icon="mdi-content-save-check" class="px-5 me-3">
{{ t('dialog.pluginMarketSetting.save') }}
</VBtn>
</VCardActions>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -53,32 +57,44 @@ async function handleReset() {
</script>
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard :title="t('dialog.rcloneConfig.title')">
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.rcloneConfig.title') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="props.conf.filepath" :label="t('dialog.rcloneConfig.filePath')" />
<VTextField
v-model="props.conf.filepath"
:label="t('dialog.rcloneConfig.filePath')"
prepend-inner-icon="mdi-file-document"
/>
</VCol>
<VCol cols="12">
<VAceEditor
v-model:value="props.conf.content"
lang="ini"
theme="monokai"
style="block-size: 30rem"
class="rounded"
class="rounded h-full min-h-[30rem]"
>
</VAceEditor>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.rcloneConfig.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.rcloneConfig.complete') }}
</VBtn>
</VCardActions>

View File

@@ -82,15 +82,18 @@ const storageOptions = computed(() => {
// 标题
const dialogTitle = computed(() => {
return t('dialog.reorganize.manualTitle')
})
// 副标题
const dialogSubtitle = computed(() => {
if (props.items) {
if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length })
return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path })
} else if (props.logids) {
return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length })
}
return t('dialog.reorganize.manualTitle')
})
// 禁用指定集数
const disableEpisodeDetail = computed(() => {
if (props.items) {
@@ -250,7 +253,12 @@ onUnmounted(() => {
<template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="dialogTitle">
<VCard>
<VCardItem class="py-2">
<template #prepend> <VIcon icon="mdi-folder-move" class="me-2" /> </template>
<VCardTitle>{{ dialogTitle }}</VCardTitle>
<VCardSubtitle>{{ dialogSubtitle }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
@@ -264,6 +272,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetStorageHint')"
persistent-hint
prepend-inner-icon="mdi-harddisk"
/>
</VCol>
<VCol cols="12" md="6">
@@ -273,6 +282,7 @@ onUnmounted(() => {
:items="transferTypeOptions"
:hint="t('dialog.reorganize.transferTypeHint')"
persistent-hint
prepend-inner-icon="mdi-swap-horizontal"
>
<template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }}
@@ -287,6 +297,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetPathHint')"
persistent-hint
prepend-inner-icon="mdi-folder-outline"
/>
</VCol>
</VRow>
@@ -302,6 +313,7 @@ onUnmounted(() => {
]"
:hint="t('dialog.reorganize.mediaTypeHint')"
persistent-hint
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12" md="6">
@@ -315,6 +327,7 @@ onUnmounted(() => {
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
@@ -327,6 +340,7 @@ onUnmounted(() => {
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
@click:append-inner="mediaSelectorDialog = true"
/>
</VCol>
@@ -339,6 +353,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
:hint="t('dialog.reorganize.episodeGroupHint')"
persistent-hint
prepend-inner-icon="mdi-view-list"
/>
</VCol>
<VCol cols="12" md="3">
@@ -348,6 +363,7 @@ onUnmounted(() => {
:items="seasonItems"
:hint="t('dialog.reorganize.seasonHint')"
persistent-hint
prepend-inner-icon="mdi-calendar"
/>
</VCol>
<VCol cols="12" md="3">
@@ -358,6 +374,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeDetailPlaceholder')"
:hint="t('dialog.reorganize.episodeDetailHint')"
persistent-hint
prepend-inner-icon="mdi-playlist-play"
/>
</VCol>
<VCol cols="12" md="6">
@@ -367,6 +384,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeFormatPlaceholder')"
:hint="t('dialog.reorganize.episodeFormatHint')"
persistent-hint
prepend-inner-icon="mdi-format-text"
/>
</VCol>
<VCol cols="12" md="6">
@@ -376,6 +394,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeOffsetPlaceholder')"
:hint="t('dialog.reorganize.episodeOffsetHint')"
persistent-hint
prepend-inner-icon="mdi-numeric"
/>
</VCol>
</VRow>
@@ -387,6 +406,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodePartPlaceholder')"
:hint="t('dialog.reorganize.episodePartHint')"
persistent-hint
prepend-inner-icon="mdi-file-multiple"
/>
</VCol>
<VCol cols="12" md="6">
@@ -397,6 +417,7 @@ onUnmounted(() => {
placeholder="0"
:hint="t('dialog.reorganize.minFileSizeHint')"
persistent-hint
prepend-inner-icon="mdi-file-document-outline"
/>
</VCol>
</VRow>
@@ -438,10 +459,10 @@ onUnmounted(() => {
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn variant="elevated" color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
<VBtn color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
{{ t('dialog.reorganize.addToQueue') }}
</VBtn>
<VBtn variant="elevated" @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
<VBtn @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
{{ t('dialog.reorganize.reorganizeNow') }}
</VBtn>
</VCardActions>

View File

@@ -6,6 +6,10 @@ import { NavMenu } from '@/@layouts/types'
import { useUserStore } from '@/stores'
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -302,29 +306,24 @@ onMounted(() => {
})
</script>
<template>
<VDialog v-model="dialog" max-width="42rem" scrollable maxHeight="85vh">
<VDialog v-model="dialog" max-width="42rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard class="search-dialog">
<!-- 搜索输入框 -->
<VCardItem class="pa-4 pa-sm-5 search-box-container">
<template #prepend>
<VIcon icon="mdi-magnify" color="primary" size="x-large" />
</template>
<VCombobox
ref="searchWordInput"
v-model="searchWord"
density="comfortable"
variant="outlined"
class="search-input"
prepend-inner-icon="mdi-magnify"
append-inner-icon="mdi-close"
@click:append-inner="emit('close')"
:placeholder="t('dialog.searchBar.searchPlaceholder')"
@keydown.enter="searchMedia('media')"
hide-details
clearable
/>
<template #append>
<IconBtn>
<VIcon icon="mdi-close" color="primary" @click="emit('close')" size="x-large" />
</IconBtn>
</template>
</VCardItem>
<VDivider />

View File

@@ -58,23 +58,16 @@ const filteredSites = computed(() => {
<!-- Site Selection Dialog -->
<VDialog max-width="40rem" fullscreen-mobile>
<VCard class="site-dialog">
<VCardTitle class="d-flex align-center pa-4">
<span class="text-h6 font-weight-medium">{{ t('dialog.searchSite.selectSites') }}</span>
<VSpacer />
<VTextField
v-model="siteFilter"
:placeholder="t('dialog.searchSite.siteSearch')"
density="compact"
variant="outlined"
hide-details
class="ml-4"
style="max-inline-size: 200px"
prepend-inner-icon="mdi-magnify"
clearable
/>
</VCardTitle>
<VDivider class="search-divider" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-web-check" />
</template>
<VCardTitle>
{{ t('dialog.searchSite.selectSites') }}
</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText style="max-block-size: 420px" class="overflow-y-auto px-4 py-4">
<!-- 站点列表 -->
<div v-if="filteredSites.length > 0">
@@ -163,27 +156,16 @@ const filteredSites = computed(() => {
</div>
</VCardText>
<VDivider class="search-divider" />
<VCardActions class="pa-4">
<VCardActions class="pt-3">
<VSpacer />
<VBtn
color="grey-darken-1"
variant="text"
@click="emit('close')"
class="mr-2 d-flex align-center justify-center"
>
{{ t('dialog.searchSite.cancel') }}
</VBtn>
<VBtn
color="primary"
variant="flat"
:disabled="selectedSites.length === 0"
@click="emit('search', selectedSites)"
prepend-icon="mdi-magnify"
class="d-flex align-center justify-center px-5"
>
{{ t('dialog.searchSite.confirm') }}
{{ t('common.search') }}
</VBtn>
</VCardActions>
</VCard>

View File

@@ -148,11 +148,14 @@ onMounted(async () => {
<template>
<VDialog scrollable :close-on-back="false" eager max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}${t('site.title')}${
props.oper !== 'add' ? ` - ${siteForm.name}` : ''
}`"
>
<VCard>
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
<template #prepend>
<VIcon :icon="oper == 'add' ? 'mdi-web-plus' : 'mdi-web'" class="me-2" />
</template>
<VCardTitle>{{ `${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}` }}</VCardTitle>
<VCardSubtitle>{{ siteForm.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
@@ -165,16 +168,18 @@ onMounted(async () => {
:rules="[requiredValidator]"
:hint="t('site.hints.url')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol cols="6" md="3">
<VSelect
<VAutocomplete
v-model="siteForm.pri"
:label="t('site.fields.priority')"
:items="priorityItems"
:rules="[requiredValidator]"
:hint="t('site.hints.priority')"
persistent-hint
prepend-inner-icon="mdi-priority-high"
/>
</VCol>
<VCol cols="6" md="3">
@@ -184,6 +189,7 @@ onMounted(async () => {
:label="t('site.fields.status')"
:hint="t('site.hints.status')"
persistent-hint
prepend-inner-icon="mdi-toggle-switch"
/>
</VCol>
</VRow>
@@ -194,6 +200,7 @@ onMounted(async () => {
:label="t('site.fields.rss')"
:hint="t('site.hints.rss')"
persistent-hint
prepend-inner-icon="mdi-rss"
/>
</VCol>
<VCol cols="12" md="3">
@@ -202,15 +209,17 @@ onMounted(async () => {
:label="t('site.fields.timeout')"
:hint="t('site.hints.timeout')"
persistent-hint
prepend-inner-icon="mdi-timer"
/>
</VCol>
<VCol cols="6" md="3">
<VSelect
<VAutocomplete
v-model="siteForm.downloader"
:label="t('site.fields.downloader')"
:items="downloaderOptions"
:hint="t('site.hints.downloader')"
persistent-hint
prepend-inner-icon="mdi-download"
/>
</VCol>
</VRow>
@@ -237,6 +246,7 @@ onMounted(async () => {
:label="t('site.fields.cookie')"
:hint="t('site.hints.cookie')"
persistent-hint
prepend-inner-icon="mdi-cookie"
/>
</VCol>
<VCol cols="12">
@@ -245,6 +255,7 @@ onMounted(async () => {
:label="t('site.fields.userAgent')"
:hint="t('site.hints.userAgent')"
persistent-hint
prepend-inner-icon="mdi-web-box"
/>
</VCol>
</VRow>
@@ -257,6 +268,7 @@ onMounted(async () => {
:label="t('site.fields.authorization')"
:hint="t('site.hints.authorization')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -265,6 +277,7 @@ onMounted(async () => {
:label="t('site.fields.apiKey')"
:hint="t('site.hints.apiKey')"
persistent-hint
prepend-inner-icon="mdi-api"
/>
</VCol>
</VRow>
@@ -283,6 +296,7 @@ onMounted(async () => {
:rules="[numberValidator]"
:hint="t('site.hints.limitInterval')"
persistent-hint
prepend-inner-icon="mdi-clock-outline"
/>
</VCol>
<VCol cols="12" md="4">
@@ -292,6 +306,7 @@ onMounted(async () => {
:rules="[numberValidator]"
:hint="t('site.hints.limitCount')"
persistent-hint
prepend-inner-icon="mdi-counter"
/>
</VCol>
<VCol cols="12" md="4">
@@ -301,6 +316,7 @@ onMounted(async () => {
:rules="[numberValidator]"
:hint="t('site.hints.limitSeconds')"
persistent-hint
prepend-inner-icon="mdi-timer-sand"
/>
</VCol>
</VRow>
@@ -326,24 +342,10 @@ onMounted(async () => {
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
v-if="props.oper === 'add'"
color="primary"
variant="elevated"
@click="addSite"
prepend-icon="mdi-plus"
class="px-5"
>
<VBtn v-if="props.oper === 'add'" color="primary" @click="addSite" prepend-icon="mdi-plus" class="px-5">
{{ t('site.actions.add') }}
</VBtn>
<VBtn
v-else
color="primary"
variant="elevated"
@click="updateSiteInfo"
prepend-icon="mdi-content-save"
class="px-5"
>
<VBtn v-else color="primary" @click="updateSiteInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -71,7 +71,7 @@ async function updateSiteCookie() {
}
</script>
<template>
<VDialog max-width="30rem">
<VDialog max-width="30rem" scrollable>
<!-- Dialog Content -->
<VCard :title="t('dialog.siteCookieUpdate.title')">
<VDialogCloseBtn @click="emit('close')" />
@@ -102,7 +102,6 @@ async function updateSiteCookie() {
<VCardActions class="mx-auto">
<VBtn
size="large"
variant="elevated"
@click="updateSiteCookie"
:disabled="updateButtonDisable"
:loading="updateButtonDisable"

View File

@@ -134,7 +134,7 @@ onMounted(() => {
<VCard>
<!-- Toolbar -->
<div>
<VToolbar color="primary">
<VToolbar color="primary" density="comfortable">
<VToolbarTitle>{{ t('dialog.siteResource.browseTitle', { name: props.site?.name }) }}</VToolbarTitle>
<VSpacer />
<VToolbarItems>
@@ -153,6 +153,7 @@ onMounted(() => {
density="compact"
:label="t('dialog.siteResource.searchKeyword')"
clearable
prepend-inner-icon="mdi-magnify"
/>
</VCol>
<VCol cols="6" md="5">
@@ -165,10 +166,13 @@ onMounted(() => {
:label="t('dialog.siteResource.resourceCategory')"
multiple
clearable
prepend-inner-icon="mdi-folder"
/>
</VCol>
<VCol cols="12" md="2" class="text-center">
<VBtn block prepend-icon="mdi-magnify" @click="getResourceList">{{ t('dialog.siteResource.search') }}</VBtn>
<VBtn variant="tonal" block prepend-icon="mdi-magnify" @click="getResourceList">
{{ t('dialog.siteResource.search') }}
</VBtn>
</VCol>
</VRow>
</div>
@@ -186,7 +190,6 @@ onMounted(() => {
fixed-header
hover
:items-per-page-text="t('dialog.siteResource.itemsPerPage')"
:page-text="t('dialog.siteResource.pageText')"
:loading-text="t('dialog.siteResource.loading')"
class="h-full"
>

View File

@@ -4,7 +4,7 @@ import { numberValidator } from '@/@validators'
import api from '@/api'
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import { useI18n } from 'vue-i18n'
import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants'
// i18n
@@ -281,18 +281,24 @@ onMounted(() => {
<template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="
props.default
? t('dialog.subscribeEdit.titleDefault')
: t('dialog.subscribeEdit.titleEditFormat', {
name: subscribeForm.name,
season: subscribeForm.season
? t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season })
: '',
})
"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-clipboard-list-outline" class="me-2" />
</template>
<VCardTitle>
{{ props.default ? t('dialog.subscribeEdit.titleDefault') : t('dialog.subscribeEdit.titleEdit') }}
</VCardTitle>
<VCardSubtitle v-if="!props.default">
{{ subscribeForm.name }}
<span v-if="subscribeForm.season">
{{ t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) }}
</span>
</VCardSubtitle>
<VCardSubtitle v-else>
{{ props.type }}
</VCardSubtitle>
</VCardItem>
<VCardText>
<VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}">
@@ -314,6 +320,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.searchKeyword')"
:hint="t('dialog.subscribeEdit.searchKeywordHint')"
persistent-hint
prepend-inner-icon="mdi-magnify"
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
@@ -323,6 +330,7 @@ onMounted(() => {
:rules="[numberValidator]"
:hint="t('dialog.subscribeEdit.totalEpisodeHint')"
persistent-hint
prepend-inner-icon="mdi-playlist-play"
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
@@ -332,41 +340,45 @@ onMounted(() => {
:rules="[numberValidator]"
:hint="t('dialog.subscribeEdit.startEpisodeHint')"
persistent-hint
prepend-inner-icon="mdi-play-circle-outline"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="4">
<VSelect
<VAutocomplete
v-model="subscribeForm.quality"
:label="t('dialog.subscribeEdit.quality')"
:items="qualityOptions"
:hint="t('dialog.subscribeEdit.qualityHint')"
persistent-hint
prepend-inner-icon="mdi-quality-high"
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
<VAutocomplete
v-model="subscribeForm.resolution"
:label="t('dialog.subscribeEdit.resolution')"
:items="resolutionOptions"
:hint="t('dialog.subscribeEdit.resolutionHint')"
persistent-hint
prepend-inner-icon="mdi-monitor"
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
<VAutocomplete
v-model="subscribeForm.effect"
:label="t('dialog.subscribeEdit.effect')"
:items="effectOptions"
:hint="t('dialog.subscribeEdit.effectHint')"
persistent-hint
prepend-inner-icon="mdi-auto-fix"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="subscribeForm.sites"
:items="selectSitesOptions"
chips
@@ -375,17 +387,19 @@ onMounted(() => {
clearable
:hint="t('dialog.subscribeEdit.subscribeSitesHint')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSelect
<VAutocomplete
v-model="subscribeForm.downloader"
:items="downloaderOptions"
:label="t('dialog.subscribeEdit.downloader')"
:hint="t('dialog.subscribeEdit.downloaderHint')"
persistent-hint
prepend-inner-icon="mdi-download"
/>
</VCol>
<VCol cols="12" md="6">
@@ -395,6 +409,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.savePath')"
:hint="t('dialog.subscribeEdit.savePathHint')"
persistent-hint
prepend-inner-icon="mdi-folder"
/>
</VCol>
</VRow>
@@ -435,6 +450,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.include')"
:hint="t('dialog.subscribeEdit.includeHint')"
persistent-hint
prepend-inner-icon="mdi-plus-circle-outline"
/>
</VCol>
<VCol cols="12" md="6">
@@ -443,12 +459,13 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.exclude')"
:hint="t('dialog.subscribeEdit.excludeHint')"
persistent-hint
prepend-inner-icon="mdi-minus-circle-outline"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="subscribeForm.filter_groups"
:items="filterRuleGroupOptions"
chips
@@ -457,25 +474,28 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.filterGroups')"
:hint="t('dialog.subscribeEdit.filterGroupsHint')"
persistent-hint
prepend-inner-icon="mdi-filter"
/>
</VCol>
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
<VSelect
<VAutocomplete
v-model="subscribeForm.episode_group"
:items="episodeGroupOptions"
:item-props="episodeGroupItemProps"
:label="t('dialog.subscribeEdit.episodeGroup')"
:hint="t('dialog.subscribeEdit.episodeGroupHint')"
persistent-hint
prepend-inner-icon="mdi-view-list"
/>
</VCol>
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
<VSelect
<VAutocomplete
v-model="subscribeForm.season"
:items="seasonItems"
:label="t('dialog.subscribeEdit.season')"
:hint="t('dialog.subscribeEdit.seasonHint')"
persistent-hint
prepend-inner-icon="mdi-calendar"
/>
</VCol>
<VCol cols="12" v-if="!props.default">
@@ -484,6 +504,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.mediaCategory')"
:hint="t('dialog.subscribeEdit.mediaCategoryHint')"
persistent-hint
prepend-inner-icon="mdi-tag"
/>
</VCol>
</VRow>
@@ -495,6 +516,7 @@ onMounted(() => {
:hint="t('dialog.subscribeEdit.customWordsHint')"
persistent-hint
:placeholder="t('dialog.subscribeEdit.customWordsPlaceholder')"
prepend-inner-icon="mdi-text"
/>
</VCol>
</VRow>
@@ -504,12 +526,11 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" variant="outlined" class="me-3">
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" class="me-3">
{{ t('dialog.subscribeEdit.cancelSubscribe') }}
</VBtn>
<VSpacer />
<VBtn
variant="elevated"
@click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
prepend-icon="mdi-content-save"
class="px-5"

View File

@@ -23,6 +23,9 @@ const emit = defineEmits(['close'])
// 订阅文件信息
const subScribeInfo = ref<SubscrbieInfo>()
// 是否加载中
const loading = ref(false)
// 下载文件表头
const downloadHeaders = [
{ title: t('dialog.subscribeFiles.episodeColumn'), key: 'episode_number', sortable: true },
@@ -39,9 +42,12 @@ const libraryHeaders = [
// 调用API查询订阅文件信息
async function loadSubscribeFilesInfo() {
try {
loading.value = true
subScribeInfo.value = await api.get(`subscribe/files/${props.subid}`)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
@@ -84,7 +90,8 @@ onBeforeMount(() => {
<VCardItem class="my-2">
<VDialogCloseBtn @click="emit('close')" />
</VCardItem>
<VCardText>
<LoadingBanner v-if="loading" />
<VCardText v-else>
<div class="media-page">
<div class="media-header">
<div class="media-poster">

View File

@@ -56,11 +56,18 @@ const $toast = useToast()
<template>
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${t('dialog.subscribeShare.shareSubscription')} - ${props.sub?.name} ${
props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : ''
}`"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-share-outline" class="me-2" />
</template>
<VCardTitle>{{ t('dialog.subscribeShare.shareSubscription') }}</VCardTitle>
<VCardSubtitle>
{{ props.sub?.name }}
{{ props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : '' }}
</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}" class="pt-2">
@@ -72,6 +79,7 @@ const $toast = useToast()
:label="t('dialog.subscribeShare.title')"
:rules="[requiredValidator]"
persistent-hint
prepend-inner-icon="mdi-format-title"
/>
</VCol>
<VCol cols="12">
@@ -81,6 +89,7 @@ const $toast = useToast()
:rules="[requiredValidator]"
:hint="t('dialog.subscribeShare.descriptionHint')"
persistent-hint
prepend-inner-icon="mdi-comment-text-outline"
/>
</VCol>
<VCol cols="12">
@@ -90,6 +99,7 @@ const $toast = useToast()
:rules="[requiredValidator]"
:hint="t('dialog.subscribeShare.shareUserHint')"
persistent-hint
prepend-inner-icon="mdi-account-outline"
/>
</VCol>
</VRow>
@@ -97,14 +107,7 @@ const $toast = useToast()
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
variant="elevated"
:disabled="shareDoing"
@click="doShare"
prepend-icon="mdi-share"
class="px-5"
:loading="shareDoing"
>
<VBtn :disabled="shareDoing" @click="doShare" prepend-icon="mdi-share" class="px-5" :loading="shareDoing">
{{ t('dialog.subscribeShare.confirmShare') }}
</VBtn>
</VCardActions>

View File

@@ -2,6 +2,10 @@
import api from '@/api'
import QrcodeVue from 'qrcode.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -111,23 +115,34 @@ onUnmounted(() => {
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="t('dialog.u115Auth.loginTitle')">
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 rounded text-center p-3 border">
<VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.u115Auth.loginTitle') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText class="pt-2 flex flex-col items-center justify-center">
<div class="mt-6 rounded text-center p-3 border">
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
<div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
</div>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.u115Auth.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.u115Auth.complete') }}
</VBtn>
</VCardActions>

View File

@@ -290,12 +290,15 @@ onMounted(() => {
</script>
<template>
<VDialog scrollable :close-on-back="false" eager max-width="40rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit')}${
props.oper !== 'add' ? ` - ${userName}` : ''
}`"
>
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
<template #prepend>
<VIcon icon="mdi-account" class="me-2" />
</template>
<VCardTitle>{{ props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit') }}</VCardTitle>
<VCardSubtitle>{{ userName }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardItem>
@@ -350,6 +353,7 @@ onMounted(() => {
density="comfortable"
:readonly="props.oper !== 'add'"
:label="t('dialog.userAddEdit.username')"
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -359,6 +363,7 @@ onMounted(() => {
clearable
:label="t('dialog.userAddEdit.email')"
type="email"
prepend-inner-icon="mdi-email"
/>
</VCol>
<VCol cols="12" md="6">
@@ -370,6 +375,7 @@ onMounted(() => {
clearable
:label="t('dialog.userAddEdit.password')"
autocomplete=""
prepend-inner-icon="mdi-lock"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
@@ -382,6 +388,7 @@ onMounted(() => {
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable
:label="t('dialog.userAddEdit.confirmPassword')"
prepend-inner-icon="mdi-lock-check"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/>
</VCol>
@@ -392,6 +399,7 @@ onMounted(() => {
clearable
:label="t('dialog.userAddEdit.nickname')"
placeholder="显示昵称,优先于用户名显示"
prepend-inner-icon="mdi-card-account-details"
/>
</VCol>
<VCol cols="12" md="6" v-if="canControl">
@@ -402,6 +410,7 @@ onMounted(() => {
item-value="value"
:label="t('dialog.userAddEdit.status')"
dense
prepend-inner-icon="mdi-toggle-switch"
/>
</VCol>
</VRow>
@@ -415,6 +424,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.wechat')"
prepend-inner-icon="mdi-wechat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -423,6 +433,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.telegram')"
prepend-inner-icon="mdi-send"
/>
</VCol>
<VCol cols="12" md="6">
@@ -431,6 +442,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.slack')"
prepend-inner-icon="mdi-slack"
/>
</VCol>
<VCol cols="12" md="6">
@@ -439,6 +451,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.vocechat')"
prepend-inner-icon="mdi-chat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -447,10 +460,17 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.synologyChat')"
prepend-inner-icon="mdi-message"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="userForm.settings.douban_userid" density="comfortable" clearable label="豆瓣用户" />
<VTextField
v-model="userForm.settings.douban_userid"
density="comfortable"
clearable
label="豆瓣用户"
prepend-inner-icon="mdi-movie"
/>
</VCol>
</VRow>
</VForm>
@@ -461,7 +481,6 @@ onMounted(() => {
v-if="props.oper === 'add'"
:disabled="isAdding"
color="primary"
variant="elevated"
@click="addUser"
prepend-icon="mdi-plus"
class="px-5"
@@ -473,7 +492,6 @@ onMounted(() => {
v-else
:disabled="isUpdating"
color="primary"
variant="elevated"
@click="updateUser"
prepend-icon="mdi-content-save"
class="px-5"

View File

@@ -4,6 +4,7 @@ import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
@@ -133,9 +134,16 @@ onMounted(async () => {
</script>
<template>
<VDialog width="40rem" max-height="85vh">
<VCard :title="t('dialog.userAuth.title')">
<VDialogCloseBtn @click="emit('close')" />
<VDialog width="40rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-user-check" class="me-2" />
{{ t('dialog.userAuth.title') }}
</VCardTitle>
<VDialogCloseBtn @click="emit('close')" />
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
@@ -146,6 +154,7 @@ onMounted(async () => {
item-title="name"
:label="t('dialog.userAuth.selectSite')"
item-props
prepend-inner-icon="mdi-web"
>
</VSelect>
</VCol>
@@ -165,14 +174,7 @@ onMounted(async () => {
</VRow>
</VCardText>
<VCardText class="text-center">
<VBtn
variant="elevated"
@click="handleDone"
prepend-icon="mdi-check"
class="px-5"
size="large"
:disabled="loading"
>
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5" size="large" :disabled="loading">
{{ t('dialog.userAuth.authBtn') }}
</VBtn>
</VCardText>

View File

@@ -200,7 +200,7 @@ const isMacOS = computed(() => {
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
<VCard class="workflow-dialog">
<!-- Toolbar -->
<VToolbar color="primary">
<VToolbar color="primary" density="comfortable">
<VToolbarItems>
<VBtn icon @click="emit('close')" class="ms-3">
<VIcon size="large" color="white" icon="mdi-close" />

View File

@@ -86,7 +86,13 @@ async function editWorkflow() {
<template>
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="title">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-clock-outline" class="me-2" />
</template>
<VCardTitle>{{ title }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
@@ -99,6 +105,7 @@ async function editWorkflow() {
:rules="[requiredValidator]"
persistent-hint
:hint="t('dialog.workflowAddEdit.namePlaceholder')"
prepend-inner-icon="mdi-workflow"
/>
</VCol>
<VCol cols="12">
@@ -109,6 +116,7 @@ async function editWorkflow() {
placeholder="5位cron表达式"
persistent-hint
:hint="t('dialog.workflowAddEdit.cronExprDesc')"
prepend-inner-icon="mdi-clock-outline"
/>
</VCol>
<VCol cols="12">
@@ -116,6 +124,7 @@ async function editWorkflow() {
v-model="workflowForm.description"
:label="t('dialog.workflowAddEdit.desc')"
:placeholder="t('dialog.workflowAddEdit.descPlaceholder')"
prepend-inner-icon="mdi-text-box-outline"
/>
</VCol>
</VRow>
@@ -123,18 +132,10 @@ async function editWorkflow() {
</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 v-if="workflow" color="primary" @click="editWorkflow" prepend-icon="mdi-content-save" class="px-5">
{{ t('dialog.workflowAddEdit.confirm') }}
</VBtn>
<VBtn v-else block color="primary" variant="elevated" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
<VBtn v-else color="primary" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
{{ t('dialog.workflowAddEdit.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { AxiosRequestConfig } from 'axios'
import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import { useToast } from 'vue-toast-notification'
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters'
@@ -599,9 +599,7 @@ onMounted(() => {
</IconBtn>
</span>
</div>
<VCardText v-if="loading" class="text-center flex flex-col items-center">
<VProgressCircular size="48" indeterminate color="primary" />
</VCardText>
<LoadingBanner v-if="loading" />
<!-- 文件详情 -->
<VCardText v-else-if="isFile && !isImage && items.length > 0" class="text-center break-all">
<div v-if="items[0]?.thumbnail" class="flex justify-center">
@@ -696,13 +694,24 @@ onMounted(() => {
</VCard>
<!-- 重命名弹窗 -->
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem">
<VCard :title="t('file.rename')">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-pencil" class="me-2" />
</template>
<VCardTitle>{{ t('file.rename') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="renamePopper = false" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="newName" :label="t('file.newName')" :loading="renameLoading" />
<VTextField
v-model="newName"
:label="t('file.newName')"
:loading="renameLoading"
prepend-inner-icon="mdi-format-text"
/>
</VCol>
<VCol cols="12" v-if="currentItem && currentItem.type == 'dir'">
<VSwitch v-model="renameAll" :label="t('file.includeSubfolders')" />
@@ -710,10 +719,10 @@ onMounted(() => {
</VRow>
</VCardText>
<VCardActions>
<VBtn color="success" variant="elevated" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
<VBtn color="success" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
{{ t('file.autoRecognizeName') }}
</VBtn>
<VBtn :disabled="!newName" variant="elevated" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
<VBtn :disabled="!newName" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('common.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -165,21 +165,28 @@ const sortIcon = computed(() => {
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
<!-- 新建文件夹 -->
<VDialog v-model="newFolderPopper" max-width="35rem">
<template #activator="{ props }">
<IconBtn>
<VIcon v-bind="props" icon="mdi-folder-plus-outline" />
</IconBtn>
</template>
<VCard :title="t('file.newFolder')">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-folder-plus-outline" class="me-2" />
</template>
<VCardTitle>{{ t('file.newFolder') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="newFolderPopper = false" />
<VDivider />
<VCardText>
<VTextField v-model="newFolderName" :label="t('common.name')" />
<VTextField v-model="newFolderName" :label="t('common.name')" prepend-inner-icon="mdi-format-text" />
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
<VBtn :disabled="!newFolderName" @click="mkdir" prepend-icon="mdi-folder-plus" class="px-5 me-3">
{{ t('common.create') }}
</VBtn>
</VCardActions>

View File

@@ -32,7 +32,7 @@ async function loadDownloaderSetting() {
})),
]
} catch (error) {
console.error('加载下载器设置失败:', error)
console.error(error)
}
}
@@ -69,7 +69,7 @@ onMounted(() => {
<VTextField
v-model="data.labels"
:label="t('workflow.addDownload.category')"
placeholder="多个使用,分隔"
:placeholder="t('workflow.addDownload.categoryPlaceholder')"
outlined
dense
/>
@@ -80,7 +80,7 @@ onMounted(() => {
storage="local"
:label="t('workflow.addDownload.savePath')"
clearable
placeholder="留空自动"
:placeholder="t('workflow.addDownload.savePathPlaceholder')"
/>
</VCol>
<VCol cols="12">

View File

@@ -100,7 +100,7 @@ const sourceTypeOptions = [
]
// 计算下拉框
const sourceOptions = computed(() => innerList.map(item => item.name))
const sourceOptions = computed(() => innerList.map(item => ({ value: item.api_path, title: item.name })))
onMounted(() => {
loadExtraRecommendSources()

View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
import api from '@/api'
import { Handle, Position } from '@vue-flow/core'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
})
interface ActionItem {
id: string
name: string
}
interface PluginAction {
plugin_id: string
plugin_name: string
actions: ActionItem[]
}
// 插件所有动作
const pluginActions = ref<PluginAction[]>([])
// 插件选项
const pluginOptions = computed(() => {
return pluginActions.value.map((item: PluginAction) => ({
title: item.plugin_name,
value: item.plugin_id,
}))
})
// 动作选项
const actionOptions = computed(() => {
return pluginActions.value
.find((item: PluginAction) => item.plugin_id === props.data.plugin_id)
?.actions.map((item: ActionItem) => ({
title: item.name,
value: item.id,
}))
})
// 用于在文本框显示和保存时转换action_params
const actionParamsText = computed({
get: () => {
try {
return typeof props.data.action_params === 'object'
? JSON.stringify(props.data.action_params, null, 2)
: props.data.action_params || ''
} catch (error) {
console.error(error)
return ''
}
},
set: (value: string) => {
try {
props.data.action_params = value ? JSON.parse(value) : {}
} catch (error) {
// 如果JSON解析失败保留原始文本
props.data.action_params = value
console.error(error)
}
},
})
// 加载动作选项
async function loadPluginActions() {
try {
pluginActions.value = await api.get('workflow/plugin/actions')
} catch (error) {
console.error(error)
}
}
onMounted(() => {
loadPluginActions()
})
</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-run" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>{{ t('workflow.invokePlugin.title') }}</VCardTitle>
<VCardSubtitle>{{ t('workflow.invokePlugin.subtitle') }}</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect
v-model="data.plugin_id"
:items="pluginOptions"
:label="t('workflow.invokePlugin.plugin')"
outlined
dense
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="data.action_id"
:items="actionOptions"
:label="t('workflow.invokePlugin.actionid')"
outlined
dense
/>
</VCol>
<VCol cols="12">
<VTextarea v-model="actionParamsText" :label="t('workflow.invokePlugin.actionParams')" outlined dense />
</VCol>
</VRow>
</VCardText>
<Handle id="edge_out" type="source" :position="Position.Right" />
</VCard>
</div>
</template>

View File

@@ -0,0 +1,88 @@
import { ref } from 'vue'
import { createApp } from 'vue'
import i18n from '@/plugins/i18n'
import vuetify from '@/plugins/vuetify'
import ConfirmDialog from '@/@core/components/ConfirmDialog.vue'
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
interface ConfirmOptions {
type?: 'info' | 'warn' | 'error'
title?: string
content?: string
confirmText?: string
cancelText?: string
width?: string | number
}
let resolvePromise: ((value: boolean) => void) | null = null
// 创建确认对话框实例
async function createConfirmDialog(options: ConfirmOptions = {}) {
return new Promise<boolean>(resolve => {
resolvePromise = resolve
// 创建容器
const container = document.createElement('div')
document.body.appendChild(container)
// 处理国际化
const i18nOptions = {
...options,
title: options.title || i18n.global.t('common.confirm'),
confirmText: options.confirmText || i18n.global.t('common.confirm'),
cancelText: options.cancelText || i18n.global.t('common.cancel'),
}
// 创建应用实例
const app = createApp(ConfirmDialog, {
modelValue: true,
...i18nOptions,
'onUpdate:modelValue': (val: boolean) => {
if (!val) {
cleanup()
}
},
onConfirm: () => {
resolvePromise?.(true)
cleanup()
},
onCancel: () => {
resolvePromise?.(false)
cleanup()
},
})
// 注册必要的组件
app.component('VDialogCloseBtn', DialogCloseBtn)
// 使用插件
app.use(vuetify)
app.use(i18n)
// 挂载应用
app.mount(container)
// 清理函数
const cleanup = () => {
app.unmount()
document.body.removeChild(container)
}
})
}
// 创建一个函数对象,同时支持直接调用和解构
const confirmFunction = Object.assign(createConfirmDialog, {
createConfirm: createConfirmDialog,
})
// 导出 useConfirm 函数
export function useConfirm() {
return confirmFunction
}
// 插件
export default {
install: (app: any) => {
app.provide('confirm', { createConfirm: createConfirmDialog })
},
}

View File

@@ -183,8 +183,8 @@ const showDynamicButton = computed(() => {
.footer-nav-card {
position: relative;
overflow: hidden;
backdrop-filter: blur(12px);
background-color: rgba(var(--v-theme-surface), 0.8);
backdrop-filter: blur(16px);
background-color: rgba(var(--v-theme-surface), 0.6);
pointer-events: auto;
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);

View File

@@ -203,7 +203,13 @@ onMounted(() => {
</VCard>
</VMenu>
<!-- 名称测试弹窗 -->
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="35rem" scrollable>
<VDialog
v-if="nameTestDialog"
v-model="nameTestDialog"
max-width="45rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -219,7 +225,13 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 网络测试弹窗 -->
<VDialog v-if="netTestDialog" v-model="netTestDialog" max-width="35rem" max-height="85vh" scrollable>
<VDialog
v-if="netTestDialog"
v-model="netTestDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -258,12 +270,18 @@ onMounted(() => {
</VCardItem>
<VDivider />
<VCardText>
<LoggingView />
<LoggingView logfile="moviepilot.log" />
</VCardText>
</VCard>
</VDialog>
<!-- 过滤规则弹窗 -->
<VDialog v-if="ruleTestDialog" v-model="ruleTestDialog" max-width="35rem" scrollable>
<VDialog
v-if="ruleTestDialog"
v-model="ruleTestDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -279,7 +297,13 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 系统健康检查弹窗 -->
<VDialog v-if="systemTestDialog" v-model="systemTestDialog" max-width="35rem" scrollable>
<VDialog
v-if="systemTestDialog"
v-model="systemTestDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -298,7 +322,7 @@ onMounted(() => {
<VDialog
v-if="messageDialog"
v-model="messageDialog"
max-width="40rem"
max-width="50rem"
scrollable
:fullscreen="!display.mdAndUp.value"
ref="messageDialogRef"

View File

@@ -13,6 +13,7 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
import { saveLocalTheme } from '@/@core/utils/theme'
import type { ThemeSwitcherTheme } from '@layouts/types'
import { useConfirm } from '@/composables/useConfirm'
// 认证 Store
const authStore = useAuthStore()
@@ -32,9 +33,6 @@ const progressDialog = ref(false)
// 站点认证对话框
const siteAuthDialog = ref(false)
// 重启确认对话框
const restartDialog = ref(false)
// 自定义CSS弹窗
const cssDialog = ref(false)
@@ -47,6 +45,9 @@ const showLanguageMenu = ref(false)
// 自定义CSS
const customCSS = ref('')
// 确认框
const { createConfirm } = useConfirm()
// 执行注销操作
function logout() {
// 清除登录状态信息
@@ -57,7 +58,6 @@ function logout() {
// 执行重启操作
async function restart() {
restartDialog.value = false
// 调用API重启
try {
// 显示等待框
@@ -79,7 +79,15 @@ async function restart() {
// 显示重启确认对话框
async function showRestartDialog() {
restartDialog.value = true
const isConfirmed = await createConfirm({
type: 'warn',
title: t('app.confirmRestart'),
content: t('app.restartTip'),
})
if (!isConfirmed) return
await restart()
}
// 显示站点认证对话框
@@ -417,32 +425,6 @@ onMounted(() => {
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
<!-- 用户认证对话框 -->
<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="d-flex align-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-weight-bold text-xl text-high-emphasis">{{ t('app.confirmRestart') }}</p>
<p>{{ t('app.restartTip') }}</p>
</div>
</div>
</VCardItem>
<VCardActions class="mx-auto">
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">{{
t('common.cancel')
}}</VBtn>
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5">{{
t('common.confirm')
}}</VBtn>
</VCardActions>
<VDialogCloseBtn @click="restartDialog = false" />
</VCard>
</VDialog>
<!-- 自定义 CSS -->
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>

View File

@@ -71,6 +71,7 @@ function getActionIcon(type: string): string {
'SendEventAction': 'mdi-send-check',
'SendMessageAction': 'mdi-message-arrow-right',
'TransferFileAction': 'mdi-file-move',
'InvokePluginAction': 'mdi-run',
}
return iconMap[type] || 'mdi-puzzle-outline'

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: 'Unsubscribe',
media: 'Media',
unknown: 'Unknown',
notice: 'Notice',
itemsPerPage: 'Items per page',
pageText: '{0}-{1} of {2}',
noDataText: 'No data',
loadingText: 'Loading...',
},
mediaType: {
movie: 'Movie',
@@ -71,6 +76,7 @@ export default {
sendEvent: 'Send Event',
sendMessage: 'Send Message',
transferFile: 'Transfer File',
invokePlugin: 'Invoke Plugin',
},
qualityOptions: {
all: 'All',
@@ -200,6 +206,10 @@ export default {
title: 'Services',
description: 'Scheduled jobs',
},
cache: {
title: 'Cache',
description: 'Torrent cache, media recognition data cache, image file cache management',
},
notification: {
title: 'Notifications',
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
@@ -278,6 +288,9 @@ export default {
nameHint: 'Name of notification channel',
type: 'Type',
typeHint: 'Type of notification channel',
customTypeHint: 'Custom notification type, used for plugin implementation scenarios',
customTypePlaceholder: 'custom',
nameRequired: 'Please enter name',
enabled: 'Enabled',
config: 'Configuration',
wechat: {
@@ -427,6 +440,8 @@ export default {
forceResume: 'Force Resume',
firstLastPiece: 'First Last Piece',
onlyLack: 'Only Download Lack Resource',
categoryPlaceholder: 'Use comma to separate multiple',
savePathPlaceholder: 'Leave empty for auto',
},
addSubscribe: {
title: 'Add Subscribe',
@@ -534,6 +549,14 @@ export default {
exclude: 'Exclude (Keywords, Regex)',
ruleGroups: 'Filter Rule Groups',
},
invokePlugin: {
title: 'Invoke Plugin',
subtitle: 'Call plugin to perform specific actions',
plugin: 'Plugin',
actionid: 'Action ID',
actionParams: 'Action Parameters',
loadPluginSettingFailed: 'Failed to load plugin settings',
},
title: 'Workflow',
noWorkflow: 'No Workflow',
noWorkflowDescription: 'Click the add button to create a workflow task.',
@@ -838,8 +861,8 @@ export default {
browserSimulation: 'Use browser simulation for authentic site access',
},
actions: {
add: 'Add',
edit: 'Edit',
add: 'Add Site',
edit: 'Edit Site',
},
messages: {
addSuccess: 'Site added successfully',
@@ -1011,6 +1034,8 @@ export default {
scrapFollowTmdb: 'Follow TMDB Recognition',
scrapFollowTmdbHint:
'When turned off, organization history will be used (if available) to avoid TMDB data changes during subscription',
scrapOriginalImage: 'Scrap TheMovieDb Original Language Image',
scrapOriginalImageHint: 'Scrap original language image from themoviedb, otherwise scrap metadata language image',
fanartEnable: 'Fanart Image Data Source',
fanartEnableHint: 'Use image data from fanart.tv',
githubProxy: 'Github Acceleration Proxy',
@@ -1067,13 +1092,28 @@ export default {
bing: 'Bing Daily Wallpaper',
mediaserver: 'Media Server',
none: 'No Wallpaper',
customize: 'Customize',
},
mb: 'MB',
hour: 'hour',
customizeWallpaperApi: 'Customize Wallpaper Api',
customizeWallpaperApiHint:
'It will get the image file extension format images that are allowed in settings in the content returned by the API.',
customizeWallpaperApiRequired: 'Required field; please enter Wallpaper API',
securityImageDomains: 'Security Image Domains',
securityImageDomainsHint: 'Allowed image domains whitelist for caching, used to control trusted image sources',
noSecurityImageDomains: 'No security domains',
securityImageDomainAdd: 'Add domain, e.g.: image.tmdb.org',
proxyHost: 'Proxy Server',
proxyHostHint: 'Set proxy server address, support: http(s), socks5, socks5h, etc.',
moviePilotAutoUpdate: 'Auto Update MoviePilot',
moviePilotAutoUpdateHint: 'Automatically update MoviePilot to the latest release version when restarting',
autoUpdateResource: 'Auto Update Resource',
autoUpdateResourceHint: 'Automatically detect and update site resource package when restarting',
},
site: {
siteSync: 'Site Synchronization',
siteSyncDesc: 'Quickly sync site data from CookieCloud.',
siteSyncDesc: 'Quickly sync site data from CookieCloud',
enableLocalCookieCloud: 'Enable Local CookieCloud Server',
enableLocalCookieCloudHint:
'Use built-in CookieCloud service to sync site data, service address: http://localhost:3000/cookiecloud',
@@ -1119,7 +1159,7 @@ export default {
},
notification: {
channels: 'Notification Channels',
channelsDesc: 'Set message sending channel parameters.',
channelsDesc: 'Set message sending channel parameters',
organizeSuccess: 'Media Import',
downloadAdded: 'Download Added',
subscribeAdded: 'Subscribe Added',
@@ -1162,10 +1202,11 @@ export default {
synologyChat: 'SynologyChat',
voceChat: 'VoceChat',
webPush: 'WebPush',
custom: 'Custom Notification',
},
words: {
customIdentifiers: 'Custom Identifiers',
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification.',
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification',
identifiersPlaceholder: 'Support regular expressions, special characters need \\ escape, one line for each rule',
identifiersHint: 'Support regular expressions, special characters need \\ escape, one line for each rule',
formatTitle: 'Supported configuration formats (mind the spaces):',
@@ -1174,7 +1215,7 @@ export default {
'Word to replace => Replacement\n' +
'Front word <> Back word >> Episode offset (EP)\n' +
'Word to replace => Replacement && Front word <> Back word >> Episode offset (EP)\n' +
'Replacement format supports: &#123;tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx&#125; to directly specify TMDBID/Douban ID, where s and e are season and episode numbers (optional)',
'Replacement format supports: &#123;[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]&#125; to directly specify TMDBID/Douban ID, where s and e are season and episode numbers (optional)',
identifierSaveSuccess: 'Custom identifiers saved successfully',
identifierSaveFailed: 'Failed to save custom identifiers!',
@@ -1205,7 +1246,7 @@ export default {
},
search: {
basicSettings: 'Basic Settings',
basicSettingsDesc: 'Set data sources, rule groups and other basic information.',
basicSettingsDesc: 'Set data sources, rule groups and other basic information',
recognizeSource: 'Recognition Data Source',
recognizeSourceDesc:
'Default is TMDB. Douban is usually more friendly for Chinese works, but some foreign works have incomplete information.',
@@ -1306,8 +1347,7 @@ export default {
},
scheduler: {
title: 'Scheduled Jobs',
subtitle:
"Includes built-in system services and plugin services. Manual execution will not affect the job's normal schedule.",
subtitle: 'Includes built-in system services and plugin services',
provider: 'Provider',
taskName: 'Task Name',
taskStatus: 'Task Status',
@@ -1354,6 +1394,55 @@ export default {
settingsSaveSuccess: 'Subscription basic settings saved successfully',
settingsSaveFailed: 'Failed to save subscription basic settings!',
},
cache: {
title: 'Cache Management',
subtitle: 'Manage torrent cache data',
filterByTitle: 'Filter by Title',
filterBySite: 'Filter by Site',
selectSite: 'Select Site',
refresh: 'Refresh Cache',
deleteSelected: 'Delete Selected',
clearAll: 'Clear All Cache',
refreshSuccess: 'Cache refresh completed',
refreshFailed: 'Failed to refresh cache',
clearSuccess: 'Cache clear completed',
clearFailed: 'Failed to clear cache',
deleteSuccess: 'Cache item deleted successfully',
deleteFailed: 'Failed to delete cache item',
deleteSelectedSuccess: 'Successfully deleted {count} cache items',
deleteSelectedFailed: 'Failed to delete cache items',
loadFailed: 'Failed to load cache data',
selectDeleteWarning: 'Please select cache items to delete',
reidentify: 'Re-identify',
reidentifySuccess: 'Re-identification completed',
reidentifyFailed: 'Re-identification failed',
poster: 'Poster',
torrentTitle: 'Title',
site: 'Site',
size: 'Size',
publishTime: 'Publish Time',
recognitionResult: 'Recognition Result',
actions: 'Actions',
unrecognized: 'Unrecognized',
noData: 'No cache data',
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
reidentifyDialog: {
title: 'Re-identify',
torrentInfo: 'Torrent Info',
tmdbId: 'TMDB ID',
tmdbIdHint: 'Optional, manually specify TMDB ID for recognition',
doubanId: 'Douban ID',
doubanIdHint: 'Optional, manually specify Douban ID for recognition',
autoHint: 'If no ID is specified, the torrent will be automatically re-identified',
cancel: 'Cancel',
confirm: 'Re-identify',
},
mediaType: {
movie: 'Movie',
tv: 'TV Show',
},
clearConfirm: 'Are you sure you want to clear all cache?',
},
},
dialog: {
progress: {
@@ -1611,7 +1700,7 @@ export default {
title: 'Plugin Market Settings',
repoUrl: 'Plugin Repository URL',
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: 'Multiple URLs separated by commas, only Github repositories are supported',
repoHint: 'Multiple URLs separated by lines, only Github repositories are supported',
close: 'Close',
save: 'Save',
saveSuccess: 'Plugin repository saved successfully',
@@ -1677,8 +1766,8 @@ export default {
previous: 'Previous',
confirm: 'Confirm',
manualTitle: 'Manual Organization',
multipleItemsTitle: 'Organize - {count} Items',
singleItemTitle: 'Organize - {path}',
multipleItemsTitle: '{count} Items',
singleItemTitle: '{path}',
targetStorage: 'Target Storage',
targetStorageHint: 'Organization target storage',
transferType: 'Organization Method',
@@ -1727,7 +1816,7 @@ export default {
},
subscribeEdit: {
titleDefault: 'Default Subscription Rules',
titleEditFormat: 'Edit Subscription - {name} {season}',
titleEdit: 'Edit Subscription',
seasonFormat: 'Season {number}',
tabs: {
basic: 'Basic',
@@ -1840,6 +1929,7 @@ export default {
peersColumn: 'Peers',
viewDetails: 'View Details',
downloadTorrent: 'Download Torrent',
pageText: '{0}-{1} of {2} items',
},
forkSubscribe: {
title: 'Copy Subscription',
@@ -1972,6 +2062,54 @@ export default {
updateHistoryTitle: '{name} Update History',
updateToLatest: 'Update to Latest Version',
updatingTo: 'Updating {name} to v{version} ...',
folderNameEmpty: 'Folder name cannot be empty',
folderExists: 'Folder already exists',
folderCreateSuccess: 'Folder created successfully',
folderRenameSuccess: 'Folder renamed successfully',
folderRenameFailed: 'Failed to rename folder',
folderDeleteSuccess: 'Folder deleted successfully',
folderDeleteFailed: 'Failed to delete folder',
removeFromFolderSuccess: 'Plugin removed from folder',
operationFailed: 'Operation failed',
saveFolderConfigFailed: 'Failed to save folder config',
newFolder: 'New Folder',
folderName: 'Folder Name',
cancel: 'Cancel',
create: 'Create',
clone: 'Clone',
cloneTitle: 'Create Plugin Clone',
cloneSubtitle: 'Create an independent clone instance for {name}',
cloneFeature: 'Plugin Clone Feature',
cloneDescription:
'Create an independent copy of the plugin with separate configuration and data, suitable for multi-account, testing environments, etc.',
suffix: 'Clone Suffix',
suffixPlaceholder: 'e.g.: Test, Backup, Site1',
suffixHint: 'Unique identifier to distinguish clones, only letters and numbers allowed',
suffixRequired: 'Clone suffix cannot be empty',
suffixFormatError: 'Only letters and numbers allowed',
suffixLengthError: 'Length cannot exceed 20 characters',
cloneName: 'Clone Name',
cloneNamePlaceholder: 'e.g.: Auto Backup Test Version',
cloneNameHint: 'Display name for the clone plugin (optional)',
cloneDefaultName: '{name} Clone',
cloneDescriptionLabel: 'Clone Description',
cloneDescriptionPlaceholder: 'Describe the purpose and features of this clone...',
cloneDescriptionHint: 'Detailed description of the clone plugin purpose (optional)',
cloneDefaultDescription: '{description} (Clone Version)',
cloneVersion: 'Version',
cloneVersionPlaceholder: 'e.g.: 1.0, 2.1.0',
cloneVersionHint: 'Custom version number for the clone plugin (optional)',
cloneIcon: 'Icon URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: 'Custom icon for the clone plugin (optional)',
cloneNotice:
'Clone plugins are disabled by default after creation and need to be manually configured and enabled. The clone suffix cannot be modified once set.',
createClone: 'Create Clone',
cloning: 'Creating clone for {name}...',
cloneSuccess: 'Plugin clone {name} created successfully!',
cloneFailed: 'Plugin clone creation failed: {message}',
cloneFailedGeneral: 'Plugin clone creation failed',
logTitle: 'Plugin Logging',
},
profile: {
personalInfo: 'Personal Information',
@@ -2354,4 +2492,23 @@ export default {
required: 'This field is required',
number: 'Please enter a number',
},
folder: {
settingAppearance: 'Appearance Settings',
rename: 'Rename',
deleteFolder: 'Delete Folder',
folderNameCannotBeEmpty: 'Folder name cannot be empty',
confirmDeleteFolder:
'Are you sure you want to delete folder "{folderName}"? Plugins in this folder will be moved back to the main list.',
folderSettingsSaved: 'Folder settings saved',
renameFolder: 'Rename Folder',
folderName: 'Folder Name',
folderAppearanceSettings: 'Folder Appearance Settings',
showFolderIcon: 'Show Folder Icon',
icon: 'Icon',
iconColor: 'Icon Color',
backgroundGradient: 'Background Gradient',
customBackgroundImageURL: 'Custom Background Image URL (Optional)',
customBackgroundImageHint: 'Supports web image URLs, leave blank for gradient background',
pluginCount: '{count} Plugins',
},
}

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: '取消订阅',
media: '媒体',
unknown: '未知',
notice: '注意',
itemsPerPage: '每页条数',
pageText: '{0}-{1} 共 {2} 条',
noDataText: '没有数据',
loadingText: '加载中...',
},
mediaType: {
movie: '电影',
@@ -71,6 +76,7 @@ export default {
sendEvent: '发送事件',
sendMessage: '发送消息',
transferFile: '整理文件',
invokePlugin: '调用插件',
},
qualityOptions: {
all: '全部',
@@ -200,6 +206,10 @@ export default {
title: '服务',
description: '定时作业',
},
cache: {
title: '缓存',
description: '种子缓存、图片文件缓存管理',
},
notification: {
title: '通知',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息发送范围',
@@ -277,6 +287,9 @@ export default {
nameHint: '通知渠道名称',
type: '类型',
typeHint: '通知渠道类型',
customTypeHint: '自定义通知类型,用于插件实现场景',
customTypePlaceholder: 'custom',
nameRequired: '请输入名称',
enabled: '启用',
config: '配置',
wechat: {
@@ -425,6 +438,8 @@ export default {
forceResume: '强制继续',
firstLastPiece: '优先首尾文件',
onlyLack: '仅下载缺失资源',
categoryPlaceholder: '多个使用,分隔',
savePathPlaceholder: '留空自动',
},
addSubscribe: {
title: '添加订阅',
@@ -532,6 +547,14 @@ export default {
exclude: '排除(关键字、正则式)',
ruleGroups: '过滤规则组',
},
invokePlugin: {
title: '调用插件',
subtitle: '调用插件执行特定操作',
plugin: '插件',
actionid: '动作ID',
actionParams: '动作参数',
loadPluginSettingFailed: '加载插件设置失败',
},
title: '工作流',
noWorkflow: '没有工作流',
noWorkflowDescription: '点击添加按钮创建工作流任务。',
@@ -835,8 +858,8 @@ export default {
browserSimulation: '使用浏览器模拟真实访问该站点',
},
actions: {
add: '新增',
edit: '编辑',
add: '新增站点',
edit: '编辑站点',
},
messages: {
addSuccess: '新增站点成功',
@@ -922,7 +945,7 @@ export default {
system: {
custom: '自定义',
basicSettings: '基础设置',
basicSettingsDesc: '设置服务器的全局功能',
basicSettingsDesc: '设置服务器的全局功能',
appDomain: '访问域名',
appDomainHint: '用于发送通知时,添加快捷跳转地址',
wallpaper: '背景壁纸',
@@ -1005,6 +1028,8 @@ export default {
metaCacheExpireMin: '元数据缓存时间必须大于等于0',
scrapFollowTmdb: '跟随TMDB识别整理',
scrapFollowTmdbHint: '关闭时以整理历史记录为准如有避免TMDB数据在订阅中途修改',
scrapOriginalImage: 'TMDB 刮削原语种图片',
scrapOriginalImageHint: '刮削原语种图片,否则刮削元数据语种图片',
fanartEnable: 'Fanart图片数据源',
fanartEnableHint: '使用 fanart.tv 的图片数据',
githubProxy: 'Github加速代理',
@@ -1058,13 +1083,27 @@ export default {
bing: 'Bing每日壁纸',
mediaserver: '媒体服务器',
none: '无壁纸',
customize: '自定义',
},
mb: 'MB',
hour: '小时',
customizeWallpaperApi: '自定义壁纸API地址',
customizeWallpaperApiHint: '会获取API返回内容中所有允许的安全域名地址的图片需要同步设置安全域名地址',
customizeWallpaperApiRequired: '必填项请输入自定义壁纸API',
securityImageDomains: '安全图片域名',
securityImageDomainsHint: '允许缓存的图片域名白名单,用于控制可信任的图片来源',
noSecurityImageDomains: '暂无安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服务器',
proxyHostHint: '设置代理服务器地址支持http(s)、socks5、socks5h 等协议',
moviePilotAutoUpdate: '自动更新MoviePilot',
moviePilotAutoUpdateHint: '重启时自动更新MoviePilot到最新发行版本',
autoUpdateResource: '自动更新站点资源',
autoUpdateResourceHint: '重启时自动检测和更新站点资源包',
},
site: {
siteSync: '站点同步',
siteSyncDesc: '从CookieCloud快速同步站点数据',
siteSyncDesc: '从CookieCloud快速同步站点数据',
enableLocalCookieCloud: '启用本地CookieCloud服务器',
enableLocalCookieCloudHint: '使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud',
serviceAddress: '服务地址',
@@ -1107,7 +1146,7 @@ export default {
},
notification: {
channels: '通知渠道',
channelsDesc: '设置消息发送渠道参数',
channelsDesc: '设置消息发送渠道参数',
organizeSuccess: '资源入库',
downloadAdded: '资源下载',
subscribeAdded: '添加订阅',
@@ -1150,10 +1189,11 @@ export default {
synologyChat: 'SynologyChat',
voceChat: 'VoceChat',
webPush: 'WebPush',
custom: '自定义通知',
},
words: {
customIdentifiers: '自定义识别词',
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
identifiersPlaceholder: '支持正则表达式,特殊字符需要\\转义,一行为一组',
identifiersHint: '支持正则表达式,特殊字符需要\\转义,一行为一组',
formatTitle: '支持的配置格式(注意空格):',
@@ -1162,7 +1202,7 @@ export default {
'被替换词 => 替换词\n' +
'前定位词 <> 后定位词 >> 集偏移量EP\n' +
'被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n' +
'其中替换词支持格式:&#123;tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx&#125; 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选',
'其中替换词支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选',
identifierSaveSuccess: '自定义识别词保存成功',
identifierSaveFailed: '自定义识别词保存失败!',
@@ -1189,7 +1229,7 @@ export default {
},
search: {
basicSettings: '基础设置',
basicSettingsDesc: '设定数据源、规则组等基础信息',
basicSettingsDesc: '设定数据源、规则组等基础信息',
recognizeSource: '识别数据源',
recognizeSourceDesc: '默认使用TMDB。豆瓣识别中文作品通常更友好但有些国外作品信息不完整。',
themoviedb: 'TheMovieDb',
@@ -1225,7 +1265,7 @@ export default {
},
directory: {
storage: '存储',
storageDesc: '设置本地或网盘存储',
storageDesc: '设置本地或网盘存储',
directory: '目录',
mediaType: '媒体类型',
directoryDesc: '设置媒体文件整理目录结构,按先后顺序依次匹配。',
@@ -1287,7 +1327,7 @@ export default {
},
scheduler: {
title: '定时作业',
subtitle: '包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。',
subtitle: '包含系统内置服务以及插件提供的服务',
provider: '提供者',
taskName: '任务名称',
taskStatus: '任务状态',
@@ -1334,6 +1374,55 @@ export default {
settingsSaveSuccess: '订阅基础设置保存成功',
settingsSaveFailed: '订阅基础设置保存失败!',
},
cache: {
title: '缓存管理',
subtitle: '管理缓存的站点资源',
filterByTitle: '按标题筛选',
filterBySite: '按站点筛选',
selectSite: '选择站点',
refresh: '刷新缓存',
deleteSelected: '删除选中',
clearAll: '清空缓存',
refreshSuccess: '缓存刷新完成',
refreshFailed: '刷新缓存失败',
clearSuccess: '缓存清理完成',
clearFailed: '清理缓存失败',
deleteSuccess: '缓存项删除成功',
deleteFailed: '删除缓存项失败',
deleteSelectedSuccess: '成功删除 {count} 个缓存项',
deleteSelectedFailed: '删除缓存项失败',
loadFailed: '加载缓存数据失败',
selectDeleteWarning: '请选择要删除的缓存项',
reidentify: '重新识别',
reidentifySuccess: '重新识别完成',
reidentifyFailed: '重新识别失败',
poster: '海报',
torrentTitle: '标题',
site: '站点',
size: '大小',
publishTime: '发布时间',
recognitionResult: '识别结果',
actions: '操作',
unrecognized: '未识别',
noData: '暂无缓存数据',
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
reidentifyDialog: {
title: '重新识别',
torrentInfo: '种子信息',
tmdbId: 'TMDB ID',
tmdbIdHint: '可选手动指定TMDB ID进行识别',
doubanId: '豆瓣 ID',
doubanIdHint: '可选手动指定豆瓣ID进行识别',
autoHint: '如果不指定ID将自动重新识别该种子',
cancel: '取消',
confirm: '重新识别',
},
mediaType: {
movie: '电影',
tv: '电视剧',
},
clearConfirm: '确认清空所有缓存吗?',
},
},
dialog: {
progress: {
@@ -1491,7 +1580,7 @@ export default {
loginTypeOptions: {
guest: '访客',
username: '用户名密码',
token: 'Token',
token: '令牌',
},
complete: '完成',
reset: '重置',
@@ -1589,7 +1678,7 @@ export default {
title: '插件市场设置',
repoUrl: '插件仓库地址',
repoPlaceholder: '格式https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: '多个地址使用逗号分隔仅支持Github仓库',
repoHint: '多个地址使用换行分隔仅支持Github仓库',
close: '关闭',
save: '保存',
saveSuccess: '插件仓库保存成功',
@@ -1655,8 +1744,8 @@ export default {
previous: '上一步',
confirm: '确认',
manualTitle: '手动整理',
multipleItemsTitle: '整理 - 共 {count} 项',
singleItemTitle: '整理 - {path}',
multipleItemsTitle: '共 {count} 项',
singleItemTitle: '{path}',
targetStorage: '目的存储',
targetStorageHint: '整理目的存储',
transferType: '整理方式',
@@ -1705,7 +1794,7 @@ export default {
},
subscribeEdit: {
titleDefault: '默认订阅规则',
titleEditFormat: '编辑订阅 - {name} {season}',
titleEdit: '编辑订阅',
seasonFormat: '第 {number} 季',
tabs: {
basic: '基础',
@@ -1818,6 +1907,7 @@ export default {
peersColumn: '下载',
viewDetails: '查看详情',
downloadTorrent: '下载种子文件',
pageText: '{0}-{1} 共 {2} 条',
},
forkSubscribe: {
title: '复制订阅',
@@ -1949,6 +2039,52 @@ export default {
updateHistoryTitle: '{name} 更新说明',
updateToLatest: '更新到最新版本',
updatingTo: '更新 {name} 到 {version} 版本...',
folderNameEmpty: '文件夹名称不能为空',
folderExists: '文件夹已存在',
folderCreateSuccess: '文件夹创建成功',
folderRenameSuccess: '文件夹重命名成功',
folderRenameFailed: '重命名文件夹失败',
folderDeleteSuccess: '文件夹删除成功',
folderDeleteFailed: '删除文件夹失败',
removeFromFolderSuccess: '插件已移出文件夹',
operationFailed: '操作失败',
saveFolderConfigFailed: '保存文件夹配置失败',
newFolder: '新建文件夹',
folderName: '文件夹名称',
cancel: '取消',
create: '创建',
clone: '分身',
cloneTitle: '创建插件分身',
cloneSubtitle: '为 {name} 创建独立的分身实例',
cloneFeature: '插件分身功能',
cloneDescription: '创建插件的独立副本,拥有独立的配置和数据,适用于多账号、测试环境等场景',
suffix: '分身后缀',
suffixPlaceholder: '例如Test、Backup、Site1',
suffixHint: '用于区分分身的唯一标识,只能包含英文字母和数字',
suffixRequired: '分身后缀不能为空',
suffixFormatError: '只能包含英文字母和数字',
suffixLengthError: '长度不能超过20个字符',
cloneName: '分身名称',
cloneNamePlaceholder: '例如:自动备份 测试版',
cloneNameHint: '分身插件的显示名称(可选)',
cloneDefaultName: '{name} 分身',
cloneDescriptionLabel: '分身描述',
cloneDescriptionPlaceholder: '描述这个分身的用途和特点...',
cloneDescriptionHint: '详细描述分身插件的用途(可选)',
cloneDefaultDescription: '{description} (分身版本)',
cloneVersion: '版本号',
cloneVersionPlaceholder: '例如1.0、2.1.0',
cloneVersionHint: '自定义分身插件的版本号(可选)',
cloneIcon: '图标URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: '自定义分身插件的图标(可选)',
cloneNotice: '分身插件创建后默认为禁用状态,需要手动配置启用。分身后缀一旦确定无法修改。',
createClone: '创建分身',
cloning: '正在创建 {name} 的分身...',
cloneSuccess: '插件分身 {name} 创建成功!',
cloneFailed: '插件分身创建失败:{message}',
cloneFailedGeneral: '插件分身创建失败',
logTitle: '插件日志',
},
profile: {
personalInfo: '个人信息',
@@ -2330,4 +2466,22 @@ export default {
required: '此项为必填项',
number: '请输入数字',
},
folder: {
settingAppearance: '设置外观',
rename: '重命名',
deleteFolder: '删除文件夹',
folderNameCannotBeEmpty: '文件夹名称不能为空',
confirmDeleteFolder: '确定要删除文件夹 "{folderName}" 吗?文件夹中的插件将移回主列表。',
folderSettingsSaved: '文件夹设置已保存',
renameFolder: '重命名文件夹',
folderName: '文件夹名称',
folderAppearanceSettings: '文件夹外观设置',
showFolderIcon: '显示文件夹图标',
icon: '图标',
iconColor: '图标颜色',
backgroundGradient: '背景渐变',
customBackgroundImageURL: '自定义背景图片URL可选',
customBackgroundImageHint: '支持网络图片URL留空则使用渐变背景',
pluginCount: '{count} 个插件',
},
}

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: '取消訂閱',
media: '媒體',
unknown: '未知',
notice: '注意',
itemsPerPage: '每頁條數',
pageText: '{0}-{1} 共 {2} 條',
noDataText: '沒有數據',
loadingText: '加載中...',
},
mediaType: {
movie: '電影',
@@ -71,6 +76,7 @@ export default {
sendEvent: '發送事件',
sendMessage: '發送消息',
transferFile: '整理文件',
invokePlugin: '調用插件',
},
qualityOptions: {
all: '全部',
@@ -201,6 +207,10 @@ export default {
title: '服務',
description: '定時作業',
},
cache: {
title: '緩存',
description: '種子緩存、識別媒體數據緩存、圖片文件緩存管理',
},
notification: {
title: '通知',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息發送範圍',
@@ -278,6 +288,9 @@ export default {
nameHint: '通知渠道名稱',
type: '類型',
typeHint: '通知渠道類型',
customTypeHint: '自定義通知類型,用於插件實現場景',
customTypePlaceholder: 'custom',
nameRequired: '請輸入名稱',
enabled: '啟用',
config: '配置',
wechat: {
@@ -426,6 +439,8 @@ export default {
forceResume: '强制继续',
firstLastPiece: '优先首尾文件',
onlyLack: '仅下载缺失资源',
categoryPlaceholder: '多個使用,分隔',
savePathPlaceholder: '留空自動',
},
addSubscribe: {
title: '添加订阅',
@@ -533,6 +548,14 @@ export default {
exclude: '排除(關鍵字、正則式)',
ruleGroups: '過濾規則組',
},
invokePlugin: {
title: '調用插件',
subtitle: '調用插件執行特定操作',
plugin: '插件',
actionid: '動作ID',
actionParams: '動作參數',
loadPluginSettingFailed: '加載插件設置失敗',
},
title: '工作流',
noWorkflow: '沒有工作流',
noWorkflowDescription: '點擊添加按鈕創建工作流任務。',
@@ -837,8 +860,8 @@ export default {
browserSimulation: '使用瀏覽器模擬真實訪問該站點',
},
actions: {
add: '新增',
edit: '編輯',
add: '新增站點',
edit: '編輯站點',
},
messages: {
addSuccess: '新增站點成功',
@@ -924,7 +947,7 @@ export default {
system: {
custom: '自定義',
basicSettings: '基礎設置',
basicSettingsDesc: '設置服務器的全局功能',
basicSettingsDesc: '設置服務器的全局功能',
appDomain: '訪問域名',
appDomainHint: '用於發送通知時,添加快捷跳轉地址',
wallpaper: '背景壁紙',
@@ -1007,6 +1030,8 @@ export default {
metaCacheExpireMin: '元數據緩存時間必須大於等於0',
scrapFollowTmdb: '跟隨TMDB識別整理',
scrapFollowTmdbHint: '關閉時以整理歷史記錄為準如有避免TMDB數據在訂閱中途修改',
scrapOriginalImage: 'TMDB 刮削原語种圖片',
scrapOriginalImageHint: '刮削原語种圖片,否则數據元数据語种圖片',
fanartEnable: 'Fanart圖片數據源',
fanartEnableHint: '使用 fanart.tv 的圖片數據',
githubProxy: 'Github加速代理',
@@ -1060,13 +1085,27 @@ export default {
bing: 'Bing每日壁紙',
mediaserver: '媒體服務器',
none: '無壁紙',
customize: '自定義',
},
mb: 'MB',
hour: '小時',
customizeWallpaperApi: '自定義壁紙API',
customizeWallpaperApiHint: '會獲取 API 返回內容中所有安全設置中允許的圖片地址,需要設置安全域名白名單',
customizeWallpaperApiRequired: '必填項請輸出自定義壁紙API',
securityImageDomains: '安全圖片域名',
securityImageDomainsHint: '允許緩存的圖片域名白名單,用於控制可信任的圖片來源',
noSecurityImageDomains: '暫無安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服務器',
proxyHostHint: '設置代理服務器地址支持http(s)、socks5、socks5h 等協議',
moviePilotAutoUpdate: '自動更新MoviePilot',
moviePilotAutoUpdateHint: '重啟時自動更新MoviePilot到最新發行版本',
autoUpdateResource: '自動更新站點資源',
autoUpdateResourceHint: '重啟時自動檢測和更新站點資源包',
},
site: {
siteSync: '站點同步',
siteSyncDesc: '從CookieCloud快速同步站點數據',
siteSyncDesc: '從CookieCloud快速同步站點數據',
enableLocalCookieCloud: '啟用本地CookieCloud服務器',
enableLocalCookieCloudHint: '使用內建CookieCloud服務同步站點數據服務地址為http://localhost:3000/cookiecloud',
serviceAddress: '服務地址',
@@ -1109,7 +1148,7 @@ export default {
},
notification: {
channels: '通知渠道',
channelsDesc: '設置消息發送渠道參數',
channelsDesc: '設置消息發送渠道參數',
organizeSuccess: '資源入庫',
downloadAdded: '資源下載',
subscribeAdded: '添加訂閱',
@@ -1152,10 +1191,11 @@ export default {
synologyChat: 'SynologyChat',
voceChat: 'VoceChat',
webPush: 'WebPush',
custom: '自定義通知',
},
words: {
customIdentifiers: '自定義識別詞',
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
identifiersPlaceholder: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
identifiersHint: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
formatTitle: '支持的配置格式(注意空格):',
@@ -1164,7 +1204,7 @@ export default {
'被替換詞 => 替換詞\n' +
'前定位詞 <> 後定位詞 >> 集偏移量EP\n' +
'被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量EP\n' +
'其中替換詞支持格式:&#123;tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx&#125; 直接指定TMDBID/豆瓣ID識別其中s、e為季數和集數可選',
'其中替換詞支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID識別其中s、e為季數和集數可選',
identifierSaveSuccess: '自定義識別詞保存成功',
identifierSaveFailed: '自定義識別詞保存失敗!',
@@ -1191,7 +1231,7 @@ export default {
},
search: {
basicSettings: '基礎設置',
basicSettingsDesc: '設定數據源、規則組等基礎信息',
basicSettingsDesc: '設定數據源、規則組等基礎信息',
recognizeSource: '識別數據源',
recognizeSourceDesc: '默認使用TMDB。豆瓣識別中文作品通常更友好但有些國外作品信息不完整。',
themoviedb: 'TheMovieDb',
@@ -1227,7 +1267,7 @@ export default {
},
directory: {
storage: '存儲',
storageDesc: '設置本地或網盤存儲',
storageDesc: '設置本地或網盤存儲',
directory: '目錄',
directoryDesc: '設置媒體文件整理目錄結構,按先後順序依次匹配。',
organizeAndScrap: '整理 & 刮削',
@@ -1288,7 +1328,7 @@ export default {
},
scheduler: {
scheduledTasks: '定時作業',
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務,手動執行不會影響作業正常的時間表。',
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務',
provider: '提供者',
taskName: '任務名稱',
taskStatus: '任務狀態',
@@ -1335,6 +1375,56 @@ export default {
settingsSaveSuccess: '訂閱基礎設置保存成功',
settingsSaveFailed: '訂閱基礎設置保存失敗!',
},
cache: {
title: '緩存',
description: '種子緩存、圖片文件緩存管理',
subtitle: '管理緩存的站點資源',
filterByTitle: '按標題篩選',
filterBySite: '按站點篩選',
selectSite: '選擇站點',
refresh: '刷新緩存',
deleteSelected: '刪除選中',
clearAll: '清空緩存',
refreshSuccess: '緩存刷新完成',
refreshFailed: '刷新緩存失敗',
clearSuccess: '緩存清理完成',
clearFailed: '清理緩存失敗',
deleteSuccess: '緩存項刪除成功',
deleteFailed: '刪除緩存項失敗',
deleteSelectedSuccess: '成功刪除 {count} 個緩存項',
deleteSelectedFailed: '刪除緩存項失敗',
loadFailed: '加載緩存數據失敗',
selectDeleteWarning: '請選擇要刪除的緩存項',
reidentify: '重新識別',
reidentifySuccess: '重新識別完成',
reidentifyFailed: '重新識別失敗',
poster: '海報',
torrentTitle: '標題',
site: '站點',
size: '大小',
publishTime: '發布時間',
recognitionResult: '識別結果',
actions: '操作',
unrecognized: '未識別',
noData: '暫無緩存數據',
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
reidentifyDialog: {
title: '重新識別',
torrentInfo: '種子信息',
tmdbId: 'TMDB ID',
tmdbIdHint: '可選手動指定TMDB ID進行識別',
doubanId: '豆瓣 ID',
doubanIdHint: '可選手動指定豆瓣ID進行識別',
autoHint: '如果不指定ID將自動重新識別該種子',
cancel: '取消',
confirm: '重新識別',
},
mediaType: {
movie: '電影',
tv: '電視劇',
},
clearConfirm: '確認清空所有緩存嗎?',
},
},
dialog: {
progress: {
@@ -1590,7 +1680,7 @@ export default {
title: '插件市場設置',
repoUrl: '插件倉庫地址',
repoPlaceholder: '格式https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: '多個地址使用逗號分隔僅支援Github倉庫',
repoHint: '多個地址使用换行分隔僅支援Github倉庫',
close: '關閉',
save: '儲存',
saveSuccess: '插件倉庫儲存成功',
@@ -1656,8 +1746,8 @@ export default {
previous: '上一步',
confirm: '確認',
manualTitle: '手動整理',
multipleItemsTitle: '整理 - 共 {count} 項',
singleItemTitle: '整理 - {path}',
multipleItemsTitle: '共 {count} 項',
singleItemTitle: '{path}',
targetStorage: '目的存儲',
targetStorageHint: '整理目的存儲',
transferType: '整理方式',
@@ -1706,7 +1796,7 @@ export default {
},
subscribeEdit: {
titleDefault: '默認訂閱規則',
titleEditFormat: '編輯訂閱 - {name} {season}',
titleEdit: '編輯訂閱',
seasonFormat: '第 {number} 季',
tabs: {
basic: '基礎',
@@ -1951,6 +2041,52 @@ export default {
updateHistoryTitle: '{name} 更新說明',
updateToLatest: '更新到最新版本',
updatingTo: '正在更新 {name} 至 v{version} ...',
folderNameEmpty: '文件夾名稱不能為空',
folderExists: '文件夾已存在',
folderCreateSuccess: '文件夾創建成功',
folderRenameSuccess: '文件夾重命名成功',
folderRenameFailed: '重命名文件夾失敗',
folderDeleteSuccess: '文件夾刪除成功',
folderDeleteFailed: '刪除文件夾失敗',
removeFromFolderSuccess: '插件已移出文件夾',
operationFailed: '操作失敗',
saveFolderConfigFailed: '保存文件夾配置失敗',
newFolder: '新建文件夾',
folderName: '文件夾名稱',
cancel: '取消',
create: '創建',
clone: '分身',
cloneTitle: '創建插件分身',
cloneSubtitle: '為 {name} 創建獨立的分身實例',
cloneFeature: '插件分身功能',
cloneDescription: '創建插件的獨立副本,擁有獨立的配置和數據,適用於多賬號、測試環境等場景',
suffix: '分身後綴',
suffixPlaceholder: '例如Test、Backup、Site1',
suffixHint: '用於區分分身的唯一標識,只能包含英文字母和數字',
suffixRequired: '分身後綴不能為空',
suffixFormatError: '只能包含英文字母和數字',
suffixLengthError: '長度不能超過20個字符',
cloneName: '分身名稱',
cloneNamePlaceholder: '例如:自動備份 測試版',
cloneNameHint: '分身插件的顯示名稱(可選)',
cloneDefaultName: '{name} 分身',
cloneDescriptionLabel: '分身描述',
cloneDescriptionPlaceholder: '描述這個分身的用途和特點...',
cloneDescriptionHint: '詳細描述分身插件的用途(可選)',
cloneDefaultDescription: '{description} (分身版本)',
cloneVersion: '版本號',
cloneVersionPlaceholder: '例如1.0、2.1.0',
cloneVersionHint: '自定義分身插件的版本號(可選)',
cloneIcon: '圖標URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: '自定義分身插件的圖標(可選)',
cloneNotice: '分身插件創建後默認為禁用狀態,需要手動配置啟用。分身後綴一旦確定無法修改。',
createClone: '創建分身',
cloning: '正在創建 {name} 的分身...',
cloneSuccess: '插件分身 {name} 創建成功!',
cloneFailed: '插件分身創建失敗:{message}',
cloneFailedGeneral: '插件分身創建失敗',
logTitle: '插件日誌',
},
profile: {
personalInfo: '個人信息',
@@ -2332,4 +2468,22 @@ export default {
required: '此項為必填項',
number: '請輸入數字',
},
folder: {
settingAppearance: '設定外觀',
rename: '重新命名',
deleteFolder: '刪除資料夾',
folderNameCannotBeEmpty: '資料夾名稱不能為空',
confirmDeleteFolder: '確定要刪除資料夾 "{folderName}" 嗎?資料夾中的插件將移回主列表。',
folderSettingsSaved: '資料夾設定已儲存',
renameFolder: '重新命名資料夾',
folderName: '資料夾名稱',
folderAppearanceSettings: '資料夾外觀設定',
showFolderIcon: '顯示資料夾圖示',
icon: '圖示',
iconColor: '圖示顏色',
backgroundGradient: '背景漸變',
customBackgroundImageURL: '自定義背景圖片URL可選',
customBackgroundImageHint: '支援網路圖片URL留空則使用漸變背景',
pluginCount: '{count} 個插件',
},
}

View File

@@ -24,7 +24,7 @@ import { fetchGlobalSettings } from './utils/globalSetting'
// 5. 其他插件和功能模块
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import ConfirmDialog from '@/composables/useConfirm'
import VueApexCharts from 'vue3-apexcharts'
// 6. 注册自定义组件
@@ -102,26 +102,7 @@ initializeApp().then(() => {
.use(ToastPlugin, {
position: 'bottom-right',
})
.use(VuetifyUseDialog, {
confirmDialog: {
dialogProps: {
maxWidth: '30rem',
},
confirmationButtonProps: {
variant: 'elevated',
color: 'primary',
class: 'me-3 px-5',
'prepend-icon': 'mdi-check',
},
cancellationButtonProps: {
variant: 'outlined',
color: 'secondary',
class: 'me-3',
},
confirmationText: i18n.global.t('common.confirm'),
cancellationText: i18n.global.t('common.cancel'),
},
})
.use(ConfirmDialog)
.use(i18n)
.mount('#app')
})

View File

@@ -1,24 +1,28 @@
<script setup lang="ts">
import PersonCardListView from '@/views/discover/PersonCardListView.vue'
// 输入参数
const props = defineProps({
// API路径
paths: Array as PropType<string[]> | PropType<string>,
})
// 路由参数
const route = useRoute()
const id = route.query?.id?.toString()
const title = route.query?.title?.toString()
const source = route.query?.source?.toString()
const type = route.query?.type?.toString()
const apipath = route.query?.apipath?.toString()
// 标题
let title = route.query?.title?.toString()
// 计算API路径
function getApiPath(paths: string[] | string) {
if (Array.isArray(paths)) return paths.join('/')
else return paths
}
</script>
<template>
<div>
<VPageContentTitle :title="title" />
<PersonCardListView
:credits-id="id"
:credits-name="title"
:credits-source="source"
:credits-type="type"
:credits-apipath="apipath"
/>
<PersonCardListView :apipath="getApiPath(props.paths || '')" />
</div>
</template>

View File

@@ -8,6 +8,7 @@ import DashboardElement from '@/components/misc/DashboardElement.vue'
import { useDisplay } from 'vuetify'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
import { VCardActions } from 'vuetify/components'
// 国际化
const { t } = useI18n()
@@ -353,7 +354,7 @@ onDeactivated(() => {
/>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" scrollable>
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" :fullscreen="!display.mdAndUp.value" scrollable>
<VCard>
<VCardItem>
<VCardTitle>
@@ -396,8 +397,7 @@ onDeactivated(() => {
<VSwitch v-model="isElevated" :label="t('dashboard.adaptiveHeight')" />
</p>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VCardActions class="pt-3">
<VSpacer />
<VBtn @click="saveDashboardConfig">
<template #prepend>
@@ -405,7 +405,7 @@ onDeactivated(() => {
</template>
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -8,6 +8,9 @@ import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
import { DiscoverSource } from '@/api/types'
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -179,7 +182,13 @@ onActivated(async () => {
</VWindowItem>
</VWindow>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-if="orderConfigDialog" v-model="orderConfigDialog" max-width="35rem" scrollable>
<VDialog
v-if="orderConfigDialog"
v-model="orderConfigDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -199,16 +208,15 @@ onActivated(async () => {
:component-data="{ 'class': 'settings-grid' }"
>
<template #item="{ element }">
<div class="setting-item enabled">
<VCard variant="text" class="setting-item enabled">
<div class="setting-item-inner cursor-move text-center">
<span class="setting-label">{{ element.name }}</span>
</div>
</div>
</VCard>
</template>
</draggable>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VCardActions class="pt-3">
<VSpacer />
<VBtn @click="saveTabOrder">
<template #prepend>
@@ -216,7 +224,7 @@ onActivated(async () => {
</template>
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCardActions>
</VCard>
</VDialog>
<!-- 快速滚动到顶部按钮 -->
@@ -261,6 +269,7 @@ onActivated(async () => {
&::before {
position: absolute;
background-color: transparent;
background-color: rgb(var(--v-theme-primary));
block-size: 100%;
content: '';
inline-size: 4px;

View File

@@ -10,6 +10,7 @@ import logo from '@images/logo.png'
import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
import { useTheme } from 'vuetify'
// 国际化
const { t } = useI18n()
@@ -42,12 +43,24 @@ const usernameInput = ref()
// 语言选择菜单
const langMenu = ref(false)
// 当前语言
const currentLocale = ref(getCurrentLocale())
// 当前主题
const vuetifyTheme = useTheme()
// 判断是否为透明主题
const isTransparentTheme = computed(() => {
return vuetifyTheme.name.value === 'transparent'
})
// 可用的语言列表
const locales = Object.values(SUPPORTED_LOCALES)
// 登录按钮 loading
const loading = ref(false)
// 切换语言
async function switchLanguage(locale: SupportedLocale) {
await setI18nLanguage(locale)
@@ -103,6 +116,8 @@ async function afterLogin(superuser: boolean) {
router.push(authStore.originalPath ?? '/')
// 订阅推送通知
if (superuser) await subscribeForPushNotifications()
// 登录按钮 loading
loading.value = false
}
// 登录获取token事件
@@ -113,6 +128,10 @@ function login() {
if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) {
return
}
// 登录按钮 loading
loading.value = true
// 用户名密码
const formData = new FormData()
@@ -155,6 +174,8 @@ function login() {
else if (error.response.status === 403) errorMessage.value = t('login.permissionDenied')
else if (error.response.status === 500) errorMessage.value = t('login.serverError')
else errorMessage.value = `${t('login.loginFailed')} ${error.response.status}${t('login.checkCredentials')}`
// 登录按钮 loading
loading.value = false
})
}
@@ -176,7 +197,12 @@ onMounted(async () => {
<div class="relative flex min-h-screen flex-col items-center justify-center">
<!-- 登录表单 -->
<div class="auth-wrapper d-flex align-center justify-center">
<VCard class="auth-card px-7 py-3 w-full h-full" max-width="24rem" border>
<VCard
class="auth-card px-7 py-3 w-full h-full"
:class="{ 'glass-effect': !isTransparentTheme }"
max-width="24rem"
border
>
<VCardItem class="justify-center">
<template #prepend>
<div class="d-flex pe-0">
@@ -252,7 +278,9 @@ onMounted(async () => {
</VCol>
<VCol cols="12">
<!-- login button -->
<VBtn block type="submit" @click="login" prepend-icon="mdi-login"> {{ t('login.login') }} </VBtn>
<VBtn block type="submit" @click="login" prepend-icon="mdi-login" :loading="loading">
{{ t('login.login') }}
</VBtn>
<VAlert v-if="errorMessage" type="error" variant="tonal" class="mt-3">
{{ errorMessage }}
</VAlert>
@@ -282,4 +310,9 @@ onMounted(async () => {
inset-block-start: 8px;
inset-inline-end: 8px;
}
.glass-effect {
backdrop-filter: blur(10px) !important;
background: rgba(var(--v-theme-surface), 0.7) !important;
}
</style>

View File

@@ -14,12 +14,6 @@ const mediaid = route.query?.mediaid?.toString()
// 类型:电影、电视剧
const type = route.query?.type?.toString()
// 媒体信息来源TMDB、豆瓣
const source = route.query?.source?.toString() || 'themoviedb'
// TMDB ID
const page = route.query?.page?.toString() || '1'
// 标题
const title = route.query?.title?.toString()
@@ -29,6 +23,6 @@ const year = route.query?.year?.toString()
<template>
<div>
<MediaDetailView :mediaid="mediaid" :type="type" :source="source" :page="page" :title="title" :year="year" />
<MediaDetailView :mediaid="mediaid" :type="type" :title="title" :year="year" />
</div>
</template>

View File

@@ -3,6 +3,9 @@ import api from '@/api'
import { RecommendSource } from '@/api/types'
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -115,14 +118,16 @@ 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,
type: source.type,
})),
)
extraRecommendSources.value.map(source => {
if (!viewList.some(item => item.apipath === source.api_path)) {
viewList.push({
apipath: source.api_path,
linkurl: `/browse/${source.api_path}&title=${source.name}`,
title: source.name,
type: source.type,
})
}
})
}
} catch (error) {
console.log(error)
@@ -233,7 +238,7 @@ onActivated(async () => {
</div>
<!-- 设置面板 -->
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable>
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable :fullscreen="!display.mdAndUp.value">
<VCard class="settings-card">
<VCardItem class="settings-card-header">
<VCardTitle>
@@ -246,7 +251,7 @@ onActivated(async () => {
<VCardText>
<p class="settings-hint">{{ t('recommend.selectContentToDisplay') }}</p>
<div class="settings-grid">
<div
<VCard
v-for="item in viewList"
:key="item.title"
class="setting-item"
@@ -266,11 +271,10 @@ onActivated(async () => {
</div>
<span class="setting-label">{{ item.title }}</span>
</div>
</div>
</VCard>
</div>
</VCardText>
<VDivider />
<VCardActions class="pt-5">
<VCardActions class="pt-3">
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = true))">
{{ t('recommend.selectAll') }}
</VBtn>
@@ -278,7 +282,7 @@ onActivated(async () => {
{{ t('recommend.selectNone') }}
</VBtn>
<VSpacer />
<VBtn @click="saveConfig" variant="elevated" color="primary" class="px-5">
<VBtn @click="saveConfig" color="primary" class="px-5">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { debounce } from 'lodash-es'
import NoDataFound from '@/components/NoDataFound.vue'
import api from '@/api'
import type { Context } from '@/api/types'
@@ -51,6 +52,9 @@ const progressText = ref('')
// 加载进度
const progressValue = ref(0)
// 进度是否有效
const progressEnabled = ref(false)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
@@ -60,23 +64,30 @@ const errorTitle = ref(t('resource.noData'))
// 错误描述
const errorDescription = ref(t('resource.noResourceFound'))
// 添加安全超时,确保进度条不会永远卡住
const watchProgressValue = watch(
progressValue,
debounce(async () => {
if (progressEventSource.value && progressValue.value < 100) {
console.warn('卡进度超时 关闭进度条')
stopLoadingProgress()
}
}, 60_000),
)
// 使用SSE监听加载进度
function startLoadingProgress() {
watchProgressValue.resume()
progressText.value = t('resource.searching')
progressValue.value = 10 // 初始进度设为10%,确保进度条显示
progressValue.value = 0
progressEnabled.value = false
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/search`)
progressEventSource.value.onmessage = event => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
// 搜索完成条件调整:只有明确完成时才关闭
if (progress.text.includes('完成') && progress.value >= 99) {
setTimeout(() => {
stopLoadingProgress()
}, 1000) // 延迟1秒关闭确保用户能看到100%
}
progressEnabled.value = progress.enable
}
}
@@ -86,26 +97,22 @@ function startLoadingProgress() {
stopLoadingProgress()
}, 1000)
}
// 添加安全超时,确保不会永远卡住
setTimeout(() => {
if (progressEventSource.value && progressValue.value < 100) {
stopLoadingProgress()
}
}, 60000) // 60秒超时
}
// 停止监听加载进度
function stopLoadingProgress() {
watchProgressValue.pause()
if (progressEventSource.value) {
progressEventSource.value.close()
progressEventSource.value = undefined
// 确保进度显示100%,然后再渐进清零
progressValue.value = 100
setTimeout(() => {
progressValue.value = 0
progressEnabled.value = false
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
}
// 确保进度显示100%,然后再渐进清零
progressValue.value = 100
setTimeout(() => {
progressValue.value = 0
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
}
// 设置视图类型
@@ -186,7 +193,7 @@ onUnmounted(() => {
<div>
<!-- 加载进度条 -->
<VFadeTransition>
<div v-if="progressValue > 0" class="search-progress-container">
<div v-if="progressValue > 0 || progressEnabled" class="search-progress-container">
<VCard elevation="3" class="search-progress-card">
<div class="progress-header">
<VIcon icon="mdi-movie-search" color="primary" size="small" class="me-2" />
@@ -273,17 +280,7 @@ onUnmounted(() => {
</div>
<!-- 初始加载状态 -->
<div v-else-if="!isRefreshed && !progressValue" class="initial-loading-container">
<div class="initial-loading-content">
<div class="wave-loader">
<div class="wave-dot"></div>
<div class="wave-dot"></div>
<div class="wave-dot"></div>
<div class="wave-dot"></div>
</div>
<div class="initial-loading-text">{{ t('resource.searching') }}</div>
</div>
</div>
<LoadingBanner v-else-if="!isRefreshed && !(progressEnabled || progressValue > 0)" />
<!-- 滚动到顶部按钮 -->
<VScrollToTopBtn />
</div>
@@ -452,70 +449,6 @@ onUnmounted(() => {
letter-spacing: 1px;
}
/* 初始的加载状态 */
.initial-loading-container {
display: flex;
align-items: center;
justify-content: center;
min-block-size: 50vh;
}
.initial-loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.wave-loader {
display: flex;
align-items: center;
block-size: 40px;
gap: 6px;
}
.wave-dot {
border-radius: 50%;
animation: wave 1.5s ease-in-out infinite;
background-color: rgb(var(--v-theme-primary));
block-size: 8px;
inline-size: 8px;
}
.wave-dot:nth-child(1) {
animation-delay: 0s;
}
.wave-dot:nth-child(2) {
animation-delay: 0.2s;
}
.wave-dot:nth-child(3) {
animation-delay: 0.4s;
}
.wave-dot:nth-child(4) {
animation-delay: 0.6s;
}
@keyframes wave {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
}
.initial-loading-text {
color: rgb(var(--v-theme-primary));
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 1px;
}
.search-results-container {
position: relative;
min-block-size: 50vh;

View File

@@ -8,9 +8,10 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingScheduler from '@/views/setting/AccountSettingScheduler.vue'
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingCache from '@/views/setting/AccountSettingCache.vue'
import { getSettingTabs } from '@/router/i18n-menu'
const route = useRoute()
@@ -81,7 +82,16 @@ const settingTabs = computed(() => getSettingTabs())
<VWindowItem value="scheduler">
<transition name="fade-slide" appear>
<div>
<AccountSettingScheduler />
<AccountSettingService />
</div>
</transition>
</VWindowItem>
<!-- 缓存 -->
<VWindowItem value="cache">
<transition name="fade-slide" appear>
<div>
<AccountSettingCache />
</div>
</transition>
</VWindowItem>

View File

@@ -39,7 +39,7 @@ export function getBrowserLocale(): SupportedLocale | null {
return navigatorLocale.includes(locale.split('-')[0])
})
return (locale as SupportedLocale) || null
return (locale as SupportedLocale) || 'zh-CN'
}
/**

View File

@@ -170,6 +170,12 @@ export function getSettingTabs() {
tab: 'scheduler',
description: t('settingTabs.scheduler.description'),
},
{
title: t('settingTabs.cache.title'),
icon: 'mdi-database',
tab: 'cache',
description: t('settingTabs.cache.description'),
},
{
title: t('settingTabs.notification.title'),
icon: 'mdi-bell',

View File

@@ -58,6 +58,10 @@ html.v-overlay-scroll-blocked {
margin-block-start: env(safe-area-inset-top);
}
.v-dialog > .v-overlay__content > .v-card > .v-card-item {
padding: 16px;
}
/* router view transition fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
@@ -115,12 +119,14 @@ html.v-overlay-scroll-blocked {
// 美化滚动条
::-webkit-scrollbar {
block-size: 8px;
inline-size: 8px;
block-size: 4px;
inline-size: 4px;
opacity: 0;
transition: opacity 0.3s;
}
::-webkit-scrollbar-thumb {
border-radius: 3px;
border-radius: 2px;
background: rgb(var(--v-theme-perfect-scrollbar-thumb));
box-shadow: inset 0 0 10px rgba(0,0,0,20%);
@@ -131,6 +137,16 @@ html.v-overlay-scroll-blocked {
}
}
// 当鼠标悬停在可滚动元素上时显示滚动条
*:hover::-webkit-scrollbar {
opacity: 1;
}
// 当元素正在滚动时显示滚动条
*:active::-webkit-scrollbar {
opacity: 1;
}
.v-alert--variant-elevated, .v-alert--variant-flat {
background: rgb(var(--v-table-header-background));
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
@@ -160,88 +176,56 @@ html.v-overlay-scroll-blocked {
}
.grid-site-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
padding-block-end: 1rem;
}
.grid-media-card {
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
}
.grid-backdrop-card {
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
padding-block-end: 1rem;
}
.grid-torrent-card {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
padding-block-end: 1rem;
}
.grid-plugin-card {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
padding-block-end: 1rem;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}
.grid-downloading-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
.grid-directory-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
.grid-filterrule-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
.grid-customrule-card {
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
padding-block-end: 1rem;
}
.grid-subscribe-card {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
padding-block-end: 1rem;
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}
.grid-user-card {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
padding-block-end: 1rem;
}
@media (width <= 600px) {
.user-list-container {
padding: 12px;
}
.grid-user-card {
gap: 1rem;
grid-template-columns: 1fr;
}
}
@media (width >= 601px) and (width <= 960px) {
.grid-user-card{
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
}
@media (width >= 961px) {
.grid-user-card {
gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
}
.grid-app-card {
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
padding-block-end: 1rem;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}
.grid-workflow-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {

View File

@@ -12,6 +12,20 @@ interface RemoteModule {
url: string
}
/**
* 获取单个远程模块信息
* @param id 远程模块ID
*/
async function fetchSingleRemoteModule(id: string): Promise<RemoteModule | null> {
try {
const modules = await fetchRemoteModules()
return modules.find(module => module.id === id) || null
} catch (error) {
console.error(`获取远程模块信息失败: ${id}`, error)
return null
}
}
/**
* 加载远程组件
* @param id 远程模块ID
@@ -22,8 +36,24 @@ export async function loadRemoteComponent(id: string, componentName: string = 'P
const module = await __federation_method_getRemote(id, `./${componentName}`)
return __federation_method_unwrapDefault(module)
} catch (error) {
console.error(`加载远程组件失败: ${id}/${componentName}`, error)
throw error
// 组件未注册,尝试重新注册
try {
const moduleInfo = await fetchSingleRemoteModule(id)
if (moduleInfo) {
console.log(`组件未注册,正在重新注册: ${id}`)
injectRemoteModule(moduleInfo)
// 重新尝试加载组件
const module = await __federation_method_getRemote(id, `./${componentName}`)
return __federation_method_unwrapDefault(module)
} else {
console.error(`无法找到远程模块信息: ${id}`)
throw new Error(`无法找到远程模块信息: ${id}`)
}
} catch (retryError) {
console.error(`重新注册并加载组件失败: ${id}/${componentName}`, retryError)
throw retryError
}
}
}

View File

@@ -69,7 +69,7 @@ onActivated(() => {
</template>
<VCardTitle>{{ t('dashboard.library') }}</VCardTitle>
</VCardItem>
<div class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
<div class="grid gap-4 grid-backdrop-card mx-3 mb-3" tabindex="0">
<LibraryCard v-for="item in libraryList" :key="item.id" :media="item" height="10rem" />
</div>
</VCard>

View File

@@ -70,7 +70,7 @@ onActivated(() => {
<VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
</VCardItem>
<div class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
<div class="grid gap-4 grid-backdrop-card mx-3 mb-3" tabindex="0">
<BackdropCard v-for="item in playingList" :key="item.id" :media="item" height="10rem" />
</div>
</VCard>

View File

@@ -112,7 +112,7 @@ async function fetchData({ done }: { done: any }) {
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible pt-3" @load="fetchData">
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible pt-3 px-2" @load="fetchData">
<template #loading />
<template #empty />
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">

View File

@@ -113,7 +113,7 @@ async function fetchData({ done }: { done: any }) {
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible" @load="fetchData">
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-3" @load="fetchData">
<template #loading />
<template #empty />
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">

View File

@@ -114,7 +114,7 @@ onBeforeMount(() => {
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg v-img :src="getPersonImage()" cover @load="isImageLoaded = true" />
<VImg :src="getPersonImage()" cover @load="isImageLoaded = true" />
</VAvatar>
<div class="ms-3">
<h1 class="text-3xl lg:text-4xl text-center text-lg-left">

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ import router from '@/router'
import { useDisplay } from 'vuetify'
import { formatFileSize } from '@/@core/utils/formatters'
import { useI18n } from 'vue-i18n'
import { storageAttributes } from '@/api/constants'
// i18n
const { t } = useI18n()
@@ -683,7 +682,7 @@ onMounted(() => {
<VDivider />
<div class="flex items-center justify-between">
<div class="w-auto">
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" flat />
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" flat class="ms-1" />
</div>
<div class="w-auto text-sm">{{ t('transferHistory.pageInfo', pageTip) }} {{ totalItems }}</div>
<VPagination

View File

@@ -0,0 +1,470 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { TorrentCacheData, TorrentCacheItem } from '@/api/types'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
import { formatFileSize, formatDateDifference } from '@core/utils/formatters'
import { useConfirm } from '@/composables/useConfirm'
// 国际化
const { t } = useI18n()
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
// 缓存数据
const cacheData = ref<TorrentCacheData>({
count: 0,
sites: 0,
data: [],
})
// 筛选条件
const titleFilter = ref<string | null>(null)
const siteFilter = ref<string | null>(null)
// 获取所有站点选项
const siteOptions = computed(() => {
const sites = new Set<string>()
cacheData.value.data.forEach(item => {
if (item.site_name) {
sites.add(item.site_name)
}
})
return Array.from(sites).sort()
})
// 筛选后的数据
const filteredData = computed(() => {
return cacheData.value.data.filter(item => {
const titleMatch = !titleFilter.value || item.title?.toLowerCase().includes(titleFilter.value?.toLowerCase())
const siteMatch = !siteFilter.value || item.site_name === siteFilter.value
return titleMatch && siteMatch
})
})
// 选中的缓存项
const selectedItems = ref<string[]>([])
// 加载状态
const loading = ref(false)
// 重新识别对话框
const reidentifyDialog = ref(false)
const currentReidentifyItem = ref<TorrentCacheItem | null>(null)
const tmdbId = ref<number | undefined>()
const doubanId = ref<string | undefined>()
const tableStyle = computed(() => {
return appMode ? '' : 'height: calc(100vh - 21rem - env(safe-area-inset-bottom)'
})
// 调用API加载缓存数据
async function loadCacheData() {
try {
loading.value = true
const res: any = await api.get('torrent/cache')
cacheData.value = res.data
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.loadFailed'))
} finally {
loading.value = false
}
}
// 清空所有缓存
async function clearAllCache() {
const isConfirmed = await createConfirm({
type: 'warn',
title: t('common.confirm'),
content: t('setting.cache.clearConfirm'),
})
if (!isConfirmed) return
try {
loading.value = true
await api.delete('torrent/cache')
$toast.success(t('setting.cache.clearSuccess'))
await loadCacheData()
selectedItems.value = []
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.clearFailed'))
} finally {
loading.value = false
}
}
// 刷新缓存
async function refreshCache() {
try {
loading.value = true
const res: any = await api.post('torrent/cache/refresh')
$toast.success(res.message || t('setting.cache.refreshSuccess'))
await loadCacheData()
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.refreshFailed'))
} finally {
loading.value = false
}
}
// 删除选中的缓存项
async function deleteSelectedItems() {
if (selectedItems.value.length === 0) {
$toast.warning(t('setting.cache.selectDeleteWarning'))
return
}
try {
loading.value = true
const deletePromises = selectedItems.value.map(hash => {
const item = cacheData.value.data.find(d => d.hash === hash)
if (item) {
return api.delete(`torrent/cache/${item.domain}/${hash}`)
}
return Promise.resolve()
})
await Promise.all(deletePromises)
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: selectedItems.value.length }))
await loadCacheData()
selectedItems.value = []
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.deleteSelectedFailed'))
} finally {
loading.value = false
}
}
// 删除单个缓存项
async function deleteSingleItem(item: TorrentCacheItem) {
try {
loading.value = true
await api.delete(`torrent/cache/${item.domain}/${item.hash}`)
$toast.success(t('setting.cache.deleteSuccess'))
await loadCacheData()
// 从选中列表中移除
const index = selectedItems.value.indexOf(item.hash)
if (index > -1) {
selectedItems.value.splice(index, 1)
}
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.deleteFailed'))
} finally {
loading.value = false
}
}
// 打开重新识别对话框
function openReidentifyDialog(item: TorrentCacheItem) {
currentReidentifyItem.value = item
tmdbId.value = undefined
doubanId.value = undefined
reidentifyDialog.value = true
}
// 重新识别
async function performReidentify() {
if (!currentReidentifyItem.value) return
try {
loading.value = true
const params: any = {}
if (tmdbId.value) params.tmdbid = tmdbId.value
if (doubanId.value) params.doubanid = doubanId.value
const res: any = await api.post(
`torrent/cache/reidentify/${currentReidentifyItem.value.domain}/${currentReidentifyItem.value.hash}`,
null,
{
params,
},
)
$toast.success(res.message || t('setting.cache.reidentifySuccess'))
await loadCacheData()
reidentifyDialog.value = false
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.reidentifyFailed'))
} finally {
loading.value = false
}
}
// 获取媒体类型颜色
function getMediaTypeColor(type: string): string {
switch (type) {
case t('setting.cache.mediaType.movie'):
return 'primary'
case t('setting.cache.mediaType.tv'):
return 'success'
default:
return 'default'
}
}
// 打开详情页面
function openPageUrl(url: string) {
window.open(url, '_blank')
}
onMounted(() => {
loadCacheData()
})
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
<template #append>
<div class="d-flex gap-2">
<VBtn icon color="primary" :loading="loading" @click="refreshCache">
<VIcon>mdi-refresh</VIcon>
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
</VBtn>
<VBtn
icon
color="warning"
:loading="loading"
:disabled="selectedItems.length === 0"
@click="deleteSelectedItems"
>
<VIcon>mdi-delete-sweep</VIcon>
<VTooltip activator="parent" location="bottom"
>{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})</VTooltip
>
</VBtn>
<VBtn icon color="error" :loading="loading" @click="clearAllCache">
<VIcon>mdi-delete-variant</VIcon>
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
</VBtn>
</div>
</template>
</VCardItem>
<!-- 筛选框 -->
<VCardText>
<VRow>
<VCol cols="6">
<VTextField
v-model="titleFilter"
:label="t('setting.cache.filterByTitle')"
prepend-inner-icon="mdi-magnify"
clearable
density="compact"
/>
</VCol>
<VCol cols="6">
<VAutocomplete
v-model="siteFilter"
:label="t('setting.cache.filterBySite')"
:items="siteOptions"
prepend-inner-icon="mdi-web"
clearable
density="compact"
:placeholder="t('setting.cache.selectSite')"
/>
</VCol>
</VRow>
</VCardText>
<!-- 缓存列表 -->
<VDataTable
v-model="selectedItems"
:headers="[
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '80px' },
{ title: t('setting.cache.torrentTitle'), key: 'title', sortable: true },
{ title: t('setting.cache.site'), key: 'site_name', sortable: true, width: '120px' },
{ title: t('setting.cache.size'), key: 'size', sortable: true, width: '100px' },
{ title: t('setting.cache.publishTime'), key: 'pubdate', sortable: true, width: '150px' },
{ title: t('setting.cache.recognitionResult'), key: 'media_info', sortable: false, width: '200px' },
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '150px' },
]"
:items="filteredData"
:loading="loading"
item-value="hash"
show-select
hover
fixed-header
:items-per-page-text="t('common.itemsPerPage')"
:no-data-text="t('common.noDataText')"
:loading-text="t('common.loadingText')"
:style="tableStyle"
>
<!-- 全选复选框 -->
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
<VCheckbox
:indeterminate="someSelected && !allSelected"
:model-value="allSelected"
@update:model-value="(value: boolean | null) => selectAll(value as boolean)"
/>
</template>
<!-- 海报列 -->
<template #item.poster="{ item }">
<div class="text-center">
<VImg
v-if="item.poster_path"
:src="item.poster_path"
:alt="item.media_name || item.title"
cover
rounded="md"
class="w-12 my-1 ms-auto"
/>
<VIcon v-else size="x-large" color="grey-lighten-1">
{{ item.media_type === 'movie' ? 'mdi-movie-open' : 'mdi-television-play' }}
</VIcon>
</div>
</template>
<!-- 标题列 -->
<template #item.title="{ item }">
<div class="d-flex flex-column min-w-40">
<div class="text-subtitle-2 font-weight-bold">
{{ item.title }}
</div>
<div v-if="item.description" class="text-caption text-grey">
{{ item.description }}
</div>
<div v-if="item.season_episode || item.resource_term" class="text-caption text-primary mt-1">
{{ item.season_episode }} {{ item.resource_term }}
</div>
</div>
</template>
<!-- 大小列 -->
<template #item.size="{ item }">
{{ formatFileSize(item.size) }}
</template>
<!-- 发布时间列 -->
<template #item.pubdate="{ item }">
{{ formatDateDifference(item.pubdate || '') }}
</template>
<!-- 识别结果列 -->
<template #item.media_info="{ item }">
<div v-if="item.media_name" class="d-flex flex-column">
<div class="text-subtitle-2">
{{ item.media_name }}
<span v-if="item.media_year" class="text-caption text-grey"> ({{ item.media_year }}) </span>
</div>
<div>
<VChip v-if="item.media_type" :color="getMediaTypeColor(item.media_type)" size="x-small">
{{ item.media_type }}
</VChip>
</div>
</div>
<div v-else class="text-caption text-grey">
{{ t('setting.cache.unrecognized') }}
</div>
</template>
<!-- 操作列 -->
<template #item.actions="{ item }">
<div class="d-flex gap-1">
<VBtn icon size="small" color="primary" variant="text" @click="openReidentifyDialog(item)">
<VIcon size="16">mdi-text-recognition</VIcon>
</VBtn>
<VBtn icon size="small" color="error" variant="text" @click="deleteSingleItem(item)">
<VIcon size="16">mdi-delete</VIcon>
</VBtn>
<VBtn
v-if="item.page_url"
icon
size="small"
color="info"
variant="text"
@click="openPageUrl(item.page_url || '')"
target="_blank"
>
<VIcon size="16">mdi-open-in-new</VIcon>
</VBtn>
</div>
</template>
<!-- 空状态 -->
<template #no-data>
<div class="text-center pa-4">
<VIcon size="64" class="mb-4"> mdi-database-off </VIcon>
<div class="text-body-2 text-grey">
{{ t('setting.cache.noData') }}
</div>
</div>
</template>
</VDataTable>
</VCard>
<!-- 重新识别对话框 -->
<VDialog v-model="reidentifyDialog" scrollable max-width="35rem">
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon>mdi-text-recognition</VIcon>
</template>
<VCardTitle>{{ t('setting.cache.reidentifyDialog.title') }}</VCardTitle>
<VCardSubtitle>{{ currentReidentifyItem?.title }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="reidentifyDialog = false" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField
v-if="globalSettings.RECOGNIZE_SOURCE === 'themoviedb'"
v-model="tmdbId"
:label="t('setting.cache.reidentifyDialog.tmdbId')"
:hint="t('setting.cache.reidentifyDialog.tmdbIdHint')"
clearable
prepend-inner-icon="mdi-id-card"
persistent-hint
/>
<VTextField
v-else
v-model="doubanId"
:label="t('setting.cache.reidentifyDialog.doubanId')"
:hint="t('setting.cache.reidentifyDialog.doubanIdHint')"
clearable
prepend-inner-icon="mdi-id-card"
persistent-hint
/>
</VCol>
</VRow>
<VAlert type="info" variant="tonal" class="mt-4">
{{ t('setting.cache.reidentifyDialog.autoHint') }}
</VAlert>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" :loading="loading" prepend-icon="mdi-check" @click="performReidentify">
{{ t('setting.cache.reidentifyDialog.confirm') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -59,19 +59,6 @@ async function loadSystemSettings() {
}
}
// 重载系统生效配置
async function reloadSystem() {
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
else $toast.error(t('setting.system.reloadFailed'))
} catch (error) {
console.log(error)
}
progressDialog.value = false
}
// 移动结束
function orderDirectoryCards() {
// 更新所有目录的优先级
@@ -124,7 +111,6 @@ async function saveDirectories() {
const result: { [key: string]: any } = await api.post('system/setting/Directories', directories.value)
if (result.success) {
$toast.success(t('setting.directory.directorySaveSuccess'))
await reloadSystem()
} else $toast.error(t('setting.directory.directorySaveFailed'))
} catch (error) {
console.log(error)
@@ -239,7 +225,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveStorages"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" class="me-2" @click="saveStorages" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal" @click="addStorage">
<VIcon icon="mdi-plus" />
</VBtn>
@@ -279,7 +267,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDirectories"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveDirectories" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal" @click="addDirectory">
<VIcon icon="mdi-plus" />
</VBtn>
@@ -305,6 +295,7 @@ onMounted(() => {
:label="t('setting.directory.scrapSource')"
:hint="t('setting.directory.scrapSourceHint')"
persistent-hint
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12">
@@ -315,6 +306,7 @@ onMounted(() => {
persistent-hint
clearable
active
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12">
@@ -325,6 +317,7 @@ onMounted(() => {
persistent-hint
clearable
active
prepend-inner-icon="mdi-television"
/>
</VCol>
</VRow>
@@ -332,7 +325,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)"> {{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -7,7 +7,10 @@ import NotificationChannelCard from '@/components/cards/NotificationChannelCard.
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
import { notificationSwitchDict } from '@/api/constants'
import { useTheme } from 'vuetify'
import { useTheme, useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -100,19 +103,6 @@ const notificationTime = ref({
end: '23:59',
})
// 重载系统生效配置
async function reloadSystem() {
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
else $toast.error(t('setting.system.reloadFailed'))
} catch (error) {
console.log(error)
}
progressDialog.value = false
}
// 添加通知渠道
function addNotification(notification: string) {
let name = `${t('setting.notification.channel')}${notifications.value.length + 1}`
@@ -196,7 +186,6 @@ async function saveNotificationSetting() {
const result: { [key: string]: any } = await api.post('system/setting/Notifications', notifications.value)
if (result.success) {
$toast.success(t('setting.notification.saveSuccess'))
await reloadSystem()
} else $toast.error(t('setting.notification.saveFailed'))
} catch (error) {
console.log(error)
@@ -209,7 +198,6 @@ async function saveNotificationTime() {
const result: { [key: string]: any } = await api.post('system/setting/NotificationSendTime', notificationTime.value)
if (result.success) {
$toast.success(t('setting.notification.timeSaveSuccess'))
await reloadSystem()
} else $toast.error(t('setting.notification.timeSaveFailed'))
} catch (error) {
console.log(error)
@@ -290,7 +278,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveNotificationSetting"> {{ t('common.save') }} </VBtn>
<VBtn mtype="submit" @click="saveNotificationSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu :activator="'parent'" :close-on-content-click="true">
@@ -313,6 +303,9 @@ onMounted(() => {
<VListItem @click="addNotification('webpush')">
<VListItemTitle>{{ t('setting.notification.webPush') }}</VListItemTitle>
</VListItem>
<VListItem @click="addNotification('custom')">
<VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
@@ -392,11 +385,12 @@ onMounted(() => {
</tr>
</tbody>
</VTable>
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveNotificationSwitchs"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveNotificationSwitchs" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -413,17 +407,29 @@ onMounted(() => {
<VCardText>
<VRow>
<VCol cols="6">
<VTextField v-model="notificationTime.start" :label="t('setting.notification.startTime')" type="time" />
<VTextField
v-model="notificationTime.start"
:label="t('setting.notification.startTime')"
type="time"
prepend-inner-icon="mdi-clock-start"
/>
</VCol>
<VCol cols="6">
<VTextField v-model="notificationTime.end" :label="t('setting.notification.endTime')" type="time" />
<VTextField
v-model="notificationTime.end"
:label="t('setting.notification.endTime')"
type="time"
prepend-inner-icon="mdi-clock-end"
/>
</VCol>
</VRow>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveNotificationTime"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveNotificationTime" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -438,13 +444,18 @@ onMounted(() => {
:indeterminate="true"
/>
<!-- 模板编辑器对话框 -->
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem">
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-code-json" class="me-2" />
</template>
<VCardTitle>
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
{{ t('setting.notification.templateConfigTitle') }}
</VCardTitle>
<VCardSubtitle>
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
</VCardSubtitle>
<VDialogCloseBtn @click="editorVisible = false" />
</VCardItem>
<VCardText class="py-0">
@@ -452,11 +463,11 @@ onMounted(() => {
v-model:value="editorContent"
lang="json"
:theme="editorTheme"
class="w-full min-h-[30rem] rounded"
class="w-full h-full min-h-[30rem] rounded"
/>
</VCardText>
<VCardActions class="mx-auto pt-3">
<VBtn variant="elevated" color="primary" @click="saveTemplate" prepend-icon="mdi-content-save" class="px-5">
<VCardActions class="pt-3">
<VBtn color="primary" @click="saveTemplate" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>
@@ -466,10 +477,10 @@ onMounted(() => {
<style scoped>
/* Monaco编辑器容器样式 */
.monaco-editor-container {
overflow: hidden;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
overflow: hidden;
margin-top: 1rem;
margin-block-start: 1rem;
}
.template-card {

View File

@@ -401,7 +401,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveCustomRules"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" class="me-2" @click="saveCustomRules" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addCustomRule">
<VIcon icon="mdi-plus" />
@@ -452,7 +454,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
<VIcon icon="mdi-plus" />
@@ -501,6 +505,7 @@ onMounted(() => {
:label="t('setting.rule.currentPriorityRules')"
:hint="t('setting.rule.currentPriorityRulesHint')"
persistent-hint
prepend-inner-icon="mdi-priority-high"
/>
</VCol>
</VRow>
@@ -509,7 +514,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTorrentPriority"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveTorrentPriority" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -205,10 +205,11 @@ onMounted(() => {
:label="t('setting.search.mediaSource')"
:hint="t('setting.search.mediaSourceHint')"
persistent-hint
prepend-inner-icon="mdi-database-search"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
<VAutocomplete
v-model="selectedFilterGroup"
multiple
clearable
@@ -217,6 +218,7 @@ onMounted(() => {
:label="t('setting.search.filterRuleGroup')"
:hint="t('setting.search.filterRuleGroupHint')"
persistent-hint
prepend-inner-icon="mdi-filter"
/>
</VCol>
</VRow>
@@ -228,6 +230,7 @@ onMounted(() => {
placeholder="MOVIEPILOT"
:hint="t('setting.search.downloadLabelHint')"
persistent-hint
prepend-inner-icon="mdi-tag"
/>
</VCol>
<VCol cols="12" md="6">
@@ -237,6 +240,7 @@ onMounted(() => {
:placeholder="t('setting.search.downloadUserPlaceholder')"
:hint="t('setting.search.downloadUserHint')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -260,7 +264,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSearchSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSearchSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -291,7 +297,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedSites"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSelectedSites" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -96,27 +96,12 @@ async function loadSiteSettings() {
}
}
// 重载系统生效配置
async function reloadSystem() {
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
else $toast.error(t('setting.system.reloadFailed'))
} catch (error) {
console.log(error)
}
progressDialog.value = false
}
// 调用API保存设置
async function saveSiteSetting(value: { [key: string]: any }) {
console.log(`正在保存设置:${JSON.stringify(value)}`)
try {
const result: { [key: string]: any } = await api.post('system/env', value)
if (result.success) {
$toast.success(t('setting.site.saveSuccess'))
await reloadSystem()
} else {
$toast.error(t('setting.site.saveFailed'))
}
@@ -161,6 +146,7 @@ onMounted(() => {
:disabled="siteSetting.CookieCloud.COOKIECLOUD_ENABLE_LOCAL"
:hint="t('setting.site.serviceAddressHint')"
persistent-hint
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -169,6 +155,7 @@ onMounted(() => {
:label="t('setting.site.userKey')"
:hint="t('setting.site.userKeyHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -180,6 +167,7 @@ onMounted(() => {
:label="t('setting.site.e2ePassword')"
:hint="t('setting.site.e2ePasswordHint')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -189,6 +177,7 @@ onMounted(() => {
:items="CookieCloudIntervalItems"
:hint="t('setting.site.autoSyncIntervalHint')"
persistent-hint
prepend-inner-icon="mdi-timer"
/>
</VCol>
<VCol cols="12" md="6">
@@ -198,6 +187,7 @@ onMounted(() => {
:placeholder="t('setting.site.syncBlacklistPlaceholder')"
:hint="t('setting.site.syncBlacklistHint')"
persistent-hint
prepend-inner-icon="mdi-block-helper"
/>
</VCol>
<VCol cols="12" md="6">
@@ -206,6 +196,7 @@ onMounted(() => {
:label="t('setting.site.userAgent')"
:hint="t('setting.site.userAgentHint')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
</VRow>
@@ -214,7 +205,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -234,6 +227,7 @@ onMounted(() => {
:items="SiteDataRefreshIntervalItems"
:hint="t('setting.site.siteDataRefreshIntervalHint')"
persistent-hint
prepend-inner-icon="mdi-refresh"
/>
</VCol>
</VRow>
@@ -252,7 +246,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -153,19 +153,6 @@ async function querySubscribeRules() {
}
}
// 重载系统生效配置
async function reloadSystem() {
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
else $toast.error(t('setting.system.reloadFailed'))
} catch (error) {
console.log(error)
}
progressDialog.value = false
}
// 保存订阅设置
async function saveSubscribeSetting() {
try {
@@ -183,7 +170,6 @@ async function saveSubscribeSetting() {
if (result1.success && result2.success && result3) {
$toast.success(t('setting.subscribe.settingsSaveSuccess'))
await reloadSystem()
} else $toast.error(t('setting.subscribe.settingsSaveFailed'))
} catch (error) {
console.log(error)
@@ -217,6 +203,7 @@ onMounted(() => {
:label="t('setting.subscribe.mode')"
:hint="t('setting.subscribe.modeHint')"
persistent-hint
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
@@ -226,10 +213,11 @@ onMounted(() => {
:label="t('setting.subscribe.rssInterval')"
:hint="t('setting.subscribe.rssIntervalHint')"
persistent-hint
prepend-inner-icon="mdi-timer"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
<VAutocomplete
v-model="selectedFilterRuleGroup"
:items="filterRuleGroupOptions"
chips
@@ -238,10 +226,11 @@ onMounted(() => {
:label="t('setting.subscribe.filterRuleGroup')"
:hint="t('setting.subscribe.filterRuleGroupHint')"
persistent-hint
prepend-inner-icon="mdi-filter"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
<VAutocomplete
v-model="selectedBestVersionRuleGroup"
:items="filterRuleGroupOptions"
chips
@@ -250,6 +239,7 @@ onMounted(() => {
:label="t('setting.subscribe.bestVersionRuleGroup')"
:hint="t('setting.subscribe.bestVersionRuleGroupHint')"
persistent-hint
prepend-inner-icon="mdi-star"
/>
</VCol>
</VRow>
@@ -276,7 +266,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSubscribeSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSubscribeSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -307,7 +299,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedRssSites"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSelectedRssSites" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -11,6 +11,9 @@ import { copyToClipboard } from '@/@core/utils/navigator'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
import { downloaderOptions, mediaServerOptions } from '@/api/constants'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -26,6 +29,7 @@ const SystemSettings = ref<any>({
RECOGNIZE_SOURCE: 'themoviedb',
GITHUB_TOKEN: null,
OCR_HOST: null,
CUSTOMIZE_WALLPAPER_API_URL: null,
},
// 高级系统设置
Advanced: {
@@ -36,6 +40,8 @@ const SystemSettings = ref<any>({
PLUGIN_STATISTIC_SHARE: true,
BIG_MEMORY_MODE: false,
DB_WAL_ENABLE: false,
AUTO_UPDATE_RESOURCE: true,
MOVIEPILOT_AUTO_UPDATE: false,
// 媒体
TMDB_API_DOMAIN: null,
TMDB_IMAGE_DOMAIN: null,
@@ -43,6 +49,7 @@ const SystemSettings = ref<any>({
META_CACHE_EXPIRE: 0,
SCRAP_FOLLOW_TMDB: true,
FANART_ENABLE: false,
TMDB_SCRAP_ORIGINAL_IMAGE: null,
// 网络
PROXY_HOST: null,
GITHUB_PROXY: null,
@@ -50,6 +57,7 @@ const SystemSettings = ref<any>({
DOH_ENABLE: false,
DOH_RESOLVERS: null,
DOH_DOMAINS: null,
SECURITY_IMAGE_DOMAINS: [],
// 日志
DEBUG: false,
LOG_LEVEL: 'INFO',
@@ -90,6 +98,29 @@ const tmdbLanguageItems = [
{ title: t('setting.system.tmdbLanguage.en'), value: 'en' },
]
// 日志等级
const logLevelItems = [
{ title: t('setting.system.logLevelItems.debug'), value: 'DEBUG' },
{ title: t('setting.system.logLevelItems.info'), value: 'INFO' },
{ title: t('setting.system.logLevelItems.warning'), value: 'WARNING' },
{ title: t('setting.system.logLevelItems.error'), value: 'ERROR' },
{ title: t('setting.system.logLevelItems.critical'), value: 'CRITICAL' },
]
// 安全域名添加变量
const newSecurityDomain = ref('')
// 添加安全域名
function addSecurityDomain() {
if (
newSecurityDomain.value &&
!SystemSettings.value.Advanced.SECURITY_IMAGE_DOMAINS.includes(newSecurityDomain.value)
) {
SystemSettings.value.Advanced.SECURITY_IMAGE_DOMAINS.push(newSecurityDomain.value)
newSecurityDomain.value = ''
}
}
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
@@ -100,19 +131,6 @@ async function loadDownloaderSetting() {
}
}
// 重载系统生效配置
async function reloadSystem() {
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
else $toast.error(t('setting.system.reloadFailed'))
} catch (error) {
console.log(error)
}
progressDialog.value = false
}
// 调用API保存下载器设置
async function saveDownloaderSetting() {
try {
@@ -127,7 +145,6 @@ async function saveDownloaderSetting() {
else $toast.error(t('setting.system.downloaderSaveFailed'))
await loadDownloaderSetting()
await reloadSystem()
} catch (error) {
console.log(error)
}
@@ -167,7 +184,6 @@ async function saveMediaServerSetting() {
else $toast.error(t('setting.system.mediaServerSaveFailed'))
await loadMediaServerSetting()
await reloadSystem()
} catch (error) {
console.log(error)
}
@@ -210,7 +226,6 @@ async function saveSystemSetting(value: { [key: string]: any }) {
async function saveBasicSettings() {
if (await saveSystemSetting(SystemSettings.value.Basic)) {
$toast.success(t('setting.system.basicSaveSuccess'))
await reloadSystem()
}
}
@@ -221,7 +236,6 @@ async function saveAdvancedSettings() {
if (await saveSystemSetting(SystemSettings.value.Advanced)) {
advancedDialog.value = false
$toast.success(t('setting.system.advancedSaveSuccess'))
await reloadSystem()
}
}
@@ -252,6 +266,7 @@ const wallpaperItems = [
{ title: t('setting.system.wallpaperItems.tmdb'), value: 'tmdb' },
{ title: t('setting.system.wallpaperItems.bing'), value: 'bing' },
{ title: t('setting.system.wallpaperItems.mediaserver'), value: 'mediaserver' },
{ title: t('setting.system.wallpaperItems.customize'), value: 'customize' },
{ title: t('setting.system.wallpaperItems.none'), value: '' },
]
@@ -294,15 +309,6 @@ const pipProxyDisplay = computed({
},
})
// 日志等级
const logLevelItems = [
{ title: t('setting.system.logLevelItems.debug'), value: 'DEBUG' },
{ title: t('setting.system.logLevelItems.info'), value: 'INFO' },
{ title: t('setting.system.logLevelItems.warning'), value: 'WARNING' },
{ title: t('setting.system.logLevelItems.error'), value: 'ERROR' },
{ title: t('setting.system.logLevelItems.critical'), value: 'CRITICAL' },
]
// 创建随机字符串
function createRandomString() {
const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
@@ -364,6 +370,16 @@ function onMediaServerChange(mediaserver: MediaServerConf, name: string) {
if (index !== -1) mediaServers.value[index] = mediaserver
}
// 添加计算属性
const moviePilotAutoUpdate = computed({
get: () => {
return ['release', 'dev'].includes(SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE)
},
set: val => {
SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE = val ? 'release' : 'false'
},
})
// 加载数据
onMounted(() => {
loadDownloaderSetting()
@@ -405,16 +421,35 @@ onDeactivated(() => {
:hint="t('setting.system.appDomainHint')"
placeholder="http://localhost:3000"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.WALLPAPER"
:label="t('setting.system.wallpaper')"
:hint="t('setting.system.wallpaperHint')"
persistent-hint
:items="wallpaperItems"
/>
<VRow>
<VCol cols="12" :md="SystemSettings.Basic.WALLPAPER === 'customize' ? 6 : 12">
<VSelect
v-model="SystemSettings.Basic.WALLPAPER"
:label="t('setting.system.wallpaper')"
:hint="t('setting.system.wallpaperHint')"
persistent-hint
:items="wallpaperItems"
prepend-inner-icon="mdi-image"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.WALLPAPER === 'customize'" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.CUSTOMIZE_WALLPAPER_API_URL"
:label="t('setting.system.customizeWallpaperApi')"
:hint="t('setting.system.customizeWallpaperApiHint')"
:placeholder="t('setting.system.customizeWallpaperApi')"
persistent-hint
:rules="[v => !!v || t('setting.system.customizeWallpaperApiRequired')]"
prepend-inner-icon="mdi-api"
/>
</VCol>
</VRow>
</VCol>
<VCol cols="12" md="6">
<VSelect
@@ -426,6 +461,7 @@ onDeactivated(() => {
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
]"
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12" md="6">
@@ -442,6 +478,7 @@ onDeactivated(() => {
(v: any) => !isNaN(v) || t('setting.system.numbersOnly'),
(v: any) => v >= 1 || t('setting.system.minInterval'),
]"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12" md="6">
@@ -451,10 +488,11 @@ onDeactivated(() => {
:hint="t('setting.system.apiTokenHint')"
:placeholder="t('setting.system.apiTokenMinChars')"
persistent-hint
prependInnerIcon="mdi-reload"
:appendInnerIcon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : ''"
@click:prependInner="createRandomString"
@click:appendInner="copyValue(SystemSettings.Basic.API_TOKEN)"
prepend-inner-icon="mdi-key"
:append-inner-icon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : 'mdi-reload'"
@click:append-inner="
SystemSettings.Basic.API_TOKEN ? copyValue(SystemSettings.Basic.API_TOKEN) : createRandomString()
"
:rules="[
(v: string) => !!v || t('setting.system.apiTokenRequired'),
(v: string) => v.length >= 16 || t('setting.system.apiTokenLength'),
@@ -468,6 +506,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.githubTokenFormat')"
:hint="t('setting.system.githubTokenHint')"
persistent-hint
prepend-inner-icon="mdi-github"
>
</VTextField>
</VCol>
@@ -478,6 +517,7 @@ onDeactivated(() => {
placeholder="https://movie-pilot.org"
:hint="t('setting.system.ocrHostHint')"
persistent-hint
prepend-inner-icon="mdi-text-recognition"
/>
</VCol>
</VRow>
@@ -486,7 +526,9 @@ onDeactivated(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveBasicSettings"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveBasicSettings" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VSpacer />
<VBtn
color="error"
@@ -531,7 +573,9 @@ onDeactivated(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDownloaderSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveDownloaderSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
@@ -579,7 +623,9 @@ onDeactivated(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveMediaServerSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveMediaServerSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
@@ -599,8 +645,15 @@ onDeactivated(() => {
</VCard>
</VCol>
</VRow>
<!-- 高级系统设置 -->
<VDialog v-if="advancedDialog" v-model="advancedDialog" scrollable max-width="60rem">
<VDialog
v-if="advancedDialog"
v-model="advancedDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VDialogCloseBtn @click="advancedDialog = false" />
@@ -677,6 +730,22 @@ onDeactivated(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="moviePilotAutoUpdate"
:label="t('setting.system.moviePilotAutoUpdate')"
:hint="t('setting.system.moviePilotAutoUpdateHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.AUTO_UPDATE_RESOURCE"
:label="t('setting.system.autoUpdateResource')"
:hint="t('setting.system.autoUpdateResourceHint')"
persistent-hint
/>
</VCol>
</VRow>
</div>
</VWindowItem>
@@ -692,6 +761,7 @@ onDeactivated(() => {
persistent-hint
:items="['api.themoviedb.org', 'api.tmdb.org']"
:rules="[(v: string) => !!v || t('setting.system.tmdbApiDomainRequired')]"
prepend-inner-icon="mdi-api"
/>
</VCol>
<VCol cols="12" md="6">
@@ -703,6 +773,7 @@ onDeactivated(() => {
persistent-hint
:items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']"
:rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]"
prepend-inner-icon="mdi-image"
/>
</VCol>
<VCol cols="12" md="6">
@@ -713,6 +784,7 @@ onDeactivated(() => {
:hint="t('setting.system.tmdbLocaleHint')"
persistent-hint
:items="tmdbLanguageItems"
prepend-inner-icon="mdi-translate"
/>
</VCol>
<VCol cols="12" md="6">
@@ -728,6 +800,7 @@ onDeactivated(() => {
(v: any) => v === 0 || !!v || t('setting.system.metaCacheExpireRequired'),
(v: any) => v >= 0 || t('setting.system.metaCacheExpireMin'),
]"
prepend-inner-icon="mdi-timer"
/>
</VCol>
</VRow>
@@ -740,6 +813,14 @@ onDeactivated(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.TMDB_SCRAP_ORIGINAL_IMAGE"
:label="t('setting.system.scrapOriginalImage')"
:hint="t('setting.system.scrapOriginalImageHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.FANART_ENABLE"
@@ -754,6 +835,16 @@ onDeactivated(() => {
<VWindowItem value="network">
<div>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.PROXY_HOST"
:label="t('setting.system.proxyHost')"
placeholder="http://127.0.0.1:7890"
:hint="t('setting.system.proxyHostHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="githubProxyDisplay"
@@ -763,9 +854,10 @@ onDeactivated(() => {
persistent-hint
:items="githubMirrorsItems"
clearable
prepend-inner-icon="mdi-github"
/>
</VCol>
<VCol cols="12" md="6">
<VCol cols="12">
<VCombobox
v-model="pipProxyDisplay"
:label="t('setting.system.pipProxy')"
@@ -774,6 +866,7 @@ onDeactivated(() => {
persistent-hint
:items="pipMirrorsItems"
clearable
prepend-inner-icon="mdi-package"
/>
</VCol>
</VRow>
@@ -793,6 +886,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.dohResolversPlaceholder')"
:hint="t('setting.system.dohResolversHint')"
persistent-hint
prepend-inner-icon="mdi-dns"
/>
</VCol>
<VCol cols="12" v-show="SystemSettings.Advanced.DOH_ENABLE">
@@ -802,9 +896,51 @@ onDeactivated(() => {
:placeholder="t('setting.system.dohDomainsPlaceholder')"
:hint="t('setting.system.dohDomainsHint')"
persistent-hint
prepend-inner-icon="mdi-domain"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<!-- 安全域名 -->
<VCard>
<VCardItem>
<VCardTitle>{{ t('setting.system.securityImageDomains') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.system.securityImageDomainsHint') }}</VCardSubtitle>
</VCardItem>
<VCardText>
<div class="d-flex flex-wrap gap-2 mb-3">
<VChip
v-for="(domain, index) in SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS"
:key="index"
closable
@click:close="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.splice(index, 1)"
>
{{ domain }}
</VChip>
<VChip v-if="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.length === 0" color="warning">
{{ t('setting.system.noSecurityImageDomains') }}
</VChip>
</div>
<div class="d-flex align-center gap-2">
<VTextField
v-model="newSecurityDomain"
:placeholder="t('setting.system.securityImageDomainAdd')"
hide-details
density="compact"
prepend-inner-icon="mdi-shield-check"
>
<template #append>
<VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain">
<VIcon icon="mdi-plus" />
</VBtn>
</template>
</VTextField>
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</div>
</VWindowItem>
<VWindowItem value="log">
@@ -826,6 +962,7 @@ onDeactivated(() => {
:hint="t('setting.system.logLevelHint')"
persistent-hint
:items="logLevelItems"
prepend-inner-icon="mdi-format-list-bulleted"
/>
</VCol>
<VCol cols="12" md="6">
@@ -838,6 +975,7 @@ onDeactivated(() => {
type="number"
:suffix="t('setting.system.mb')"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'), (v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin')]"
prepend-inner-icon="mdi-file-document"
/>
</VCol>
<VCol cols="12" md="6">
@@ -849,6 +987,7 @@ onDeactivated(() => {
min="1"
type="number"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'), (v: any) => v >= 1 || t('setting.system.logBackupCountMin')]"
prepend-inner-icon="mdi-backup-restore"
/>
</VCol>
<VCol cols="12">
@@ -857,6 +996,7 @@ onDeactivated(() => {
:label="t('setting.system.logFileFormat')"
:hint="t('setting.system.logFileFormatHint')"
persistent-hint
prepend-inner-icon="mdi-format-text"
/>
</VCol>
</VRow>
@@ -897,13 +1037,7 @@ onDeactivated(() => {
<VCardActions class="pt-3">
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
color="primary"
variant="elevated"
prepend-icon="mdi-content-save"
@click="saveAdvancedSettings"
class="px-5"
>
<VBtn color="primary" prepend-icon="mdi-content-save" @click="saveAdvancedSettings" class="px-5">
{{ t('common.save') }}
</VBtn>
</div>

View File

@@ -143,6 +143,7 @@ onMounted(() => {
:placeholder="t('setting.words.identifiersPlaceholder')"
:hint="t('setting.words.identifiersHint')"
persistent-hint
prepend-inner-icon="mdi-tag-text"
/>
</VCardText>
<VCardText>
@@ -153,7 +154,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomIdentifiers">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveCustomIdentifiers" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -173,12 +176,15 @@ onMounted(() => {
:placeholder="t('setting.words.releaseGroupsPlaceholder')"
:hint="t('setting.words.releaseGroupsHint')"
persistent-hint
prepend-inner-icon="mdi-account-group"
/>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomReleaseGroups">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveCustomReleaseGroups" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -198,12 +204,15 @@ onMounted(() => {
:placeholder="t('setting.words.customizationPlaceholder')"
:hint="t('setting.words.customizationHint')"
persistent-hint
prepend-inner-icon="mdi-code-braces"
/>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomization">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveCustomization" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -223,12 +232,15 @@ onMounted(() => {
:placeholder="t('setting.words.excludeWordsPlaceholder')"
:hint="t('setting.words.excludeWordsHint')"
persistent-hint
prepend-inner-icon="mdi-block-helper"
/>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTransferExcludeWords">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveTransferExcludeWords" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

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