Compare commits

...

580 Commits

Author SHA1 Message Date
jxxghp
4c64f7a2c3 优化 HTML 结构,调整 CSS 样式 2025-08-25 13:03:45 +08:00
jxxghp
262927e459 fix:整理中的所以取消 2025-08-24 18:50:07 +08:00
jxxghp
b16c566004 fix ui 2025-08-24 18:32:29 +08:00
jxxghp
1af82dbee6 优化 TransferQueueDialog 组件,合并相同 title_year 的媒体记录和任务 2025-08-24 18:25:59 +08:00
jxxghp
2e9a5a4e13 更新 TransferQueueDialog 组件 2025-08-24 18:18:53 +08:00
jxxghp
b455f603dc 调整 TransferQueueDialog 组件 2025-08-24 18:11:36 +08:00
jxxghp
37c0c3e339 优化 TransferQueueDialog 组件的 SSE 连接管理 2025-08-24 18:02:15 +08:00
jxxghp
b6cb341082 fix 整理进度显示 2025-08-24 17:50:03 +08:00
jxxghp
1af1a06700 优化 SSE 管理器 2025-08-24 17:34:43 +08:00
jxxghp
79e4ecfdbe 引入 crypto-js 库以计算文件路径的 MD5 值 2025-08-24 17:05:36 +08:00
jxxghp
1585271e37 为 TransferQueueDialog 组件优化 SSE 监听管理 2025-08-24 16:16:33 +08:00
jxxghp
c240b171e4 更新 package.json 版本号至 2.7.6 2025-08-24 13:03:25 +08:00
jxxghp
9c405e90ac 重构 TransferQueueDialog 组件,添加整体和当前文件进度管理 2025-08-24 13:02:40 +08:00
jxxghp
3ec3212ca5 更新 service-worker.ts 2025-08-24 08:16:14 +08:00
jxxghp
b1289f6177 实现订阅批量管理功能 2025-08-23 21:20:09 +08:00
jxxghp
64b7ba48c8 fix ios 2025-08-23 20:39:40 +08:00
jxxghp
f093053ea4 优化对话框状态管理 2025-08-23 19:32:23 +08:00
jxxghp
9faa0ded59 为对话框组件添加防止滚动穿透的样式 2025-08-23 19:14:12 +08:00
jxxghp
0f7dafeb23 控制合集搜索项的显示 2025-08-23 19:03:50 +08:00
jxxghp
472d1960d9 重构对话框组件,将所有 DialogWrapper 替换为 VDialog,并更新缓存版本至 v1.1.0 2025-08-23 18:55:34 +08:00
jxxghp
6e50acf106 更新 service-worker.ts 2025-08-23 10:22:31 +08:00
jxxghp
a3fb4b1534 更新 package.json 版本号至 2.7.5 2025-08-23 08:56:14 +08:00
jxxghp
382cae32a2 fix site import dialog 2025-08-23 08:47:16 +08:00
jxxghp
0aa4851f8e Merge pull request #380 from jxxghp/cursor/implement-site-batch-import-and-export-2694 2025-08-23 07:32:40 +08:00
Cursor Agent
65271e6d13 Remove package-lock.json from version control
Co-authored-by: jxxghp <jxxghp@live.cn>
2025-08-22 23:13:47 +00:00
Cursor Agent
671cf8d588 Refactor site import/export feature with improved toast notifications
Co-authored-by: jxxghp <jxxghp@live.cn>
2025-08-22 23:12:01 +00:00
Cursor Agent
afc7c81028 Add site batch import/export functionality with preview and validation
Co-authored-by: jxxghp <jxxghp@live.cn>
2025-08-22 23:09:03 +00:00
jxxghp
c330aee560 增强消息视图的SSE连接管理 2025-08-21 09:22:57 +08:00
jxxghp
eafe63c886 更新 package.json 2025-08-20 10:34:29 +08:00
jxxghp
53206d05b8 更新 service-worker.ts 2025-08-20 10:34:10 +08:00
jxxghp
af085d457e 更新 PluginCardListView.vue 2025-08-20 10:33:52 +08:00
jxxghp
fb36033939 修复数据库类型判断 2025-08-19 13:18:02 +08:00
jxxghp
584e7672df 更新版本号至2.7.3 2025-08-19 13:08:23 +08:00
jxxghp
d4f7a5a1c0 fix https://github.com/jxxghp/MoviePilot/issues/4769 2025-08-17 11:38:17 +08:00
jxxghp
2a9ea81ad4 feat: 优化SSE连接延迟,添加初始化状态提示 2025-08-17 08:39:02 +08:00
jxxghp
276948dd68 feat: 修复成功率计算和统计总览功能 2025-08-12 15:28:58 +08:00
jxxghp
990c5583f2 移除不必要的 TMDB 图片域名选项 2025-08-12 14:27:01 +08:00
jxxghp
644f1b5640 Merge pull request #377 from Sowevo/v2 2025-08-12 06:52:26 +08:00
sowevo
5261fbe870 🚨 0 2025-08-12 05:44:27 +08:00
sowevo
e4f2d85e2b 🚨 多余参数 2025-08-12 05:18:23 +08:00
sowevo
8e3ccdc24a feat: 透明倒影 2025-08-12 05:09:58 +08:00
sowevo
cd6d93affd feat: 透明背景 2025-08-12 04:32:51 +08:00
sowevo
6096ab0c9b feat: 调整间距 2025-08-12 04:31:19 +08:00
sowevo
0a87bb1db1 canvas固定宽和高 2025-08-12 04:14:13 +08:00
jxxghp
a19042c655 在设置中添加浏览器仿真选项 2025-08-11 21:35:20 +08:00
jxxghp
a889687a6a 更新 package.json 2025-08-10 18:16:09 +08:00
jxxghp
e1cdc715aa 更新 GitHub Actions 配置,启用最新版本标记功能 2025-08-06 16:37:09 +08:00
jxxghp
a82b3a0a29 优化消息处理逻辑 2025-08-05 15:47:46 +08:00
jxxghp
d93a71f0be 更新 TorrentRowListView.vue 2025-08-03 11:56:32 +08:00
jxxghp
899dc765bc 更新 TorrentCardListView.vue 2025-08-03 11:55:53 +08:00
jxxghp
449490e52d 更新 SiteStatisticsDialog.vue 2025-08-02 14:53:49 +08:00
jxxghp
5541d7974e 更新 SiteStatisticsDialog.vue 2025-08-02 14:34:59 +08:00
jxxghp
ae3eb36183 添加站点耗时统计信息展示 2025-08-02 14:20:17 +08:00
jxxghp
d57e9a397c 优化样式以支持动态颜色显示。 2025-08-02 11:12:27 +08:00
jxxghp
9d4fd16d81 优化透明主题下的模糊度和透明度设置 2025-07-29 11:49:59 +08:00
jxxghp
3b16e7a123 优化透明主题的模糊度和透明度设置 2025-07-29 09:46:23 +08:00
jxxghp
1c4a2176e9 实现透明主题的透明度和模糊度设置功能 2025-07-29 08:20:16 +08:00
jxxghp
62f9243714 更新 service-worker.ts 2025-07-29 07:05:17 +08:00
jxxghp
03bd23d314 更新文件系统资源检查的相关提示信息 2025-07-26 23:11:29 +08:00
jxxghp
27497d1812 更新 SiteAddEditDialog.vue 2025-07-26 08:34:22 +08:00
jxxghp
f36c1bd2b5 整合主题管理器,优化主题切换逻辑 2025-07-25 13:39:47 +08:00
jxxghp
cf72b2cdb9 更新加载动画的样式和逻辑。 2025-07-23 20:33:02 +08:00
jxxghp
44f6950fea Merge pull request #376 from wumode/fix_recommend
fix: 修复推荐页面外部推荐源URL参数拼接问题
2025-07-23 20:24:32 +08:00
wumode
308ddfedea fix: 修复推荐页面外部推荐源URL参数拼接问题 2025-07-23 20:12:36 +08:00
jxxghp
ac7c330e2f 优化工作流卡片和对话框中的事件类型显示逻辑 2025-07-23 15:33:43 +08:00
jxxghp
1bde3492da 更新 package.json 2025-07-23 12:01:03 +08:00
jxxghp
f884518df3 优化工作流任务卡片的状态显示 2025-07-23 11:52:54 +08:00
jxxghp
1f7f9ce9db 新增工作流触发类型和事件类型支持 2025-07-22 20:58:55 +08:00
jxxghp
58acde2292 优化支持站点的显示逻辑 2025-07-21 12:49:55 +08:00
jxxghp
4e0fe2f449 更新 AccountSettingAbout.vue 2025-07-21 12:38:37 +08:00
jxxghp
536793ab25 新增支持站点折叠功能,并更新相关国际化文本 2025-07-21 11:53:29 +08:00
jxxghp
23a48e07a2 优化订阅列表视图的状态筛选逻辑 2025-07-21 09:57:10 +08:00
jxxghp
1e55557154 优化订阅列表视图的状态筛选逻辑 2025-07-21 09:53:38 +08:00
jxxghp
752231086d 新增订阅功能的状态筛选选项 2025-07-21 09:38:59 +08:00
jxxghp
6f315a408a 移除站点链接的 href 属性 2025-07-20 15:40:58 +08:00
jxxghp
6fa4caa85e fix https://github.com/jxxghp/MoviePilot/issues/4635 2025-07-20 12:34:22 +08:00
jxxghp
1b36c1752f 优化消息弹窗的滚动逻辑 2025-07-20 08:39:33 +08:00
jxxghp
cd58498971 加载消息时按时间排序以确保最新消息在最后 2025-07-20 08:32:10 +08:00
jxxghp
1586137a5d 优化离线状态管理逻辑 2025-07-20 08:25:20 +08:00
jxxghp
6cb8bf74df 在滚动锁定功能中添加事件传播停止,以增强用户体验 2025-07-19 17:45:43 +08:00
jxxghp
787802d0db 优化模块测试视图 2025-07-19 08:55:08 +08:00
jxxghp
b4ad39db12 优化全局滚动锁定功能 2025-07-18 16:39:25 +08:00
jxxghp
c13edbe017 更新 package.json 2025-07-18 11:07:29 +08:00
jxxghp
7546da4f90 新增订阅分享页面及相关搜索功能 2025-07-18 11:05:05 +08:00
jxxghp
76b9a8d9e7 新增支持站点查看功能 2025-07-17 20:46:46 +08:00
jxxghp
d6d52338e9 优化排名展示效果 2025-07-16 12:59:09 +08:00
jxxghp
caa67a0f49 新增订阅分享统计功能 2025-07-16 09:37:34 +08:00
jxxghp
6ddc3ea996 fix #375 2025-07-15 20:25:42 +08:00
jxxghp
7edbf7c724 更新用户资料和账户设置中的链接 2025-07-15 17:31:15 +08:00
jxxghp
4f233ca886 更新 package.json 版本号至 2.6.6 2025-07-15 14:54:49 +08:00
jxxghp
457831536a 移除AccountSettingSite.vue中的USER_AGENT字段 2025-07-14 12:30:54 +08:00
jxxghp
ccef0d87db 更新缓存版本至v1.0.3 2025-07-13 13:52:16 +08:00
jxxghp
584d290283 增强全局滚动锁定功能 2025-07-13 13:46:28 +08:00
jxxghp
2ab14fa33b fix 2025-07-13 13:35:25 +08:00
jxxghp
f0317e1d74 为明亮主题优化Footer组件的背景色透明度 2025-07-13 13:32:05 +08:00
jxxghp
17a206e0f4 更新 DownloadingCard.vue 2025-07-13 11:40:23 +08:00
jxxghp
8ea352cc2f 优化DownloadingCard组件 2025-07-13 11:31:26 +08:00
jxxghp
0f10920898 fix #374 2025-07-13 11:22:27 +08:00
jxxghp
eb098ca775 增强滚动锁定功能 2025-07-13 09:46:38 +08:00
jxxghp
e25caddfef 更新 package.json 2025-07-12 15:15:48 +08:00
jxxghp
c74cf6cf6e 移除构建Plex深度链接时的警告弹窗 2025-07-12 15:13:18 +08:00
jxxghp
ce2d04fa64 更新Plex深度链接构建逻辑 2025-07-12 15:04:36 +08:00
jxxghp
40a4e29c7e 重构深度链接功能 2025-07-12 14:57:03 +08:00
jxxghp
60385715e6 新增媒体服务器深度链接功能 2025-07-12 13:47:00 +08:00
jxxghp
3cce92e83d 优化媒体查询条件,增强响应式样式支持 2025-07-12 13:16:30 +08:00
jxxghp
602b0067d2 Merge pull request #373 from jtcymc/v2 2025-07-12 07:17:57 +08:00
shaw
51d07db99b refactor(dialog): 将日志输出级别从 log 改为 warn
- 在 SubscribeEditDialog.vue 和 SubscribeSeasonDialog.vue 组件中- 当 tmdbid 未设置或为空时,使用 console.warn替代 console.log
- 此修改提高了日志的可见性和严重性级别,以便更好地提醒开发者注意潜在问题
2025-07-12 00:01:55 +08:00
shaw
33d121fd64 fix(dialog): 修复剧集分组查询时 TMDBID 未设置或为空的问题
- 在 SubscribeEditDialog 和 SubscribeSeasonDialog 组件中添加了对 TMDBID 的空值检查
- 如果 TMDBID 未设置或为空,将不会执行剧集分组查询,避免出现错误
2025-07-11 23:57:01 +08:00
jxxghp
e409dbd5b8 优化可用高度计算 2025-07-11 15:14:16 +08:00
jxxghp
79d203470a 更新 service-worker.ts 2025-07-11 07:26:01 +08:00
jxxghp
0f1341615b fix size 2025-07-11 07:06:46 +08:00
jxxghp
97f5410b1c Add files via upload 2025-07-10 23:31:48 +08:00
jxxghp
195f6b7e50 优化卡片组件样式 2025-07-10 22:45:59 +08:00
jxxghp
6691f40c49 优化卡片组件样式 2025-07-10 22:00:06 +08:00
jxxghp
bc1849f0a0 fix 全屏弹窗背景 2025-07-10 21:06:54 +08:00
jxxghp
0f64ea1403 为垂直导航布局的固定导航栏添加内边距,以改善滚动时的视觉效果 2025-07-10 17:29:48 +08:00
jxxghp
320fc1604c 更新 SubscribeListView.vue 中的 Teleport 组件条件,以支持根据订阅类型动态渲染 2025-07-10 16:54:00 +08:00
jxxghp
a8eaf3b995 移除 vite.config.ts 中的缓存键处理逻辑以提高代码简洁性 2025-07-10 16:50:35 +08:00
jxxghp
308a951f78 修正 Vuetify 变量路径为相对路径 2025-07-10 16:40:49 +08:00
jxxghp
9f98b549e9 重构scss文件结构 2025-07-10 16:39:22 +08:00
jxxghp
0e2a259999 优化 WorkflowShareCard 组件 2025-07-10 15:08:10 +08:00
jxxghp
b3d3561111 将 useDialogScrollLock 替换为 useScrollLock 2025-07-10 12:56:51 +08:00
jxxghp
ad857b0810 删除 useScrollLock 组合式 API 2025-07-10 12:52:57 +08:00
jxxghp
0918fa1685 将所有 VDialog 组件替换为 DialogWrapper 组件 2025-07-10 12:44:37 +08:00
jxxghp
273d1f8ef2 更新 _misc.scss 文件,调整媒体查询条件 2025-07-10 11:25:59 +08:00
jxxghp
af1e0a2a60 在 PWAInstallPrompt.vue 中添加 HTTPS 环境检查 2025-07-10 10:59:02 +08:00
jxxghp
79ae772367 优化垂直导航栏样式 2025-07-09 19:53:14 +08:00
jxxghp
d57c8aa305 更新 FetchTorrentsAction.vue 2025-07-09 14:39:20 +08:00
jxxghp
bbd8c1b6d4 更新 yarn.lock 2025-07-09 13:17:30 +08:00
jxxghp
ced9288ed7 优化浏览器警告 2025-07-09 13:16:56 +08:00
jxxghp
cf87e2d5ac 添加工作流备注功能 2025-07-09 12:22:08 +08:00
jxxghp
153d4c1d01 更新工作流分享卡片和对话框 2025-07-09 11:44:52 +08:00
jxxghp
1c50fa228e 更新 ForkWorkflowDialog.vue 2025-07-09 11:28:02 +08:00
jxxghp
0067dc6be3 更新 package.json 2025-07-09 11:17:15 +08:00
jxxghp
36389a5b8c 优化 PWA 状态恢复逻辑 2025-07-09 11:07:24 +08:00
jxxghp
c7443d993e 更新工作流分享功能 2025-07-09 10:46:33 +08:00
jxxghp
9f8dbf3c75 fix workflow 2025-07-09 00:11:19 +08:00
jxxghp
35332544e4 Add workflow sharing functionality (#371)
* Add workflow sharing feature with share, fork, and browse functionality

Co-authored-by: jxxghp <jxxghp@163.com>

* Refactor workflow page with dynamic tabs and internationalization support

Co-authored-by: jxxghp <jxxghp@163.com>

* Remove workflow share implementation documentation

Co-authored-by: jxxghp <jxxghp@163.com>

* Fix indentation and structure in Chinese locale files

Co-authored-by: jxxghp <jxxghp@163.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-08 23:31:22 +08:00
jxxghp
f2bc832aca rollback service.js 2025-07-08 22:17:41 +08:00
jxxghp
a6847f7f53 Merge pull request #369 from jxxghp/cursor/fix-pwa-install-prompt-text-display-14c8
Fix PWA install prompt text display
2025-07-08 14:36:18 +08:00
Cursor Agent
396ab64874 Refactor PWA install steps to use dynamic translation keys
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-08 06:19:07 +00:00
jxxghp
59ee3d8ceb fix 订阅历史按钮 2025-07-08 13:48:45 +08:00
jxxghp
3e152bd389 优化 PWAInstallPrompt 组件中的文本提取逻辑 2025-07-08 13:37:18 +08:00
jxxghp
56e8f61bbf 将 PWAInstallPrompt 组件中的 div 替换为 VCard 2025-07-08 13:26:12 +08:00
jxxghp
83c00b0544 fix PWAInstallPrompt 2025-07-08 13:03:11 +08:00
jxxghp
5f82cc715e 优化 PWA 安装提示组件 2025-07-07 23:18:32 +08:00
jxxghp
3ce7fc34f0 fix PWAInstallPrompt 2025-07-07 23:11:27 +08:00
jxxghp
9fc5291fec 优化服务工作者 2025-07-07 22:56:36 +08:00
jxxghp
27c7a842db Merge pull request #367 from jxxghp/cursor/fix-sync-queue-data-corruption-issues-ee60
Cursor/fix sync queue data corruption issues ee60
2025-07-07 22:49:17 +08:00
jxxghp
ffe1992df1 Merge pull request #368 from jxxghp/cursor/fix-ios-detection-inconsistency-in-pwa-2971
Fix iOS detection inconsistency in PWA
2025-07-07 22:48:57 +08:00
Cursor Agent
a80877bab7 Fix PWA install detection on iOS with additional check for MSStream
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-07 14:48:00 +00:00
jxxghp
c787a3c786 Merge pull request #366 from jxxghp/cursor/fix-pwa-support-detection-bug-0f65 2025-07-07 22:43:47 +08:00
Cursor Agent
abda382b96 Refactor service worker types and extract type definitions
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-07 14:35:25 +00:00
Cursor Agent
c5ab0a2cc6 Refactor IndexedDB sync mechanism with dedicated store and improved handling
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-07 14:32:55 +00:00
Cursor Agent
15340dd550 Improve PWA install support detection for various platforms
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-07 14:25:12 +00:00
jxxghp
deaf444864 Merge pull request #364 from jxxghp/cursor/evaluate-app-shell-model-compliance-1502
Evaluate app shell model compliance
2025-07-07 22:09:11 +08:00
Cursor Agent
a5413d1116 Remove PWA optimization documentation files
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-07 14:05:12 +00:00
Cursor Agent
6cb6a5822b Implement PWA optimizations with advanced caching and install features
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-07 13:46:09 +00:00
Cursor Agent
2ffd6f7430 Enhance PWA caching strategy with offline support and optimization docs
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-07 13:35:58 +00:00
jxxghp
cd9eaf4fd7 fix Teleport 2025-07-07 17:24:58 +08:00
jxxghp
3cfe27b7b3 fix 2025-07-07 15:17:49 +08:00
jxxghp
44d78fd2ea Merge pull request #363 from cddjr/fix_top_level_await 2025-07-07 14:07:20 +08:00
jxxghp
0cf3342449 重构PWA状态管理 2025-07-07 14:05:11 +08:00
景大侠
7e4c6516c5 修复 Safari14兼容Top-level await特性
v2.4.4引入的问题
2025-07-07 13:57:35 +08:00
jxxghp
73d7eb65b8 移除可见性状态管理器 2025-07-07 11:32:38 +08:00
jxxghp
fca4afb606 优化PWA状态管理 2025-07-07 11:28:57 +08:00
jxxghp
b15672d593 fix 2025-07-07 11:19:45 +08:00
jxxghp
7a37a18f23 修复下拉快速访问 2025-07-07 11:17:01 +08:00
jxxghp
a14806e840 Merge pull request #361 from jxxghp/cursor/enhance-pwa-state-restoration-features-24b1 2025-07-07 07:50:41 +08:00
Cursor Agent
bbd2851f36 Improve element selector generation for scroll position tracking
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 23:34:58 +00:00
Cursor Agent
48418771d4 Remove PWA state management documentation files
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 23:27:03 +00:00
Cursor Agent
a81071a50a Refactor PWA state management to simplify and streamline implementation
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 23:24:31 +00:00
Cursor Agent
304b990994 Implement lightweight PWA state management with zero-overhead approach
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 23:14:50 +00:00
Cursor Agent
8824869cd1 Enhance PWA state management with advanced scroll, form, and modal tracking
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 23:03:42 +00:00
jxxghp
325cce5f82 Merge pull request #360 from jxxghp/cursor/analyze-factors-causing-ios-to-kill-pwa-ac82
Analyze factors causing iOS to kill PWA
2025-07-07 06:40:43 +08:00
jxxghp
85db26a704 Merge branch 'v2' into cursor/analyze-factors-causing-ios-to-kill-pwa-ac82 2025-07-06 23:41:42 +08:00
Cursor Agent
65b0acdcb4 Refactor data refresh mechanism with conditional timer support
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 15:29:44 +00:00
Cursor Agent
9a27af8c5a Bump version to 2.6.3 and remove optimization documentation files
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 15:13:08 +00:00
jxxghp
93ad0859e8 重构PWA状态管理,统一检测方法并优化状态恢复逻辑 2025-07-06 23:04:34 +08:00
Cursor Agent
5e62bac245 Implement background optimization composable for data refresh and SSE
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 15:01:17 +00:00
Cursor Agent
bea6c1e326 Optimize PWA background performance with SSE and timer management
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 14:52:58 +00:00
Cursor Agent
df76b01826 Add background and SSE managers for improved app lifecycle management
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 14:36:31 +00:00
jxxghp
5d22cb84bf 更新 package.json 2025-07-06 20:03:25 +08:00
jxxghp
f01c61e09f 更新 App.vue 2025-07-06 19:52:37 +08:00
jxxghp
d50e67f3bc Merge pull request #359 from jxxghp/cursor/pwa-5007
分析PWA状态切换体验问题
2025-07-06 18:35:53 +08:00
Cursor Agent
3726c472fc Remove console logs for silent PWA state restoration optimization
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 10:31:35 +00:00
Cursor Agent
dc174e81cf Optimize PWA state restoration for seamless, silent background switching
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 10:14:40 +00:00
Cursor Agent
c9867bc453 Optimize PWA state restoration and loading experience
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 08:30:12 +00:00
Cursor Agent
8e282fb216 Add PWA performance analysis report for background-to-foreground experience
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 07:22:01 +00:00
jxxghp
e9c0792cb3 Merge pull request #358 from jxxghp/cursor/prevent-ios-from-killing-pwa-background-245b
fix: PWA状态管理器初始化在DOM已加载时失败的问题
2025-07-06 15:04:45 +08:00
Cursor Agent
e7e1b4c43f fix: PWA状态管理器初始化在DOM已加载时失败的问题
- 修复DOMContentLoaded事件监听器可能不触发的问题
- 检查document.readyState状态,如果DOM已就绪则立即初始化
- 确保PWA状态管理器在所有情况下都能正确初始化
- 解决main.ts作为模块加载时的时序问题
2025-07-06 06:58:15 +00:00
jxxghp
dc56c177b7 Merge pull request #356 from jxxghp/cursor/prevent-ios-from-killing-pwa-background-245b 2025-07-06 14:54:36 +08:00
Cursor Agent
c0ee998874 feat: 实现PWA状态管理防止iOS后台被杀
- 添加多层存储策略(localStorage + sessionStorage + IndexedDB + Service Worker缓存)
- 实现智能状态恢复决策机制
- 自动监听页面生命周期事件进行状态保存和恢复
- 支持表单数据、滚动位置、UI状态的自动保存
- 专为iOS设备PWA优化,解决后台被杀导致状态丢失的问题
- 版本号更新至 2.6.3
2025-07-06 06:52:18 +00:00
Cursor Agent
e1ff50e1e3 Refactor usePWAState and useGlobalPWAState composables
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 06:50:30 +00:00
Cursor Agent
0e440955c8 Implement PWA state management for improved iOS background persistence
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 06:44:06 +00:00
Cursor Agent
a16dd497c4 Add comprehensive PWA state management solution for iOS background handling
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-06 06:34:25 +00:00
jxxghp
5aa4e9339d Merge pull request #355 from jxxghp/cursor/fix-sse-issues-in-public-service-js-b303 2025-07-06 08:10:30 +08:00
Cursor Agent
723fa96519 Refactor SSE proxy middleware for improved event streaming support
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-05 23:59:58 +00:00
Cursor Agent
75252fded6 Refactor proxy middleware for improved SSE and API request handling
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-05 23:56:51 +00:00
jxxghp
51fbcdfa56 更新 Footer.vue 2025-07-06 00:51:44 +08:00
jxxghp
61c9b97d70 优化垂直导航布局和动态标签页的样式 2025-07-06 00:43:06 +08:00
jxxghp
23b09d09ce 优化移动端Header UI 2025-07-06 00:43:06 +08:00
jxxghp
a00f6ab8ff 更新 HeaderTab.vue 2025-07-05 21:56:28 +08:00
jxxghp
bb59095bad 优化 HeaderTab 组件的模糊背景样式 2025-07-05 20:47:18 +08:00
jxxghp
da57124d5e Merge pull request #354 from jxxghp/cursor/add-blurred-background-to-headertab-d515 2025-07-05 17:31:17 +08:00
Cursor Agent
a00800a128 Remove demo.html file with header tab scroll effect example
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-05 09:24:44 +00:00
Cursor Agent
a98db1699d Create demo page for HeaderTab scroll transparency effect
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-05 09:21:25 +00:00
Cursor Agent
e3d9e736ad Add blur background effect on header tab when scrolling
Co-authored-by: jxxghp <jxxghp@163.com>
2025-07-05 09:12:18 +00:00
jxxghp
28f38d8b80 Fix tab selection on discovery page (#353)
* Fix: Ensure first tab is selected when no valid tab is active

Co-authored-by: jxxghp <jxxghp@163.com>

* Fix discover page tab selection logic when no tab is active

Co-authored-by: jxxghp <jxxghp@163.com>

* Fix tab selection logic in discover page lifecycle hooks

Co-authored-by: jxxghp <jxxghp@163.com>

* Remove unnecessary onMounted hook in discover page

Co-authored-by: jxxghp <jxxghp@163.com>

* Move dynamic header tab registration after tabs initialization

Co-authored-by: jxxghp <jxxghp@163.com>

* Refactor dynamic header tab registration in discover page

Co-authored-by: jxxghp <jxxghp@163.com>

* Fix tab selection logic in discover page to ensure first tab is selected

Co-authored-by: jxxghp <jxxghp@163.com>

* Update useDynamicHeaderTab to sync modelValue in tab configuration

Co-authored-by: jxxghp <jxxghp@163.com>

* Remove auto-tab selection logic from discover page

Co-authored-by: jxxghp <jxxghp@163.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-05 16:41:24 +08:00
jxxghp
3b7c34258f 更新 _misc.scss 2025-07-05 14:46:54 +08:00
jxxghp
9dde646695 修改 tab-header 的圆角样式 2025-07-05 14:34:30 +08:00
jxxghp
4bdee63f28 优化动态标签页注册逻辑 2025-07-05 14:26:57 +08:00
jxxghp
20dced021d 优化动态标签页注册逻辑 2025-07-05 14:06:00 +08:00
jxxghp
17cf640e23 优化 PageContentTitle 组件的样式,调整了边距;在 discover 页面中引入 useDynamicHeaderTab 组合函数;移除 UserListView 中未使用的 useDisplay 导入。 2025-07-05 12:22:08 +08:00
jxxghp
24369daea0 v2.6.2 2025-07-05 12:13:53 +08:00
jxxghp
873bf905ab 优化动态标签页注册逻辑 2025-07-05 12:13:53 +08:00
jxxghp
da0756adf0 动态Tab组件 2025-07-05 12:13:53 +08:00
jxxghp
09942ec946 更新 SubscribeEditDialog.vue 2025-07-05 09:21:25 +08:00
jxxghp
2650bc6068 添加离线状态管理和网络请求处理 2025-07-05 08:23:06 +08:00
jxxghp
6bd7274c9c Update index.html 2025-07-05 06:49:58 +08:00
jxxghp
129ccf9e39 更新 index.html 2025-07-05 06:43:38 +08:00
jxxghp
e2b789cfbc 优化加载动画逻辑 2025-07-04 21:26:44 +08:00
jxxghp
bb70e91277 重构服务工作者逻辑优 2025-07-04 18:32:04 +08:00
jxxghp
f6c07a29ce 更新服务工作者逻辑 2025-07-04 17:30:01 +08:00
jxxghp
4347983fc7 更新vite.config.ts,扩展缓存策略以支持更多文件类型和API请求 2025-07-04 16:57:51 +08:00
jxxghp
12b463d9e8 更新vite.config.ts,增加页面缓存配置 2025-07-04 16:39:31 +08:00
jxxghp
edc0949bed 移除全局设置store并更新引用路径 2025-07-04 16:21:05 +08:00
jxxghp
85780917c2 整合全局设置store,优化PWA模式检测 2025-07-04 16:19:50 +08:00
jxxghp
e45919cac1 优化PWA支持 2025-07-04 13:33:06 +08:00
jxxghp
c61821ef4e 在App.vue中优化加载动画逻辑,移除不必要的延迟 2025-07-04 12:12:13 +08:00
jxxghp
011902598b 在App.vue中添加主题支持以配置ApexCharts 2025-07-04 08:12:10 +08:00
jxxghp
3186c6ca0e 更新 AnalyticsNetwork.vue 2025-07-03 22:12:28 +08:00
jxxghp
3a680a132f 添加可拖拽排序功能 2025-07-03 20:05:08 +08:00
jxxghp
455dda54e8 添加存储后自动保存 2025-07-03 19:57:29 +08:00
jxxghp
5ea5ab07d9 移除WorkflowActionsDialog组件中的VSpacer元素 2025-07-03 19:50:04 +08:00
jxxghp
35c8025b00 在仪表板中添加网络流量组件 2025-07-03 19:14:31 +08:00
jxxghp
615c162663 插件图标使用缓存 2025-07-03 17:09:56 +08:00
jxxghp
c4bd15e5a0 fix storage save 2025-07-03 15:41:44 +08:00
jxxghp
edc92905f7 在MediaInfoCard组件中添加web_source信息的显示 2025-07-03 14:02:53 +08:00
jxxghp
bf5bbd3689 添加SMB网络共享支持 2025-07-03 12:43:42 +08:00
jxxghp
eb70ca233b 重构DefaultLayout.vue组件 2025-07-03 08:48:44 +08:00
jxxghp
8718816fce 将多个组件中的VFab按钮包裹在Teleport中,以确保在移动设备上正确显示 2025-07-03 07:18:31 +08:00
jxxghp
7d36330b4b 在PluginDataDialog组件中添加show_switch属性的绑定 2025-07-02 21:55:02 +08:00
jxxghp
1fa0474fef 调整DownloaderCard、MediaServerCard和StorageCard组件中图标的上边距 2025-07-02 21:49:42 +08:00
jxxghp
4070b27148 调整QuickAccess.vue组件的过渡时间为0.6秒 2025-07-02 21:39:41 +08:00
jxxghp
3892b0ed05 添加PluginDataDialog组件的show_switch属性 2025-07-02 21:30:44 +08:00
jxxghp
a06cf69d7a 优化QuickAccess.vue组件样式 2025-07-02 20:43:33 +08:00
jxxghp
61dc2568e8 优化快速访问组件 2025-07-02 20:28:58 +08:00
jxxghp
ac6362e698 更新 QuickAccess.vue 2025-07-02 17:55:19 +08:00
jxxghp
94afdf5495 更新样式和布局 2025-07-02 17:41:58 +08:00
jxxghp
d96f8acdbc 优化默认布局和快速访问组件 2025-07-02 17:12:14 +08:00
jxxghp
d39c795f92 更新快速访问组件的导入方式 2025-07-02 16:11:12 +08:00
jxxghp
8e12e0562b 更改快速访问组件的导入路径 2025-07-02 16:08:27 +08:00
jxxghp
7a1babb418 重构插件快速访问组件 2025-07-02 16:07:18 +08:00
jxxghp
8d65f0c2a8 优化快速访问插件的下拉手势逻辑 2025-07-02 15:59:11 +08:00
jxxghp
b8dff560f0 添加插件快速访问功能,支持下拉手势触发 2025-07-02 14:18:58 +08:00
jxxghp
b48c26ee73 调整日历视图的背景颜色 2025-07-02 12:31:30 +08:00
jxxghp
8328e51ae0 调整存储添加逻辑 2025-07-02 08:58:16 +08:00
jxxghp
7070eb8a7d 更改流媒体平台的源芯片背景颜色 2025-07-01 17:32:24 +08:00
jxxghp
d0aa26441c 单独显示流媒体平台 2025-07-01 17:14:03 +08:00
jxxghp
1bba7103c8 调整主题背景颜色为深灰色以提升视觉效果 2025-07-01 12:54:01 +08:00
jxxghp
7f8dd744f2 调整表格和输入框的背景颜色以适应透明主题 2025-07-01 12:39:44 +08:00
jxxghp
2f4a707498 为筛选菜单添加内边距样式 2025-07-01 11:58:57 +08:00
jxxghp
569bc3c8ec 站点添加筛选功能 2025-07-01 11:38:00 +08:00
jxxghp
b01421aa94 优化组件加载逻辑 2025-06-30 20:38:50 +08:00
jxxghp
30d933bd85 更新 package.json 2025-06-30 20:16:14 +08:00
jxxghp
377998335b 简化导航状态管理 2025-06-30 20:14:31 +08:00
jxxghp
21d21aa438 优化图片加载逻辑,添加导航状态管理 2025-06-30 19:55:27 +08:00
jxxghp
18cf1ea3d7 更新 FileList.vue、FileNavigator.vue 和 FileToolbar.vue 中 axios 属性的类型定义为 Function 2025-06-30 19:39:02 +08:00
jxxghp
60ea884fe2 添加全局请求和图片优化器 2025-06-30 17:37:30 +08:00
jxxghp
999fa9d9a6 自定义存储类型添加索引以区分不同的自定义存储 2025-06-29 11:21:40 +08:00
jxxghp
e80034e7f8 更新 package.json 2025-06-29 07:54:18 +08:00
jxxghp
b16f99941a Merge pull request #350 from tbc0309/v2 2025-06-29 07:52:27 +08:00
ERROR204
3503e7d5b1 fix service.js 2025-06-29 03:06:31 +08:00
ERROR204
d1d80acef8 fix service.js 2025-06-29 03:00:25 +08:00
jxxghp
16fe916b07 将 AList 更名为 OpenList 2025-06-28 08:32:36 +08:00
jxxghp
d754c3dae3 更新 NoDataFound 组件 2025-06-27 23:26:43 +08:00
jxxghp
1b32a3e8cd 在消息视图中添加倒序功能 2025-06-27 20:39:15 +08:00
jxxghp
15a6f215b4 更新 TorrentCard.vue 2025-06-27 18:09:55 +08:00
jxxghp
38014ba342 添加发布时间显示功能,并在排序中支持按发布时间排序 2025-06-27 17:43:43 +08:00
jxxghp
7dcc293a09 fix mobile toast 2025-06-27 10:03:18 +08:00
jxxghp
35ce244490 Merge pull request #348 from Aqr-K/fix-progress 2025-06-26 15:47:08 +08:00
Aqr-K
3bade2060a fix(progress): 修复重复点击时,progressEventSource 被覆盖会产生孤儿事件的情况。 2025-06-26 14:31:51 +08:00
jxxghp
f8307f25c9 fix service.js 2025-06-26 12:32:16 +08:00
jxxghp
5c9ebb9aae 为Toast组件添加隐藏进度条选项以优化用户体验 2025-06-25 19:47:24 +08:00
jxxghp
ebc2a764c2 将vue-toast-notification替换为vue-toastification,并更新相关样式和依赖项 2025-06-25 17:42:36 +08:00
jxxghp
bed21856ab 调整背景透明度 2025-06-23 19:57:35 +08:00
jxxghp
61805d13ab 为通知列表添加细 scrollbar 样式以改善用户体验 2025-06-23 11:23:57 +08:00
jxxghp
e47d8d5d2b 修复通知列表的溢出问题 2025-06-23 11:18:55 +08:00
jxxghp
0bd81499f6 更新版本号至2.5.7-1 2025-06-17 20:00:07 +08:00
jxxghp
201ae2c237 fix https://github.com/jxxghp/MoviePilot/issues/4456 2025-06-17 19:59:23 +08:00
jxxghp
df4c3c7676 fix https://github.com/jxxghp/MoviePilot/issues/4455 2025-06-16 14:07:45 +08:00
jxxghp
667693902f 为MessageCard组件添加最小高度属性 2025-06-16 12:29:24 +08:00
jxxghp
9e261d30f8 v2.5.7 2025-06-16 11:52:21 +08:00
jxxghp
5f6bade809 为MessageCard组件添加图片加载占位符 2025-06-16 11:51:34 +08:00
jxxghp
273168ae5c 优化消息滚动逻辑 2025-06-15 13:40:01 +08:00
jxxghp
a55269e9e6 优化消息滚动逻辑 2025-06-15 08:05:05 +08:00
jxxghp
9c386f8533 为MessageCard组件添加图片加载事件,更新MessageView以处理图片加载完成后的滚动事件 2025-06-15 07:59:35 +08:00
jxxghp
17ee5f456a 实现未读消息的全局事件处理 2025-06-14 14:17:26 +08:00
jxxghp
6cefdb5d37 调整应用启动时的延迟时间和重试机制 2025-06-14 13:16:51 +08:00
jxxghp
74fc8bd131 优化插件和订阅的加载顺序配置,移除本地存储逻辑,增加错误处理 2025-06-14 11:08:42 +08:00
jxxghp
aa9dab5d96 优化未读消息弹窗的打开逻辑 2025-06-14 10:11:41 +08:00
jxxghp
5b461f8e1f 更新网络测试视图 2025-06-14 08:13:44 +08:00
jxxghp
bde06be3df Merge pull request #347 from cddjr/feat_nettest
feat 网络测试支持GitHub加速代理、新增pip测试
2025-06-14 07:57:19 +08:00
jxxghp
fe17986b2a 更新桌面图标徽章的逻辑 2025-06-14 07:52:55 +08:00
景大侠
e9160ecefd feat 网络测试支持GitHub加速代理、新增pip测试 2025-06-13 18:38:59 +08:00
jxxghp
05ebd48f09 Merge pull request #346 from wumode/fix_download_api 2025-06-13 14:25:18 +08:00
wumode
6dbc3f4bab fix: 无法设置非默认下载器状态 2025-06-13 08:50:54 +08:00
jxxghp
bc7166789b 更新 package.json 2025-06-12 23:05:11 +08:00
jxxghp
750b91db66 新增未读消息计数和桌面图标徽章更新功能 2025-06-12 22:58:16 +08:00
jxxghp
b69a338e13 fix https://github.com/jxxghp/MoviePilot/pull/4434 2025-06-12 18:42:42 +08:00
jxxghp
036fe65b12 Merge pull request #345 from alfchao/v2 2025-06-12 16:16:09 +08:00
xuchao3
732017ac77 fix:修改清华pip源地址 2025-06-12 11:25:04 +08:00
jxxghp
5bd71b4688 fix https://github.com/jxxghp/MoviePilot/issues/4424#issuecomment-2964853532 2025-06-12 11:13:37 +08:00
jxxghp
44ba2dff78 调整用户编辑对话框的样式 2025-06-11 20:38:20 +08:00
jxxghp
0954e4bde2 新增季NFO相关翻译及设置选项 2025-06-11 19:50:56 +08:00
jxxghp
5b183d31e2 更新 Footer.vue 2025-06-11 19:29:59 +08:00
jxxghp
b2017764eb 更新 Footer.vue 2025-06-11 19:22:07 +08:00
jxxghp
f27cd796b6 调整多个组件的高度计算逻辑 2025-06-11 13:21:42 +08:00
jxxghp
3c051b8698 优化用户信息展示和权限显示逻辑 2025-06-11 12:56:10 +08:00
jxxghp
052d6edd13 更新版本号至 2.5.5 2025-06-11 00:02:26 +08:00
jxxghp
e7dc61e3d9 移除 TOKENIZED_SEARCH 设定 2025-06-11 00:01:34 +08:00
jxxghp
f0aefdfdf8 更新管理描述,明确下载管理和站点管理功能 2025-06-10 23:55:43 +08:00
jxxghp
0beec368b8 重构用户卡片和用户编辑对话框中的权限显示逻辑 2025-06-10 23:52:45 +08:00
jxxghp
3f1d03a127 在多个组件中实现权限管理功能 2025-06-10 23:44:06 +08:00
jxxghp
eb143c28e3 新增用户权限管理功能 2025-06-10 23:25:59 +08:00
jxxghp
1631951a24 优化Footer组件中的按钮图标大小和样式 2025-06-10 22:55:14 +08:00
jxxghp
31bdd89373 更新刮削开关设置界面 2025-06-10 21:21:38 +08:00
jxxghp
ad5ae12d44 新增刮削开关设置功能 2025-06-10 19:56:12 +08:00
jxxghp
c838db262c 优化重启流程,增加重启状态管理和轮询清理逻辑 2025-06-09 21:09:12 +08:00
jxxghp
623b807a11 在插件安装成功后,清空过滤条件以确保数据刷新时的准确性 2025-06-09 20:54:16 +08:00
jxxghp
ce9335a842 优化插件卡片列表视图中的按钮颜色逻辑 2025-06-09 20:47:55 +08:00
jxxghp
1c62465c3e 新增插件市场手动刷新功能 2025-06-09 20:35:05 +08:00
jxxghp
a2c176bdee 新增服务状态检测与轮询功能,优化重启流程,增加超时提示信息 2025-06-09 16:21:31 +08:00
jxxghp
bff8c0f86b 优化注销流程,增加10秒延迟后再执行注销操作 2025-06-09 15:56:03 +08:00
jxxghp
1065973e07 feat:插件筛选运行中插件 2025-06-09 12:49:41 +08:00
jxxghp
8e042d5691 移除内存监控相关设置及其翻译,简化系统设置界面 2025-06-08 18:37:02 +08:00
jxxghp
d9a6b32e5f add apple-touch-icon-precomposed 2025-06-06 21:33:37 +08:00
jxxghp
eed3f97fbf 更新 package.json 2025-06-06 14:04:46 +08:00
jxxghp
6b9a8ed108 feat:内存监控开关 2025-06-06 13:50:09 +08:00
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
jxxghp
af287f50bb 更新 Vite 配置,添加 dummy 远程模块,简化共享库列表,提升代码可读性和维护性。 2025-05-08 23:54:34 +08:00
jxxghp
3199392637 优化 Vite 配置,更新共享库列表,移除不必要的 Rollup 选项,并简化远程模块加载逻辑,提升代码可读性和维护性。 2025-05-08 23:32:13 +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
jxxghp
4e3a61b8a8 增强共享作用域管理,添加初始化和dummy远程模块功能,防止生产环境中的共享作用域问题,同时为远程模块URL添加版本标记以防止缓存。 2025-05-08 23:08:48 +08:00
jxxghp
3b1e65fc75 修复 Vite 配置中的压缩选项,将 drop_console 设置为 false,以保留控制台日志。 2025-05-08 22:53:56 +08:00
jxxghp
32b4b944cc fix remoteEntry url 2025-05-08 22:42:20 +08:00
jxxghp
22a51a524e fix ui 2025-05-08 19:48:35 +08:00
jxxghp
ac0cbbdb95 Merge pull request #331 from Seed680/v2 2025-05-08 17:50:06 +08:00
qiaoyun680
2260f23d3c feat(storge): 添加存储重置选项 2025-05-08 17:39:36 +08:00
jxxghp
d43952c0bf 优化 Vuetify 实例配置,添加默认主题和主题设置,以提升主题管理的灵活性和可维护性。 2025-05-08 17:32:28 +08:00
jxxghp
bd368123d2 更新 package.json 中的版本号,从 2.4.4-1 升级至 2.4.5 2025-05-08 17:26:31 +08:00
jxxghp
cbdd70427e fix 联邦组件样式冲突问题 2025-05-08 17:24:04 +08:00
jxxghp
d7526f5283 移除 Vite 配置中的 PostCSS 插件设置,并在 SiteCard 组件中为按钮添加统一的大小属性,以提升界面一致性。 2025-05-08 16:09:32 +08:00
jxxghp
08e914a968 优化 SiteCard 组件,调整按钮样式和图标大小,提升界面一致性和可读性。 2025-05-08 15:53:32 +08:00
jxxghp
53a8835b6d 更新 Tailwind CSS 配置,添加重要性设置;在 Vite 配置中添加 PostCSS 插件;优化多个组件的 VCard 结构,移除多余的 class 属性以提升代码整洁性。 2025-05-08 15:31:57 +08:00
jxxghp
e3bff71a91 优化 Vue 渲染模式下的组件结构,将动态组件包裹在 VCard 和 VCardText 中,以提升布局一致性和可读性,同时在 DashboardElement.vue 中为动态插件组件添加 API 属性。 2025-05-08 14:48:33 +08:00
jxxghp
6276009e88 修复文件浏览器工具栏中的“向上”按钮,仅在路径段存在时显示 2025-05-08 13:13:49 +08:00
jxxghp
ddc5320f71 fix https://github.com/jxxghp/MoviePilot/issues/4211#issuecomment-2858674237 订阅排序相互干扰的问题 2025-05-08 10:35:07 +08:00
jxxghp
15af66aaaf fix 资源筛选 2025-05-08 07:11:29 +08:00
jxxghp
fe7a080553 更新插件组件的依赖版本 2025-05-07 17:25:59 +08:00
jxxghp
66bfc3e868 更新 Vite 配置 2025-05-07 17:16:39 +08:00
jxxghp
93aa3fb95d 更新示例 2025-05-07 16:20:28 +08:00
jxxghp
4f5caf1712 更新 module-federation-guide.md 2025-05-07 15:45:42 +08:00
jxxghp
9d27e967cd 优化示例UI 2025-05-07 14:49:24 +08:00
jxxghp
eb3e035a7c 更新 Vite 配置以支持 vuetify/styles 的版本要求,优化共享依赖配置 2025-05-07 14:34:10 +08:00
jxxghp
04200e94ff 移除 Config.vue 和 Dashboard.vue 中的样式,更新 Page.vue 以使用 API 获取最近记录,增强组件功能和代码整洁性。 2025-05-07 13:28:42 +08:00
jxxghp
ae9a13e0fa 添加“综合筛选”和“清除全部”功能 2025-05-07 12:22:54 +08:00
jxxghp
df8857fb52 fix example 2025-05-07 10:56:28 +08:00
jxxghp
9642fed1f1 移除 DashboardItem 接口中的 component_url 属性,简化类型定义。 2025-05-07 10:56:28 +08:00
jxxghp
1a273ea2d6 更新 module-federation-guide.md 2025-05-07 09:32:41 +08:00
jxxghp
c0ba921a7e 更新 Config.vue 2025-05-07 09:31:58 +08:00
jxxghp
8bbad227eb 更新 Page.vue 2025-05-07 09:31:29 +08:00
jxxghp
d3f9c04209 更新插件组件文档,调整多个组件以支持关闭功能,增强用户交互体验,并修正配置示例以反映最新的代码结构和依赖关系。 2025-05-07 08:21:44 +08:00
jxxghp
d3a6703a77 添加关闭页面的功能,更新多个组件以支持新的事件通知机制,并调整文档以反映最新的功能变化,提升用户交互体验。 2025-05-07 06:59:53 +08:00
jxxghp
1100fa47be 更新 PluginConfigDialog.vue 2025-05-07 00:25:19 +08:00
jxxghp
1e33087786 更新插件组件文档 2025-05-07 00:05:25 +08:00
jxxghp
e59423e912 将样式标签修改为 scoped,以提高样式的局部作用域,增强组件的样式隔离性。 2025-05-07 00:00:02 +08:00
jxxghp
146a1fe23d 更新多个组件以支持新的事件通知机制,添加切换到配置页面的功能,调整文档以反映组件文件名的变化,提升用户交互体验。 2025-05-06 23:56:28 +08:00
jxxghp
4586f6982a 优化多个组件的远程加载逻辑,移除不必要的属性绑定,增强错误处理机制,提升用户体验。 2025-05-06 23:31:37 +08:00
jxxghp
703204c69a 优化 Vite 配置,移除不再使用的代理规则,更新多个组件以增强远程组件加载逻辑,添加错误处理和加载状态显示,提升用户体验。 2025-05-06 21:34:49 +08:00
jxxghp
05cc160311 在 Vite 配置中添加新的代理规则,支持 '/plugin_static' 路径的请求转发至本地 API 2025-05-06 13:16:35 +08:00
jxxghp
0568f8a85d 更新 Vite 配置,修改主机名称为 'MoviePilot',优化插件配置,调整共享依赖格式以简化配置。 2025-05-06 13:00:36 +08:00
jxxghp
36b113ef1c 更新 module-federation-guide.md 2025-05-06 11:58:01 +08:00
jxxghp
520180f6f5 更新模块联邦文档,调整远程组件API路径格式,优化组件加载逻辑,移除不必要的注册步骤,增强代码可读性。 2025-05-06 11:44:08 +08:00
jxxghp
d349d2b500 增强模块联邦支持,添加动态导入远程模块的声明,更新示例项目以展示新组件结构和配置,调整 Vite 配置以支持更灵活的远程组件加载。 2025-05-06 08:53:33 +08:00
jxxghp
643ca35aed 增强远程组件注册机制,添加动态注册和获取功能,更新相关文档以指导插件开发者。调整多个组件以支持新注册逻辑。 2025-05-05 22:13:07 +08:00
jxxghp
36ef7ba589 更新 README 文档,调整开发和配置部分的标题格式,以提高可读性和一致性。 2025-05-05 21:44:22 +08:00
jxxghp
b5761bd18d 更新组件声明,移除 LocaleSwitcher 和 ThemeSwitcher,更新 README 文档以增强模块联邦功能的描述,并调整 Vite 配置以支持 ESNext 目标。 2025-05-05 21:41:03 +08:00
jxxghp
047e827884 添加 @originjs/vite-plugin-federation 依赖,并在多个组件中实现远程组件加载功能 2025-05-05 21:26:53 +08:00
jxxghp
48828fd72d 更新消息模板标题,添加国际化支持 2025-05-05 20:34:13 +08:00
jxxghp
3f4165e4b1 更新国际化文件,添加消息模板相关文本,并优化模板编辑器界面 2025-05-05 20:31:15 +08:00
jxxghp
6d789ed73b Merge pull request #330 from wikrin/template
feat(setting): 添加消息推送自定义选项
2025-05-05 19:30:42 +08:00
Attente
e77297f7b2 feat(setting): 添加消息推送自定义选项
- wikrin/MoviePilot@20c1f30
2025-05-05 05:43:48 +08:00
jxxghp
bb52a4704f 更新 zh-TW.ts 2025-05-04 14:44:13 +08:00
jxxghp
127df15674 更新 zh-CN.ts 2025-05-04 14:43:45 +08:00
jxxghp
95bcc263e8 更新 en-US.ts 2025-05-04 14:43:04 +08:00
jxxghp
20b120c247 在 PluginConfigDialog.vue 和 PluginDataDialog.vue 中,将错误提示从 $toast 更改为 console.error,以便更好地调试组件加载失败的情况,并在错误信息中添加详细的错误描述。 2025-05-03 23:29:24 +08:00
jxxghp
e644f6bacc 更新 DashboardElement.vue 2025-05-03 22:55:03 +08:00
jxxghp
5e9c7124ce 更新 PluginConfigDialog.vue 2025-05-03 22:54:19 +08:00
jxxghp
84e121bc0e 更新 PluginDataDialog.vue 2025-05-03 22:53:32 +08:00
jxxghp
abff2071bd 在多个组件中优化动态加载逻辑,使用 API 获取组件并处理加载失败情况,以提升用户体验和代码健壮性 2025-05-03 22:31:38 +08:00
jxxghp
078afd5174 在多语言文件中添加管理员和用户ID列表的占位符文本,以提升用户输入体验 2025-05-03 21:36:38 +08:00
jxxghp
4a8cf16012 在多个组件中添加渲染模式支持,优化插件配置和数据加载逻辑,增强用户体验和代码可读性 2025-05-03 10:04:50 +08:00
jxxghp
04e9b68e4a 在 custom.scss 中添加 .match-height.v-row 样式,确保 .v-card 组件的高度为 100%,以提升布局一致性 2025-05-02 22:03:17 +08:00
jxxghp
f12c3dac9f 在 FileBrowser.vue 组件中,更新存储图标逻辑,添加默认图标以提升用户体验 2025-05-02 21:50:08 +08:00
jxxghp
73b9663b27 优化 resource.vue 中搜索标题栏的样式,调整字体类名和布局,以提升界面美观性和一致性 2025-05-02 21:14:24 +08:00
jxxghp
a73068069c 调整 TorrentItem.vue 组件,修改图标和名称的布局样式,以提升界面美观性和可读性 2025-05-02 21:07:39 +08:00
jxxghp
2f963ba7ab 调整 resource.vue 中搜索标题的字体大小,从 1.5rem 修改为 1.2rem,以改善界面一致性 2025-05-02 20:50:31 +08:00
jxxghp
9df70e5485 优化 StorageCard.vue 组件,简化存储信息显示逻辑,提升代码可读性 2025-05-02 20:43:37 +08:00
jxxghp
dfa34f090b 更新 resource.vue 2025-05-02 20:26:04 +08:00
jxxghp
388e9987cd 更新 resource.vue 2025-05-02 20:17:28 +08:00
jxxghp
9fe434849c 优化用户通知组件,增加通知列表容器样式以支持滚动,提升用户体验 2025-05-02 13:28:59 +08:00
jxxghp
95edaa99b6 更新国际化文本,将“类型”和“内容”替换为“渠道”和“用户ID”以提升用户体验 2025-05-02 12:09:14 +08:00
jxxghp
b3501d791e 更新 StorageCard.vue 2025-05-02 09:57:16 +08:00
jxxghp
5f2e93dde3 添加下载器和媒体服务器选项,重构相关组件以支持新功能,并更新国际化文本以提升用户体验 2025-05-02 08:01:19 +08:00
jxxghp
bf22d7f5e9 更新 package.json 2025-05-01 22:44:35 +08:00
jxxghp
08bbe8d841 优化文件重命名对话框,添加“自动识别名称”按钮,并更新国际化文本以支持新功能 2025-05-01 20:27:56 +08:00
jxxghp
572293bb4d 重构存储选项,更新为存储属性,优化存储字典和图标字典的生成逻辑,提升组件对存储配置的支持 2025-05-01 19:56:06 +08:00
jxxghp
f56d1c68c7 更新存储卡组件,添加自定义存储配置对话框,优化存储名称显示,并完善国际化文本 2025-05-01 19:35:08 +08:00
jxxghp
900dd6e958 添加自定义存储选项,更新存储卡组件以支持关闭事件,并完善国际化文本 2025-05-01 13:51:13 +08:00
jxxghp
5327c04e7e 添加自定义下载器和媒体服务器选项 2025-05-01 13:41:02 +08:00
jxxghp
f1835dd46c 更新验证器:为必填项和数字输入添加国际化支持,提升用户体验 2025-04-30 10:57:35 +08:00
272 changed files with 27447 additions and 7055 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
@@ -56,7 +57,7 @@ jobs:
name: ${{ env.frontend_version }}
draft: false
prerelease: false
make_latest: false
make_latest: true
files: |
dist.zip
env:

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"
]
}

View File

@@ -4,15 +4,34 @@
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目NodeJS版本>= `v20.12.1`
## 推荐的IDE设置
## 特性
- 基于 Vue 3 和 Vuetify 3 构建的现代化界面
- 使用 Vite 作为构建工具,提供快速的开发体验
- 支持多语言(中文/英文)
- 完整的插件系统支持,包括远程组件动态加载
## 模块联邦功能
MoviePilot 现已支持模块联邦Module Federation功能允许插件开发者创建可动态加载的远程组件实现更丰富的插件用户界面。
### 相关文档
- [模块联邦开发指南](docs/module-federation-guide.md) - 如何开发远程组件插件
- [模块联邦问题排查指南](docs/federation-troubleshooting.md) - 常见问题和解决方案
- [插件远程组件示例](examples/plugin-component/) - 开发插件组件的完整示例项目
## 开发部署
### 推荐的IDE设置
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (并禁用 Vetur).
## 配置Vite
### 配置Vite
请参阅 [Vite 配置参考](https://vitejs.dev/config/).
## 依赖安装
### 依赖安装
```sh
yarn

View File

@@ -2,39 +2,57 @@
*[中文](README.md) | English*
Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS version: >= `v20.12.1`.
Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS version required: >= `v20.12.1`.
## Recommended IDE Setup
## Features
- Modern interface built with Vue 3 and Vuetify 3
- Fast development experience with Vite build tool
- Multi-language support (Chinese/English)
- Complete plugin system with dynamic remote component loading
## Module Federation
MoviePilot now supports Module Federation, allowing plugin developers to create dynamically loadable remote components for richer plugin user interfaces.
### Documentation
- [Module Federation Troubleshooting Guide](docs/federation-troubleshooting.md) - Common issues and solutions
- [Plugin Remote Component Example](examples/plugin-component/) - Complete example project for developing plugin components
## Development
### Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (disable Vetur).
## Vite Configuration
### Configure Vite
Please refer to [Vite Configuration Reference](https://vitejs.dev/config/).
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Installation
### Install Dependencies
```sh
yarn
```
### Development
### Development Server
```sh
yarn dev
```
### Build
### Build for Production
```sh
yarn build
```
### Production Deployment
### Static Deployment
1. Use `nginx` or other web servers to host the `dist` static files. See `public/nginx.conf` for nginx configuration reference.
1. Host the `dist` static files using a web server like `nginx`. Refer to `public/nginx.conf` for nginx configuration.
2. Use `node` command to run `service.js` directly. It listens on port `3000` by default. Set the environment variable `NGINX_PORT` to adjust the port.
2. Alternatively, run the `service.js` directly with the `node` command. It listens on port `3000` by default. Set the `NGINX_PORT` environment variable to adjust the port.
```shell
node dist/service.js

3
components.d.ts vendored
View File

@@ -8,17 +8,16 @@ 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']
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
LocaleSwitcher: typeof import('./src/@core/components/LocaleSwitcher.vue')['default']
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
PageContentTitle: typeof import('./src/@core/components/PageContentTitle.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollToTopBtn: typeof import('./src/@core/components/ScrollToTopBtn.vue')['default']
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
}
}

View File

@@ -0,0 +1,110 @@
# MoviePilot 模块联邦问题排查指南
本文档提供了针对 MoviePilot 项目中使用模块联邦时可能遇到的常见问题及解决方案。
## 远程组件注册机制
MoviePilot 使用自动注册机制来加载远程组件:
1. 对于使用 Vue 渲染模式的插件,自动注册其远程组件
2. 每个远程组件根据插件 ID 唯一标识,确保不会冲突
3. 在需要加载组件时,会优先检查已注册的组件信息
这种设计使得插件开发者只需专注于组件开发,而不需要担心加载机制的复杂性。
## 常见错误
### 1. "Module name 'vue' does not resolve to a valid URL"
**原因**:远程组件无法正确解析共享依赖的 URL通常是因为共享依赖配置不正确。
**解决方案**
1.**插件组件项目**`vite.config.js` 中正确配置共享依赖:
```js
federation({
// ...
shared: {
vue: {
singleton: true,
requiredVersion: false // 关闭版本检查
}
}
})
```
2.**主应用**`vite.config.ts` 中确保共享依赖配置正确:
```ts
federation({
name: 'host',
remotes: {},
shared: ['vue', 'vuetify']
})
```
### 2. "Top-level await is not available in the configured target environment"
**原因**:模块联邦使用了顶层 await但目标构建环境不支持此功能。
**解决方案**
**主应用****插件组件项目** 的构建配置中添加 `target: 'esnext'`
```js
build: {
target: 'esnext', // 支持顶层await
// 其他配置...
}
```
### 3. "TypeError: Failed to fetch dynamically imported module"
**原因**:远程组件 JS 文件无法被正确加载,可能是路径错误或网络问题。
**解决方案**
1. 检查网络请求是否成功状态码200
2. 确认组件 URL 是否正确
3. 确保服务器允许访问该 JS 文件CORS 配置)
4. 检查插件后端是否正确提供了静态文件服务
### 4. 组件加载后渲染为空白或出现错误
**原因**:组件内部代码错误或与主应用不兼容。
**解决方案**
1. 检查浏览器控制台错误信息
2. 确保组件代码没有语法错误
3. 避免在组件中使用主应用未提供的依赖
4. 确保所有路径如图片、API请求URL等都是正确的
## 调试技巧
### 1. 启用详细日志
在浏览器控制台中设置:
```js
localStorage.setItem('debug', 'vite:*')
```
### 2. 分析网络请求
1. 打开浏览器开发者工具
2. 转到 Network 标签页
3. 确认远程组件 JS 文件请求是否成功
4. 分析响应内容是否为有效的 JavaScript
### 3. 隔离测试远程组件
创建一个独立的简单页面来测试插件组件,排除主应用的干扰因素。
## 其他资源
- [MoviePilot 插件组件示例](../examples/plugin-component/)
- [Vite 模块联邦插件文档](https://github.com/originjs/vite-plugin-federation)
- [Vite 官方文档](https://vitejs.dev/guide/build.html)
- [Origin.js 模块联邦示例](https://github.com/originjs/vite-plugin-federation/tree/main/packages/examples)

View File

@@ -0,0 +1,380 @@
# MoviePilot前端远程模块开发指南
## 1. 概述
MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态加载和集成。本文档详细说明如何开发符合要求的远程模块以便在MoviePilot中作为插件使用。
关联阅读后端插件开发文档:[第三方插件开发说明](https://github.com/jxxghp/MoviePilot-Plugins/blob/main/README.md)
## 2. 技术要求
- Node.js 20+
- Vue 3
- Vite 4+
- TypeScript 5+
## 3. 核心概念
每个插件需要提供三个标准组件:
| 组件名称 | 文件名 | 用途 |
|---------|-------|------|
| Page | Page.vue | 插件详情页面 |
| Config | Config.vue | 插件配置页面 |
| Dashboard | Dashboard.vue | 仪表板组件 |
## 4. 快速开始
### 创建项目
```bash
# 创建项目
npm create vite@latest my-plugin -- --template vue-ts
# 进入项目目录
cd my-plugin
# 安装依赖
yarn
```
### 配置vite.config.ts
```typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
vue(),
federation({
name: 'MyPlugin',
filename: 'remoteEntry.js',
exposes: {
'./Page': './src/components/Page.vue',
'./Config': './src/components/Config.vue',
'./Dashboard': './src/components/Dashboard.vue',
},
shared: {
vue: {
requiredVersion: false,
generate: false,
},
vuetify: {
requiredVersion: false,
generate: false,
singleton: true,
},
'vuetify/styles': {
requiredVersion: false,
generate: false,
singleton: true,
},
},
format: 'esm'
})
],
build: {
target: 'esnext', // 必须设置为esnext以支持顶层await
minify: false, // 开发阶段建议关闭混淆
cssCodeSplit: true, // 改为true以便能分离样式文件
},
css: {
preprocessorOptions: {
scss: {
additionalData: '/* 覆盖vuetify样式 */',
}
},
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
}
}
},
{
postcssPlugin: 'vuetify-filter',
Root(root) {
// 过滤掉所有vuetify相关的CSS
root.walkRules(rule => {
if (rule.selector && (
rule.selector.includes('.v-') ||
rule.selector.includes('.mdi-'))) {
rule.remove();
}
});
}
}
]
}
},
server: {
port: 5001, // 使用不同于主应用的端口
cors: true, // 启用CORS
origin: 'http://localhost:5001'
},
})
```
## 5. 组件开发规范
### 5.1 Page组件详情页面
```vue
<script setup lang="ts">
// 自定义事件,用于通知主应用刷新数据
const emit = defineEmits(['action', 'switch', 'close'])
// 接收API对象
const props = defineProps({
api: {
type: Object,
default: () => {}
}
})
// 页面逻辑代码...
// 通知主应用刷新数据
function notifyRefresh() {
emit('action')
}
// 通知主应用切换到配置页面
function notifySwitch() {
emit('switch')
}
// 通知主应用关闭当前页面
function notifyClose() {
emit('close')
}
</script>
<template>
<div class="plugin-page">
<!-- 插件详情页面操作按钮示例 -->
<v-btn @click="notifyRefresh">刷新数据</v-btn>
<v-btn @click="notifySwitch">配置插件</v-btn>
<v-btn @click="notifyClose">关闭页面</v-btn>
</div>
</template>
```
### 5.2 Config组件配置页面
```vue
<script setup lang="ts">
// 接收初始配置和API对象
const props = defineProps({
initialConfig: {
type: Object,
default: () => ({})
},
api: {
type: Object,
default: () => {}
}
})
// 配置数据
const config = ref({...props.initialConfig})
// 自定义事件,用于保存配置
const emit = defineEmits(['save', 'close', 'switch'])
// 保存配置
function saveConfig() {
emit('save', config.value)
}
// 通知主应用切换到详情页面
function notifySwitch() {
emit('switch')
}
// 通知主应用关闭当前页面
function notifyClose() {
emit('close')
}
</script>
<template>
<div class="plugin-config">
<!-- 配置表单示例 -->
<v-text-field v-model="config.someField" label="配置项"></v-text-field>
<!-- 保存按钮示例 -->
<v-btn color="primary" @click="saveConfig">保存配置</v-btn>
<!-- 关闭按钮示例 -->
<v-btn color="primary" @click="notifyClose">关闭页面</v-btn>
<!-- 切换按钮示例 -->
<v-btn color="primary" @click="notifySwitch">切换到详情页面</v-btn>
</div>
</template>
```
### 5.3 Dashboard组件仪表板
```vue
<script setup lang="ts">
// 接收配置和刷新控制
const props = defineProps({
config: {
type: Object,
default: () => ({})
},
allowRefresh: {
type: Boolean,
default: true
}
})
// 仪表板逻辑...
</script>
<template>
<div class="dashboard-widget">
<!-- 仪表板内容 -->
<v-card>
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
<v-card-text>
<!-- 组件内容 -->
</v-card-text>
</v-card>
</div>
</template>
```
## 6. 构建和部署
### 构建项目
```bash
yarn build
```
- 将生成的dist文件夹上传到插件后端目录下默认为`dist/assets`
**注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*``_plugin-vue_export-helper-*``remoteEntry.js`
- 在插件的后端python代码中实现以下方法来集成远程组件
```python
def get_render_mode() -> Tuple[str, str]:
"""
获取插件渲染模式
:return: 1、渲染模式支持vue/vuetify默认vuetify
:return: 2、组件路径默认 dist/assets
"""
return "vue", "dist/assets"
```
- 需要在插件前端页面调用后端接口时通过传入的api模块发起调用后端api接口声明认证类型为`bear`
```typescript
// 演示使用api模块调用插件接口
recentItems.value = await props.api.get(`plugin/MyPlugin/history`)
```
```python
def get_api(self) -> List[Dict[str, Any]]:
"""
注册插件API
"""
return [
{
"path": "/history",
"endpoint": self.get_history,
"methods": ["GET"],
"auth": "bear", # 认证类型设为bear
"summary": "查询历史记录"
}
]
```
## 7. 调试与排错
### 常见问题
1. **模块无法加载**
- 检查网络请求是否成功状态码200
- 确认文件路径是否正确
- 检查CORS跨域设置
2. **模块加载但组件不显示**
- 检查控制台错误信息
- 确认组件是否正确导出
- 验证共享依赖配置
3. **"Module name 'vue' does not resolve to a valid URL"**
- 检查`shared`配置是否正确
- 设置`requiredVersion: false`尝试解决
4. **"Top-level await is not available"**
- 确保`build.target`设置为`esnext`
## 8. 高级配置
### 8.1 CSS隔离
为防止样式冲突建议使用CSS Modules或scoped样式
```vue
<style scoped>
/* 组件样式 */
</style>
```
### 8.2 共享更多依赖
如果您的插件需要共享更多依赖可以扩展shared配置
```js
shared: {
vue: { requiredVersion: false },
vuetify: { requiredVersion: false },
'@vueuse/core': { requiredVersion: false },
pinia: { requiredVersion: false }
}
```
### 8.3 开发环境测试
开发期间可以使用以下配置在本地测试:
```typescript
// vite.config.ts
export default defineConfig({
server: {
port: 5001, // 使用不同于主应用的端口
cors: true, // 启用CORS
origin: 'http://localhost:5001'
}
})
```
## 9. 示例代码
- [插件远程组件示例](../examples/plugin-component/) - 开发插件组件的完整示例项目
- [模块联邦问题排查指南](./federation-troubleshooting.md) - 常见问题排查
## 10. 参考资料
- [Vite Plugin Federation](https://github.com/originjs/vite-plugin-federation)
- [Vue 3官方文档](https://vuejs.org/)
---
如有问题请提交Issue。

7
env.d.ts vendored
View File

@@ -8,3 +8,10 @@ declare module 'vue-router' {
navActiveLink?: RouteLocationRaw
}
}
// 支持动态导入远程模块
declare module '*' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -0,0 +1,42 @@
# MoviePilot 插件远程组件示例
这是 MoviePilot 插件远程组件的示例项目展示了如何正确配置和开发与主应用兼容的远程组件。本示例实现了三个标准组件Page详情页面、Config配置页面和Dashboard仪表板组件
## 1. 开发环境准备
### 安装依赖
```bash
npm install
# 或
yarn
```
### 开发模式运行
```bash
npm run dev
# 或
yarn dev
```
## 2. 项目结构
```
plugin-component/
├── src/
│ ├── components/
│ │ ├── Page.vue # 插件详情页面组件
│ │ ├── Config.vue # 插件配置页面组件
│ │ └── Dashboard.vue # 插件仪表板组件
│ ├── App.vue # 本地开发入口组件
│ └── main.js # 本地开发入口文件
├── vite.config.js # Vite和模块联邦配置
├── index.html # 本地开发HTML入口
└── package.json # 依赖配置
```
## 3. 开发指引
- [模块联邦开发指南](../../docs/module-federation-guide.md)
- [模块联邦问题排查指南](../../docs/federation-troubleshooting.md)。

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MoviePilot插件组件示例</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet" />
<style>
body {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
{
"name": "moviepilot-plugin-component",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13",
"vuetify": "3.7.3",
"echarts": "^5.4.3",
"vue-echarts": "^6.6.1",
"@vueuse/core": "^12.4.0"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.4.1",
"@vitejs/plugin-vue": "^4.4.0",
"vite": "^5.4.11"
}
}

View File

@@ -0,0 +1,128 @@
<template>
<div class="app-container">
<v-app>
<v-app-bar color="primary" app>
<v-app-bar-title>MoviePilot插件组件示例</v-app-bar-title>
</v-app-bar>
<v-main>
<v-container>
<v-tabs v-model="activeTab" bg-color="primary">
<v-tab value="page">详情页面</v-tab>
<v-tab value="config">配置页面</v-tab>
<v-tab value="dashboard">仪表板</v-tab>
</v-tabs>
<v-window v-model="activeTab" class="mt-4">
<v-window-item value="page">
<h2 class="text-h5 mb-4">Page组件</h2>
<div class="component-preview">
<page-component @action="handleAction"></page-component>
</div>
</v-window-item>
<v-window-item value="config">
<h2 class="text-h5 mb-4">Config组件</h2>
<div class="component-preview">
<config-component :initial-config="initialConfig" @save="handleConfigSave"></config-component>
</div>
</v-window-item>
<v-window-item value="dashboard">
<h2 class="text-h5 mb-4">Dashboard组件</h2>
<v-switch v-model="dashboardConfig.attrs.border" label="显示边框" color="primary" class="mb-4"></v-switch>
<div class="component-preview">
<dashboard-component :config="dashboardConfig" :allow-refresh="true"></dashboard-component>
</div>
</v-window-item>
</v-window>
</v-container>
</v-main>
<v-footer app color="primary" class="text-center d-flex justify-center">
<span class="text-white">MoviePilot 模块联邦示例 ©{{ new Date().getFullYear() }}</span>
</v-footer>
</v-app>
<!-- 通知弹窗 -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="snackbar.timeout">
{{ snackbar.text }}
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false"> 关闭 </v-btn>
</template>
</v-snackbar>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import PageComponent from './components/Page.vue'
import ConfigComponent from './components/Config.vue'
import DashboardComponent from './components/Dashboard.vue'
// 活动标签页
const activeTab = ref('page')
// 配置初始值
const initialConfig = {
name: '测试插件',
description: '这是一个测试配置',
enable_notifications: true,
update_interval: 30,
api_url: 'https://api.example.com',
api_key: 'test_api_key_123',
concurrent_tasks: 2,
tags: ['电影', '测试'],
}
// 仪表板配置
const dashboardConfig = reactive({
id: 'test_plugin',
name: '测试插件',
attrs: {
title: '仪表板示例',
subtitle: '插件数据展示',
border: true,
},
})
// 通知状态
const snackbar = reactive({
show: false,
text: '',
color: 'success',
timeout: 3000,
})
// 显示通知
function showNotification(text, color = 'success') {
snackbar.text = text
snackbar.color = color
snackbar.show = true
}
// 处理详情页面操作
function handleAction() {
showNotification('Page组件触发了action事件')
}
// 处理配置保存
function handleConfigSave(config) {
console.log('配置已保存:', config)
showNotification('配置已保存')
}
</script>
<style scoped>
/* 为了使测试应用更美观 */
.app-container {
block-size: 100vh;
inline-size: 100vw;
}
.component-preview {
overflow: hidden;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="plugin-config">
<v-card>
<v-card-item>
<v-card-title>插件配置</v-card-title>
<template #append>
<v-btn icon color="primary" variant="text" @click="notifyClose">
<v-icon left>mdi-close</v-icon>
</v-btn>
</template>
</v-card-item>
<v-card-text class="overflow-y-auto">
<v-alert v-if="error" type="error" class="mb-4">{{ error }}</v-alert>
<v-form ref="form" v-model="isFormValid" @submit.prevent="saveConfig">
<!-- 基本设置区域 -->
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">基本设置</div>
<v-row>
<v-col cols="12">
<v-switch
v-model="config.enable"
label="启用插件"
color="primary"
inset
hint="启用插件后,插件将开始工作"
persistent-hint
></v-switch>
</v-col>
<v-col cols="12">
<v-text-field
v-model="config.name"
label="插件名称"
variant="outlined"
:rules="[v => !!v || '名称不能为空']"
hint="显示在插件列表中的名称"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
v-model="config.description"
label="插件描述"
variant="outlined"
rows="3"
hint="简要说明插件的功能和用途"
></v-textarea>
</v-col>
</v-row>
<!-- 功能配置区域 -->
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">功能配置</div>
<v-row>
<v-col cols="12">
<v-select
v-model="config.update_interval"
label="更新频率"
:items="updateIntervalOptions"
variant="outlined"
item-title="text"
item-value="value"
></v-select>
</v-col>
</v-row>
<!-- API配置区域 -->
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">API设置</div>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="config.api_url"
label="API地址"
variant="outlined"
hint="外部服务API地址"
:rules="[v => !v || v.startsWith('http') || '请输入有效的URL']"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="config.api_key"
label="API密钥"
variant="outlined"
:append-inner-icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'"
:type="showApiKey ? 'text' : 'password'"
@click:append-inner="showApiKey = !showApiKey"
></v-text-field>
</v-col>
</v-row>
<!-- 高级选项区域 -->
<v-expansion-panels variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title>高级选项</v-expansion-panel-title>
<v-expansion-panel-text>
<v-slider
v-model="config.concurrent_tasks"
label="并发任务数"
min="1"
max="10"
step="1"
thumb-label
></v-slider>
<v-combobox
v-model="config.tags"
label="标签"
variant="outlined"
chips
multiple
closable-chips
></v-combobox>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn color="secondary" @click="resetForm">重置</v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" :disabled="!isFormValid" @click="saveConfig" :loading="saving">保存配置</v-btn>
</v-card-actions>
</v-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
// 接收初始配置
const props = defineProps({
initialConfig: {
type: Object,
default: () => ({}),
},
api: {
type: Object,
default: () => {},
},
})
// 表单状态
const form = ref(null)
const isFormValid = ref(true)
const error = ref(null)
const saving = ref(false)
const showApiKey = ref(false)
// 更新频率选项
const updateIntervalOptions = [
{ text: '5分钟', value: 5 },
{ text: '15分钟', value: 15 },
{ text: '30分钟', value: 30 },
{ text: '1小时', value: 60 },
{ text: '2小时', value: 120 },
{ text: '6小时', value: 360 },
{ text: '12小时', value: 720 },
{ text: '1天', value: 1440 },
]
// 配置数据,使用默认值和初始配置合并
const defaultConfig = {
name: '我的插件',
description: '',
enable: true,
update_interval: 60,
api_url: '',
api_key: '',
concurrent_tasks: 3,
tags: [],
}
// 合并默认配置和初始配置
const config = reactive({ ...defaultConfig })
// 初始化配置
onMounted(() => {
// 加载初始配置
if (props.initialConfig) {
Object.keys(props.initialConfig).forEach(key => {
if (key in config) {
config[key] = props.initialConfig[key]
}
})
}
})
// 自定义事件,用于保存配置
const emit = defineEmits(['save', 'close', 'switch'])
// 保存配置
async function saveConfig() {
if (!isFormValid.value) {
error.value = '请修正表单错误'
return
}
saving.value = true
error.value = null
try {
// 模拟API调用等待
await new Promise(resolve => setTimeout(resolve, 1000))
// 发送保存事件
emit('save', { ...config })
} catch (err) {
console.error('保存配置失败:', err)
error.value = err.message || '保存配置失败'
} finally {
saving.value = false
}
}
// 重置表单
function resetForm() {
Object.keys(defaultConfig).forEach(key => {
config[key] = defaultConfig[key]
})
if (form.value) {
form.value.resetValidation()
}
}
// 通知主应用关闭组件
function notifyClose() {
emit('close')
}
</script>

View File

@@ -0,0 +1,298 @@
<template>
<div class="dashboard-widget">
<v-card v-if="!config?.attrs?.border" flat>
<v-card-text class="pa-0">
<div class="dashboard-content">
<!-- 加载中状态 -->
<div v-if="loading" class="d-flex justify-center align-center py-4">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<!-- 数据内容 -->
<div v-else>
<!-- 数据图表 -->
<div v-if="chartData" class="chart-container">
<v-chart class="chart" :option="chartOptions" autoresize />
</div>
<!-- 数据列表 -->
<v-list v-if="items.length" density="compact" class="py-0">
<v-list-item v-for="(item, index) in items" :key="index" :title="item.title" :subtitle="item.subtitle">
<template v-slot:prepend>
<v-avatar :color="getStatusColor(item.status)" size="small">
<v-icon size="small" color="white">{{ getStatusIcon(item.status) }}</v-icon>
</v-avatar>
</template>
<template v-slot:append v-if="item.value">
<span class="text-caption">{{ item.value }}</span>
</template>
</v-list-item>
</v-list>
</div>
</div>
</v-card-text>
</v-card>
<!-- 带边框的卡片 -->
<v-card v-else>
<v-card-item>
<v-card-title>{{ config?.attrs?.title || '仪表板组件' }}</v-card-title>
<v-card-subtitle v-if="config?.attrs?.subtitle">{{ config.attrs.subtitle }}</v-card-subtitle>
</v-card-item>
<v-card-text>
<!-- 加载中状态 -->
<div v-if="loading" class="d-flex justify-center align-center py-4">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<!-- 数据内容 -->
<div v-else>
<!-- 数据图表 -->
<div v-if="chartData" class="chart-container">
<v-chart class="chart" :option="chartOptions" autoresize />
</div>
<!-- 数据列表 -->
<v-list v-if="items.length" density="compact" class="rounded pa-0">
<v-list-item v-for="(item, index) in items" :key="index" :title="item.title" :subtitle="item.subtitle">
<template v-slot:prepend>
<v-avatar :color="getStatusColor(item.status)" size="small">
<v-icon size="small" color="white">{{ getStatusIcon(item.status) }}</v-icon>
</v-avatar>
</template>
<template v-slot:append v-if="item.value">
<span class="text-caption">{{ item.value }}</span>
</template>
</v-list-item>
</v-list>
</div>
</v-card-text>
</v-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, PieChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components'
// 注册ECharts组件
try {
use([CanvasRenderer, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent])
} catch (e) {
console.warn('ECharts components registration failed', e)
}
// 接收仪表板配置
const props = defineProps({
config: {
type: Object,
default: () => ({}),
},
allowRefresh: {
type: Boolean,
default: true,
},
})
// 组件状态
const loading = ref(true)
const items = ref([])
const chartData = ref(null)
let refreshTimer = null
// 获取状态图标
function getStatusIcon(status) {
const icons = {
'success': 'mdi-check-circle',
'warning': 'mdi-alert',
'error': 'mdi-alert-circle',
'info': 'mdi-information',
'running': 'mdi-play-circle',
'pending': 'mdi-clock-outline',
'completed': 'mdi-check-circle-outline',
}
return icons[status] || 'mdi-help-circle'
}
// 获取状态颜色
function getStatusColor(status) {
const colors = {
'success': 'success',
'warning': 'warning',
'error': 'error',
'info': 'info',
'running': 'primary',
'pending': 'secondary',
'completed': 'success',
}
return colors[status] || 'grey'
}
// 图表选项
const chartOptions = computed(() => {
if (!chartData.value) return {}
const { type, data } = chartData.value
if (type === 'line') {
return {
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: data.xAxis,
axisLabel: {
color: '#888',
},
},
yAxis: {
type: 'value',
axisLabel: {
color: '#888',
},
},
series: data.series.map(series => ({
name: series.name,
type: 'line',
smooth: true,
data: series.data,
areaStyle: { opacity: 0.1 },
})),
}
}
if (type === 'pie') {
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
series: [
{
name: data.name,
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '12',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: data.items,
},
],
}
}
return {}
})
// 获取仪表板数据
async function fetchDashboardData() {
if (!props.allowRefresh) return
loading.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
// 随机决定显示饼图或折线图
const showPie = Math.random() > 0.5
if (showPie) {
// 饼图数据
chartData.value = {
type: 'pie',
data: {
name: '文件分布',
items: [
{ value: Math.floor(Math.random() * 50) + 30, name: '电影' },
{ value: Math.floor(Math.random() * 40) + 20, name: '电视剧' },
{ value: Math.floor(Math.random() * 30) + 10, name: '动漫' },
{ value: Math.floor(Math.random() * 20) + 5, name: '纪录片' },
],
},
}
} else {
// 折线图数据
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
chartData.value = {
type: 'line',
data: {
xAxis: days,
series: [
{
name: '下载量',
data: days.map(() => Math.floor(Math.random() * 10) + 1),
},
{
name: '完成量',
data: days.map(() => Math.floor(Math.random() * 8) + 1),
},
],
},
}
}
// 生成列表数据
const statuses = ['success', 'warning', 'error', 'info', 'running', 'pending', 'completed']
items.value = Array.from({ length: 5 }, (_, i) => {
const status = statuses[Math.floor(Math.random() * statuses.length)]
return {
title: `项目 ${i + 1}`,
subtitle: `上次更新: ${new Date().toLocaleTimeString()}`,
status,
value: Math.floor(Math.random() * 100) + '%',
}
})
} catch (error) {
console.error('获取仪表板数据失败:', error)
} finally {
loading.value = false
}
}
// 设置定时刷新
function setupRefreshTimer() {
if (props.allowRefresh) {
// 每30秒刷新一次
refreshTimer = setInterval(() => {
fetchDashboardData()
}, 30000)
}
}
// 初始化
onMounted(() => {
fetchDashboardData()
setupRefreshTimer()
})
// 清理
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
}
})
</script>

View File

@@ -0,0 +1,169 @@
<template>
<div class="plugin-page">
<v-card>
<v-card-item>
<v-card-title>{{ title }}</v-card-title>
<template #append>
<v-btn icon color="primary" variant="text" @click="notifyClose">
<v-icon left>mdi-close</v-icon>
</v-btn>
</template>
</v-card-item>
<v-card-text>
<v-alert v-if="error" type="error" class="mb-4">{{ error }}</v-alert>
<v-skeleton-loader v-if="loading" type="card"></v-skeleton-loader>
<div v-else>
<!-- 数据统计展示 -->
<v-row v-if="stats">
<v-col v-for="(value, key) in stats" :key="key" cols="12" sm="6" md="4">
<v-card variant="outlined" class="text-center">
<v-card-text>
<div class="text-h4 font-weight-bold">{{ value }}</div>
<div class="text-subtitle-1">{{ key }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 最近记录展示 -->
<div v-if="recentItems && recentItems.length" class="mt-4">
<div class="text-h6 mb-2">最近记录</div>
<v-timeline density="compact">
<v-timeline-item
v-for="(item, index) in recentItems"
:key="index"
:dot-color="getItemColor(item.type)"
size="small"
>
<div class="d-flex align-center">
<v-icon :color="getItemColor(item.type)" size="small" class="mr-2">
{{ getItemIcon(item.type) }}
</v-icon>
<span class="font-weight-medium">{{ item.title }}</span>
</div>
<div class="text-caption text-secondary">{{ item.time }}</div>
</v-timeline-item>
</v-timeline>
</div>
<!-- 当前状态 -->
<div class="mt-4 text-subtitle-2">
<div>
<strong>状态:</strong>
<v-chip size="small" :color="status === 'running' ? 'success' : 'warning'">{{ status }}</v-chip>
</div>
<div><strong>最后更新:</strong> {{ lastUpdated }}</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="refreshData" :loading="loading">
<v-icon left>mdi-refresh</v-icon>
刷新数据
</v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" @click="notifySwitch">
<v-icon left>mdi-cog</v-icon>
配置
</v-btn>
</v-card-actions>
</v-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 接收初始配置
const props = defineProps({
model: {
type: Object,
default: () => {},
},
api: {
type: Object,
default: () => {},
},
})
// 组件状态
const title = ref('插件详情页面')
const loading = ref(true)
const error = ref(null)
const stats = ref(null)
const recentItems = ref([])
const status = ref('running')
const lastUpdated = ref('')
// 自定义事件,用于通知主应用刷新数据
const emit = defineEmits(['action', 'switch', 'close'])
// 获取状态图标
function getItemIcon(type) {
const icons = {
'movie': 'mdi-movie',
'tv': 'mdi-television-classic',
'download': 'mdi-download',
'error': 'mdi-alert-circle',
'success': 'mdi-check-circle',
}
return icons[type] || 'mdi-information'
}
// 获取状态颜色
function getItemColor(type) {
const colors = {
'movie': 'blue',
'tv': 'green',
'download': 'purple',
'error': 'red',
'success': 'success',
}
return colors[type] || 'grey'
}
// 获取和刷新数据
async function refreshData() {
loading.value = true
error.value = null
try {
// 模拟数据
stats.value = {
'电影': Math.floor(Math.random() * 100) + 50,
'电视剧': Math.floor(Math.random() * 100) + 30,
'动漫': Math.floor(Math.random() * 100) + 20,
'纪录片': Math.floor(Math.random() * 100) + 10,
'综艺': Math.floor(Math.random() * 100) + 5,
}
// 演示使用api模块调用插件接口
recentItems.value = await props.api.get(`plugin/MyPlugin/history`)
status.value = Math.random() > 0.2 ? 'running' : 'paused'
lastUpdated.value = new Date().toLocaleString()
} catch (err) {
console.error('获取数据失败:', err)
error.value = err.message || '获取数据失败'
} finally {
loading.value = false
// 通知主应用组件已更新
emit('action')
}
}
// 通知主应用切换到配置页面
function notifySwitch() {
emit('switch')
}
// 通知主应用关闭组件
function notifyClose() {
emit('close')
}
// 组件挂载时加载数据
onMounted(() => {
refreshData()
})
</script>

View File

@@ -0,0 +1,25 @@
import { createApp } from 'vue'
import App from './App.vue'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import defaults from './vuetify/defaults'
import theme from './vuetify/theme'
import 'vuetify/styles'
// 创建Vuetify实例
const vuetify = createVuetify({
components,
directives,
theme,
defaults
})
// 创建应用
const app = createApp(App)
// 使用插件
app.use(vuetify)
// 挂载应用
app.mount('#app')

View File

@@ -0,0 +1,148 @@
export default {
IconBtn: {
icon: true,
color: 'default',
variant: 'text',
VIcon: {
size: 24,
},
},
VAlert: {
VBtn: {
color: undefined,
},
},
VAvatar: {
// Remove after next release
variant: 'flat',
VIcon: {
size: 24,
},
},
VBadge: {
// set v-badge default color to primary
color: 'primary',
},
VBtn: {
// set v-btn default color to primary
color: 'primary',
elevation: 0,
},
VCard: {
elevation: 0,
rounded: 'lg',
},
VMenu: {
elevation: 0,
},
VChip: {
elevation: 0,
},
VBottomSheet: {
elevation: 0,
},
VDialog: {
elevation: 0,
rounded: 'lg',
},
VExpansionPanels: {
elevation: 0,
},
VList: {
color: 'primary',
elevation: 0,
},
VListItem: {
rounded: 'md',
},
VPagination: {
activeColor: 'primary',
},
VTabs: {
// set v-tabs default color to primary
color: 'primary',
VSlideGroup: {
showArrows: true,
},
},
VTooltip: {
// set v-tooltip default location to top
location: 'top',
},
VCheckboxBtn: {
color: 'primary',
hideDetails: 'auto',
},
VCheckbox: {
// set v-checkbox default color to primary
color: 'primary',
hideDetails: 'auto',
},
VRadioGroup: {
color: 'primary',
hideDetails: 'auto',
},
VRadio: {
color: 'primary',
hideDetails: 'auto',
},
VSelect: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto',
menuProps: { elevation: 0 },
},
VRangeSlider: {
// set v-range-slider default color to primary
color: 'primary',
density: 'comfortable',
thumbLabel: true,
hideDetails: 'auto',
},
VRating: {
// set v-rating default color to primary
color: 'rgba(var(--v-theme-on-background),0.23)',
activeColor: 'warning',
halfIncrements: true,
},
VProgressCircular: {
// set v-progress-circular default color to primary
color: 'primary',
},
VSlider: {
// set v-slider default color to primary
color: 'primary',
hideDetails: 'auto',
},
VTextField: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto',
},
VAutocomplete: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto',
},
VCombobox: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto',
menuProps: { elevation: 0 },
},
VFileInput: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto',
},
VTextarea: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto',
},
VSwitch: {
// set v-switch default color to primary
color: 'primary',
hideDetails: 'auto',
},
}

View File

@@ -0,0 +1,216 @@
import type { VuetifyOptions } from 'vuetify'
const theme: VuetifyOptions['theme'] = {
defaultTheme: 'light',
themes: {
light: {
dark: false,
colors: {
'primary': '#9155FD',
'secondary': '#8A8D93',
'on-secondary': '#FFFFFF',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
'error': '#FF4C51',
'on-primary': '#FFFFFF',
'on-success': '#FFFFFF',
'on-warning': '#FFFFFF',
'background': '#F4F5FA',
'on-background': '#3A3541',
'on-surface': '#3A3541',
'grey-50': '#FAFAFA',
'grey-100': '#F0F2F8',
'grey-200': '#EEEEEE',
'grey-300': '#E0E0E0',
'grey-400': '#BDBDBD',
'grey-500': '#9E9E9E',
'grey-600': '#757575',
'grey-700': '#616161',
'grey-800': '#424242',
'grey-900': '#212121',
'perfect-scrollbar-thumb': '#DBDADE',
'skin-bordered-background': '#FFFFFF',
'skin-bordered-surface': '#FFFFFF',
},
variables: {
'code-color': '#D400FF',
'overlay-scrim-background': '#3A3541',
'overlay-scrim-opacity': 0.5,
'hover-opacity': 0.04,
'focus-opacity': 0.1,
'selected-opacity': 0.12,
'activated-opacity': 0.1,
'pressed-opacity': 0.14,
'dragged-opacity': 0.1,
'border-color': '#3A3541',
'table-header-background': '#F9FAFC',
'custom-background': '#F9F8F9',
// Shadows
'shadow-key-umbra-opacity': 'rgba(var(--v-theme-on-surface), 0.08)',
'shadow-key-penumbra-opacity': 'rgba(var(--v-theme-on-surface), 0.12)',
'shadow-key-ambient-opacity': 'rgba(var(--v-theme-on-surface), 0.04)',
},
},
dark: {
dark: true,
colors: {
'primary': '#6E66ED',
'secondary': '#8A8D93',
'on-secondary': '#FFFFFF',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
'error': '#FF4C51',
'on-primary': '#FFFFFF',
'on-success': '#FFFFFF',
'on-warning': '#FFFFFF',
'background': '#0E1116',
'on-background': '#E7E3FC',
'surface': '#14161F',
'on-surface': '#E7E3FC',
'grey-50': '#2A2E42',
'grey-100': '#474360',
'grey-200': '#4A5072',
'grey-300': '#5E6692',
'grey-400': '#7983BB',
'grey-500': '#8692D0',
'grey-600': '#AAB3DE',
'grey-700': '#B6BEE3',
'grey-800': '#CFD3EC',
'grey-900': '#E7E9F6',
'perfect-scrollbar-thumb': '#4A5072',
'skin-bordered-background': '#312d4b',
'skin-bordered-surface': '#312d4b',
},
variables: {
'code-color': '#d400ff',
'overlay-scrim-background': '#191D21',
'overlay-scrim-opacity': 0.6,
'hover-opacity': 0.04,
'focus-opacity': 0.1,
'selected-opacity': 0.12,
'activated-opacity': 0.1,
'pressed-opacity': 0.14,
'dragged-opacity': 0.1,
'border-color': '#E7E3FC',
'table-header-background': '#14161F',
'custom-background': '#373452',
// Shadows
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
},
},
purple: {
dark: true,
colors: {
'primary': '#9155FD',
'secondary': '#8A8D93',
'on-secondary': '#FFFFFF',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
'error': '#FF4C51',
'on-primary': '#FFFFFF',
'on-success': '#FFFFFF',
'on-warning': '#FFFFFF',
'background': '#28243D',
'on-background': '#E7E3FC',
'surface': '#312D4B',
'on-surface': '#E7E3FC',
'grey-50': '#2A2E42',
'grey-100': '#474360',
'grey-200': '#4A5072',
'grey-300': '#5E6692',
'grey-400': '#7983BB',
'grey-500': '#8692D0',
'grey-600': '#AAB3DE',
'grey-700': '#B6BEE3',
'grey-800': '#CFD3EC',
'grey-900': '#E7E9F6',
'perfect-scrollbar-thumb': '#4A5072',
'skin-bordered-background': '#312d4b',
'skin-bordered-surface': '#312d4b',
},
variables: {
'code-color': '#d400ff',
'overlay-scrim-background': '#2C2942',
'overlay-scrim-opacity': 0.6,
'hover-opacity': 0.04,
'focus-opacity': 0.1,
'selected-opacity': 0.12,
'activated-opacity': 0.1,
'pressed-opacity': 0.14,
'dragged-opacity': 0.1,
'border-color': '#E7E3FC',
'table-header-background': '#3D3759',
'custom-background': '#373452',
// Shadows
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
},
},
transparent: {
dark: true,
colors: {
'primary': '#A370F7',
'secondary': '#8A8D93',
'on-secondary': '#FFFFFF',
'success': '#66BB6A',
'info': '#42A5F5',
'warning': '#FFA726',
'error': '#EF5350',
'on-primary': '#FFFFFF',
'on-success': '#FFFFFF',
'on-warning': '#FFFFFF',
'background': '#000000',
'on-background': '#E7E3FC',
'surface': 'rgba(30, 30, 30, 0.3)',
'on-surface': '#E7E3FC',
'surface-variant': 'rgba(30, 30, 30, 0.2)',
'on-surface-variant': 'rgba(255, 255, 255, 0.65)',
'grey-50': 'rgba(42, 46, 66, 0.15)',
'grey-100': 'rgba(71, 67, 96, 0.15)',
'grey-200': 'rgba(74, 80, 114, 0.15)',
'grey-300': 'rgba(94, 102, 146, 0.15)',
'grey-400': 'rgba(121, 131, 187, 0.15)',
'grey-500': 'rgba(134, 146, 208, 0.15)',
'grey-600': 'rgba(170, 179, 222, 0.15)',
'grey-700': 'rgba(182, 190, 227, 0.15)',
'grey-800': 'rgba(207, 211, 236, 0.15)',
'grey-900': 'rgba(231, 233, 246, 0.15)',
'perfect-scrollbar-thumb': 'rgba(158, 158, 190, 0.4)',
'skin-bordered-background': 'rgba(30, 30, 30, 0.3)',
'skin-bordered-surface': 'rgba(30, 30, 30, 0.3)',
'card-background': 'rgba(30, 30, 30, 0.3)',
},
variables: {
'code-color': '#6D9EEB',
'overlay-scrim-background': '0, 0, 0',
'overlay-scrim-opacity': 0.7,
'hover-opacity': 0.1,
'focus-opacity': 0.15,
'selected-opacity': 0.2,
'activated-opacity': 0.15,
'pressed-opacity': 0.2,
'dragged-opacity': 0.15,
'border-color': '#E7E3FC',
'table-header-background': 'rgba(30, 30, 30, 0.3)',
'custom-background': 'rgba(30, 30, 30, 0.3)',
'card-background': 'rgba(30, 30, 30, 0.3)',
// Shadows
'shadow-key-umbra-opacity': 'rgba(0, 0, 0, 0.07)',
'shadow-key-penumbra-opacity': 'rgba(0, 0, 0, 0.1)',
'shadow-key-ambient-opacity': 'rgba(0, 0, 0, 0.05)',
},
},
},
}
export default theme

View File

@@ -0,0 +1,79 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
vue(),
federation({
name: 'MyPlugin',
filename: 'remoteEntry.js',
exposes: {
'./Page': './src/components/Page.vue',
'./Config': './src/components/Config.vue',
'./Dashboard': './src/components/Dashboard.vue',
},
shared: {
vue: {
requiredVersion: false,
generate: false,
},
vuetify: {
requiredVersion: false,
generate: false,
singleton: true,
},
'vuetify/styles': {
requiredVersion: false,
generate: false,
singleton: true,
},
},
format: 'esm'
})
],
build: {
target: 'esnext', // 必须设置为esnext以支持顶层await
minify: false, // 开发阶段建议关闭混淆
cssCodeSplit: true, // 改为true以便能分离样式文件
},
css: {
preprocessorOptions: {
scss: {
additionalData: '/* 覆盖vuetify样式 */',
}
},
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
}
}
},
{
postcssPlugin: 'vuetify-filter',
Root(root) {
// 过滤掉所有vuetify相关的CSS
root.walkRules(rule => {
if (rule.selector && (
rule.selector.includes('.v-') ||
rule.selector.includes('.mdi-'))) {
rule.remove();
}
});
}
}
]
}
},
server: {
port: 5001, // 使用不同于主应用的端口
cors: true, // 启用CORS
origin: 'http://localhost:5001'
},
})

View File

@@ -0,0 +1,561 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
"@babel/parser@^7.25.3":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.1.tgz#c55d5bed74449d1223701f1869b9ee345cc94cc9"
integrity sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==
dependencies:
"@babel/types" "^7.27.1"
"@babel/types@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@esbuild/aix-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
"@esbuild/android-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
"@esbuild/android-arm@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
"@esbuild/android-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
"@esbuild/darwin-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
"@esbuild/darwin-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
"@esbuild/freebsd-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
"@esbuild/freebsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
"@esbuild/linux-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
"@esbuild/linux-arm@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
"@esbuild/linux-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
"@esbuild/linux-loong64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
"@esbuild/linux-mips64el@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
"@esbuild/linux-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
"@esbuild/linux-riscv64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
"@esbuild/linux-s390x@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
"@esbuild/linux-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
"@esbuild/netbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
"@esbuild/openbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
"@esbuild/sunos-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
"@esbuild/win32-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
"@esbuild/win32-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
"@esbuild/win32-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@originjs/vite-plugin-federation@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@originjs/vite-plugin-federation/-/vite-plugin-federation-1.4.1.tgz#e6abc8f18f2cf82783eb87853f4d03e6358b43c2"
integrity sha512-Uo08jW5pj1t58OUKuZNkmzcfTN2pqeVuAWCCiKf/75/oll4Efq4cHOqSE1FXMlvwZNGDziNdDyBbQ5IANem3CQ==
dependencies:
estree-walker "^3.0.2"
magic-string "^0.27.0"
"@rollup/rollup-android-arm-eabi@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz#c228d00a41f0dbd6fb8b7ea819bbfbf1c1157a10"
integrity sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==
"@rollup/rollup-android-arm64@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz#e2b38d0c912169fd55d7e38d723aada208d37256"
integrity sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==
"@rollup/rollup-darwin-arm64@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz#1fddb3690f2ae33df16d334c613377f05abe4878"
integrity sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==
"@rollup/rollup-darwin-x64@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz#818298d11c8109e1112590165142f14be24b396d"
integrity sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==
"@rollup/rollup-freebsd-arm64@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz#91a28dc527d5bed7f9ecf0e054297b3012e19618"
integrity sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==
"@rollup/rollup-freebsd-x64@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz#28acadefa76b5c7bede1576e065b51d335c62c62"
integrity sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==
"@rollup/rollup-linux-arm-gnueabihf@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz#819691464179cbcd9a9f9d3dc7617954840c6186"
integrity sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==
"@rollup/rollup-linux-arm-musleabihf@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz#d149207039e4189e267e8724050388effc80d704"
integrity sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==
"@rollup/rollup-linux-arm64-gnu@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz#fa72ebddb729c3c6d88973242f1a2153c83e86ec"
integrity sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==
"@rollup/rollup-linux-arm64-musl@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz#2054216e34469ab8765588ebf343d531fc3c9228"
integrity sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==
"@rollup/rollup-linux-loongarch64-gnu@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz#818de242291841afbfc483a84f11e9c7a11959bc"
integrity sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==
"@rollup/rollup-linux-powerpc64le-gnu@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz#0bb4cb8fc4a2c635f68c1208c924b2145eb647cb"
integrity sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==
"@rollup/rollup-linux-riscv64-gnu@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz#4b3b8e541b7b13e447ae07774217d98c06f6926d"
integrity sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==
"@rollup/rollup-linux-riscv64-musl@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz#e065405e67d8bd64a7d0126c931bd9f03910817f"
integrity sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==
"@rollup/rollup-linux-s390x-gnu@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz#dda3265bbbfe16a5d0089168fd07f5ebb2a866fe"
integrity sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==
"@rollup/rollup-linux-x64-gnu@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz#90993269b8b995b4067b7b9d72ff1c360ef90a17"
integrity sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==
"@rollup/rollup-linux-x64-musl@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz#fdf5b09fd121eb8d977ebb0fda142c7c0167b8de"
integrity sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==
"@rollup/rollup-win32-arm64-msvc@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz#6397e1e012db64dfecfed0774cb9fcf89503d716"
integrity sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==
"@rollup/rollup-win32-ia32-msvc@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz#df0991464a52a35506103fe18d29913bf8798a0c"
integrity sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==
"@rollup/rollup-win32-x64-msvc@4.40.2":
version "4.40.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz#8dae04d01a2cbd84d6297d99356674c6b993f0fc"
integrity sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==
"@types/estree@1.0.7", "@types/estree@^1.0.0":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
"@types/web-bluetooth@^0.0.21":
version "0.0.21"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
"@vitejs/plugin-vue@^4.4.0":
version "4.6.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz#057d2ded94c4e71b94e9814f92dcd9306317aa46"
integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
"@vue/compiler-core@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05"
integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/shared" "3.5.13"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
dependencies:
"@vue/compiler-core" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/compiler-sfc@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46"
integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/compiler-core" "3.5.13"
"@vue/compiler-dom" "3.5.13"
"@vue/compiler-ssr" "3.5.13"
"@vue/shared" "3.5.13"
estree-walker "^2.0.2"
magic-string "^0.30.11"
postcss "^8.4.48"
source-map-js "^1.2.0"
"@vue/compiler-ssr@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba"
integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==
dependencies:
"@vue/compiler-dom" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/reactivity@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==
dependencies:
"@vue/shared" "3.5.13"
"@vue/runtime-core@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455"
integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==
dependencies:
"@vue/reactivity" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/runtime-dom@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215"
integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==
dependencies:
"@vue/reactivity" "3.5.13"
"@vue/runtime-core" "3.5.13"
"@vue/shared" "3.5.13"
csstype "^3.1.3"
"@vue/server-renderer@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7"
integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==
dependencies:
"@vue/compiler-ssr" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/shared@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
"@vueuse/core@^12.4.0":
version "12.8.2"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-12.8.2.tgz#007c6dd29a7d1f6933e916e7a2f8ef3c3f968eaa"
integrity sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==
dependencies:
"@types/web-bluetooth" "^0.0.21"
"@vueuse/metadata" "12.8.2"
"@vueuse/shared" "12.8.2"
vue "^3.5.13"
"@vueuse/metadata@12.8.2":
version "12.8.2"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3"
integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==
"@vueuse/shared@12.8.2":
version "12.8.2"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930"
integrity sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==
dependencies:
vue "^3.5.13"
csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
echarts@^5.4.3:
version "5.6.0"
resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.6.0.tgz#2377874dca9fb50f104051c3553544752da3c9d6"
integrity sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==
dependencies:
tslib "2.3.0"
zrender "5.6.1"
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
esbuild@^0.21.3:
version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
optionalDependencies:
"@esbuild/aix-ppc64" "0.21.5"
"@esbuild/android-arm" "0.21.5"
"@esbuild/android-arm64" "0.21.5"
"@esbuild/android-x64" "0.21.5"
"@esbuild/darwin-arm64" "0.21.5"
"@esbuild/darwin-x64" "0.21.5"
"@esbuild/freebsd-arm64" "0.21.5"
"@esbuild/freebsd-x64" "0.21.5"
"@esbuild/linux-arm" "0.21.5"
"@esbuild/linux-arm64" "0.21.5"
"@esbuild/linux-ia32" "0.21.5"
"@esbuild/linux-loong64" "0.21.5"
"@esbuild/linux-mips64el" "0.21.5"
"@esbuild/linux-ppc64" "0.21.5"
"@esbuild/linux-riscv64" "0.21.5"
"@esbuild/linux-s390x" "0.21.5"
"@esbuild/linux-x64" "0.21.5"
"@esbuild/netbsd-x64" "0.21.5"
"@esbuild/openbsd-x64" "0.21.5"
"@esbuild/sunos-x64" "0.21.5"
"@esbuild/win32-arm64" "0.21.5"
"@esbuild/win32-ia32" "0.21.5"
"@esbuild/win32-x64" "0.21.5"
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
estree-walker@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
dependencies:
"@types/estree" "^1.0.0"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
magic-string@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.13"
magic-string@^0.30.11:
version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
postcss@^8.4.43, postcss@^8.4.48:
version "8.5.3"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
dependencies:
nanoid "^3.3.8"
picocolors "^1.1.1"
source-map-js "^1.2.1"
resize-detector@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/resize-detector/-/resize-detector-0.3.0.tgz#fe495112e184695500a8f51e0389f15774cb1cfc"
integrity sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==
rollup@^4.20.0:
version "4.40.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.2.tgz#778e88b7a197542682b3e318581f7697f55f0619"
integrity sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==
dependencies:
"@types/estree" "1.0.7"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.40.2"
"@rollup/rollup-android-arm64" "4.40.2"
"@rollup/rollup-darwin-arm64" "4.40.2"
"@rollup/rollup-darwin-x64" "4.40.2"
"@rollup/rollup-freebsd-arm64" "4.40.2"
"@rollup/rollup-freebsd-x64" "4.40.2"
"@rollup/rollup-linux-arm-gnueabihf" "4.40.2"
"@rollup/rollup-linux-arm-musleabihf" "4.40.2"
"@rollup/rollup-linux-arm64-gnu" "4.40.2"
"@rollup/rollup-linux-arm64-musl" "4.40.2"
"@rollup/rollup-linux-loongarch64-gnu" "4.40.2"
"@rollup/rollup-linux-powerpc64le-gnu" "4.40.2"
"@rollup/rollup-linux-riscv64-gnu" "4.40.2"
"@rollup/rollup-linux-riscv64-musl" "4.40.2"
"@rollup/rollup-linux-s390x-gnu" "4.40.2"
"@rollup/rollup-linux-x64-gnu" "4.40.2"
"@rollup/rollup-linux-x64-musl" "4.40.2"
"@rollup/rollup-win32-arm64-msvc" "4.40.2"
"@rollup/rollup-win32-ia32-msvc" "4.40.2"
"@rollup/rollup-win32-x64-msvc" "4.40.2"
fsevents "~2.3.2"
source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
vite@^5.4.11:
version "5.4.19"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959"
integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"
rollup "^4.20.0"
optionalDependencies:
fsevents "~2.3.3"
vue-demi@^0.13.11:
version "0.13.11"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
vue-echarts@^6.6.1:
version "6.7.3"
resolved "https://registry.yarnpkg.com/vue-echarts/-/vue-echarts-6.7.3.tgz#30efafc51a4a9de1b8117d3b63e74b0c761ff3ba"
integrity sha512-vXLKpALFjbPphW9IfQPOVfb1KjGZ/f8qa/FZHi9lZIWzAnQC1DgnmEK3pJgEkyo6EP7UnX6Bv/V3Ke7p+qCNXA==
dependencies:
resize-detector "^0.3.0"
vue-demi "^0.13.11"
vue@^3.5.13:
version "3.5.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
dependencies:
"@vue/compiler-dom" "3.5.13"
"@vue/compiler-sfc" "3.5.13"
"@vue/runtime-dom" "3.5.13"
"@vue/server-renderer" "3.5.13"
"@vue/shared" "3.5.13"
vuetify@3.7.3:
version "3.7.3"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.7.3.tgz#0e89f7f0298d452510bcbc01b0e9b53a5ce6e883"
integrity sha512-bpuvBpZl1/+nLlXDgdVXekvMNR6W/ciaoa8CYlpeAzAARbY8zUFSoBq05JlLhkIHI58AnzKVy4c09d0OtfYAPg==
zrender@5.6.1:
version "5.6.1"
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.6.1.tgz#e08d57ecf4acac708c4fcb7481eb201df7f10a6b"
integrity sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==
dependencies:
tslib "2.3.0"

View File

@@ -1,105 +1,260 @@
<!DOCTYPE html>
<html lang="en" style="
<html lang="zh-CN" style="
overflow: hidden auto;
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
min-block-size: 100vh;
min-block-size: 100dvh;
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-top: env(safe-area-inset-top);
background: var(--initial-loader-bg, #fff);
">
<head>
<meta http-equiv="pragma" content="no-cache" />
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="expires" content="0" />
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
<title>MoviePilot</title>
<meta charset="UTF-8" />
<!-- 核心viewport设置 - 针对PWA优化 -->
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
<!-- 防止缩放和选择,提供原生应用体验 -->
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
<!-- 基础信息 -->
<meta name="description" content="MoviePilot - 智能影视媒体库管理工具" />
<meta name="author" content="MoviePilot" />
<meta name="keywords" content="MoviePilot,影视,媒体库,管理" />
<!-- 安全和隐私 -->
<meta name="Robots" content="noindex,nofollow,noarchive" />
<meta name="referrer" content="origin" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="referrer" content="no-referrer" />
<!-- PWA - 基础图标 -->
<link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="icon" type="image/png" href="/logo.png" sizes="any" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<!-- iOS Safari PWA 优化 -->
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
<link rel="apple-touch-startup-image" href="/splash/apple-splash.png" />
<!-- iOS Safari 全屏模式 -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
<meta name="description" content="MoviePilot" />
<meta name="format-detection" content="telephone=no" />
<meta name="referrer" content="never" />
<meta name="msapplication-TileColor" content="#7D34FD" />
<meta name="color-scheme" content="light dark" />
<!-- iOS Safari 防止自动识别 -->
<meta name="apple-mobile-web-app-orientations" content="portrait" />
<!-- Android Chrome PWA 优化 -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-title" content="MoviePilot" />
<!-- Microsoft Windows PWA -->
<meta name="msapplication-TileColor" content="#0E1116" />
<meta name="msapplication-TileImage" content="/android-chrome-192x192.png" />
<meta name="msapplication-config" content="none" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="msapplication-navbutton-color" content="#0E1116" />
<!-- 主题色彩 - 适配深色和浅色模式 -->
<meta name="theme-color" content="#0E1116" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
<meta name="color-scheme" content="dark light" />
<!-- 屏幕方向锁定 -->
<meta name="screen-orientation" content="portrait" />
<meta name="x5-orientation" content="portrait" />
<meta name="x5-fullscreen" content="true" />
<meta name="x5-page-mode" content="app" />
<!-- UC浏览器优化 -->
<meta name="browsermode" content="application" />
<meta name="wap-font-scale" content="no" />
<!-- 360浏览器优化 -->
<meta name="renderer" content="webkit" />
<!-- 触摸优化 -->
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<link rel="stylesheet" type="text/css" href="/loader.css" />
<!-- 缓存控制 -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<!-- DNS预解析和预连接 -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//cdn.jsdelivr.net" />
<link rel="dns-prefetch" href="//image.tmdb.org" />
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<!-- 预加载关键资源 -->
<link rel="preload" href="/logo.png" as="image" />
<link rel="modulepreload" href="/src/main.ts" />
<!-- 内联关键CSS -->
<style>
/* 关键路径CSS - 从loader.css内联 */
#loading-bg {
position: fixed;
z-index: 99999;
display: block;
background: var(--initial-loader-bg, #fff);
block-size: 100vh;
inline-size: 100vw;
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
}
.loading-logo {
position: absolute;
inset-block-start: 35%;
inset-inline-start: calc(50% - 5rem);
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
}
/* 添加logo完成动画 - 放大虚化效果 */
.loading-complete .loading-logo {
filter: blur(10px);
opacity: 0;
transform: scale(1.5);
}
/* 添加加载背景消失动画 - 放大虚化效果 */
.loading-complete {
filter: blur(15px);
opacity: 0;
transform: scale(1.2);
}
.loading {
position: absolute;
box-sizing: border-box;
border: 3px solid transparent;
border-radius: 50%;
block-size: 55px;
inline-size: 55px;
inset-block-start: 80%;
inset-inline-start: calc(50% - 27.5px);
transition: opacity 0.6s ease;
}
/* 完成时隐藏加载动画 */
.loading-complete .loading {
opacity: 0;
}
.loading .effect-1,
.loading .effect-2,
.loading .effect-3 {
position: absolute;
box-sizing: border-box;
border: 3px solid transparent;
border-radius: 50%;
block-size: 100%;
border-inline-start: 3px solid var(--initial-loader-color, #eee);
inline-size: 100%;
}
.loading .effect-1 {
animation: rotate 1s ease infinite;
}
.loading .effect-2 {
animation: rotate-opacity 1s ease infinite 0.1s;
}
.loading .effect-3 {
animation: rotate-opacity 1s ease infinite 0.2s;
}
.loading .effects {
transition: all 0.3s ease;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(1turn);
}
}
@keyframes rotate-opacity {
0% {
opacity: 0.1;
transform: rotate(0deg);
}
100% {
opacity: 1;
transform: rotate(1turn);
}
}
</style>
<!-- 初始化脚本 -->
<script>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
// 检测系统主题是否为深色模式
function checkPrefersColorSchemeIsDark() {
try {
return window.matchMedia('(prefers-color-scheme: dark)').matches
} catch (e) {
return false
}
}
// 主题色彩初始化
let loaderColor = localStorage.getItem('materio-initial-loader-bg')
let primaryColor = localStorage.getItem('materio-initial-loader-color')
// 检查主题设置
const savedTheme = localStorage.getItem('theme')
const isAutoTheme = savedTheme === 'auto'
// 如果是自动主题或者没有保存的背景色,根据系统主题设置背景色
if (isAutoTheme || !loaderColor) {
loaderColor = checkPrefersColorSchemeIsDark() ? '#0E1116' : '#FFFFFF'
}
if (!primaryColor) {
primaryColor = '#9155FD'
}
// 应用主题色彩
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
// 状态栏适配
if (window.navigator.standalone) {
document.documentElement.style.setProperty('--status-bar-height', '20px')
}
// 安全区域适配
function updateSafeArea() {
const safeAreaTop = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)')
const safeAreaBottom = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-bottom)',
)
if (safeAreaTop) document.documentElement.style.setProperty('--safe-area-top', safeAreaTop)
if (safeAreaBottom) document.documentElement.style.setProperty('--safe-area-bottom', safeAreaBottom)
}
updateSafeArea()
window.addEventListener('resize', updateSafeArea)
window.addEventListener('orientationchange', updateSafeArea)
</script>
</head>
<body style="margin: 0">
<body style="margin: 0; overflow: hidden; overscroll-behavior: none; -webkit-overflow-scrolling: touch">
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
<svg width="160px" height="160px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
<style>
/* 添加SVG内部的动画样式 */
@keyframes pulse {
0%,
100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
@keyframes glow {
0%,
100% {
filter: drop-shadow(0 0 3px rgba(141, 81, 249, 0.3));
}
50% {
filter: drop-shadow(0 0 6px rgba(141, 81, 249, 0.6));
}
}
/* 为各个元素添加动画 */
#a2-c {
filter: drop-shadow(0 0 5px rgba(141, 81, 249, 0.3));
animation: glow 3s ease-in-out infinite;
}
path {
animation: pulse 2s ease-in-out infinite;
}
/* 错开不同元素的动画开始时间 */
g:nth-child(2) path {
animation-delay: 0.3s;
}
g:nth-child(3) path {
animation-delay: 0.6s;
}
g:nth-child(4) path {
animation-delay: 0.9s;
}
g:nth-child(5) path {
animation-delay: 1.2s;
}
</style>
<g transform="matrix(1,0,0,1,-2606,-236)">
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
<rect x="0" y="0" width="192" height="192" style="fill: none" />
@@ -211,4 +366,4 @@
<script type="module" src="/src/main.ts"></script>
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.4.3",
"version": "2.7.6",
"private": true,
"type": "module",
"bin": "dist/service.js",
@@ -27,6 +27,7 @@
"@fullcalendar/timegrid": "^6.1.15",
"@fullcalendar/vue3": "^6.1.15",
"@iconify/utils": "^2.2.1",
"@types/crypto-js": "^4.2.2",
"@types/js-cookie": "^3.0.6",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
@@ -42,9 +43,11 @@
"axios": "^1.7.9",
"colorthief": "^2.6.0",
"copy-to-clipboard": "^3.3.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"express": "^4.21.2",
"express-http-proxy": "^2.1.1",
"http-proxy-middleware": "^3.0.0",
"js-cookie": "^3.0.5",
"lodash-es": "^4.17.21",
"mousetrap": "^1.6.5",
@@ -56,13 +59,12 @@
"tailwindcss": "^ 3.4.17",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-toast-notification": "^3.1.3",
"vue-toastification": "^2.0.0-rc.5",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.8.0",
"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": {
@@ -70,6 +72,7 @@
"@iconify/tools": "^4.0.4",
"@iconify/vue": "^4.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@originjs/vite-plugin-federation": "^1.4.1",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
@@ -104,6 +107,7 @@
"vite": "^5.4.11",
"vite-plugin-pages": "^0.32.1",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "2.0.4",
"vue-shepherd": "^4.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,97 +0,0 @@
#loading-bg {
position: fixed;
z-index: 9999;
display: block;
background: var(--initial-loader-bg, #fff);
block-size: 100vh;
inline-size: 100vw;
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
}
.loading-logo {
position: absolute;
inset-block-start: 35%;
inset-inline-start: calc(50% - 5rem);
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
}
/* 添加logo完成动画 - 放大虚化效果 */
.loading-complete .loading-logo {
filter: blur(10px);
opacity: 0;
transform: scale(1.5);
}
/* 添加加载背景消失动画 - 放大虚化效果 */
.loading-complete {
filter: blur(15px);
opacity: 0;
transform: scale(1.2);
}
.loading {
position: absolute;
box-sizing: border-box;
border: 3px solid transparent;
border-radius: 50%;
block-size: 55px;
inline-size: 55px;
inset-block-start: 80%;
inset-inline-start: calc(50% - 27.5px);
transition: opacity 0.6s ease;
}
/* 完成时隐藏加载动画 */
.loading-complete .loading {
opacity: 0;
}
.loading .effect-1,
.loading .effect-2,
.loading .effect-3 {
position: absolute;
box-sizing: border-box;
border: 3px solid transparent;
border-radius: 50%;
block-size: 100%;
border-inline-start: 3px solid var(--initial-loader-color, #eee);
inline-size: 100%;
}
.loading .effect-1 {
animation: rotate 1s ease infinite;
}
.loading .effect-2 {
animation: rotate-opacity 1s ease infinite 0.1s;
}
.loading .effect-3 {
animation: rotate-opacity 1s ease infinite 0.2s;
}
.loading .effects {
transition: all 0.3s ease;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(1turn);
}
}
@keyframes rotate-opacity {
0% {
opacity: 0.1;
transform: rotate(0deg);
}
100% {
opacity: 1;
transform: rotate(1turn);
}
}

160
public/offline.html Normal file
View File

@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>MoviePilot - 离线</title>
<link rel="icon" href="/favicon.ico">
<style>
:root {
--primary-color: #9155FD;
--surface-color: #FFFFFF;
--text-color: #333333;
--border-color: rgba(0, 0, 0, 0.12);
}
@media (prefers-color-scheme: dark) {
:root {
--surface-color: #0E1116;
--text-color: #FFFFFF;
--border-color: rgba(255, 255, 255, 0.12);
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--surface-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.offline-container {
text-align: center;
max-width: 400px;
width: 100%;
padding: 40px;
background: var(--surface-color);
border-radius: 24px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1), 0 0 0 1px var(--border-color);
}
.icon-wrapper {
width: 120px;
height: 120px;
margin: 0 auto 32px;
background: rgba(145, 85, 253, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
width: 64px;
height: 64px;
fill: var(--primary-color);
}
h1 {
font-size: 2rem;
margin-bottom: 16px;
font-weight: 600;
}
p {
font-size: 1.1rem;
line-height: 1.6;
opacity: 0.7;
margin-bottom: 32px;
}
.retry-button {
background: var(--primary-color);
color: white;
border: none;
padding: 12px 32px;
font-size: 1rem;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: opacity 0.2s;
}
.retry-button:hover {
opacity: 0.9;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 24px;
padding: 8px 16px;
background: rgba(145, 85, 253, 0.1);
border-radius: 20px;
font-size: 0.875rem;
}
.status-dot {
width: 8px;
height: 8px;
background: #EF5350;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
</style>
</head>
<body>
<div class="offline-container">
<div class="icon-wrapper">
<svg class="icon" viewBox="0 0 24 24">
<path d="M12,2.03C17.73,2.5 22,7.08 22,12.75C22,13.84 21.79,14.89 21.4,15.86L19.53,14C19.5,13.83 19.5,13.67 19.5,13.5A2.5,2.5 0 0,0 17,11A2.5,2.5 0 0,0 14.5,13.5A2.5,2.5 0 0,0 17,16A2.5,2.5 0 0,0 19.5,13.5C19.5,13.67 19.5,13.83 19.53,14L21.4,15.86C20.04,19.09 16.9,21.47 13.19,21.97L11.75,20.53C11.83,20.5 11.92,20.5 12,20.5A2.5,2.5 0 0,0 14.5,18A2.5,2.5 0 0,0 12,15.5A2.5,2.5 0 0,0 9.5,18C9.5,18.08 9.5,18.17 9.53,18.25L7.66,16.38C7.25,15.96 6.86,15.5 6.5,15H8.17C8.06,14.7 8,14.35 8,14A3,3 0 0,1 11,11A3,3 0 0,1 14,14C14,14.35 13.94,14.7 13.83,15H15.5C15.14,15.5 14.75,15.96 14.34,16.38L12.47,14.5C12.5,14.42 12.5,14.33 12.47,14.25L10.6,12.38C10.18,11.97 9.72,11.59 9.23,11.25L7.36,9.38C6.94,8.96 6.5,8.61 6,8.31V6.64L4.14,4.78C3.6,5.55 3.17,6.4 2.86,7.31L1,5.45V4.46L2.05,3.41C2.5,2.86 3.05,2.41 3.66,2.06L20,18.4L18.73,19.67L12.47,13.41L11.75,20.53C11.83,20.5 11.92,20.5 12,20.5A2.5,2.5 0 0,0 14.5,18A2.5,2.5 0 0,0 12,15.5A2.5,2.5 0 0,0 9.5,18C9.5,18.08 9.5,18.17 9.53,18.25L7.66,16.38C7.25,15.96 6.86,15.5 6.5,15H8.17C8.06,14.7 8,14.35 8,14A3,3 0 0,1 11,11A3,3 0 0,1 14,14C14,14.35 13.94,14.7 13.83,15H15.5C15.14,15.5 14.75,15.96 14.34,16.38L2.46,4.5C3.5,3.17 4.9,2.15 6.5,1.58V3.25C5.43,3.7 4.47,4.33 3.66,5.11L2.61,6.16V8.03C3.16,7.33 3.82,6.73 4.57,6.25V8.31C3.57,9.14 2.75,10.19 2.21,11.39L1,10.18V8.65C1.5,6.16 3.03,4.03 5.11,2.71L6.39,4C8.97,2.73 12.03,2.24 14.97,3.03L16.84,4.9C18.17,5.86 19.25,7.16 19.94,8.68L18.07,6.81C17.07,5.5 15.66,4.5 14,4.04V5.71C15.93,6.17 17.5,7.53 18.33,9.3L16.46,7.43C15.46,6.61 14.2,6.08 12.82,6V7.67C13.69,7.79 14.47,8.11 15.14,8.58L13.27,6.71C12.94,6.66 12.6,6.63 12.25,6.63L10.38,4.76C10.87,4.66 11.37,4.59 11.88,4.56L10,2.68C10.66,2.56 11.33,2.5 12,2.5V2.03Z" />
</svg>
</div>
<h1>您当前处于离线状态</h1>
<p>无法连接到 MoviePilot 服务器。请检查您的网络连接后重试。</p>
<button class="retry-button" onclick="window.location.reload()">
重新加载
</button>
<div class="status-badge">
<span class="status-dot"></span>
<span>离线状态</span>
</div>
</div>
<script>
// 监听网络状态变化
window.addEventListener('online', function() {
window.location.reload();
});
// Service Worker 消息处理
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', function(event) {
if (event.data && event.data.type === 'OFFLINE_STATUS' && !event.data.offline) {
window.location.reload();
}
});
}
</script>
</body>
</html>

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

@@ -5,7 +5,7 @@ defineProps({
})
</script>
<template>
<div v-if="title" class="my-3 md:flex md:items-center md:justify-between">
<div v-if="title" class="my-3 mx-3 md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1 mx-0">
<h2
class="ms-1 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-3xl sm:leading-9 md:mb-0"

View File

@@ -51,8 +51,8 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
gap: 16px;
inset-block-end: 30px;
inset-inline-end: 30px;
inset-block-end: 2rem;
inset-inline-end: 2rem;
}
.global-action-button {

View File

@@ -3,6 +3,30 @@
@use "@layouts/styles/_placeholders";
@use "@configured-variables" as variables;
// 👉 Alert
.v-alert {
.v-alert__close {
.v-icon {
block-size: 20px !important;
font-size: 20px !important;
inline-size: 20px !important;
}
}
&:not(.v-alert--prominent) .v-alert__prepend {
.v-icon {
block-size: 1.375rem !important;
font-size: 1.375rem !important;
inline-size: 1.375rem !important;
}
}
.v-alert-title {
line-height: 1.5rem;
margin-block-end: 0.25rem;
}
}
// 👉 Avatar font-size
.v-avatar {
@include mixins.avatar-font-sizes($map: variables.$avatar-font-sizes);
@@ -33,6 +57,23 @@
}
}
// 👉 Button
.v-btn {
/* stylelint-disable-next-line no-descending-specificity */
&:not(.v-btn--icon) .v-icon {
--v-icon-size-multiplier: 0.9525 !important;
}
}
// 👉 Chip
.v-chip.v-chip--size-default .v-avatar {
--v-avatar-height: 24px;
}
.v-chip.v-chip--density-comfortable {
line-height: 1;
}
// Dialog responsive width
.v-dialog {
.v-card {
@@ -40,7 +81,7 @@
}
}
@media (min-width: 576px) {
@media (width >= 576px) {
.v-dialog {
&.v-dialog-sm,
&.v-dialog-lg,
@@ -50,7 +91,7 @@
}
}
@media (min-width: 992px) {
@media (width >= 992px) {
.v-dialog {
&.v-dialog-lg,
&.v-dialog-xl {
@@ -59,18 +100,32 @@
}
}
@media (min-width: 1200px) {
@media (width >= 1200px) {
.v-dialog.v-dialog-xl,
.v-dialog.v-dialog-xl .v-overlay__content > .v-card {
inline-size: 1165px !important;
}
}
// v-tab with pill support
// 👉 Expansion Panel
.v-expansion-panel {
.v-expansion-panel-text {
font-size: 1rem;
}
}
// 👉 Tooltip
.v-tooltip > .v-overlay__content {
font-weight: 500;
line-height: 0.875rem;
}
// 👉 List
// 👉 Tab with pill support
.v-tabs.v-tabs-pill {
.v-tab.v-btn {
border-radius: 0.375rem !important;
border-radius: 6px !important;
min-inline-size: 8.125rem;
transition: none;
@@ -94,7 +149,7 @@
}
}
// 👉 added box shadow
// 👉 Timeline added box shadow
.v-timeline-item {
.v-timeline-divider__dot {
.v-timeline-divider__inner-dot {
@@ -160,7 +215,6 @@
}
// 👉 Slider
.v-slider.v-input--horizontal .v-slider-track__fill {
block-size: var(--v-slider-track-size);
}
@@ -171,7 +225,19 @@
.v-slider-thumb {
.v-slider-thumb__label {
background: rgb(117, 117, 117);
color: rgb(var(--v-theme-on-primary));
&::before {
color: rgb(117, 117, 117);
}
}
}
// 👉 Switch
.v-switch {
.v-selection-control:not(.v-selection-control--dirty) .v-switch__thumb {
color: #fff;
}
}
@@ -179,5 +245,45 @@
.v-table--density-default > .v-table__wrapper > table > tbody > tr > td,
.v-table--density-default > .v-table__wrapper > table > thead > tr > td,
.v-table--density-default > .v-table__wrapper > table > tfoot > tr > td {
block-size: 50px;
block-size: 50px !important;
}
.v-table {
--v-table-header-height: 54px !important;
th {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
font-size: 0.75rem;
.v-data-table-header__content {
display: flex;
justify-content: space-between;
}
}
.v-selection-control {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
font-size: 1rem;
}
}
.v-data-table {
th {
background: rgb(var(--v-table-header-background)) !important;
}
}
// 👉 Pagination
.v-pagination {
.v-btn {
border-radius: 4px;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 14px;
font-weight: 400;
}
}
// 👉 SnackBar
.v-snackbar--variant-elevated {
@include mixins.elevation(6);
}

View File

@@ -1,7 +1,7 @@
@use "@configured-variables" as variables;
@use "placeholders" as *;
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "../misc";
@use "misc";
@use "mixins";
$header: ".layout-navbar";
@@ -16,25 +16,43 @@ $header: ".layout-navbar";
@if variables.$vertical-nav-navbar-style == "elevated" {
// Add transition
#{$header} {
transition: padding 0.2s ease, background-color 0.18s ease;
transition: padding 0.2s ease;
}
// If navbar is contained => Add border radius to header
@if variables.$layout-vertical-nav-navbar-is-contained {
#{$header} {
// #{$header} {
// border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
}
// }
}
// Scrolled styles for sticky navbar
@at-root {
/* 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
/* Only apply scrolled styles when window is actually scrolled,
not when dialog is opened without scroll
*/
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-fixed,
&.window-scrolled.layout-navbar-fixed {
#{$header} {
padding-inline: 1rem;
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
}
.navbar-blur#{$header} {
@extend %blurry-bg;
}
}
/* Ensure header styles are preserved when dialog is opened,
but only if window was scrolled before dialog opened
*/
html.v-overlay-scroll-blocked &.window-scrolled.layout-navbar-fixed {
#{$header} {
padding-inline: 1rem;
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
}
@@ -63,7 +81,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

@@ -1,5 +1,5 @@
@use "@core/scss/placeholders";
@use "@core/scss/variables" as core-vars;
@use "placeholders";
@use "variables" as core-vars;
.layout-navbar {
@if core-vars.$navbar-high-emphasis-text {

View File

@@ -11,11 +11,58 @@
// adding styling for code tag
code {
background: rgba(var(--v-code-background-color), var(--v-focus-opacity));
border-radius: 3px;
background: rgba(var(--v-code-background-color), var(--v-focus-opacity));
color: currentcolor;
font-size: 85%;
font-weight: 400;
padding-block: 0.2em;
padding-inline: 0.4em;
}
%blurry-bg {
position: relative;
box-shadow: 0 1px 3px rgba(0, 0, 0, 4%), 0 1px 2px rgba(0, 0, 0, 2%);
@media (width >= 1280px) and (hover: hover) {
background: rgba(var(--v-theme-background), 1);
.v-theme--transparent & {
backdrop-filter: blur(var(--transparent-blur-light, 5px));
background: rgba(var(--v-theme-background), var(--transparent-opacity-light, 0.1)) !important;
}
}
@media (width < 1280px), (hover: none) {
background: transparent;
&::before {
position: absolute;
z-index: -1;
backdrop-filter: blur(24px);
block-size: calc(env(safe-area-inset-top, 0px) + var(--navbar-tab-height) + 4rem);
content: "";
inset-block-start: 0;
inset-inline: 0;
pointer-events: none;
transition: padding 0.3s ease-in-out;
.v-theme--light & {
background: rgba(var(--v-theme-surface), 0.6);
}
.v-theme--dark & {
background: rgba(var(--v-theme-background), 0.5);
}
.v-theme--purple & {
background: rgba(var(--v-theme-background), 0.5);
}
.v-theme--transparent & {
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
background: rgba(var(--v-theme-background), var(--transparent-opacity-heavy, 0.5));
}
}
}
}

View File

@@ -1,4 +1,6 @@
@use "sass:map";
@use "vuetify/lib/styles/settings" as vuetify_settings;
@use "@styles/variables/_vuetify.scss" as vuetify;
@mixin themed($property, $light-value, $dark-value) {
@at-root {
@@ -17,11 +19,12 @@
// This mixin is inspired from vuetify for adding hover styles via before pseudo element
@mixin before-pseudo() {
position: relative;
&::before {
position: absolute;
border-radius: inherit;
background: currentcolor;
block-size: 100%;
border-radius: inherit;
content: "";
inline-size: 100%;
inset: 0;
@@ -43,8 +46,8 @@
&::before {
position: absolute;
background-color: currentcolor;
border-radius: inherit;
background-color: currentcolor;
content: "";
inset: 0;
opacity: $opacity;
@@ -56,10 +59,81 @@
@mixin avatar-font-sizes($map: $avatar-sizes) {
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
/* stylelint-disable-next-line scss/no-global-function-names */
$size: map-get($map, $sizeName);
$size: map.get($map, $sizeName);
&.v-avatar--size-#{$sizeName} {
font-size: #{$size}px;
}
}
}
@mixin elevation($z, $important: false) {
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
}
@mixin bordered-skin($component, $border-property: "border", $important: false) {
#{$component} {
box-shadow: none !important;
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
}
}
@mixin selected-states($selector) {
#{$selector} {
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
}
&:hover
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
}
&:focus-visible
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
@supports not selector(:focus-visible) {
&:focus {
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
}
}
}
@mixin push-anchors() {
:target {
scroll-margin-block-start: 90px;
}
}
@mixin xs {
@media (width >= 0) and (width <= 599.98px) {
@content;
}
}
@mixin sm {
@media (width >= 600px) and (width <= 959.98px) {
@content;
}
}
@mixin md {
@media (width >= 960px) and (width <= 1279.98px) {
@content;
}
}
@mixin lg {
@media (width >= 1280px) and (width <= 1919.98px) {
@content;
}
}
@mixin xl {
@media (width >= 1920px) {
@content;
}
}

View File

@@ -1,73 +1,25 @@
@use "@configured-variables" as variables;
// 👉 Demo spacers
// TODO: Use vuetify SCSS variable here
$card-spacer-content: 16px;
.demo-space-x {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-block-start: -$card-spacer-content;
& > * {
margin-block-start: $card-spacer-content;
margin-inline-end: $card-spacer-content;
}
}
.demo-space-y {
& > * {
margin-block-end: $card-spacer-content;
&:last-child {
margin-block-end: 0;
}
}
}
// 👉 Card match height
.match-height.v-row {
.v-card {
block-size: 100%;
}
}
// 👉 Whitespace
.whitespace-no-wrap {
white-space: nowrap;
}
// 👉 Colors
/*
Vuetify is applying `.text-white` class to badge icon but don't provide its styles
Moreover, we also use this class in some places
In vuetify 2 with `$color-pack: false` SCSS var config this class was getting generated but this is not the case in v3
We also need !important to get correct color in badge icon
*/
.text-white {
color: #fff !important;
}
.bg-var-theme-background {
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)) !important;
}
// [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })],
@each $color-name in variables.$theme-colors-name {
.bg-light-#{$color-name} {
background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important;
// 👉 Pagination small-select dropdown for table
// TODO: remove this class after vuetify datatable implememtation
.per-page-select {
margin-block: auto;
.v-field__input {
align-items: center;
padding: 2px;
font-size: 14px;
}
.v-field__append-inner {
align-items: center;
padding: 0;
.v-icon {
margin-inline-start: 0 !important;
}
}
}
// 👉 Typography
.font-weight-semibold {
font-weight: 600 !important;
}
.leading-normal {
line-height: normal !important;
}

View File

@@ -9,20 +9,53 @@
- You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header)
*/
@use "sass:map";
// 使用模板中的变量,不再进行配置
@use "@layouts/styles/variables" as layouts-vars;
@use "utils";
@use "vuetify/lib/styles/tools/functions" as *;
// 👉 Default layout
// 合并两个文件中的@forward配置
@forward "@layouts/styles/variables" with (
// 来自_variables.scss的配置
$layout-vertical-nav-collapsed-width: 68px !default,
// 来自template/_variables.scss的配置
$layout-vertical-nav-z-index: 1004,
$layout-overlay-z-index: 1003
);
$navbar-high-emphasis-text: true !default;
// 使用命名空间来避免变量冲突
@use "@layouts/styles/variables" as layouts-vars;
// 移除@forward配置已合并到template/_variables.scss
// @forward "@layouts/styles/variables" with (
// $layout-vertical-nav-collapsed-width: 68px !default,
// );
// @use "@layouts/styles/variables" as *;
$vertical-nav-horizontal-padding-custom: 1.375rem 1rem;
// We created this SCSS var to extract the start padding
// Docs: https://sass-lang.com/documentation/modules/string
// $vertical-nav-horizontal-padding => 0 8px;
// string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
// string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
// string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-custom) !default;
$vertical-nav-items-icon-margin-inline-end: 0.625rem !default;
// Vertical Nav Configuration
$vertical-nav-collapsed-width: 68px !default;
// This is used to keep consistency between nav items and nav header left & right margin
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 0 1.125rem !default;
$vertical-nav-horizontal-padding: $vertical-nav-horizontal-padding-custom !default;
// Vertical nav header padding
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default;
// 👉 Custom Variables
$avatar-font-sizes: (
"x-small":12,
"small":14,
"default":18,
"large":20,
"x-large":24
) !default;
$theme-colors-name: (
"primary",
@@ -41,31 +74,16 @@ $default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
// This is used to keep consistency between nav items and nav header left & right margin
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 0 1.125rem !default;
$vertical-nav-horizontal-padding: 1.375rem 1rem !default;
/*
We created this SCSS var to extract the start padding
Docs: https://sass-lang.com/documentation/modules/string
$vertical-nav-horizontal-padding => 0 8px;
string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
*/
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding);
// Vertical nav header height. Mostly we will align it with navbar height;
$vertical-nav-header-height: layouts-vars.$layout-vertical-nav-navbar-height !default;
$vertical-nav-navbar-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
$vertical-nav-navbar-elevation: 3 !default;
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
$vertical-nav-floating-navbar-top: 1rem !default;
// Vertical nav header padding
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default;
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
// Move logo when vertical nav is mini (collapsed but not hovered)
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px;
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
// Space between logo and title
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
@@ -77,22 +95,130 @@ $vertical-nav-section-title-mt: 1.5rem !default;
$vertical-nav-section-title-mb: 0.5rem !default;
// Vertical nav icons
$vertical-nav-items-icon-size: 1.5rem;
$vertical-nav-items-icon-margin-inline-end: 0.625rem;
$vertical-nav-items-icon-size: 1.5rem !default;
$vertical-nav-items-nested-icon-size: 0.9rem !default;
// Transition duration for nav group arrow
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
// Timing function for nav group arrow
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
// 👉 Horizontal nav
/*
❗ Heads up
==================
Here we assume we will always use shorthand property which will apply same padding on four side
This is because this have been used as value of top property by `.popper-content`
*/
$horizontal-nav-padding: 0.6875rem !default;
// Gap between top level horizontal nav items
$horizontal-nav-top-level-items-gap: 4px !default;
// Horizontal nav icons
$horizontal-nav-items-icon-size: 1.5rem !default;
$horizontal-nav-third-level-icon-size: 0.9rem !default;
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
// We used SCSS variable because we want to allow users to update max height of popper content
// 120px is combined height of navbar & horizontal nav
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
// This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
// 👉 Plugins
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35);
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
// 👉 Custom Variables
$avatar-font-sizes: () !default;
$avatar-font-sizes: map.deep-merge(
// 👉 Vuetify
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
$vuetify-reduce-default-compact-button-icon-size: true !default;
// 👉 Custom variables
// for utility classes
$font-sizes: () !default;
$font-sizes: map-deep-merge(
(
"x-small":12,
"small":14,
"default":18,
"large":20,
"x-large":24
"xs": 0.75rem,
"sm": 0.875rem,
"base": 1rem,
"lg": 1.125rem,
"xl": 1.25rem,
"2xl": 1.5rem,
"3xl": 1.875rem,
"4xl": 2.25rem,
"5xl": 3rem,
"6xl": 3.75rem,
"7xl": 4.5rem,
"8xl": 6rem,
"9xl": 8rem
),
$avatar-font-sizes
$font-sizes
);
// line height
$font-line-height: () !default;
$font-line-height: map-deep-merge(
(
"xs": 1rem,
"sm": 1.25rem,
"base": 1.5rem,
"lg": 1.75rem,
"xl": 1.75rem,
"2xl": 2rem,
"3xl": 2.25rem,
"4xl": 2.5rem,
"5xl": 1,
"6xl": 1,
"7xl": 1,
"8xl": 1,
"9xl": 1
),
$font-line-height
);
// gap utility class
$gap: () !default;
$gap: map-deep-merge(
(
"0": 0,
"1": 0.25rem,
"2": 0.5rem,
"3": 0.75rem,
"4": 1rem,
"5": 1.25rem,
"6":1.5rem,
"7": 1.75rem,
"8": 2rem,
"9": 2.25rem,
"10": 2.5rem,
"11": 2.75rem,
"12": 3rem,
"14": 3.5rem,
"16": 4rem,
"20": 5rem,
"24": 6rem,
"28": 7rem,
"32": 8rem,
"36": 9rem,
"40": 10rem,
"44": 11rem,
"48": 12rem,
"52": 13rem,
"56": 14rem,
"60": 15rem,
"64": 16rem,
"72": 18rem,
"80": 20rem,
"96": 24rem
),
$gap
);
// 👉 Default layout
$navbar-high-emphasis-text: true !default;

View File

@@ -1,6 +1,6 @@
@use "./placeholders";
@use "@configured-variables" as variables;
@use "@core/scss/mixins" as mixins;
@use "./mixins" as mixins;
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
@use "vuetify/lib/styles/tools/elevation" as elevation;
@@ -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

@@ -1,5 +1,44 @@
@use "sass:map";
@use "template/index";
// 保留这个引用以向后兼容但实际功能已经移至template/index.scss
// 基础变量和配置
@use "variables";
@use "mixins";
@use "utils";
// 布局相关
@use "default-layout";
@use "vertical-nav";
@use "default-layout-w-vertical-nav";
// 组件样式
@use "components";
// 工具类
@use "utilities";
// 其他样式
@use "misc";
@use "dark";
// 第三方库样式
@use "libs/perfect-scrollbar";
@use "libs/apex-chart";
@use "libs/full-calendar";
@use "libs/vuetify";
// 全局样式
a {
color: rgb(var(--v-theme-primary));
text-decoration: none;
}
// Vuetify 3 don't provide margin bottom style like vuetify 2
p {
margin-block-end: 1rem;
}
// Iconify icon size
svg.iconify {
block-size: 1em;
inline-size: 1em;
}

View File

@@ -1,67 +1,88 @@
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@configured-variables" as variables;
@use "../mixins";
// 👉 Apex chart
.apexcharts-canvas {
&line[stroke="transparent"] {
display: "none";
// For RTL alignment
.apexcharts-yaxis-texts-g {
text-align: start;
}
// Tooltip
.apexcharts-tooltip {
@include mixins_elevation.elevation(3);
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-surface));
line-height: 1.5;
.apexcharts-tooltip-title {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-surface));
font-weight: 500;
margin-block-end: 0.25rem;
padding-inline: 1rem;
}
.apexcharts-tooltip-text {
display: flex;
align-items: center;
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
font-size: inherit;
gap: 0.5rem;
line-height: inherit;
}
.apexcharts-tooltip-text-label,
.apexcharts-tooltip-text-value {
font-weight: 600;
line-height: 1.5;
}
.apexcharts-tooltip-series-group {
padding-block: 0 0.5rem;
padding-inline: 1rem;
&:last-child {
padding-block-end: 1rem;
}
&.active {
padding-block-start: 0;
}
}
&.apexcharts-theme-light {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
&.apexcharts-theme-dark {
color: white;
}
.apexcharts-tooltip-series-group:first-of-type {
padding-block-end: 0;
border-color: rgb(var(--v-border-color));
background: rgb(var(--v-theme-surface));
box-shadow: none;
.apexcharts-tooltip-text-label,
.apexcharts-tooltip-text-value {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
}
}
.apexcharts-xaxistooltip {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-grey-50));
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
&::after {
border-block-end-color: rgb(var(--v-theme-grey-50));
}
&::before {
border-block-end-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
.apexcharts-marker {
transition: none;
}
// 👉 stroke-dasharray
.apexcharts-radialbar,
.apexcharts-radialbar-slice-current {
stroke-linecap: round;
}
.apexcharts-xaxistooltip,
.apexcharts-yaxistooltip {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-grey-50));
&::after {
border-inline-start-color: rgb(var(--v-theme-grey-50));
}
border-color: rgb(var(--v-border-color));
background: rgb(var(--v-theme-surface));
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
&::after,
&::before {
border-inline-start-color: rgba(var(--v-border-color), var(--v-border-opacity));
border-block-end-color: rgb(var(--v-border-color));
}
}
.apexcharts-xaxistooltip-text,
.apexcharts-yaxistooltip-text {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
// 👉 Text color
.apexcharts-text,
.apexcharts-tooltip-text,
.apexcharts-datalabel-label,
@@ -69,19 +90,16 @@
.apexcharts-xaxistooltip-text,
.apexcharts-yaxistooltip-text,
.apexcharts-legend-text {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)) !important;
font-family: inherit !important;
}
.apexcharts-pie-label {
fill: white;
filter: none;
}
.apexcharts-marker {
box-shadow: none;
}
.apexcharts-legend-marker {
margin-inline-end: 0.3875rem;
// 👉 Annotation Label
.apexcharts-annotation-rect {
&.apexcharts-xaxis-annotation-rect,
&.apexcharts-yaxis-annotation-rect {
fill-opacity: 0.05;
stroke-opacity: 0;
}
}
}

View File

@@ -1,5 +1,5 @@
@use "@core/scss/utils";
@use "@configured-variables" as variables;
@use "../../utils";
// 👉 Application
// We need accurate vh in mobile devices as well
@@ -45,6 +45,17 @@ h6,
}
}
// 👉 Button
@if variables.$vuetify-reduce-default-compact-button-icon-size {
.v-btn--density-compact.v-btn--size-default {
.v-btn__content > svg {
block-size: 22px;
font-size: 22px;
inline-size: 22px;
}
}
}
// 👉 Card
// Removes padding-top for immediately placed v-card-text after itself
.v-card-text {
@@ -71,7 +82,9 @@ h6,
&.v-checkbox-btn,
&.v-radio,
&.v-radio-btn {
margin-inline-start: -0.5625rem;
.v-selection-control__wrapper {
margin-inline-start: -0.5625rem;
}
}
}
@@ -79,7 +92,9 @@ h6,
&.v-radio,
&.v-radio-btn,
&.v-checkbox-btn {
margin-inline-start: -0.3125rem;
.v-selection-control__wrapper {
margin-inline-start: -0.3125rem;
}
}
}
@@ -87,7 +102,9 @@ h6,
&.v-checkbox-btn,
&.v-radio,
&.v-radio-btn {
margin-inline-start: -0.6875rem;
.v-selection-control__wrapper {
margin-inline-start: -0.6875rem;
}
}
}
@@ -154,13 +171,141 @@ h6,
padding-block: 0 !important;
padding-inline: 0 !important;
> .v-ripple__container {
opacity: 0;
}
&:not(:last-child) {
padding-block-end: var(--v-card-list-gap) !important;
}
}
.v-list-item:hover,
.v-list-item:focus,
.v-list-item:active,
.v-list-item.active {
> .v-list-item__overlay {
opacity: 0 !important;
}
}
}
// 👉 Table
.v-table {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
// 👉 Divider
.v-divider {
color: rgb(var(--v-border-color));
}
// 👉 DataTable
.v-data-table {
/* stylelint-disable-next-line no-descending-specificity */
.v-checkbox-btn .v-selection-control__wrapper {
margin-inline-start: 0 !important;
}
.v-selection-control {
display: flex !important;
}
.v-pagination {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
}
// 👉 v-field
.v-field:hover .v-field__outline {
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
}
// 👉 VLabel
.v-label {
opacity: 1 !important;
&:not(.v-field-label--floating) {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
}
// 👉 Overlay
.v-overlay__scrim,
.v-navigation-drawer__scrim {
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity));
opacity: 1;
}
// 透明主题下全屏弹窗的overlay背景透明度调整
html[data-theme="transparent"] .v-dialog--fullscreen .v-overlay__scrim {
background: rgba(var(--v-overlay-scrim-background), 0.3);
}
// 👉 VMessages
.v-messages {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
opacity: 1;
}
// 👉 Alert close btn
.v-alert__close {
.v-btn--icon .v-icon {
--v-icon-size-multiplier: 1.5;
}
}
// 👉 Badge icon alignment
.v-badge__badge {
display: flex;
align-items: center;
justify-content: center;
}
// 👉 Dialog
.v-dialog--fullscreen {
background-color: rgb(var(--v-theme-surface));
}
// 透明主题下全屏弹窗背景透明
html[data-theme="transparent"] .v-dialog--fullscreen {
background-color: transparent !important;
}
// For dialog card title
.v-card-item + .v-card-text {
padding-block-start: 0 !important;
}
// 👉 v-slide-group (List of chips)
.v-slide-group {
.v-slide-group__container {
display: flex;
flex-wrap: wrap;
// Spacing between buttons in v-slide-group
.v-slide-group-item:not(:last-child) {
margin-inline-end: 0.5rem;
}
}
}
// 👉 Expansion Panel
.v-expansion-panels {
.v-expansion-panel-title {
min-block-size: unset !important;
padding-block: 1rem !important;
}
}
// 👉 v-textarea
.v-textarea {
textarea {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
&:hover,
&:focus {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
}
}
// 👉 Cursor
.cursor-pointer {
cursor: pointer;
}

View File

@@ -1,22 +1,24 @@
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
/* stylelint-disable-next-line max-line-length */
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
// 👉 Card transition properties
$card-transition-property-custom: box-shadow, opacity;
@forward "vuetify/settings" with (
// 👉 General settings
// 👉 General settings
$color-pack: false !default,
$body-font-family: $font-family-custom !default,
$border-radius-root: 6px !default,
// 👉 Shadow opacity
// 👉 Shadow opacity
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
$body-font-family: $font-family-custom !default,
$border-radius-root: 6px !default,
$shadow-key-umbra: (
0: (0 0 0 0 var(--v-shadow-key-umbra-opacity)),
1: (0 2px 1px -1px var(--v-shadow-key-umbra-opacity)),
@@ -119,6 +121,18 @@ $card-transition-property-custom: box-shadow, opacity;
24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom)
) !default,
// 👉 Card
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
$card-elevation: 6 !default,
$card-title-line-height: 2rem !default,
$card-actions-min-height: unset !default,
$card-text-padding: 1.25rem !default,
$card-item-padding: 1.25rem !default,
$card-actions-padding: 0 12px 12px !default,
$card-transition-property: $card-transition-property-custom !default,
$card-subtitle-opacity: 1 !default,
$card-title-letter-spacing: 0.0094rem !default,
// 👉 Typography
$typography: (
"h1": (
@@ -170,29 +184,14 @@ $card-transition-property-custom: box-shadow, opacity;
)
) !default,
// 👉 States
$states: ("activated": 0.08) !default,
// 👉 Card
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
$card-elevation: 6 !default,
$card-title-line-height: 1.6 !default,
$card-actions-min-height: unset !default,
$card-text-padding: 20px !default,
$card-item-padding: 15px 20px !default,
$card-actions-padding: 0 12px 12px !default,
$card-title-letter-spacing: 0.0094rem !default,
$card-subtitle-opacity: 1 !default,
$card-transition-property: $card-transition-property-custom !default,
// 👉 Navigation Drawer
$navigation-drawer-color: rgba(var(--v-theme-on-surface), var(--v-high-medium-opacity)) !default,
// 👉 Table
$table-color: rgba(var(--v-theme-on-surface), var(--v-high-medium-opacity)) !default,
// 👉 List
$list-item-icon-margin-end: 16px !default,
$list-item-icon-margin-start: 16px !default,
$list-item-subtitle-opacity: 1 !default,
$list-subheader-text-opacity: 1 !default,
// 👉 Tooltip
$tooltip-background-color:#212121 !default,
$tooltip-background-color: #212121 !default,
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
$tooltip-font-size: 0.75rem !default,
$tooltip-border-radius: 4px !default,
@@ -205,6 +204,8 @@ $card-transition-property-custom: box-shadow, opacity;
// 👉 Badge
$badge-border-color:rgb(var(--v-theme-surface)) !default,
$badge-dot-height: 0.5rem !default,
$badge-dot-width: 0.5rem !default,
// 👉 Button
$button-height: 38px !default,
@@ -212,6 +213,7 @@ $card-transition-property-custom: box-shadow, opacity;
$button-border-radius: 5px !default,
$button-padding-ratio: 1.7 !default,
$button-text-letter-spacing: 0.025rem !default,
$button-icon-density: ("default": 0.5, "comfortable": -2, "compact": -3) !default,
// 👉 Dialog
$dialog-card-header-padding: 20px !default,
@@ -220,6 +222,7 @@ $card-transition-property-custom: box-shadow, opacity;
// 👉 Chip
$chip-label-border-radius: 4px !default,
$chip-close-size: 20px !default,
// 👉 Expansion panel
$expansion-panel-title-padding: 16px 20px !default,
@@ -232,9 +235,6 @@ $card-transition-property-custom: box-shadow, opacity;
// 👉 Menu
$menu-content-border-radius: 5px !default,
// 👉 List
$list-subheader-text-opacity: 1 !default,
// 👉 Snackbar
$snackbar-background:#212121 !default,
$snackbar-border-radius: 4px !default,
@@ -243,7 +243,12 @@ $card-transition-property-custom: box-shadow, opacity;
// 👉 Tabs
$tabs-height: 40px !default,
// 👉 Timeline
// 👉 Slider
$slider-track-active-size: 4px !default,
$slider-thumb-label-padding: 4px 12px !default,
$slider-thumb-label-font-size: 0.875rem !default,
// 👉 Timeline
$timeline-dot-size: 34px !default,
$timeline-dot-divider-background: transparent !default,
@@ -252,4 +257,7 @@ $card-transition-property-custom: box-shadow, opacity;
// 👉 Navigation Drawer
$navigation-drawer-scrim-opacity:0.5 !default,
// 👉 Table
$table-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)),
);

View File

@@ -1 +1,2 @@
@use "variables";
@use "overrides";

View File

@@ -1,3 +1,21 @@
%layout-navbar {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
// Vertical nav scrolled sticky elevated nav
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
background-color: rgb(var(--v-theme-surface));
box-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
}
// Floating navbar and sticky elevated navbar scrolled
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
background-color: rgb(var(--v-theme-surface));
box-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
}
// Floating navbar overlay
%default-layout-vertical-nav-floating-navbar-overlay {
backdrop-filter: blur(8px);
background-color: rgba(var(--v-theme-surface), 0.9);
}

View File

@@ -1,7 +1,7 @@
@use "@core/scss/mixins";
@use "../mixins";
@use "@configured-variables" as variables;
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
@use "@core/scss/utils";
@use "../utils";
// Nav items styles (including section title)
%vertical-nav-item {

View File

@@ -1,193 +0,0 @@
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@configured-variables" as variables;
@use "mixins";
// 👉 Alert
.v-alert {
.v-alert__close {
.v-icon {
block-size: 20px !important;
font-size: 20px !important;
inline-size: 20px !important;
}
}
&:not(.v-alert--prominent) .v-alert__prepend {
.v-icon {
block-size: 1.375rem !important;
font-size: 1.375rem !important;
inline-size: 1.375rem !important;
}
}
.v-alert-title {
line-height: 1.5rem;
margin-block-end: 0.25rem;
}
}
// 👉 Avatar font-size
.v-avatar {
@include mixins.avatar-font-sizes($map: variables.$avatar-font-sizes);
}
// 👉 Button
.v-btn {
/* stylelint-disable-next-line no-descending-specificity */
&:not(.v-btn--icon) .v-icon {
--v-icon-size-multiplier: 0.9525 !important;
}
}
// 👉 Chip
.v-chip.v-chip--size-default .v-avatar {
--v-avatar-height: 24px;
}
.v-chip.v-chip--density-comfortable {
line-height: 1;
}
// 👉 Expansion Panel
.v-expansion-panel {
.v-expansion-panel-text {
font-size: 1rem;
}
}
// 👉 Tooltip
.v-tooltip > .v-overlay__content {
font-weight: 500;
line-height: 0.875rem;
}
// 👉 List
// 👉 Tab with pill support
.v-tabs.v-tabs-pill {
.v-tab.v-btn {
border-radius: 6px !important;
}
}
// 👉 Timeline added box shadow
.v-timeline-item {
.v-timeline-divider__dot {
.v-timeline-divider__inner-dot {
box-shadow: 0 0 0 0.1875rem rgb(var(--v-theme-on-surface-variant));
@each $color-name in variables.$theme-colors-name {
&.bg-#{$color-name} {
box-shadow: 0 0 0 0.1875rem rgba(var(--v-theme-#{$color-name}), 0.12);
}
}
}
}
}
// 👉 Timeline Outlined style
.v-timeline-variant-outlined.v-timeline {
.v-timeline-divider__dot {
.v-timeline-divider__inner-dot {
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-on-surface-variant));
@each $color-name in variables.$theme-colors-name {
background-color: rgb(var(--v-theme-surface)) !important;
&.bg-#{$color-name} {
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-#{$color-name}));
}
}
}
}
}
// 👉 Expansion panels
.v-expansion-panel-title,
.v-expansion-panel-title--active,
.v-expansion-panel-title:hover,
.v-expansion-panel-title:focus,
.v-expansion-panel-title:focus-visible,
.v-expansion-panel-title--active:focus,
.v-expansion-panel-title--active:hover {
.v-expansion-panel-title__overlay {
opacity: 0 !important;
}
}
// 👉 Set Elevation when panel open
.v-expansion-panels:not(.v-expansion-panels--variant-accordion) {
.v-expansion-panel.v-expansion-panel--active {
.v-expansion-panel__shadow {
@include mixins_elevation.elevation(3);
}
}
}
// 👉 Slider
.v-slider-thumb {
.v-slider-thumb__label {
background: rgb(117, 117, 117);
color: rgb(var(--v-theme-on-primary));
&::before {
color: rgb(117, 117, 117);
}
}
}
// 👉 Switch
.v-switch {
.v-selection-control:not(.v-selection-control--dirty) .v-switch__thumb {
color: #fff;
}
}
// 👉 Table
.v-table--density-default > .v-table__wrapper > table > tbody > tr > td,
.v-table--density-default > .v-table__wrapper > table > thead > tr > td,
.v-table--density-default > .v-table__wrapper > table > tfoot > tr > td {
block-size: 50px !important;
}
.v-table {
--v-table-header-height: 54px !important;
th {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
font-size: 0.75rem;
.v-data-table-header__content {
display: flex;
justify-content: space-between;
}
}
.v-selection-control {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
font-size: 1rem;
}
}
.v-data-table {
th {
background: rgb(var(--v-table-header-background)) !important;
}
}
// 👉 Pagination
.v-pagination {
.v-btn {
border-radius: 4px;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 14px;
font-weight: 400;
}
}
// 👉 SnackBar
.v-snackbar--variant-elevated {
@include mixins.elevation(6);
}

View File

@@ -1,101 +0,0 @@
@use "sass:map";
@use "vuetify/lib/styles/settings" as vuetify_settings;
@use "@styles/variables/_vuetify.scss" as vuetify;
@mixin avatar-font-sizes($map: $avatar-sizes) {
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
/* stylelint-disable-next-line scss/no-global-function-names */
$size: map.get($map, $sizeName);
&.v-avatar--size-#{$sizeName} {
font-size: #{$size}px;
}
}
}
@mixin elevation($z, $important: false) {
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
}
@mixin before-pseudo() {
position: relative;
&::before {
position: absolute;
border-radius: inherit;
background: currentcolor;
block-size: 100%;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
}
}
@mixin bordered-skin($component, $border-property: "border", $important: false) {
#{$component} {
box-shadow: none !important;
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
}
}
@mixin selected-states($selector) {
#{$selector} {
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
}
&:hover
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
}
&:focus-visible
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
@supports not selector(:focus-visible) {
&:focus {
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
}
}
}
@mixin push-anchors() {
:target {
scroll-margin-block-start: 90px;
}
}
@mixin xs {
@media (width >= 0) and (width <= 599.98px) {
@content;
}
}
@mixin sm {
@media (width >= 600px) and (width <= 959.98px) {
@content;
}
}
@mixin md {
@media (width >= 960px) and (width <= 1279.98px) {
@content;
}
}
@mixin lg {
@media (width >= 1280px) and (width <= 1919.98px) {
@content;
}
}
@mixin xl {
@media (width >= 1920px) {
@content;
}
}

View File

@@ -1,25 +0,0 @@
.bg-var-theme-background {
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)) !important;
}
// 👉 Pagination small-select dropdown for table
// TODO: remove this class after vuetify datatable implememtation
.per-page-select {
margin-block: auto;
.v-field__input {
align-items: center;
padding: 2px;
font-size: 14px;
}
.v-field__append-inner {
align-items: center;
padding: 0;
.v-icon {
margin-inline-start: 0 !important;
}
}
}

View File

@@ -1,41 +0,0 @@
@use "sass:string";
/*
This function is helpful when we have multi dimensional value
Assume we have padding variable `$nav-padding-horizontal: 10px;`
With above variable let's say we use it in some style:
```scss
.selector {
margin-left: $nav-padding-horizontal;
}
```
Now, problem is we can also have value as `$nav-padding-horizontal: 10px 15px;`
In this case above style will be invalid.
This function will extract the left most value from the variable value.
$nav-padding-horizontal: 10px; => 10px;
$nav-padding-horizontal: 10px 15px; => 10px;
This is safe:
```scss
.selector {
margin-left: get-first-value($nav-padding-horizontal);
}
```
*/
@function get-first-value($var) {
$start-at: string.index(#{$var}, " ");
@if $start-at {
@return string.slice(
#{$var},
0,
$start-at
);
} @else {
@return $var;
}
}

View File

@@ -1,227 +0,0 @@
@use "sass:map";
@use "utils";
@use "vuetify/lib/styles/tools/functions" as *;
$vertical-nav-horizontal-padding-custom: 1.375rem 1rem;
// We created this SCSS var to extract the start padding
// Docs: https://sass-lang.com/documentation/modules/string
// $vertical-nav-horizontal-padding => 0 8px;
// string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
// string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
// string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-custom) !default;
$vertical-nav-items-icon-margin-inline-end: 0.625rem !default;
// Vertical Nav Configuration
$vertical-nav-collapsed-width: 68px !default;
// This is used to keep consistency between nav items and nav header left & right margin
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 0 1.125rem !default;
$vertical-nav-horizontal-padding: $vertical-nav-horizontal-padding-custom !default;
// Vertical nav header padding
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default;
// 👉 Custom Variables
$avatar-font-sizes: (
"x-small":12,
"small":14,
"default":18,
"large":20,
"x-large":24
) !default;
// 合并两个文件中的@forward配置
@forward "@layouts/styles/variables" with (
// 来自_variables.scss的配置
$layout-vertical-nav-collapsed-width: 68px !default,
// 来自template/_variables.scss的配置
$layout-vertical-nav-z-index: 1004,
$layout-overlay-z-index: 1003
);
// 使用命名空间来避免变量冲突
@use "@layouts/styles/variables" as layouts-vars;
$theme-colors-name: (
"primary",
"secondary",
"error",
"info",
"success",
"warning"
) !default;
// 👉 Default layout with vertical nav
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
// 👉 Vertical nav
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
// This is used to keep consistency between nav items and nav header left & right margin
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 1rem !default;
$vertical-nav-horizontal-padding: 0.75rem !default;
// Vertical nav header height. Mostly we will align it with navbar height;
$vertical-nav-header-height: layouts-vars.$layout-vertical-nav-navbar-height !default;
$vertical-nav-navbar-elevation: 3 !default;
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
$vertical-nav-floating-navbar-top: 1rem !default;
// Vertical nav header padding
$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default;
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
// Move logo when vertical nav is mini (collapsed but not hovered)
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
// Space between logo and title
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
// Section title margin top (when its not first child)
$vertical-nav-section-title-mt: 1.5rem !default;
// Section title margin bottom
$vertical-nav-section-title-mb: 0.5rem !default;
// Vertical nav icons
$vertical-nav-items-icon-size: 1.5rem !default;
$vertical-nav-items-nested-icon-size: 0.9rem !default;
$vertical-nav-items-icon-margin-inline-end: 0.5rem !default;
// Transition duration for nav group arrow
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
// Timing function for nav group arrow
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
// 👉 Horizontal nav
/*
❗ Heads up
==================
Here we assume we will always use shorthand property which will apply same padding on four side
This is because this have been used as value of top property by `.popper-content`
*/
$horizontal-nav-padding: 0.6875rem !default;
// Gap between top level horizontal nav items
$horizontal-nav-top-level-items-gap: 4px !default;
// Horizontal nav icons
$horizontal-nav-items-icon-size: 1.5rem !default;
$horizontal-nav-third-level-icon-size: 0.9rem !default;
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
// We used SCSS variable because we want to allow users to update max height of popper content
// 120px is combined height of navbar & horizontal nav
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
// This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
// 👉 Plugins
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
// 👉 Vuetify
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
$vuetify-reduce-default-compact-button-icon-size: true !default;
// 👉 Custom variables
// for utility classes
$font-sizes: () !default;
$font-sizes: map-deep-merge(
(
"xs": 0.75rem,
"sm": 0.875rem,
"base": 1rem,
"lg": 1.125rem,
"xl": 1.25rem,
"2xl": 1.5rem,
"3xl": 1.875rem,
"4xl": 2.25rem,
"5xl": 3rem,
"6xl": 3.75rem,
"7xl": 4.5rem,
"8xl": 6rem,
"9xl": 8rem
),
$font-sizes
);
// line height
$font-line-height: () !default;
$font-line-height: map-deep-merge(
(
"xs": 1rem,
"sm": 1.25rem,
"base": 1.5rem,
"lg": 1.75rem,
"xl": 1.75rem,
"2xl": 2rem,
"3xl": 2.25rem,
"4xl": 2.5rem,
"5xl": 1,
"6xl": 1,
"7xl": 1,
"8xl": 1,
"9xl": 1
),
$font-line-height
);
// gap utility class
$gap: () !default;
$gap: map-deep-merge(
(
"0": 0,
"1": 0.25rem,
"2": 0.5rem,
"3": 0.75rem,
"4": 1rem,
"5": 1.25rem,
"6":1.5rem,
"7": 1.75rem,
"8": 2rem,
"9": 2.25rem,
"10": 2.5rem,
"11": 2.75rem,
"12": 3rem,
"14": 3.5rem,
"16": 4rem,
"20": 5rem,
"24": 6rem,
"28": 7rem,
"32": 8rem,
"36": 9rem,
"40": 10rem,
"44": 11rem,
"48": 12rem,
"52": 13rem,
"56": 14rem,
"60": 15rem,
"64": 16rem,
"72": 18rem,
"80": 20rem,
"96": 24rem
),
$gap
);
// Avatar sizes map
$avatar-font-sizes: (
"x-small": 0.625rem,
"small": 0.75rem,
"default": 0.875rem,
"large": 1rem,
"x-large": 1.125rem,
) !default;

View File

@@ -1,42 +0,0 @@
@use "sass:map";
// Layout
@use "../vertical-nav";
@use "../default-layout";
@use "default-layout-w-vertical-nav";
// Components
@use "components";
// Utilities
@use "utilities";
@use "../utils";
// Misc
@use "../misc";
// Dark
@use "../dark";
// Variables
@use "variables";
// libs
@use "libs/perfect-scrollbar";
@use "libs/vuetify";
a {
color: rgb(var(--v-theme-primary));
text-decoration: none;
}
// Vuetify 3 don't provide margin bottom style like vuetify 2
p {
margin-block-end: 1rem;
}
// Iconify icon size
svg.iconify {
block-size: 1em;
inline-size: 1em;
}

View File

@@ -1,106 +0,0 @@
@use "@configureTheme" as theme;
@use "@configured-variables" as variables;
@use "../mixins";
// 👉 Apex chart
.apexcharts-canvas {
// For RTL alignment
.apexcharts-yaxis-texts-g {
text-align: start;
}
// Tooltip
.apexcharts-tooltip {
line-height: 1.5;
.apexcharts-tooltip-title {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-surface));
font-weight: 500;
margin-block-end: 0.25rem;
padding-inline: 1rem;
}
.apexcharts-tooltip-text {
display: flex;
align-items: center;
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
font-size: inherit;
gap: 0.5rem;
line-height: inherit;
}
.apexcharts-tooltip-text-label,
.apexcharts-tooltip-text-value {
font-weight: 600;
line-height: 1.5;
}
.apexcharts-tooltip-series-group {
padding-block: 0 0.5rem;
padding-inline: 1rem;
&:last-child {
padding-block-end: 1rem;
}
&.active {
padding-block-start: 0;
}
}
&.apexcharts-theme-light {
border-color: rgb(var(--v-border-color));
background: rgb(var(--v-theme-surface));
box-shadow: none;
.apexcharts-tooltip-text-label,
.apexcharts-tooltip-text-value {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
}
}
.apexcharts-marker {
transition: none;
}
// 👉 stroke-dasharray
.apexcharts-radialbar,
.apexcharts-radialbar-slice-current {
stroke-linecap: round;
}
.apexcharts-xaxistooltip,
.apexcharts-yaxistooltip {
border-color: rgb(var(--v-border-color));
background: rgb(var(--v-theme-surface));
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
&::after,
&::before {
border-block-end-color: rgb(var(--v-border-color));
}
}
// 👉 Text color
.apexcharts-text,
.apexcharts-tooltip-text,
.apexcharts-datalabel-label,
.apexcharts-datalabel,
.apexcharts-xaxistooltip-text,
.apexcharts-yaxistooltip-text,
.apexcharts-legend-text {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)) !important;
font-family: inherit !important;
}
// 👉 Annotation Label
.apexcharts-annotation-rect {
&.apexcharts-xaxis-annotation-rect,
&.apexcharts-yaxis-annotation-rect {
fill-opacity: 0.05;
stroke-opacity: 0;
}
}
}

View File

@@ -1,301 +0,0 @@
@use "@configured-variables" as variables;
@use "../../../utils";
// 👉 Application
// We need accurate vh in mobile devices as well
.v-application__wrap {
/* stylelint-disable-next-line liberty/use-logical-spec */
min-height: calc(var(--vh, 1vh) * 100);
}
// 👉 Typography
h1,
h2,
h3,
h4,
h5,
h6,
.text-h1,
.text-h2,
.text-h3,
.text-h4,
.text-h5,
.text-h6,
.text-button,
.text-overline,
.v-card-title {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
.text-body-1,
.text-body-2,
.text-subtitle-1,
.text-subtitle-2 {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
// 👉 Grid
// Remove margin-bottom of v-input_details inside grid (validation error message)
.v-row {
.v-col,
[class^="v-col-*"] {
.v-input__details {
margin-block-end: 0;
}
}
}
// 👉 Button
@if variables.$vuetify-reduce-default-compact-button-icon-size {
.v-btn--density-compact.v-btn--size-default {
.v-btn__content > svg {
block-size: 22px;
font-size: 22px;
inline-size: 22px;
}
}
}
// 👉 Card
// Removes padding-top for immediately placed v-card-text after itself
.v-card-text {
& + & {
padding-block-start: 0 !important;
}
}
/*
👉 Checkbox & Radio Ripple
TODO Checkbox and switch component. Remove it when vuetify resolve the extra spacing: https://github.com/vuetifyjs/vuetify/issues/15519
We need this because form elements likes checkbox and switches are by default set to height of textfield height which is way big than we want
Tested with checkbox & switches
*/
.v-checkbox.v-input,
.v-switch.v-input {
--v-input-control-height: auto;
flex: unset;
}
.v-selection-control--density-comfortable {
&.v-checkbox-btn,
&.v-radio,
&.v-radio-btn {
.v-selection-control__wrapper {
margin-inline-start: -0.5625rem;
}
}
}
.v-selection-control--density-compact {
&.v-radio,
&.v-radio-btn,
&.v-checkbox-btn {
.v-selection-control__wrapper {
margin-inline-start: -0.3125rem;
}
}
}
.v-selection-control--density-default {
&.v-checkbox-btn,
&.v-radio,
&.v-radio-btn {
.v-selection-control__wrapper {
margin-inline-start: -0.6875rem;
}
}
}
.v-radio-group {
.v-selection-control-group {
.v-radio:not(:last-child) {
margin-inline-end: 0.9rem;
}
}
}
/*
👉 Tabs
Disable tab transition
This is for tabs where we don't have card wrapper to tabs and have multiple cards as tab content.
This class will disable transition and adds `overflow: unset` on `VWindow` to allow spreading shadow
*/
.disable-tab-transition {
overflow: unset !important;
.v-window__container {
block-size: auto !important;
}
.v-window-item:not(.v-window-item--active) {
display: none !important;
}
.v-window__container .v-window-item {
transform: none !important;
}
}
// 👉 List
.v-list {
// Set icons opacity to .87
.v-list-item__prepend > .v-icon,
.v-list-item__append > .v-icon {
opacity: var(--v-high-emphasis-opacity);
}
}
// 👉 Card list
/*
Custom class
Remove list spacing inside card
This is because card title gets padding of 20px and list item have padding of 16px. Moreover, list container have padding-bottom as well.
*/
.card-list {
--v-card-list-gap: 20px;
&.v-list {
padding-block: 0;
}
.v-list-item {
min-block-size: unset;
min-block-size: auto !important;
padding-block: 0 !important;
padding-inline: 0 !important;
> .v-ripple__container {
opacity: 0;
}
&:not(:last-child) {
padding-block-end: var(--v-card-list-gap) !important;
}
}
.v-list-item:hover,
.v-list-item:focus,
.v-list-item:active,
.v-list-item.active {
> .v-list-item__overlay {
opacity: 0 !important;
}
}
}
// 👉 Divider
.v-divider {
color: rgb(var(--v-border-color));
}
// 👉 DataTable
.v-data-table {
/* stylelint-disable-next-line no-descending-specificity */
.v-checkbox-btn .v-selection-control__wrapper {
margin-inline-start: 0 !important;
}
.v-selection-control {
display: flex !important;
}
.v-pagination {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
}
// 👉 v-field
.v-field:hover .v-field__outline {
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
}
// 👉 VLabel
.v-label {
opacity: 1 !important;
&:not(.v-field-label--floating) {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
}
// 👉 Overlay
.v-overlay__scrim,
.v-navigation-drawer__scrim {
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity)) !important;
opacity: 1 !important;
}
// 👉 VMessages
.v-messages {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
opacity: 1;
}
// 👉 Alert close btn
.v-alert__close {
.v-btn--icon .v-icon {
--v-icon-size-multiplier: 1.5;
}
}
// 👉 Badge icon alignment
.v-badge__badge {
display: flex;
align-items: center;
justify-content: center;
}
// 👉 Dialog
.v-dialog--fullscreen {
background-color: rgb(var(--v-theme-surface));
}
// For dialog card title
.v-card-item + .v-card-text {
padding-block-start: 0 !important;
}
// 👉 v-slide-group (List of chips)
.v-slide-group {
.v-slide-group__container {
display: flex;
flex-wrap: wrap;
// Spacing between buttons in v-slide-group
.v-slide-group-item:not(:last-child) {
margin-inline-end: 0.5rem;
}
}
}
// 👉 Expansion Panel
.v-expansion-panels {
.v-expansion-panel-title {
min-block-size: unset !important;
padding-block: 1rem !important;
}
}
// 👉 v-textarea
.v-textarea {
textarea {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
&:hover,
&:focus {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
}
}
// 👉 Cursor
.cursor-pointer {
cursor: pointer;
}

View File

@@ -1,263 +0,0 @@
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
/* stylelint-disable-next-line max-line-length */
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
// 👉 Card transition properties
$card-transition-property-custom: box-shadow, opacity;
@forward "vuetify/settings" with (
// 👉 General settings
$color-pack: false !default,
// 👉 Shadow opacity
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
$body-font-family: $font-family-custom !default,
$border-radius-root: 6px !default,
$shadow-key-umbra: (
0: (0 0 0 0 var(--v-shadow-key-umbra-opacity)),
1: (0 2px 1px -1px var(--v-shadow-key-umbra-opacity)),
2: (0 3px 1px -2px var(--v-shadow-key-umbra-opacity)),
// Modified
3: (0 4px 14px -4px var(--v-shadow-key-umbra-opacity)),
4: (0 2px 4px -1px var(--v-shadow-key-umbra-opacity)),
5: (0 3px 5px -1px var(--v-shadow-key-umbra-opacity)),
// Modified
6: (0 4px 5px -2px var(--v-shadow-key-umbra-opacity)),
7: (0 4px 5px -2px var(--v-shadow-key-umbra-opacity)),
8: (0 5px 5px -3px var(--v-shadow-key-umbra-opacity)),
9: (0 5px 6px -3px var(--v-shadow-key-umbra-opacity)),
10: (0 6px 6px -3px var(--v-shadow-key-umbra-opacity)),
11: (0 6px 7px -4px var(--v-shadow-key-umbra-opacity)),
12: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)),
13: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)),
14: (0 7px 9px -4px var(--v-shadow-key-umbra-opacity)),
15: (0 8px 9px -5px var(--v-shadow-key-umbra-opacity)),
16: (0 8px 10px -5px var(--v-shadow-key-umbra-opacity)),
17: (0 8px 11px -5px var(--v-shadow-key-umbra-opacity)),
18: (0 9px 11px -5px var(--v-shadow-key-umbra-opacity)),
19: (0 9px 12px -6px var(--v-shadow-key-umbra-opacity)),
20: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)),
21: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)),
22: (0 10px 14px -6px var(--v-shadow-key-umbra-opacity)),
23: (0 11px 14px -7px var(--v-shadow-key-umbra-opacity)),
24: (0 11px 15px -7px var(--v-shadow-key-umbra-opacity))
) !default,
$shadow-key-penumbra: (
0: (0 0 0 0 $shadow-key-penumbra-opacity-custom),
1: (0 1px 1px 0 $shadow-key-penumbra-opacity-custom),
2: (0 2px 2px 0 $shadow-key-penumbra-opacity-custom),
// Modified
3: (0 4px 8px -4px $shadow-key-penumbra-opacity-custom),
4: (0 4px 5px 0 $shadow-key-penumbra-opacity-custom),
5: (0 5px 8px 0 $shadow-key-penumbra-opacity-custom),
// Modified
6: (0 2px 10px 1px $shadow-key-penumbra-opacity-custom),
7: (0 7px 10px 1px $shadow-key-penumbra-opacity-custom),
8: (0 8px 10px 1px $shadow-key-penumbra-opacity-custom),
9: (0 9px 12px 1px $shadow-key-penumbra-opacity-custom),
10: (0 10px 14px 1px $shadow-key-penumbra-opacity-custom),
11: (0 11px 15px 1px $shadow-key-penumbra-opacity-custom),
12: (0 12px 17px 2px $shadow-key-penumbra-opacity-custom),
13: (0 13px 19px 2px $shadow-key-penumbra-opacity-custom),
14: (0 14px 21px 2px $shadow-key-penumbra-opacity-custom),
15: (0 15px 22px 2px $shadow-key-penumbra-opacity-custom),
16: (0 16px 24px 2px $shadow-key-penumbra-opacity-custom),
17: (0 17px 26px 2px $shadow-key-penumbra-opacity-custom),
18: (0 18px 28px 2px $shadow-key-penumbra-opacity-custom),
19: (0 19px 29px 2px $shadow-key-penumbra-opacity-custom),
20: (0 20px 31px 3px $shadow-key-penumbra-opacity-custom),
21: (0 21px 33px 3px $shadow-key-penumbra-opacity-custom),
22: (0 22px 35px 3px $shadow-key-penumbra-opacity-custom),
23: (0 23px 36px 3px $shadow-key-penumbra-opacity-custom),
24: (0 24px 38px 3px $shadow-key-penumbra-opacity-custom)
) !default,
$shadow-key-ambient: (
0: (0 0 0 0 $shadow-key-ambient-opacity-custom),
1: (0 1px 3px 0 $shadow-key-ambient-opacity-custom),
2: (0 1px 5px 0 $shadow-key-ambient-opacity-custom),
// Modified
3: (0 4px 8px -4px $shadow-key-ambient-opacity-custom),
4: (0 1px 10px 0 $shadow-key-ambient-opacity-custom),
5: (0 1px 14px 0 $shadow-key-ambient-opacity-custom),
// Modified
6: (0 2px 16px 1px $shadow-key-ambient-opacity-custom),
7: (0 2px 16px 1px $shadow-key-ambient-opacity-custom),
8: (0 3px 14px 2px $shadow-key-ambient-opacity-custom),
9: (0 3px 16px 2px $shadow-key-ambient-opacity-custom),
10: (0 4px 18px 3px $shadow-key-ambient-opacity-custom),
11: (0 4px 20px 3px $shadow-key-ambient-opacity-custom),
12: (0 5px 22px 4px $shadow-key-ambient-opacity-custom),
13: (0 5px 24px 4px $shadow-key-ambient-opacity-custom),
14: (0 5px 26px 4px $shadow-key-ambient-opacity-custom),
15: (0 6px 28px 5px $shadow-key-ambient-opacity-custom),
16: (0 6px 30px 5px $shadow-key-ambient-opacity-custom),
17: (0 6px 32px 5px $shadow-key-ambient-opacity-custom),
18: (0 7px 34px 6px $shadow-key-ambient-opacity-custom),
19: (0 7px 36px 6px $shadow-key-ambient-opacity-custom),
20: (0 8px 38px 7px $shadow-key-ambient-opacity-custom),
21: (0 8px 40px 7px $shadow-key-ambient-opacity-custom),
22: (0 8px 42px 7px $shadow-key-ambient-opacity-custom),
23: (0 9px 44px 8px $shadow-key-ambient-opacity-custom),
24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom)
) !default,
// 👉 Card
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
$card-elevation: 6 !default,
$card-title-line-height: 2rem !default,
$card-actions-min-height: unset !default,
$card-text-padding: 1.25rem !default,
$card-item-padding: 1.25rem !default,
$card-actions-padding: 0 12px 12px !default,
$card-transition-property: $card-transition-property-custom !default,
$card-subtitle-opacity: 1 !default,
$card-title-letter-spacing: 0.0094rem !default,
// 👉 Typography
$typography: (
"h1": (
"weight": 500,
"line-height": 7rem,
"letter-spacing": -0.0938rem
),
"h2": (
"weight": 500,
"line-height": 4.5rem,
"letter-spacing": -0.0313rem
),
"h3": (
"weight": 500,
"line-height": 3.5rem
),
"h4": (
"weight": 500,
"line-height": 2.625rem,
"letter-spacing": 0.0156rem
),
"h5": (
"weight": 500,
"line-height": 2rem
),
"h6": (
"letter-spacing": 0.0094rem
),
"subtitle-1": (
"letter-spacing": 0.0094rem
),
"subtitle-2": (
"line-height": 1.375rem,
"letter-spacing": 0.0063rem,
),
"body-1": (
"letter-spacing": 0.0094rem,
),
"body-2": (
"letter-spacing": 0.0094rem,
),
"caption": (
"letter-spacing": 0.025rem,
),
"overline": (
"weight": 400,
"line-height": 1.125rem,
"letter-spacing": 0.0625rem,
)
) !default,
// 👉 List
$list-item-icon-margin-end: 16px !default,
$list-item-icon-margin-start: 16px !default,
$list-item-subtitle-opacity: 1 !default,
$list-subheader-text-opacity: 1 !default,
// 👉 Tooltip
$tooltip-background-color: #212121 !default,
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
$tooltip-font-size: 0.75rem !default,
$tooltip-border-radius: 4px !default,
$tooltip-padding: 4px 8px !default,
// 👉 Alert
$alert-title-font-size: 1rem !default,
$alert-border-radius: 5px !default,
$alert-title-letter-spacing: 0.15px !default,
// 👉 Badge
$badge-border-color:rgb(var(--v-theme-surface)) !default,
$badge-dot-height: 0.5rem !default,
$badge-dot-width: 0.5rem !default,
// 👉 Button
$button-height: 38px !default,
$button-elevation: ("default": 3, "hover": 4, "active": 8) !default,
$button-border-radius: 5px !default,
$button-padding-ratio: 1.7 !default,
$button-text-letter-spacing: 0.025rem !default,
$button-icon-density: ("default": 0.5, "comfortable": -2, "compact": -3) !default,
// 👉 Dialog
$dialog-card-header-padding: 20px !default,
$dialog-card-header-text-padding-top: 0 !default,
$dialog-card-text-padding: 20px !default,
// 👉 Chip
$chip-label-border-radius: 4px !default,
$chip-close-size: 20px !default,
// 👉 Expansion panel
$expansion-panel-title-padding: 16px 20px !default,
$expansion-panel-title-font-size: 1rem !default,
$expansion-panel-disabled-overlay: 0 !default,
$expansion-panel-active-title-min-height: 51px !default,
$expansion-panel-title-min-height: 51px !default,
$expansion-panel-text-padding: 0 20px 20px !default,
// 👉 Menu
$menu-content-border-radius: 5px !default,
// 👉 Snackbar
$snackbar-background:#212121 !default,
$snackbar-border-radius: 4px !default,
$snackbar-color: rgb(var(--v-theme-on-primary)) !default,
// 👉 Tabs
$tabs-height: 40px !default,
// 👉 Slider
$slider-track-active-size: 4px !default,
$slider-thumb-label-padding: 4px 12px !default,
$slider-thumb-label-font-size: 0.875rem !default,
// 👉 Timeline
$timeline-dot-size: 34px !default,
$timeline-dot-divider-background: transparent !default,
// 👉 Overlay
$overlay-opacity: 0.5 !default,
// 👉 Navigation Drawer
$navigation-drawer-scrim-opacity:0.5 !default,
// 👉 Table
$table-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)),
);

View File

@@ -1,2 +0,0 @@
@use "variables";
@use "overrides";

View File

@@ -1,25 +0,0 @@
.layout-blank {
.misc-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.25rem;
overflow: hidden;
.misc-footer-img {
position: absolute;
inline-size: 100%;
inset-block-end: 0;
}
.misc-footer-tree {
position: absolute;
z-index: 1;
}
}
.misc-avatar {
z-index: 1;
}
}

View File

@@ -1,54 +0,0 @@
.layout-blank {
.auth-wrapper {
min-block-size: calc(var(--vh, 1vh) * 100);
.auth-footer-mask {
position: absolute;
inset-block-end: 0;
min-inline-size: 100%;
}
.auth-footer-start-tree,
.auth-footer-end-tree {
position: absolute;
z-index: 1;
}
.auth-footer-start-tree {
inset-block-end: 0;
inset-inline-start: 0;
}
.auth-footer-end-tree {
inset-block-end: 0;
inset-inline-end: 0;
}
.auth-illustration {
z-index: 1;
}
}
.auth-card {
z-index: 1 !important;
}
}
@media (min-width: 960px) {
.skin--bordered {
.auth-card-v2 {
border-inline-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
}
}
}
.auth-logo {
position: absolute;
z-index: 1;
inset-block-start: 2rem;
inset-inline-start: 2.3rem;
}
.auth-card-v2 {
background-color: rgb(var(--v-theme-surface));
}

View File

@@ -1,45 +0,0 @@
@use "@configured-variables" as variables;
@use "misc";
@use "../mixins";
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
background-color: rgb(var(--v-theme-surface));
}
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
// @include mixins.elevation(variables.$vertical-nav-navbar-elevation);
// If navbar is contained => Squeeze navbar content on scroll
@if variables.$layout-vertical-nav-navbar-is-contained {
padding-inline: 1rem;
}
}
%default-layout-vertical-nav-floating-navbar-overlay {
isolation: isolate;
&::after {
position: absolute;
z-index: -1;
/* stylelint-disable property-no-vendor-prefix */
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
/* stylelint-enable */
background:
linear-gradient(
180deg,
rgba(var(--v-theme-background), 70%) 44%,
rgba(var(--v-theme-background), 43%) 73%,
rgba(var(--v-theme-background), 0%)
);
background-repeat: repeat;
block-size: calc(variables.$layout-vertical-nav-navbar-height + variables.$vertical-nav-floating-navbar-top + 0.5rem);
content: "";
inset-block-start: -(variables.$vertical-nav-floating-navbar-top);
inset-inline: 0;
/* stylelint-disable property-no-vendor-prefix */
-webkit-mask: linear-gradient(black, black 18%, transparent 100%);
mask: linear-gradient(black, black 18%, transparent 100%);
/* stylelint-enable */
}
}

View File

@@ -1,3 +0,0 @@
%layout-navbar {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}

View File

@@ -1,5 +0,0 @@
@forward "nav";
@forward "vertical-nav";
@forward "default-layout";
@forward "default-layout-vertical-nav";
@forward "misc";

View File

@@ -1,120 +0,0 @@
%blurry-bg {
position: relative;
background: transparent;
box-shadow: none;
&::before {
position: absolute;
z-index: -1;
block-size: calc(env(safe-area-inset-top, 0px) + 5rem);
content: "";
inset-block-start: 0;
inset-inline: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out, background 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), 0.0) 100%
);
}
.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), 0.0) 100%
);
}
.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%
);
}
}
}
// 移动端样式
@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,8 +0,0 @@
%nav-link-active {
background:
linear-gradient(
-72.47deg,
rgb(var(--v-theme-primary)) 22.16%,
rgba(var(--v-theme-primary), 0.7) 76.47%
) !important;
}

View File

@@ -1,64 +0,0 @@
@use "@configured-variables" as variables;
// Add divider around section title
%vertical-nav-section-title {
/*
We will use this to add gap between divider and text.
Moreover, we will use this to adjust the `flex-basis` property of left divider
*/
$divider-gap: 0.625rem;
// Thanks: https://stackoverflow.com/a/62359101/10796681
.title-text {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
column-gap: $divider-gap;
&::before,
&::after {
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
content: "";
}
&::after {
flex: 1 1 auto;
}
&::before {
flex: 0 1 calc(variables.$vertical-nav-horizontal-padding-start - $divider-gap);
margin-inline-start: -#{variables.$vertical-nav-horizontal-padding-start};
}
}
// Update the margin-inline-end when vertical nav is in mini state. We done same for link & group.
@at-root {
.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) .nav-section-title {
margin-inline: 4px 0;
}
}
}
%vertical-nav-item-interactive {
// Add pill shape styles
block-size: 2.625rem !important;
border-end-end-radius: 3.125rem !important;
border-end-start-radius: 0 !important;
border-start-end-radius: 3.125rem !important;
border-start-start-radius: 0 !important;
}
%vertical-nav-item-interactive {
// Wobble effect
// transition: margin-inline 0.4s ease-in-out;
// will-change: margin-inline;
transition: margin-inline 0.15s ease-in-out;
will-change: margin-inline;
// Reduce margin inline end when vertical nav is in collapsed mode and not hovered
.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) & {
margin-inline: 0 5px;
}
}

View File

@@ -2,8 +2,7 @@ import ColorThief from 'colorthief'
// 将 RGB 转换为十六进制
function rgbStringToHex(rgbArray: number[]): string {
if (rgbArray.length !== 3 || rgbArray.some(isNaN))
throw new Error('Invalid RGB string format')
if (rgbArray.length !== 3 || rgbArray.some(isNaN)) throw new Error('Invalid RGB string format')
const [r, g, b] = rgbArray
@@ -21,3 +20,27 @@ export async function getDominantColor(image: HTMLImageElement): Promise<string>
const dominantColor = colorThief.getColor(image)
return rgbStringToHex(dominantColor)
}
// 预加载图片
export async function preloadImage(url: string): Promise<boolean> {
return new Promise(resolve => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
// 设置超时,防止图片长时间加载
const timeout = setTimeout(() => {
img.src = ''
resolve(false)
}, 5000) // 5秒超时
img.src = url
// 如果图片已经缓存onload可能不会触发
if (img.complete) {
clearTimeout(timeout)
resolve(true)
}
})
}

View File

@@ -65,3 +65,6 @@ export function getQueryValue(key: string, url = window.location.href): string {
const res = reg.exec(url)
return res ? res[1] : ''
}
// 导出 navigator 相关函数
export { isMobileDevice, isIOSDevice, isAndroidDevice } from './navigator'

View File

@@ -43,3 +43,56 @@ export const isPWA = async (): Promise<boolean> => {
}
return (window.navigator as any).standalone === true
}
// 同步检测PWA显示模式
export const isPWADisplayMode = (): boolean => {
return (
window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone ||
document.referrer.includes('android-app://')
)
}
// 全面的PWA检测推荐使用
export const checkPWAStatus = async () => {
const hasServiceWorker = await isPWA()
const isStandaloneMode = isPWADisplayMode()
return {
// 是否有PWA功能Service Worker
hasPWAFeatures: hasServiceWorker,
// 是否在独立显示模式下运行
isStandaloneMode,
// 综合判断更宽松的检测在移动设备上默认启用PWA功能
isPWAEnvironment: hasServiceWorker || isStandaloneMode || isMobileDevice(),
// 完整的PWA体验既有功能又在独立模式下运行
isFullPWA: hasServiceWorker && isStandaloneMode,
}
}
// 检测是否为移动设备
export const isMobileDevice = (): boolean => {
// 检查用户代理字符串
const userAgent = navigator.userAgent || ''
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i
// 检查触摸屏支持
const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0
// 检查屏幕尺寸小于768px认为是移动设备
const isMobileSize = window.innerWidth < 768
return mobileRegex.test(userAgent) || hasTouchScreen || isMobileSize
}
// 检测是否为iOS设备
export const isIOSDevice = (): boolean => {
const userAgent = navigator.userAgent.toLowerCase()
return /iphone|ipad|ipod/.test(userAgent) && !(window as any).MSStream
}
// 检测是否为Android设备
export const isAndroidDevice = (): boolean => {
const userAgent = navigator.userAgent.toLowerCase()
return /android/.test(userAgent)
}

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

@@ -17,11 +17,34 @@ export default defineComponent({
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
const scrollDistance = ref(window.scrollY)
const isDialogOpen = ref(false)
const wasScrolledBeforeDialog = ref(false)
// 监听弹窗状态变化
const checkDialogState = () => {
const wasDialogOpen = isDialogOpen.value
isDialogOpen.value = document.documentElement.classList.contains('v-overlay-scroll-blocked')
// 当弹窗刚打开时,记录当前的滚动状态
if (!wasDialogOpen && isDialogOpen.value) {
wasScrolledBeforeDialog.value = scrollDistance.value > 0
}
}
onMounted(() => {
window.addEventListener('scroll', () => {
scrollDistance.value = window.scrollY
})
// 初始检查弹窗状态
checkDialogState()
// 监听 DOM 变化以检测弹窗状态
const observer = new MutationObserver(checkDialogState)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
})
})
return () => {
@@ -38,15 +61,25 @@ export default defineComponent({
)
// 👉 Navbar
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
h(
'div',
{ class: 'navbar-content-container' },
slots.navbar?.({
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
}),
),
])
const navbar = h(
'header',
{ class: ['layout-navbar navbar-blur'] },
[
h(
'div',
{ class: 'navbar-content-container' },
[
slots.navbar?.({
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
}),
// 👉 Dynamic Header Tab in NavBar
slots['dynamic-header-tab']?.()
? h('div', { class: 'layout-dynamic-header-tab' }, slots['dynamic-header-tab']?.())
: null,
].filter(Boolean),
),
].filter(Boolean),
)
const main = h(
'main',
@@ -86,7 +119,7 @@ export default defineComponent({
'layout-navbar-fixed',
mdAndDown.value && 'layout-overlay-nav',
route.meta.layoutWrapperClasses,
scrollDistance.value && 'window-scrolled',
(scrollDistance.value > 5 || (isDialogOpen.value && wasScrolledBeforeDialog.value)) && 'window-scrolled',
],
},
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
@@ -127,7 +160,9 @@ export default defineComponent({
inset-block-start: 0;
.navbar-content-container {
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
block-size: calc(
env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height + var(--navbar-tab-height)
);
}
@at-root {
@@ -135,10 +170,6 @@ export default defineComponent({
.layout-navbar {
@if variables.$layout-vertical-nav-navbar-is-contained {
@include mixins.boxed-content;
} @else {
.navbar-content-container {
// @include mixins.boxed-content;
}
}
}
}

View File

@@ -6,8 +6,9 @@
html {
background: rgb(var(--v-theme-background));
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
overflow-y: overlay;
min-block-size: 100vh;
min-block-size: 100dvh;
overflow-y: hidden;
}
body {
@@ -40,8 +41,8 @@ body,
// TODO: Use grid gutter variable here;
padding-block: 1.5rem;
padding-inline: 0.5rem;
padding-block-start: calc(env(safe-area-inset-top) + 4.5rem);
padding-inline: 0.5rem;
// display: flex;display

View File

@@ -1,7 +1,11 @@
import type { ValidationRule } from 'vuetify/types/services/validation'
// 必输校验
export const requiredValidator: ValidationRule = (value: any) => !!value || '此项为必填项'
export const requiredValidator: ValidationRule = (value: any) => {
return !!value
}
// 数字校验
export const numberValidator: ValidationRule = (value: any) => !isNaN(value) || '请输入数字'
export const numberValidator: ValidationRule = (value: any) => {
return !isNaN(value)
}

View File

@@ -3,13 +3,15 @@ import { useTheme } from 'vuetify'
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 { useAuthStore, useGlobalSettingsStore } from '@/stores'
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
import { SupportedLocale } from '@/types/i18n'
// 国际化
const { t } = useI18n()
import { checkAndEmitUnreadMessages } from '@/utils/badge'
import { preloadImage } from './@core/utils/image'
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
import { themeManager } from '@/utils/themeManager'
// 生效主题
const { global: globalTheme } = useTheme()
@@ -21,21 +23,20 @@ globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
const localeValue = getBrowserLocale()
setI18nLanguage(localeValue as SupportedLocale)
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 显示状态
const show = ref(false)
// 检查是否登录
const authStore = useAuthStore()
const isLogin = computed(() => authStore.token)
// 全局设置store
const globalSettingsStore = useGlobalSettingsStore()
// 生成背景图片key
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
// 背景图片
const backgroundImages = ref<string[]>([])
const activeImageIndex = ref(0)
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
let backgroundRotationTimer: NodeJS.Timeout | null = null
// ApexCharts 全局配置
declare global {
@@ -44,108 +45,94 @@ declare global {
}
}
if (window.Apex) {
// 数据标签
window.Apex.dataLabels = {
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
// 如果有小数点,保留两位小数,否则保留整数
const data = w.config.series[seriesIndex]
return data.toFixed(data % 1 === 0 ? 0 : 1)
},
}
// 图例
window.Apex.legend = {
labels: {
useSeriesColors: true,
},
}
// 标题
window.Apex.title = {
style: {
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
},
// 配置 ApexCharts 全局选项
function configureApexCharts() {
if (typeof window !== 'undefined' && window.Apex) {
try {
// 获取当前主题
const currentTheme = globalTheme.name.value
const isDark = currentTheme === 'dark' || currentTheme === 'transparent'
// 数据标签
window.Apex.dataLabels = {
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
// 如果有小数点,保留两位小数,否则保留整数
const data = w.config.series[seriesIndex]
return data.toFixed(data % 1 === 0 ? 0 : 1)
},
}
// 图例
window.Apex.legend = {
labels: {
useSeriesColors: true,
},
}
// 标题
window.Apex.title = {
style: {
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
},
}
// 鼠标悬浮提示
window.Apex.tooltip = {
theme: isDark ? 'dark' : 'light',
}
} catch (error) {
console.warn('ApexCharts 全局配置失败:', error)
}
}
}
// 更新data-theme属性以便CSS选择器能正确匹配
function updateHtmlThemeAttribute(themeName: string) {
document.documentElement.setAttribute('data-theme', themeName)
// 确保body元素也有相同的主题属性以便更好地选择弹出窗口
document.body.setAttribute('data-theme', themeName)
}
// 获取背景图片
async function fetchBackgroundImages() {
try {
backgroundImages.value = await api.get(`/login/wallpapers`)
const controller = new AbortController()
backgroundImages.value = await api.get(`/login/wallpapers`, {
signal: controller.signal,
})
activeImageIndex.value = 0
} catch (e) {
console.error(e)
throw e
}
}
// 背景图片轮换函数
function rotateBackgroundImage() {
if (backgroundImages.value.length > 1) {
// 计算下一个图片索引
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
// 预加载下一张图片
preloadImage(backgroundImages.value[nextIndex]).then(success => {
// 只有图片成功加载才切换
if (success) {
activeImageIndex.value = nextIndex
}
})
}
}
// 开始背景图片轮换
function startBackgroundRotation() {
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
// 清除现有定时器
removeBackgroundTimer('background-rotation')
if (backgroundImages.value.length > 1) {
backgroundRotationTimer = setInterval(() => {
// 计算下一个图片索引
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
// 预加载下一张图片
preloadImage(backgroundImages.value[nextIndex]).then(success => {
// 只有图片成功加载才切换
if (success) {
activeImageIndex.value = nextIndex
}
})
}, 10000) // 每10秒切换一次
}
}
// 预加载图片
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)
// 设置超时,防止图片长时间加载
const timeout = setTimeout(() => {
img.src = ''
resolve(false)
}, 5000) // 5秒超时
img.src = imageUrl
// 如果图片已经缓存onload可能不会触发
if (img.complete) {
clearTimeout(timeout)
resolve(true)
}
})
}
// 计算图片地址
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())
}
// 使用优化的定时器管理器,后台时自动暂停
addBackgroundTimer(
'background-rotation',
rotateBackgroundImage,
10000, // 每10秒切换一次
{
runInBackground: false, // 后台时不运行
skipInitialRun: true, // 不需要立即执行
},
)
}
}
@@ -153,71 +140,127 @@ function handleVisibilityChange() {
function animateAndRemoveLoader() {
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
if (loadingBg) {
// 先添加完成动画类
loadingBg.classList.add('loading-complete')
// 等待动画完成后再移除元素
setTimeout(() => {
removeEl('#loading-bg')
// 将background属性从html的style中移除
document.documentElement.style.removeProperty('background')
// 显示页面
show.value = true
}, 500) // 与CSS动画持续时间匹配
removeEl('#loading-bg')
document.documentElement.style.removeProperty('background')
}
}
onMounted(() => {
// 检查PWA状态并移除加载界面
async function removeLoadingWithStateCheck() {
try {
// 设置各个组件的加载状态
globalLoadingStateManager.setLoadingState('pwa-state', true)
globalLoadingStateManager.setLoadingState('global-settings', true)
globalLoadingStateManager.setLoadingState('background-images', true)
// 静默检查PWA状态恢复
const pwaController = (window as any).pwaStateController
if (pwaController) {
await pwaController.waitForStateRestore()
}
globalLoadingStateManager.setLoadingState('pwa-state', false)
// 并行加载关键资源
await Promise.all([
globalSettingsStore.initialize().then(() => {
globalLoadingStateManager.setLoadingState('global-settings', false)
}),
new Promise(resolve => {
setTimeout(() => {
globalLoadingStateManager.setLoadingState('background-images', false)
resolve(void 0)
}, 50)
}),
])
// 等待所有加载完成
await globalLoadingStateManager.waitForAllComplete()
// 移除加载界面
animateAndRemoveLoader()
// 检查未读消息
checkAndEmitUnreadMessages()
} catch (error) {
// 即使出错也要移除加载界面
globalLoadingStateManager.reset()
animateAndRemoveLoader()
}
}
// 加载背景图片
async function loadBackgroundImages(retryCount = 0) {
const maxRetries = 3
try {
await fetchBackgroundImages()
startBackgroundRotation()
} catch (error: any) {
const isAbortError = error.name === 'AbortError' || error.code === 'ERR_CANCELED'
if (retryCount < maxRetries) {
const baseDelay = isAbortError ? 1000 : 3000
const retryDelay = Math.min(baseDelay * Math.pow(2, retryCount), 10000)
setTimeout(() => {
loadBackgroundImages(retryCount + 1)
}, retryDelay)
}
}
}
onMounted(async () => {
// 配置 ApexCharts
configureApexCharts()
// 初始化data-theme属性
updateHtmlThemeAttribute(globalTheme.name.value)
// 加载背景图片并开始轮换
fetchBackgroundImages().then(() => startBackgroundRotation())
// 初始化主题管理器 - 统一处理主题初始化
await themeManager.setTheme(themeValue)
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', handleVisibilityChange)
// 监听主题变化
watch(
() => globalTheme.name.value,
newTheme => {
// 更新HTML主题属性
updateHtmlThemeAttribute(newTheme)
// 重新配置ApexCharts以适应新主题
configureApexCharts()
},
)
// 加载背景图片
loadBackgroundImages()
// 使用优化后的加载界面移除逻辑
ensureRenderComplete(() => {
nextTick(() => {
setTimeout(() => {
// 移除加载动画
animateAndRemoveLoader()
}, 1500)
})
nextTick(removeLoadingWithStateCheck)
})
})
onUnmounted(() => {
// 移除页面可见性监听
document.removeEventListener('visibilitychange', handleVisibilityChange)
// 清除轮换定时器
if (backgroundRotationTimer) {
clearInterval(backgroundRotationTimer)
backgroundRotationTimer = null
}
// 清除背景轮换定时器
removeBackgroundTimer('background-rotation')
})
</script>
<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>
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
<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 v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
</div>
<!-- 页面内容 -->
<VApp>
<RouterView />
<!-- PWA安装提示 -->
<PWAInstallPrompt />
</VApp>
</div>
</template>

View File

@@ -1,39 +1,84 @@
import i18n from '@/plugins/i18n'
export const storageOptions = [
export const storageAttributes = [
{
title: i18n.global.t('storage.local'),
value: 'local',
type: 'local',
icon: 'mdi-folder-multiple-outline',
remote: false,
},
{
title: i18n.global.t('storage.alipan'),
value: 'alipan',
type: 'alipan',
icon: 'mdi-cloud-outline',
remote: true,
},
{
title: i18n.global.t('storage.u115'),
value: 'u115',
type: 'u115',
icon: 'mdi-cloud-outline',
remote: true,
},
{
title: i18n.global.t('storage.rclone'),
value: 'rclone',
type: 'rclone',
icon: 'mdi-server-network-outline',
remote: true,
},
{
title: i18n.global.t('storage.alist'),
value: 'alist',
type: 'alist',
icon: 'mdi-server-network-outline',
remote: true,
},
{
type: 'smb',
icon: 'mdi-folder-network-outline',
remote: true,
},
]
export const storageDict = storageOptions.reduce((dict, item) => {
export const storageIconDict = storageAttributes.reduce((dict, item) => {
dict[item.type] = item.icon
return dict
}, {} as Record<string, string>)
export const storageRemoteDict = storageAttributes.reduce((dict, item) => {
dict[item.type] = item.remote
return dict
}, {} as Record<string, boolean>)
export const downloaderOptions = [
{
value: 'qbittorrent',
title: i18n.global.t('setting.system.qbittorrent'),
},
{
value: 'transmission',
title: i18n.global.t('setting.system.transmission'),
},
]
export const downloaderDict = downloaderOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
export const mediaServerOptions = [
{
value: 'emby',
title: i18n.global.t('setting.system.emby'),
},
{
value: 'jellyfin',
title: i18n.global.t('setting.system.jellyfin'),
},
{
value: 'plex',
title: i18n.global.t('setting.system.plex'),
},
{
value: 'trimemedia',
title: i18n.global.t('setting.system.trimeMedia'),
},
]
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
@@ -295,6 +340,14 @@ export const actionStepOptions = [
title: i18n.global.t('actionStep.transferFile'),
value: '整理文件',
},
{
title: i18n.global.t('actionStep.invokePlugin'),
value: '调用插件',
},
{
title: i18n.global.t('actionStep.note'),
value: '备注',
},
]
// 操作步骤字典

View File

@@ -1,12 +1,27 @@
import axios from 'axios'
import router from '@/router'
import { useAuthStore } from '@/stores'
import { initializeRequestOptimizer } from '@/utils/requestOptimizer'
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
// 创建axios实例
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
})
// 声明全局变量类型
declare global {
interface Window {
MoviePilotAPI: typeof api
}
}
// 将 API 实例暴露到全局,供插件使用
window.MoviePilotAPI = api
// 初始化请求优化器(必须在其他拦截器之前)
initializeRequestOptimizer(api)
// 添加请求拦截器
api.interceptors.request.use(config => {
// 认证 Store
@@ -18,15 +33,47 @@ api.interceptors.request.use(config => {
return config
})
// 离线状态管理
const globalOfflineStatus = useGlobalOfflineStatus()
// 添加响应拦截器
api.interceptors.response.use(
response => {
// 成功响应时,清除应用离线状态并重置连续错误计数
globalOfflineStatus.setAppOffline(false)
globalOfflineStatus.resetConsecutiveErrors()
return response.data
},
error => {
if (!error.response) {
// 请求超时
return Promise.reject(new Error(error))
// 网络错误或请求超时 - 通知离线状态管理系统
const isNetworkError =
error.code === 'NETWORK_ERROR' ||
error.code === 'ERR_NETWORK' ||
error.code === 'ECONNABORTED' ||
error.name === 'NetworkError'
if (isNetworkError) {
let reason = 'Network connection failed'
if (error.code === 'ECONNABORTED') {
reason = 'Request timeout'
}
// 记录网络错误,只有连续三次才会设置为离线模式
globalOfflineStatus.recordNetworkError(reason)
}
if (error.code === 'NETWORK_ERROR' || error.code === 'ERR_NETWORK') {
// 网络连接问题
return Promise.reject(new Error('Network connection failed, please check your network status'))
} else if (error.code === 'ECONNABORTED') {
// 请求超时
return Promise.reject(new Error('Request timeout, please try again later'))
} else if (error.name === 'AbortError') {
// 请求被中止(路由切换等)
return Promise.reject(new Error('Request cancelled'))
}
// 其他网络错误
return Promise.reject(new Error(error.message || 'Network error'))
} else if (error.response.status === 403) {
// 认证 Store
const authStore = useAuthStore()
@@ -41,17 +88,3 @@ api.interceptors.response.use(
)
export default api
export async function fetchGlobalSettings() {
try {
const result: { [key: string]: any } = await api.get('system/global', {
params: {
token: 'moviepilot',
},
})
return result.data || {}
} catch (error) {
console.error('Failed to fetch global settings', error)
throw error
}
}

View File

@@ -144,6 +144,42 @@ export interface SubscribeShare {
episode_group?: string
}
// 工作流分享
export interface WorkflowShare {
// 分享ID
id?: string
// 工作流ID
workflow_id?: string
// 分享标题
share_title?: string
// 分享说明
share_comment?: string
// 分享人
share_user?: string
// 分享人唯一ID
share_uid?: string
// 工作流名称
name?: string
// 工作流描述
description?: string
// 定时器
timer?: string
// 触发类型timer-定时触发 event-事件触发 manual-手动触发
trigger_type?: string
// 事件类型当trigger_type为event时使用
event_type?: string
// 动作列表
actions?: any[]
// 动作流
flows?: any[]
// 上下文
context?: string
// 时间
date?: string
// 复用次数
count?: number
}
// 历史记录
export interface TransferHistory {
// ID
@@ -565,9 +601,9 @@ export interface NotExistMediaInfo {
// 插件
export interface Plugin {
id?: string
id: string
// 插件名称
plugin_name?: string
plugin_name: string
// 插件描述
plugin_desc?: string
// 插件图标
@@ -631,6 +667,8 @@ export interface DashboardItem {
cols: { [key: string]: number }
// 页面元素
elements: RenderProps[]
// 渲染方式
render_mode?: string
}
// 种子信息
@@ -767,6 +805,8 @@ export interface MetaInfo {
audio_term: string
// 资源类型+特效
edition: string
// 流媒体平台
web_source: string
// 应用的自定义识别词
apply_words: string[]
}
@@ -950,6 +990,8 @@ export interface MediaServerPlayItem {
link?: string
// 播放百分比
percent?: number
// 媒体服务器类型
server_type?: string
}
// 媒体服务器媒体库
@@ -970,6 +1012,8 @@ export interface MediaServerLibrary {
image_list?: string[]
// 链接
link?: string
// 媒体服务器类型
server_type?: string
}
// 消息通知
@@ -1006,6 +1050,8 @@ export interface SystemNotification {
text: string
// 通知时间
date: string
// 是否已读
read?: boolean
}
// 下载器配置
@@ -1286,6 +1332,10 @@ export interface Workflow {
description?: string
// 定时器
timer?: string
// 触发类型timer-定时触发 event-事件触发 manual-手动触发
trigger_type?: string
// 事件类型当trigger_type为event时使用
event_type?: string
// 状态
state?: string
// 当前执行动作
@@ -1303,3 +1353,59 @@ 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[]
}
// 订阅分享统计
export interface SubscribeShareStatistics {
// 分享人
share_user?: string
// 分享数量
share_count?: number
// 总复用人次
total_reuse_count?: number
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -3,12 +3,7 @@ import FileList from './filebrowser/FileList.vue'
import FileToolbar from './filebrowser/FileToolbar.vue'
import FileNavigator from './filebrowser/FileNavigator.vue'
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
import { storageOptions } from '@/api/constants'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
import { storageIconDict } from '@/api/constants'
// 输入参数
const props = defineProps({
@@ -33,12 +28,6 @@ const props = defineProps({
// 对外事件
const emit = defineEmits(['pathchanged'])
// 显示器宽度
const display = useDisplay()
// APP
const appMode = inject('pwaMode') && display.mdAndDown.value
const fileIcons = {
// 压缩包
zip: 'mdi-folder-zip-outline',
@@ -140,10 +129,19 @@ 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(() => {
const storageCodes = props.storages?.map(item => item.type)
return storageOptions.filter(item => storageCodes?.includes(item.value))
return props.storages?.map(item => ({
title: item.name,
value: item.type,
icon: storageIconDict[item.type] ?? 'mdi-server-network-outline',
}))
})
// 方法
@@ -182,19 +180,57 @@ function fileListUpdated(items: FileItem[]) {
fileListItems.value = items
}
// 外层DIV大小控制
const scrollStyle = computed(() => {
return appMode
? 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom) - 6.5rem)'
: 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom)'
})
// 阻止选择事件
function preventSelect(event: Event) {
event.preventDefault()
return false
}
// 文件列表大小限制
const fileListStyle = computed(() => {
return appMode
? 'height: calc(100vh - 14rem - env(safe-area-inset-bottom) - 7rem)'
: 'height: calc(100vh - 14rem - env(safe-area-inset-bottom)'
})
// 拖动分隔条相关方法
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 = ''
}
</script>
<template>
@@ -212,7 +248,7 @@ const fileListStyle = computed(() => {
@foldercreated="refreshPending = true"
@sortchanged="sortChanged"
/>
<div class="flex" :style="scrollStyle">
<div class="flex">
<FileNavigator
v-if="showDirTree"
:storage="activeStorage"
@@ -220,8 +256,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"
@@ -230,8 +272,8 @@ const fileListStyle = computed(() => {
:axios="axios"
:refreshpending="refreshPending"
:sort="sort"
:listStyle="fileListStyle"
:showTree="showDirTree"
:style="{ flex: 1 }"
@pathchanged="pathChanged"
@loading="loadingChanged"
@refreshed="refreshPending = false"
@@ -244,3 +286,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

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import page404 from '@images/pages/404.svg'
// 国际化
const { t } = useI18n()
@@ -19,16 +20,7 @@ interface Props {
<div class="no-data-container">
<!-- 图标容器 -->
<div class="icon-wrapper">
<div class="icon-glow"></div>
<div class="icon-container">
<VIcon
:icon="props.icon || 'mdi-file-search-outline'"
:color="props.iconColor || 'white'"
size="48"
class="main-icon"
/>
</div>
<div class="pulse-ring"></div>
<img :src="page404" alt="404" />
</div>
<!-- 标题 -->
@@ -57,8 +49,7 @@ interface Props {
justify-content: center;
inline-size: 100%;
min-block-size: 300px;
padding-block: 3rem;
padding-inline: 1rem;
padding-block-start: 3rem;
text-align: center;
}
@@ -68,109 +59,17 @@ interface Props {
display: flex;
align-items: center;
justify-content: center;
block-size: 100px;
inline-size: 100px;
margin-block: 0 2rem;
inline-size: 15rem;
margin-block: 0 1rem;
margin-inline: auto;
}
.icon-glow {
position: absolute;
border-radius: 50%;
animation: pulse 3s infinite ease-in-out;
background: radial-gradient(circle, rgba(var(--v-theme-primary), 0.8) 0%, rgba(var(--v-theme-primary), 0) 70%);
block-size: 80px;
filter: blur(15px);
inline-size: 80px;
opacity: 0.8;
}
.icon-container {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.9), rgba(var(--v-theme-secondary), 0.8));
block-size: 80px;
inline-size: 80px;
}
.main-icon {
animation: slight-bounce 3s infinite ease-in-out;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 30%));
}
.pulse-ring {
position: absolute;
z-index: 1;
border: 2px solid rgba(var(--v-theme-primary), 0.5);
border-radius: 50%;
animation: ripple 2s infinite ease-out;
block-size: 100px;
inline-size: 100px;
inset-block-start: 50%;
inset-inline-start: 50%;
opacity: 0;
transform: translate(-50%, -50%);
}
.pulse-ring::before {
position: absolute;
border: 2px solid rgba(var(--v-theme-primary), 0.3);
border-radius: 50%;
animation: ripple 2s infinite 0.5s ease-out;
block-size: 85px;
content: '';
inline-size: 85px;
inset-block-start: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
}
@keyframes ripple {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(0.9);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(1.5);
}
}
@keyframes pulse {
0%,
100% {
opacity: 0.5;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.1);
}
}
@keyframes slight-bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
/* 文字样式 */
.error-title {
position: relative;
color: rgba(var(--v-theme-on-surface), 0.95);
font-size: 1.75rem;
font-weight: 700;
font-size: 1.5rem;
font-weight: 500;
margin-block-end: 0.75rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
}
@@ -181,69 +80,15 @@ interface Props {
background: linear-gradient(90deg, rgba(var(--v-theme-primary), 0.8), rgba(var(--v-theme-primary), 0.2));
block-size: 3px;
content: '';
inline-size: 40px;
margin-block: 0.5rem 0;
inline-size: 60px;
margin-inline: auto;
}
.error-description {
color: rgba(var(--v-theme-on-surface), 0.75);
font-size: 1.1rem;
line-height: 1.6;
margin-block-end: 1.5rem;
font-size: 1rem;
margin-block-end: 1rem;
margin-inline: auto;
max-inline-size: 80%;
}
.actions-container {
margin-block-start: 1.5rem;
}
.actions-container :deep(.v-btn) {
transform: translateY(0);
transition: transform 0.2s ease;
}
.actions-container :deep(.v-btn:hover) {
transform: translateY(-2px);
}
/* 响应式调整 */
@media (width <= 600px) {
.no-data-container {
padding-block: 2rem;
padding-inline: 1rem;
}
.icon-wrapper {
block-size: 80px;
inline-size: 80px;
margin-block-end: 1.5rem;
}
.icon-container {
block-size: 70px;
inline-size: 70px;
}
.icon-glow {
block-size: 70px;
inline-size: 70px;
}
.pulse-ring,
.pulse-ring::before {
block-size: 80px;
inline-size: 80px;
}
.error-title {
font-size: 1.4rem;
}
.error-description {
font-size: 0.95rem;
max-inline-size: 90%;
}
}
</style>

View File

@@ -0,0 +1,201 @@
<script setup lang="ts">
import { usePWAInstall } from '@/composables/usePWAInstall'
import { useAuthStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification'
const { t, locale, messages } = useI18n()
const { isInstalled, showInstallPrompt, getInstallInstructions } = usePWAInstall()
const showBanner = ref(false)
const showInstructions = ref(false)
const dismissed = ref(false)
// 检查是否登录
const authStore = useAuthStore()
const isLogin = computed(() => authStore.token)
// 检查当前是不是https环境
const isHttps = computed(() => {
return window.location.protocol === 'https:'
})
// 检查是否应该显示横幅
const shouldShowBanner = computed(() => {
return !isInstalled.value && !dismissed.value && !showInstructions.value && isLogin.value && isHttps.value
})
// 显示延迟(避免立即显示)
onMounted(() => {
setTimeout(() => {
// 检查本地存储,看用户是否已经关闭过提示
const dismissedTime = localStorage.getItem('pwa-install-dismissed')
if (dismissedTime) {
const dismissedDate = new Date(dismissedTime)
const now = new Date()
const daysDiff = (now.getTime() - dismissedDate.getTime()) / (1000 * 60 * 60 * 24)
// 如果距离上次关闭不到30天不显示
if (daysDiff < 30) {
dismissed.value = true
return
}
}
showBanner.value = true
}, 5000) // 5秒后显示
})
// 处理安装
const handleInstall = async () => {
const installed = await showInstallPrompt()
if (installed) {
showBanner.value = false
// 显示成功消息
useToast().success(t('pwa.installSuccess'))
} else {
// 如果用户拒绝,显示手动安装说明
showInstructions.value = true
}
}
// 关闭横幅
const dismissBanner = () => {
showBanner.value = false
dismissed.value = true
// 记录关闭时间
localStorage.setItem('pwa-install-dismissed', new Date().toISOString())
}
// 获取平台特定的安装说明
const instructions = computed(() => {
const rawInstructions = getInstallInstructions()
const platformKey = rawInstructions.platformKey
// 获取平台显示名称
const platformName = t(`pwa.platforms.${platformKey}`)
// 直接使用t函数获取安装步骤避免编译对象的问题
const steps = []
const maxSteps = 10 // 最大步骤数,防止无限循环
for (let i = 0; i < maxSteps; i++) {
try {
const stepKey = `pwa.installSteps.${platformKey}.${i}`
const stepText = t(stepKey)
// 如果返回的是键名本身,说明没有找到对应的翻译
if (stepText === stepKey) {
break
}
steps.push(stepText)
} catch (error) {
// 如果出现错误,说明没有更多步骤
break
}
}
return {
platform: platformName,
steps,
}
})
</script>
<template>
<!-- 安装横幅 -->
<Teleport to="body">
<Transition
enter-active-class="transition-all duration-300"
enter-from-class="translate-y-full opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition-all duration-300"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-full opacity-0"
>
<VCard v-if="shouldShowBanner && showBanner" class="pwa-install-banner">
<div class="banner-content">
<VIcon icon="mdi-cellphone-link" size="24" class="me-3" />
<div class="flex-grow-1">
<div class="font-weight-medium">{{ t('pwa.installApp') }}</div>
<div class="text-sm opacity-70">{{ t('pwa.installDescription') }}</div>
</div>
<VBtn color="primary" size="small" variant="flat" @click="handleInstall">
{{ t('pwa.install') }}
</VBtn>
<VBtn icon size="small" variant="text" @click="dismissBanner">
<VIcon icon="mdi-close" />
</VBtn>
</div>
</VCard>
</Transition>
</Teleport>
<!-- 手动安装说明对话框 -->
<VDialog v-model="showInstructions" max-width="500">
<VCard>
<VCardItem>
<VCardTitle class="d-flex align-center">
<VIcon icon="mdi-information-outline" class="me-2" />
{{ t('pwa.installGuide') }}
</VCardTitle>
</VCardItem>
<VCardText>
<div class="mb-4">
<div class="text-subtitle-1 mb-2">
{{ t('pwa.installInstructions', { platform: instructions.platform }) }}
</div>
<VList density="compact">
<VListItem
v-for="(step, index) in instructions.steps"
:key="index"
:prepend-icon="`mdi-numeric-${index + 1}-circle`"
>
<VListItemTitle>{{ step }}</VListItemTitle>
</VListItem>
</VList>
</div>
<VAlert type="info" variant="tonal" density="compact">
{{ t('pwa.installNote') }}
</VAlert>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" variant="text" @click="showInstructions = false">
{{ t('pwa.gotIt') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>
<style scoped>
.pwa-install-banner {
position: fixed;
z-index: 1000;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 12px;
background: rgb(var(--v-theme-surface));
box-shadow: 0 4px 20px rgba(0, 0, 0, 10%);
inset-block-end: 5rem;
inset-inline: 20px;
}
.banner-content {
display: flex;
align-items: center;
padding: 16px;
gap: 8px;
}
@media (width >= 600px) {
.pwa-install-banner {
inset-inline: auto 20px;
max-inline-size: 400px;
}
}
</style>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { MediaServerPlayItem } from '@/api/types'
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
// 输入参数
const props = defineProps({
media: Object as PropType<MediaServerPlayItem>,
@@ -16,8 +17,10 @@ function imageLoadHandler() {
}
// 跳转播放
function goPlay() {
if (props.media?.link) window.open(props.media?.link, '_blank')
async function goPlay() {
if (props.media?.link) {
await openMediaServerWithAutoDetect(props.media.link, undefined, props.media.server_type)
}
}
// 计算图片地址
@@ -28,7 +31,7 @@ const getImgUrl = computed(() => {
</script>
<template>
<VHover v-bind="props">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
@@ -48,16 +51,18 @@ const getImgUrl = computed(() => {
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
</div>
</template>
<VCardText
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
>
<h1
class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ..."
<template #default>
<VCardText
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
>
{{ props.media?.title }}
</h1>
<span class="text-shadow">{{ props.media?.subtitle }}</span>
</VCardText>
<h1
class="mb-1 text-white text-shadow font-bold text-lg line-clamp-2 overflow-hidden text-ellipsis ..."
>
{{ props.media?.title }}
</h1>
<span class="text-shadow text-sm">{{ props.media?.subtitle }}</span>
</VCardText>
</template>
</VImg>
</template>
<div class="w-full absolute bottom-0">

View File

@@ -1,10 +1,14 @@
<script lang="ts" setup>
import { CustomRule } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import { useToast } from 'vue-toastification'
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({
@@ -83,6 +87,12 @@ function saveRuleInfo() {
emit('done')
}
// 验证规则ID输入
function validateRuleId() {
// 只允许英文和数字,不允许空格
ruleInfo.value.id = ruleInfo.value.id.replace(/[^a-zA-Z0-9]/g, '')
}
// 按钮点击
function onClose() {
emit('close')
@@ -106,8 +116,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 })" class="rounded-t">
<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 +143,8 @@ function onClose() {
:hint="t('customRule.hint.ruleId')"
persistent-hint
active
prepend-inner-icon="mdi-identifier"
@input="validateRuleId"
/>
</VCol>
<VCol cols="12" md="6">
@@ -131,6 +155,7 @@ function onClose() {
:hint="t('customRule.hint.ruleName')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12">
@@ -141,6 +166,7 @@ function onClose() {
:hint="t('customRule.hint.include')"
persistent-hint
active
prepend-inner-icon="mdi-plus-circle"
/>
</VCol>
<VCol cols="12">
@@ -151,6 +177,7 @@ function onClose() {
:hint="t('customRule.hint.exclude')"
persistent-hint
active
prepend-inner-icon="mdi-minus-circle"
/>
</VCol>
<VCol cols="6">
@@ -161,6 +188,7 @@ function onClose() {
:hint="t('customRule.hint.sizeRange')"
persistent-hint
active
prepend-inner-icon="mdi-harddisk"
/>
</VCol>
<VCol cols="6">
@@ -171,6 +199,7 @@ function onClose() {
:hint="t('customRule.hint.seeders')"
persistent-hint
active
prepend-inner-icon="mdi-account-group"
/>
</VCol>
<VCol cols="6">
@@ -181,13 +210,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

@@ -1,9 +1,9 @@
<script lang="ts" setup>
import type { TransferDirectoryConf } from '@/api/types'
import type { StorageConf, TransferDirectoryConf } from '@/api/types'
import api from '@/api'
import { nextTick } from 'vue'
import { storageOptions } from '@/api/constants'
import { useI18n } from 'vue-i18n'
import { storageRemoteDict } from '@/api/constants'
// 国际化
const { t } = useI18n()
@@ -19,6 +19,10 @@ const props = defineProps({
type: Object as PropType<{ [key: string]: any }>,
required: true,
},
storages: {
type: Array as PropType<StorageConf[]>,
required: true,
},
width: String,
height: String,
})
@@ -29,13 +33,26 @@ const isCollapsed = ref(true)
// 类型下拉字典
const typeItems = computed(() => [
{ title: t('common.all'), value: '' },
{ title: t('media.movie'), value: '电影' },
{ title: t('media.tv'), value: '电视剧' },
{ title: t('mediaType.movie'), value: '电影' },
{ title: t('mediaType.tv'), value: '电视剧' },
])
// 计算资源存储字典(整理方式为下载器时不能为远程存储)
const resourceStorageOptions = computed(() => {
return storageOptions.filter(item => !item.remote || props.directory.monitor_type !== 'downloader')
return props.storages
.filter(item => !storageRemoteDict[item.type] || props.directory.monitor_type !== 'downloader')
.map(item => ({
title: item.name,
value: item.type,
}))
})
// 存储字典
const libraryStorageOptions = computed(() => {
return props.storages.map(item => ({
title: item.name,
value: item.type,
}))
})
// 自动整理方式下拉字典
@@ -197,7 +214,7 @@ watch(
<VForm>
<VRow>
<VCol cols="6">
<VSelect
<VAutocomplete
v-model="props.directory.media_type"
variant="underlined"
:items="typeItems"
@@ -206,7 +223,7 @@ watch(
/>
</VCol>
<VCol cols="6">
<VSelect
<VAutocomplete
v-model="props.directory.media_category"
variant="underlined"
:items="getCategories"
@@ -214,7 +231,7 @@ watch(
/>
</VCol>
<VCol cols="4">
<VSelect
<VAutocomplete
v-model="props.directory.storage"
variant="underlined"
:items="resourceStorageOptions"
@@ -260,10 +277,10 @@ watch(
/>
</VCol>
<VCol cols="4">
<VSelect
<VAutocomplete
v-model="props.directory.library_storage"
variant="underlined"
:items="storageOptions"
:items="libraryStorageOptions"
:label="t('directory.libraryStorage')"
/>
</VCol>

View File

@@ -2,15 +2,23 @@
import api from '@/api'
import { formatFileSize } from '@/@core/utils/formatters'
import { DownloaderConf } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import { useToast } from 'vue-toastification'
import type { DownloaderInfo } from '@/api/types'
import qbittorrent_image from '@images/logos/qbittorrent.png'
import transmission_image from '@images/logos/transmission.png'
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'
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
// 显示器宽度
const display = useDisplay()
// 获取i18n实例
const { t } = useI18n()
const { useConditionalDataRefresh } = useBackgroundOptimization()
// 定义输入
const props = defineProps({
@@ -37,9 +45,6 @@ const emit = defineEmits(['close', 'done', 'change'])
// 提示框
const $toast = useToast()
// timeout定时器
let timeoutTimer: NodeJS.Timeout | undefined = undefined
// 上传速率
const upload_rate = ref(0)
@@ -58,9 +63,15 @@ const downloaderInfo = ref<DownloaderConf>({
config: {},
})
// 下载器是否应该刷新数据的计算属性
const shouldRefresh = computed(() => props.allowRefresh && props.downloader.enabled)
// 调用API查询下载器数据
async function loadDownloaderInfo() {
if (!props.allowRefresh) {
if (!shouldRefresh.value) {
// 当下载器被禁用时,重置速率数据
upload_rate.value = 0
download_rate.value = 0
return
}
try {
@@ -73,11 +84,6 @@ async function loadDownloaderInfo() {
if (res) {
upload_rate.value = res.upload_speed
download_rate.value = res.download_speed
// 定时查询
clearTimeout(timeoutTimer)
if (props.downloader.enabled) {
timeoutTimer = setTimeout(loadDownloaderInfo, 3000)
}
}
} catch (e) {
console.log(e)
@@ -126,7 +132,7 @@ const getIcon = computed(() => {
case 'transmission':
return transmission_image
default:
return qbittorrent_image
return custom_image
}
})
@@ -135,14 +141,17 @@ function onClose() {
emit('close')
}
onMounted(async () => {
if (props.downloader.enabled) {
await loadDownloaderInfo()
}
})
// 使用条件性数据刷新定时器(只在下载器启用时运行)
const { stop: stopRefresh } = useConditionalDataRefresh(
`downloader-${props.downloader.name}`,
loadDownloaderInfo,
shouldRefresh, // 响应式条件只有当allowRefresh为true且downloader启用时才运行
3000, // 3秒间隔
true, // 立即执行一次
)
onUnmounted(() => {
if (timeoutTimer) clearTimeout(timeoutTimer)
stopRefresh()
})
</script>
<template>
@@ -172,19 +181,36 @@ onUnmounted(() => {
/>
<span class="text-h6">{{ downloader.name }}</span>
</div>
<div class="mt-1 flex flex-wrap text-sm" v-if="props.downloader.enabled">
<div v-if="downloaderDict[downloader.type] && props.downloader.enabled" class="mt-1 flex flex-wrap text-sm">
<span class="me-2">{{ `${formatFileSize(upload_rate, 1)}/s ` }}</span>
<span>{{ `${formatFileSize(download_rate, 1)}/s` }}</span>
</div>
<div v-else-if="!downloaderDict[downloader.type]" class="mt-1 flex flex-wrap text-sm">
<span class="me-2">自定义下载器</span>
</div>
</div>
<div class="h-20">
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
<VImg :src="getIcon" cover class="mt-8 me-3" max-width="3rem" min-width="3rem" />
</div>
</VCardText>
</VCard>
</VHover>
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`" class="rounded-t">
<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>
@@ -210,6 +236,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -220,6 +247,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -229,6 +257,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -239,6 +268,7 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -278,7 +308,7 @@ onUnmounted(() => {
/>
</VCol>
</VRow>
<VRow v-if="downloaderInfo.type == 'transmission'">
<VRow v-else-if="downloaderInfo.type == 'transmission'">
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.name"
@@ -287,6 +317,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -297,6 +328,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -306,6 +338,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -316,13 +349,36 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
</VRow>
<VRow v-else>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.type"
:label="t('downloader.type')"
:hint="t('downloader.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.name"
:label="t('downloader.name')"
: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

@@ -6,6 +6,7 @@ import { formatFileSize } from '@/@core/utils/formatters'
// 输入参数
const props = defineProps({
info: Object as PropType<DownloadingInfo>,
downloaderName: String,
})
// 是否显示卡片
@@ -42,16 +43,15 @@ function imageLoadHandler() {
imageLoaded.value = true
}
// 计算文本类
function getTextClass() {
return imageLoaded.value ? 'text-white' : ''
}
// 下载状态控制
async function toggleDownload() {
const operation = isDownloading.value ? 'stop' : 'start'
try {
const result: { [key: string]: any } = await api.get(`download/${operation}/${props.info?.hash}`)
const result: { [key: string]: any } = await api.get(`download/${operation}/${props.info?.hash}`, {
params: {
name: props.downloaderName,
},
})
if (result.success) isDownloading.value = !isDownloading.value
} catch (error) {
@@ -62,7 +62,7 @@ async function toggleDownload() {
// 删除下截
async function deleteDownload() {
try {
await api.delete(`download/${props.info?.hash}`)
await api.delete(`download/${props.info?.hash}`, { params: { name: props.downloaderName } })
cardState.value = false
} catch (error) {
console.error(error)
@@ -71,35 +71,52 @@ async function deleteDownload() {
</script>
<template>
<VCard v-if="cardState" :key="props.info?.hash">
<VCard v-if="cardState" :key="props.info?.hash" class="flex flex-col h-full" min-height="150">
<template #image>
<VImg :src="props.info?.media.image" aspect-ratio="2/3" cover class="brightness-50" @load="imageLoadHandler" />
<VImg :src="props.info?.media.image" aspect-ratio="2/3" cover @load="imageLoadHandler" position="top">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
<template #default>
<div class="absolute inset-0 outline-none downloading-card-background"></div>
</template>
</VImg>
</template>
<VCardTitle class="break-words whitespace-normal" :class="getTextClass()">
{{ props.info?.media.title || props.info?.name }}
{{
props.info?.media.episode
? `${props.info?.media.season} ${props.info?.media.episode}`
: props.info?.season_episode
}}
</VCardTitle>
<div>
<VCardTitle class="break-words whitespace-normal text-white">
{{ props.info?.media.title || props.info?.name }}
{{
props.info?.media.episode
? `${props.info?.media.season} ${props.info?.media.episode}`
: props.info?.season_episode
}}
</VCardTitle>
<VCardSubtitle class="break-words whitespace-normal" :class="getTextClass()">
{{ props.info?.title }}
</VCardSubtitle>
<VCardSubtitle class="break-words whitespace-normal text-white">
{{ props.info?.title }}
</VCardSubtitle>
<VCardText class="text-subtitle-1 pt-3 pb-1" :class="getTextClass()">
{{ getSpeedText() }}
</VCardText>
<VCardText class="text-subtitle-1 pt-3 pb-1 text-white">
{{ getSpeedText() }}
</VCardText>
<VCardText v-if="getPercentage() > 0" :class="getTextClass()">
<VProgressLinear :model-value="getPercentage()" />
</VCardText>
<VCardText v-if="getPercentage() > 0" class="text-white">
<VProgressLinear :model-value="getPercentage()" bg-color="success" color="success" />
</VCardText>
<VCardActions class="justify-space-between">
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
</VCardActions>
<VCardActions class="justify-space-between">
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
</VCardActions>
</div>
</VCard>
</template>
<style lang="scss" scoped>
.downloading-card-background {
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
}
</style>

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

@@ -3,11 +3,15 @@ import draggable from 'vuedraggable'
import { copyToClipboard } from '@/@core/utils/navigator'
import { CustomRule, FilterRuleGroup } from '@/api/types'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import { useToast } from 'vue-toast-notification'
import { useToast } from 'vue-toastification'
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,8 +223,14 @@ 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">
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`" class="rounded-t">
<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 />
<VCardItem class="pt-1">
@@ -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

@@ -4,6 +4,7 @@ import plex from '@images/misc/plex.png'
import emby from '@images/misc/emby.png'
import jellyfin from '@images/misc/jellyfin.png'
import trimemedia from '@images/logos/trimemedia.png'
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
// 输入参数
const props = defineProps({
@@ -36,16 +37,18 @@ function imageErrorHandler() {
// 默认图片
function getDefaultImage() {
if (props.media?.server === 'plex') return plex
else if (props.media?.server === 'emby') return emby
else if (props.media?.server === 'jellyfin') return jellyfin
else if (props.media?.server === 'trimemedia') return trimemedia
if (props.media?.server_type === 'plex') return plex
else if (props.media?.server_type === 'emby') return emby
else if (props.media?.server_type === 'jellyfin') return jellyfin
else if (props.media?.server_type === 'trimemedia') return trimemedia
else return plex
}
// 跳转播放
function goPlay() {
if (props.media?.link) window.open(props.media?.link, '_blank')
async function goPlay() {
if (props.media?.link) {
await openMediaServerWithAutoDetect(props.media.link, undefined, props.media.server_type)
}
}
// 生成图片代理路径
@@ -69,20 +72,18 @@ async function drawImages(imageList: string[]) {
if (!canvas) return getDefaultImage()
// 画布参数
const POSTER_WIDTH = (canvas.width - 32) / 4
const POSTER_HEIGHT = canvas.height * 0.75 - 8
const MARGIN_WIDTH = 4
const MARGIN_HEIGHT = 4
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
const POSTER_WIDTH = (canvas.width - 40) / 4 // 左右边框8px + 3个间隔24px = 40px
const POSTER_HEIGHT = 256 // 上方海报高256
const MARGIN_WIDTH = 8 // 左右间隔为8
const MARGIN_HEIGHT = 4 // 海报和倒影之间的间隔为4
const REFLECTION_HEIGHT = canvas.height - POSTER_HEIGHT - MARGIN_HEIGHT // 下方倒影使用剩余全部高度
// 获取画布上下文
const ctx = canvas.getContext('2d')
if (!ctx) return getDefaultImage()
// 设置背景色为黑色
ctx.fillStyle = '#000000'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 设置背景色为透明
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制图片
async function drawImageWithReflection(imgSrc: string, index: number) {
@@ -101,12 +102,12 @@ async function drawImages(imageList: string[]) {
} catch (error) {
console.error(error)
ctx.fillStyle = '#e5e7eb'
ctx.fillRect(MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1), MARGIN_HEIGHT, POSTER_WIDTH, POSTER_HEIGHT)
ctx.fillRect(MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1), 0, POSTER_WIDTH, POSTER_HEIGHT)
return
}
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
const y = MARGIN_HEIGHT
const y = 0 // 海报紧贴顶部
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
@@ -120,17 +121,18 @@ async function drawImages(imageList: string[]) {
img.width,
img.height,
x,
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
0,
POSTER_WIDTH,
REFLECTION_HEIGHT,
)
const gradient = ctx.createLinearGradient(0, REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT, 0, REFLECTION_HEIGHT)
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height - (POSTER_HEIGHT + MARGIN_HEIGHT))
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.7)')
ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = gradient
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_HEIGHT)
ctx.restore()
}
@@ -151,7 +153,7 @@ onMounted(async () => {
</script>
<template>
<VHover v-bind="props" :height="props.height" :width="props.width">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
@@ -163,20 +165,22 @@ onMounted(async () => {
@click="goPlay"
>
<template #image>
<canvas ref="canvasRef" class="w-full h-full hidden" />
<canvas ref="canvasRef" width="640" height="360" class="w-full h-full hidden" />
<VImg :src="imgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
</div>
</template>
<VCardText
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
>
<h1 class="mb-1 text-white text-shadow font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.name }}
</h1>
</VCardText>
<template #default>
<VCardText
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
>
<h1 class="mb-1 text-white text-shadow font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.name }}
</h1>
</VCardText>
</template>
</VImg>
</template>
</VCard>

View File

@@ -4,17 +4,18 @@ import tmdbImage from '@images/logos/tmdb.png'
import doubanImage from '@images/logos/douban-black.png'
import bangumiImage from '@images/logos/bangumi.png'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { useToast } from 'vue-toastification'
import { formatSeason, formatRating } from '@/@core/utils/formatters'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { MediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
import router, { registerAbortController } from '@/router'
import { useUserStore } from '@/stores'
import router from '@/router'
import { useUserStore, useGlobalSettingsStore } from '@/stores'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
import SubscribeSeasonDialog from '../dialog/SubscribeSeasonDialog.vue'
import { useI18n } from 'vue-i18n'
import { mediaTypeDict } from '@/api/constants'
import { hasPermission } from '@/utils/permission'
// 国际化
const { t } = useI18n()
@@ -27,7 +28,9 @@ const props = defineProps({
})
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 全局设置
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 用户 Store
const userStore = useUserStore()
@@ -231,9 +234,6 @@ async function handleCheckSubscribe() {
// 查询当前媒体是否已入库
async function handleCheckExists() {
try {
const abortController = new AbortController()
registerAbortController(abortController)
const { signal } = abortController
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
params: {
tmdbid: props.media?.tmdb_id,
@@ -242,7 +242,6 @@ async function handleCheckExists() {
season: props.media?.season,
mtype: props.media?.type,
},
signal,
})
if (result.success) isExists.value = true
@@ -254,16 +253,13 @@ async function handleCheckExists() {
// 调用API检查是否已订阅电视剧需要指定季
async function checkSubscribe(season = 0) {
try {
const abortController = new AbortController()
registerAbortController(abortController)
const { signal } = abortController
// AbortController 现在由全局请求优化器自动管理
const mediaid = getMediaId()
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: {
season,
title: props.media?.title,
},
signal,
})
return result.id || null
@@ -472,17 +468,29 @@ onBeforeUnmount(() => {
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
>
<span class="font-bold">{{ props.media?.year }}</span>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
<span class="font-semibold text-sm">{{ props.media?.year }}</span>
<h1 class="media-card-title font-bold mb-2 text-white line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }}
</h1>
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
<p class="media-card-overview line-clamp-3 overflow-hidden text-ellipsis ...">
{{ props.media?.overview }}
</p>
<div v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
<div v-else class="flex align-center justify-between">
<IconBtn icon="mdi-magnify" color="white" @click.stop="clickSearch" />
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
<IconBtn
v-if="hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search')"
icon="mdi-magnify"
color="white"
size="small"
@click.stop="clickSearch"
/>
<VSpacer />
<IconBtn
icon="mdi-heart"
:color="isSubscribed ? 'error' : 'white'"
size="small"
@click.stop="handleSubscribe"
/>
</div>
</VCardText>
<!-- 类型角标 -->
@@ -548,3 +556,14 @@ onBeforeUnmount(() => {
@close="chooseSiteDialog = false"
/>
</template>
<style scoped>
.media-card-title {
font-size: 1.125rem;
line-height: 1.25rem;
}
.media-card-overview {
font-size: 0.875rem;
line-height: 1rem;
}
</style>

View File

@@ -47,10 +47,12 @@ function openTmdbPage(type: string, tmdbId: number) {
</div>
<div class="flex-grow">
<VCardItem class="pb-1">
<VCardTitle class="text-center text-md-left">
<div class="text-center text-md-left text-h6 font-weight-bold line-clamp-2 overflow-hidden text-ellipsis">
{{ context?.media_info?.title || context?.meta_info?.name }}
{{ context?.meta_info?.season_episode }}
</VCardTitle>
<span v-if="context?.meta_info?.season_episode" class="text-sm text-medium-emphasis align-top">
{{ context?.meta_info?.season_episode }}
</span>
</div>
<VCardSubtitle class="text-center text-md-left">
{{ context?.media_info?.year || context?.meta_info?.year }}
</VCardSubtitle>
@@ -87,6 +89,9 @@ function openTmdbPage(type: string, tmdbId: number) {
{{ context?.media_info?.tmdb_id }}
</VChip>
<!-- meta_info -->
<VChip v-if="context?.meta_info?.web_source" variant="elevated" class="me-1 mb-1 text-white bg-purple-500">
{{ context?.meta_info?.web_source }}
</VChip>
<VChip v-if="context?.meta_info?.edition" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.meta_info?.edition }}
</VChip>

View File

@@ -1,13 +1,19 @@
<script setup lang="ts">
import { MediaServerConf, MediaServerLibrary, MediaStatistic } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import { useToast } from 'vue-toastification'
import emby_image from '@images/logos/emby.png'
import jellyfin_image from '@images/logos/jellyfin.png'
import plex_image from '@images/logos/plex.png'
import trimemedia_image from '@images/logos/trimemedia.png'
import custom_image from '@images/logos/mediaserver.png'
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()
@@ -108,8 +114,10 @@ const getIcon = computed(() => {
return jellyfin_image
case 'trimemedia':
return trimemedia_image
default:
case 'plex':
return plex_image
default:
return custom_image
}
})
@@ -183,17 +191,34 @@ onMounted(() => {
<VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start flex-1">
<div class="text-h6 mb-1">{{ mediaserver.name }}</div>
<div class="text-sm mt-5 flex flex-wrap">
<div v-if="mediaServerDict[mediaserver.type] && mediaserver.enabled" class="text-sm mt-5 flex flex-wrap">
<span v-for="item in infoItems" :key="item.title" class="me-2 mb-1">
<VIcon rounded :icon="item.avatar" class="me-1" />{{ item.amount }}
</span>
</div>
<div v-else-if="!mediaServerDict[mediaserver.type]" class="text-sm mt-5 flex flex-wrap">
<span class="me-2 mb-1">自定义媒体服务器</span>
</div>
</div>
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
<VImg :src="getIcon" cover class="mt-8 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')}`" class="rounded-t">
<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>
@@ -212,6 +237,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -222,6 +248,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -232,6 +259,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -241,137 +269,11 @@ onMounted(() => {
:hint="t('mediaserver.embyApiKeyHint')"
persistent-hint
active
/>
</VCol>
</VRow>
<VRow v-if="mediaServerInfo.type == 'jellyfin'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.apikey"
:label="t('mediaserver.apiKey')"
:hint="t('mediaserver.jellyfinApiKeyHint')"
persistent-hint
active
/>
</VCol>
</VRow>
<VRow v-if="mediaServerInfo.type == 'trimemedia'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="mediaServerInfo.config.username" :label="t('mediaserver.username')" active />
</VCol>
<VCol cols="12" md="6">
<VTextField
type="password"
v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')"
active
/>
</VCol>
</VRow>
<VRow v-if="mediaServerInfo.type == 'plex'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.token"
:label="t('mediaserver.plexToken')"
:hint="t('mediaserver.plexTokenHint')"
persistent-hint
active
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSelect
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
@@ -382,14 +284,224 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
</VRow>
<VRow v-else-if="mediaServerInfo.type == 'jellyfin'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.apikey"
:label="t('mediaserver.apiKey')"
:hint="t('mediaserver.jellyfinApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
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"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
</VRow>
<VRow v-else-if="mediaServerInfo.type == 'trimemedia'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
: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
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
type="password"
v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')"
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
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"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
</VRow>
<VRow v-else-if="mediaServerInfo.type == 'plex'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.token"
:label="t('mediaserver.plexToken')"
:hint="t('mediaserver.plexTokenHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
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"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
</VRow>
<VRow v-else>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.type"
: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
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

@@ -10,6 +10,9 @@ const props = defineProps({
height: String,
})
// 定义事件
const emit = defineEmits(['imageload'])
// 图片是否加载完成
const isImageLoaded = ref(false)
@@ -19,6 +22,7 @@ const imageLoadError = ref(false)
// 图片加载完成
async function imageLoaded() {
isImageLoaded.value = true
emit('imageload')
}
// 链接打开新窗口
@@ -55,7 +59,14 @@ function replaceNewLine(value: string) {
position="top"
@load="imageLoaded"
@error="imageLoadError = true"
/>
min-height="10rem"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</div>
<div
v-if="

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 { useToast } from 'vue-toast-notification'
import custom_image from '@images/logos/notification.png'
import { useToast } from 'vue-toastification'
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')}`" class="rounded-t">
<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,21 @@ function onClose() {
:placeholder="t('notification.telegram.adminsPlaceholder')"
:hint="t('notification.telegram.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.API_URL"
:label="t('notification.telegram.apiUrl')"
:placeholder="t('notification.telegram.apiUrlPlaceholder')"
:hint="t('notification.telegram.apiUrlHint')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</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 +322,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 +332,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 +342,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 +352,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 +364,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 +373,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 +382,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 +394,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 +403,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 +412,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 +422,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 +434,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 +443,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

@@ -2,6 +2,7 @@
import personIcon from '@images/misc/person-icon.png'
import type { Person } from '@/api/types'
import router from '@/router'
import { useGlobalSettingsStore } from '@/stores'
const personProps = defineProps({
person: Object as PropType<Person>,
@@ -10,7 +11,9 @@ const personProps = defineProps({
})
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 全局设置
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 当前人物
const personInfo = ref(personProps.person)
@@ -82,14 +85,12 @@ 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">
<VAvatar
size="120"
size="100"
:class="{
'ring-1 ring-gray-700': isImageLoaded,
}"
@@ -100,10 +101,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" />

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