Showing preview only (2,803K chars total). Download the full file or copy to clipboard to get everything.
Repository: jxxghp/MoviePilot-Frontend
Branch: v2
Commit: b7857691382b
Files: 377
Total size: 2.4 MB
Directory structure:
gitextract_ghdmazve/
├── .editorconfig
├── .eslintrc.js
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── discussion.yml
│ │ ├── feature_request.yml
│ │ └── rfc.yml
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .stylelintrc.json
├── .vscode/
│ ├── anchor-comments.code-snippets
│ ├── extensions.json
│ ├── settings.json
│ ├── vue-ts.code-snippets
│ ├── vue.code-snippets
│ └── vuetify.code-snippets
├── LICENSE
├── README.md
├── README_EN.md
├── auto-imports.d.ts
├── components.d.ts
├── docs/
│ ├── federation-troubleshooting.md
│ └── module-federation-guide.md
├── env.d.ts
├── examples/
│ └── plugin-component/
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── components/
│ │ │ ├── AppPage.vue
│ │ │ ├── AppPageSettings.vue
│ │ │ ├── Config.vue
│ │ │ ├── Dashboard.vue
│ │ │ └── Page.vue
│ │ ├── main.js
│ │ └── vuetify/
│ │ ├── defaults.ts
│ │ └── theme.ts
│ └── vite.config.js
├── index.html
├── package.json
├── postcss.config.js
├── public/
│ ├── nginx.conf
│ ├── offline.html
│ ├── robots.txt
│ └── service.js
├── shims.d.ts
├── src/
│ ├── @core/
│ │ ├── components/
│ │ │ ├── ConfirmDialog.vue
│ │ │ ├── DialogCloseBtn.vue
│ │ │ ├── ErrorHeader.vue
│ │ │ ├── ExistIcon.vue
│ │ │ ├── LoadingBanner.vue
│ │ │ ├── MoreBtn.vue
│ │ │ ├── PageContentTitle.vue
│ │ │ ├── ScrollToTopBtn.vue
│ │ │ └── StatIcon.vue
│ │ ├── libs/
│ │ │ └── apex-chart/
│ │ │ └── apexCharConfig.ts
│ │ ├── scss/
│ │ │ ├── README.md
│ │ │ ├── _components.scss
│ │ │ ├── _dark.scss
│ │ │ ├── _default-layout-w-vertical-nav.scss
│ │ │ ├── _default-layout.scss
│ │ │ ├── _misc.scss
│ │ │ ├── _mixins.scss
│ │ │ ├── _utilities.scss
│ │ │ ├── _utils.scss
│ │ │ ├── _variables.scss
│ │ │ ├── _vertical-nav.scss
│ │ │ ├── index.scss
│ │ │ ├── libs/
│ │ │ │ ├── apex-chart.scss
│ │ │ │ ├── full-calendar.scss
│ │ │ │ ├── perfect-scrollbar.scss
│ │ │ │ └── vuetify/
│ │ │ │ ├── _overrides.scss
│ │ │ │ ├── _variables.scss
│ │ │ │ └── index.scss
│ │ │ ├── pages/
│ │ │ │ ├── misc.scss
│ │ │ │ └── page-auth.scss
│ │ │ └── placeholders/
│ │ │ ├── _default-layout.scss
│ │ │ ├── _index.scss
│ │ │ ├── _nav.scss
│ │ │ └── _vertical-nav.scss
│ │ └── utils/
│ │ ├── compatibility.ts
│ │ ├── dom.ts
│ │ ├── formatters.ts
│ │ ├── image.ts
│ │ ├── index.ts
│ │ ├── navigator.ts
│ │ ├── theme.ts
│ │ └── workflow.ts
│ ├── @iconify/
│ │ ├── build-icons.ts
│ │ ├── tsconfig.json
│ │ └── tsconfig.tsbuildinfo
│ ├── @layouts/
│ │ ├── components/
│ │ │ ├── VerticalNav.vue
│ │ │ ├── VerticalNavLayout.vue
│ │ │ ├── VerticalNavLink.vue
│ │ │ └── VerticalNavSectionTitle.vue
│ │ ├── components.ts
│ │ ├── index.ts
│ │ ├── styles/
│ │ │ ├── _classes.scss
│ │ │ ├── _default-layout.scss
│ │ │ ├── _global.scss
│ │ │ ├── _mixins.scss
│ │ │ ├── _placeholders.scss
│ │ │ ├── _rtl.scss
│ │ │ ├── _variables.scss
│ │ │ └── index.scss
│ │ ├── types.d.ts
│ │ └── utils.ts
│ ├── @validators/
│ │ └── index.ts
│ ├── App.vue
│ ├── ace-config.ts
│ ├── api/
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ ├── nprogress.ts
│ │ └── types.ts
│ ├── components/
│ │ ├── FileBrowser.vue
│ │ ├── NoDataFound.vue
│ │ ├── PWAInstallPrompt.vue
│ │ ├── cards/
│ │ │ ├── BackdropCard.vue
│ │ │ ├── CustomRuleCard.vue
│ │ │ ├── DirectoryCard.vue
│ │ │ ├── DownloaderCard.vue
│ │ │ ├── DownloadingCard.vue
│ │ │ ├── FilterRuleCard.vue
│ │ │ ├── FilterRuleGroupCard.vue
│ │ │ ├── LibraryCard.vue
│ │ │ ├── MediaCard.vue
│ │ │ ├── MediaInfoCard.vue
│ │ │ ├── MediaServerCard.vue
│ │ │ ├── MessageCard.vue
│ │ │ ├── NotificationChannelCard.vue
│ │ │ ├── PersonCard.vue
│ │ │ ├── PluginAppCard.vue
│ │ │ ├── PluginCard.vue
│ │ │ ├── PluginFolderCard.vue
│ │ │ ├── PluginMixedSortCard.vue
│ │ │ ├── PosterCard.vue
│ │ │ ├── SiteCard.vue
│ │ │ ├── StorageCard.vue
│ │ │ ├── SubscribeCard.vue
│ │ │ ├── SubscribeShareCard.vue
│ │ │ ├── TorrentCard.vue
│ │ │ ├── TorrentItem.vue
│ │ │ ├── UserCard.vue
│ │ │ ├── WorkflowShareCard.vue
│ │ │ └── WorkflowTaskCard.vue
│ │ ├── dialog/
│ │ │ ├── AboutDialog.vue
│ │ │ ├── AddDownloadDialog.vue
│ │ │ ├── AlistConfigDialog.vue
│ │ │ ├── AliyunAuthDialog.vue
│ │ │ ├── CategoryEditDialog.vue
│ │ │ ├── ForkSubscribeDialog.vue
│ │ │ ├── ForkWorkflowDialog.vue
│ │ │ ├── ImportCodeDialog.vue
│ │ │ ├── MediaInfoDialog.vue
│ │ │ ├── OTPAuthDialog.vue
│ │ │ ├── PasskeyDialog.vue
│ │ │ ├── PluginConfigDialog.vue
│ │ │ ├── PluginDataDialog.vue
│ │ │ ├── PluginMarketSettingDialog.vue
│ │ │ ├── ProgressDialog.vue
│ │ │ ├── RcloneConfigDialog.vue
│ │ │ ├── ReorganizeDialog.vue
│ │ │ ├── SearchBarDialog.vue
│ │ │ ├── SearchSiteDialog.vue
│ │ │ ├── SiteAddEditDialog.vue
│ │ │ ├── SiteCookieUpdateDialog.vue
│ │ │ ├── SiteImportDialog.vue
│ │ │ ├── SiteResourceDialog.vue
│ │ │ ├── SiteStatisticsDialog.vue
│ │ │ ├── SiteUserDataDialog.vue
│ │ │ ├── SmbConfigDialog.vue
│ │ │ ├── SubscribeEditDialog.vue
│ │ │ ├── SubscribeFilesDialog.vue
│ │ │ ├── SubscribeHistoryDialog.vue
│ │ │ ├── SubscribeSeasonDialog.vue
│ │ │ ├── SubscribeShareDialog.vue
│ │ │ ├── SubscribeShareStatisticsDialog.vue
│ │ │ ├── TransferQueueDialog.vue
│ │ │ ├── U115AuthDialog.vue
│ │ │ ├── UserAddEditDialog.vue
│ │ │ ├── UserAuthDialog.vue
│ │ │ ├── WorkflowActionsDialog.vue
│ │ │ ├── WorkflowAddEditDialog.vue
│ │ │ └── WorkflowShareDialog.vue
│ │ ├── field/
│ │ │ ├── CronField.vue
│ │ │ └── PathField.vue
│ │ ├── filebrowser/
│ │ │ ├── FileList.vue
│ │ │ ├── FileNavigator.vue
│ │ │ └── FileToolbar.vue
│ │ ├── filter/
│ │ │ └── TorrentFilterBar.vue
│ │ ├── input/
│ │ │ ├── CronInput.vue
│ │ │ └── PathInput.vue
│ │ ├── misc/
│ │ │ ├── DashboardElement.vue
│ │ │ ├── FilterOption.vue
│ │ │ ├── MediaIdSelector.vue
│ │ │ └── VersionHistory.vue
│ │ ├── render/
│ │ │ ├── DashboardRender.vue
│ │ │ ├── FormRender.vue
│ │ │ └── PageRender.vue
│ │ ├── slide/
│ │ │ ├── SlideView.vue
│ │ │ └── SlideViewTitle.vue
│ │ ├── toast/
│ │ │ └── VersionUpdateToast.vue
│ │ └── workflow/
│ │ ├── AddDownloadAction.vue
│ │ ├── AddSubscribeAction.vue
│ │ ├── FetchDownloadsAction.vue
│ │ ├── FetchMediasAction.vue
│ │ ├── FetchRssAction.vue
│ │ ├── FetchTorrentsAction.vue
│ │ ├── FilterMediasAction.vue
│ │ ├── FilterTorrentsAction.vue
│ │ ├── InvokePluginAction.vue
│ │ ├── NoteAction.vue
│ │ ├── ScanFileAction.vue
│ │ ├── ScrapeFileAction.vue
│ │ ├── SendEventAction.vue
│ │ ├── SendMessageAction.vue
│ │ └── TransferFileAction.vue
│ ├── composables/
│ │ ├── useAvailableHeight.ts
│ │ ├── useBackgroundOptimization.ts
│ │ ├── useCacheManager.ts
│ │ ├── useConfirm.ts
│ │ ├── useDynamicButton.ts
│ │ ├── useDynamicHeaderTab.ts
│ │ ├── useInfiniteScroll.ts
│ │ ├── useOfflineStatus.ts
│ │ ├── usePWA.ts
│ │ ├── usePWAInstall.ts
│ │ ├── usePullDownGesture.ts
│ │ ├── useRecentPlugins.ts
│ │ ├── useSetupWizard.ts
│ │ ├── useStateRestore.ts
│ │ ├── useTorrentFilter.ts
│ │ └── useVersionChecker.ts
│ ├── layouts/
│ │ ├── blank.vue
│ │ ├── components/
│ │ │ ├── DefaultLayout.vue
│ │ │ ├── DropzoneBackground.vue
│ │ │ ├── Footer.vue
│ │ │ ├── HeaderTab.vue
│ │ │ ├── OfflinePage.vue
│ │ │ ├── QuickAccess.vue
│ │ │ ├── SearchBar.vue
│ │ │ ├── ShortcutBar.vue
│ │ │ ├── UserNotification.vue
│ │ │ ├── UserProfile.vue
│ │ │ └── WorkflowSidebar.vue
│ │ └── default.vue
│ ├── locales/
│ │ ├── en-US.ts
│ │ ├── zh-CN.ts
│ │ └── zh-TW.ts
│ ├── main.ts
│ ├── pages/
│ │ ├── [...all].vue
│ │ ├── appcenter.vue
│ │ ├── browse.vue
│ │ ├── calendar.vue
│ │ ├── credits.vue
│ │ ├── dashboard.vue
│ │ ├── discover.vue
│ │ ├── downloading.vue
│ │ ├── filemanager.vue
│ │ ├── history.vue
│ │ ├── login.vue
│ │ ├── media.vue
│ │ ├── person.vue
│ │ ├── plugin-app.vue
│ │ ├── plugin.vue
│ │ ├── profile.vue
│ │ ├── recommend.vue
│ │ ├── resource.vue
│ │ ├── setting.vue
│ │ ├── setup.vue
│ │ ├── site.vue
│ │ ├── subscribe-share.vue
│ │ ├── subscribe.vue
│ │ ├── user.vue
│ │ └── workflow.vue
│ ├── plugins/
│ │ ├── i18n.ts
│ │ ├── stateRestore.ts
│ │ ├── vuetify/
│ │ │ ├── defaults.ts
│ │ │ ├── icons.ts
│ │ │ ├── index.ts
│ │ │ └── theme.ts
│ │ └── webfontloader.ts
│ ├── router/
│ │ ├── i18n-menu.ts
│ │ └── index.ts
│ ├── service-worker.ts
│ ├── stores/
│ │ ├── auth.ts
│ │ ├── global.ts
│ │ ├── index.ts
│ │ ├── pluginSidebarNav.ts
│ │ ├── types.ts
│ │ └── user.ts
│ ├── styles/
│ │ ├── common.scss
│ │ ├── main.scss
│ │ ├── themes/
│ │ │ └── transparent.scss
│ │ └── variables/
│ │ ├── _template.scss
│ │ └── _vuetify.scss
│ ├── types/
│ │ ├── global.d.ts
│ │ ├── i18n.ts
│ │ ├── service-worker-sync.d.ts
│ │ └── workbox-precaching.d.ts
│ ├── utils/
│ │ ├── appDeepLink.ts
│ │ ├── backgroundManager.ts
│ │ ├── badge.ts
│ │ ├── colorUtils.ts
│ │ ├── federationLoader.ts
│ │ ├── globalSetting.ts
│ │ ├── imageUtils.ts
│ │ ├── loadingStateManager.ts
│ │ ├── permission.ts
│ │ ├── pluginSidebarNav.ts
│ │ ├── requestOptimizer.ts
│ │ ├── sseManager.ts
│ │ └── themeManager.ts
│ └── views/
│ ├── dashboard/
│ │ ├── AnalyticsCpu.vue
│ │ ├── AnalyticsMediaStatistic.vue
│ │ ├── AnalyticsMemory.vue
│ │ ├── AnalyticsNetwork.vue
│ │ ├── AnalyticsProcesses.vue
│ │ ├── AnalyticsScheduler.vue
│ │ ├── AnalyticsSpeed.vue
│ │ ├── AnalyticsStorage.vue
│ │ ├── AnalyticsWeeklyOverview.vue
│ │ ├── MediaServerLatest.vue
│ │ ├── MediaServerLibrary.vue
│ │ └── MediaServerPlaying.vue
│ ├── discover/
│ │ ├── BangumiView.vue
│ │ ├── DoubanView.vue
│ │ ├── ExtraSourceView.vue
│ │ ├── MediaCardListView.vue
│ │ ├── MediaCardSlideView.vue
│ │ ├── MediaDetailView.vue
│ │ ├── PersonCardListView.vue
│ │ ├── PersonCardSlideView.vue
│ │ ├── PersonDetailView.vue
│ │ └── TheMovieDbView.vue
│ ├── plugin/
│ │ └── PluginCardListView.vue
│ ├── reorganize/
│ │ ├── DownloadingListView.vue
│ │ ├── FileBrowserView.vue
│ │ └── TransferHistoryView.vue
│ ├── setting/
│ │ ├── AccountSettingDirectory.vue
│ │ ├── AccountSettingNotification.vue
│ │ ├── AccountSettingRule.vue
│ │ ├── AccountSettingSearch.vue
│ │ ├── AccountSettingSite.vue
│ │ ├── AccountSettingSubscribe.vue
│ │ └── AccountSettingSystem.vue
│ ├── setup/
│ │ ├── AgentSettingsStep.vue
│ │ ├── BasicSettingsStep.vue
│ │ ├── ConnectivityTest.vue
│ │ ├── DownloaderSettingsStep.vue
│ │ ├── MediaServerSettingsStep.vue
│ │ ├── NotificationSettingsStep.vue
│ │ ├── PreferencesSettingsStep.vue
│ │ ├── SiteAuthSettingsStep.vue
│ │ └── StorageSettingsStep.vue
│ ├── site/
│ │ └── SiteCardListView.vue
│ ├── subscribe/
│ │ ├── FullCalendarView.vue
│ │ ├── SubscribeListView.vue
│ │ ├── SubscribePopularView.vue
│ │ └── SubscribeShareView.vue
│ ├── system/
│ │ ├── CacheView.vue
│ │ ├── LoggingView.vue
│ │ ├── MessageView.vue
│ │ ├── ModuleTestView.vue
│ │ ├── NameTestView.vue
│ │ ├── NetTestView.vue
│ │ ├── RuleTestView.vue
│ │ ├── ServiceView.vue
│ │ └── WordsView.vue
│ ├── user/
│ │ ├── UserListView.vue
│ │ └── UserProfileView.vue
│ └── workflow/
│ ├── WorkflowListView.vue
│ └── WorkflowShareView.vue
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# 2 space indentation
[*.{vue,scss,ts}]
indent_style = space
indent_size = 2
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
================================================
FILE: .eslintrc.js
================================================
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['@antfu/eslint-config-vue', 'plugin:sonarjs/recommended'],
ignorePatterns: ['src/@iconify/*.js', 'node_modules', 'dist', '*.d.ts'],
plugins: ['regex'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'sonarjs/no-duplicate-string': 'warn',
'vue/valid-v-slot': ['error', {
allowModifiers: true,
}],
// https://github.com/gmullerb/eslint-plugin-regex
'regex/invalid': [
'error',
[
{
regex: '@/assets/images',
replacement: '@images',
message: 'Use \'@images\' path alias for image imports',
},
{
regex: '@/styles',
replacement: '@styles',
message: 'Use \'@styles\' path alias for importing styles from \'src/styles\'',
},
// {
// id: 'Disallow icon of icon library',
// regex: 'tabler-\\w',
// message: 'Only \'mdi\' icons are allowed',
// },
{
regex: '@core/\\w',
message: 'You can\'t use @core when you are in @layouts module',
files: {
inspect: '@layouts/.*',
},
},
{
regex: 'useLayouts\\(',
message:
'`useLayouts` composable is only allowed in @layouts & @core directory. Please use `useThemeConfig` composable instead.',
files: {
inspect: '^(?!.*(@core|@layouts)).*',
},
},
],
// Ignore files
'.eslintrc.js',
],
},
settings: {
'import/resolver': {
node: {
extensions: ['.ts', '.js', '.tsx', '.jsx', '.mjs', '.png', '.jpg'],
},
typescript: {},
},
},
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 问题反馈
description: File a bug report
title: "[错误报告]:请在此处简单描述你的问题"
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
请确认以下信息:
1. 请按此模板提交issues,不按模板提交的问题将直接关闭。
2. 如果你的问题可以直接在以往 issue 或者 Telegram频道 中找到,那么你的 issue 将会被直接关闭。
3. 提交问题务必描述清楚、附上日志,描述不清导致无法理解和分析的问题会被直接关闭。
4. 此仓库为前端仓库,如果是后端问题请在[后端仓库](https://github.com/jxxghp/MoviePilot)提 issue。
- type: checkboxes
id: ensure
attributes:
label: 确认
description: 在提交 issue 之前,请确认你已经阅读并确认以下内容
options:
- label: 我的版本是最新版本,我的版本号与 [version](https://github.com/jxxghp/MoviePilot-Frontend/releases/latest) 相同。
required: true
- label: 我已经 [issue](https://github.com/jxxghp/MoviePilot-Frontend/issues) 中搜索过,确认我的问题没有被提出过。
required: true
- label: 我已经 [Telegram频道](https://t.me/moviepilot_channel) 中搜索过,确认我的问题没有被提出过。
required: true
- label: 我已经修改标题,将标题中的 描述 替换为我遇到的问题。
required: true
- type: input
id: version
attributes:
label: 当前程序版本
description: 遇到问题时程序所在的版本号
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 问题描述
description: 请详细描述你碰到的问题
placeholder: "问题描述"
validations:
required: true
- type: textarea
id: logs
attributes:
label: 发生问题时系统日志和配置文件
description: 问题出现时,程序运行日志请复制到这里。
render: bash
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Telegram 频道
url: https://t.me/moviepilot_channel
about: 更新日志
- name: Telegram 交流群
url: https://t.me/moviepilot_official
about: 交流互助
================================================
FILE: .github/ISSUE_TEMPLATE/discussion.yml
================================================
name: 项目讨论
description: discussion
title: "[Discussion]: "
labels: ["discussion"]
body:
- type: markdown
attributes:
value: |
[BUG](https://github.com/jxxghp/MoviePilot-Frontend/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D%3A) 与 [Feature Request](https://github.com/jxxghp/MoviePilot-Frontend/issues/new?assignees=&labels=feature+request&template=feature_request.yml&title=%5BFeature+Request%5D%3A+) 请转到对应位置提交。
- type: textarea
id: discussion
attributes:
label: 项目讨论
description: 请详细描述需要讨论的内容。
placeholder: "项目讨论"
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 功能改进
description: Feature Request
title: "[Feature Request]: "
labels: ["feature request"]
body:
- type: markdown
attributes:
value: |
请说明你希望添加的功能。
- type: input
id: version
attributes:
label: 当前程序版本
description: 目前使用的程序版本
validations:
required: true
- type: textarea
id: feature-request
attributes:
label: 功能改进
description: 请详细描述需要改进或者添加的功能。
placeholder: "功能改进"
validations:
required: true
- type: textarea
id: references
attributes:
label: 参考资料
description: 可以列举一些参考资料,但是不要引用同类但商业化软件的任何内容。
placeholder: "参考资料"
================================================
FILE: .github/ISSUE_TEMPLATE/rfc.yml
================================================
name: 功能提案
description: Request for Comments
title: '[RFC]'
labels: ['RFC']
body:
- type: markdown
attributes:
value: |
一份提案(RFC)定位为 **「在某功能/重构的具体开发前,用于开发者间 review 技术设计/方案的文档」**,
目的是让协作的开发者间清晰的知道「要做什么」和「具体会怎么做」,以及所有的开发者都能公开透明的参与讨论;
以便评估和讨论产生的影响 (遗漏的考虑、向后兼容性、与现有功能的冲突),
因此提案侧重在对解决问题的 **方案、设计、步骤** 的描述上。
如果仅希望讨论是否添加或改进某功能本身,请使用 -> [Issue: 功能改进](https://github.com/jxxghp/MoviePilot/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml&title=%5BFeature+Request%5D%3A+)
- type: textarea
id: background
attributes:
label: 背景 or 问题
description: 简单描述遇到的什么问题或需要改动什么。可以引用其他 issue、讨论、文档等。
validations:
required: true
- type: textarea
id: goal
attributes:
label: '目标 & 方案简述'
description: 简单描述提案此提案实现后,**预期的目标效果**,以及简单大致描述会采取的方案/步骤,可能会/不会产生什么影响。
validations:
required: true
- type: textarea
id: design
attributes:
label: '方案设计 & 实现步骤'
description: |
详细描述你设计的具体方案,可以考虑拆分列表或要点,一步步描述具体打算如何实现的步骤和相关细节。
这部份不需要一次性写完整,即使在创建完此提案 issue 后,依旧可以再次编辑修改。
validations:
required: false
- type: textarea
id: alternative
attributes:
label: '替代方案 & 对比'
description: |
[可选] 为来实现目标效果,还考虑过什么其他方案,有什么对比?
validations:
required: false
================================================
FILE: .github/workflows/build.yml
================================================
name: Build Moviepilot-Frontend v2
on:
workflow_dispatch:
push:
branches:
- v2
paths:
- 'package.json'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Release version
id: release_version
run: |
frontend_version=$(jq -r '.version' package.json)
echo "frontend_version=v$frontend_version" >> $GITHUB_ENV
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'yarn'
- name: Download Icons
run: |
pwd
curl -sL "https://github.com/jxxghp/MoviePilot-Plugins/archive/refs/heads/main.zip" | busybox unzip -d /tmp -
mv /tmp/MoviePilot-Plugins-main/icons public/plugin_icon
rm -rf /tmp/MoviePilot-Plugins-main
- name: Build frontend
id: build_frontend
run: |
yarn
yarn build
echo "$frontend_version" > dist/version.txt
zip -r dist.zip dist
- 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
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.frontend_version }}
name: ${{ env.frontend_version }}
draft: false
prerelease: false
make_latest: true
files: |
dist.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
dev-dist
*.local
package-lock.json
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/*.code-snippets
!.vscode/tours
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.yarn
# iconify dist files
src/@iconify/*.js
public/plugin_icon/**
================================================
FILE: .prettierignore
================================================
dist
node_modules
================================================
FILE: .prettierrc.json
================================================
{
"arrowParens": "avoid",
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "preserve",
"requirePragma": false,
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"endOfLine": "lf",
"singleAttributePerLine": false
}
================================================
FILE: .stylelintrc.json
================================================
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-idiomatic-order"
],
"plugins": [
"stylelint-use-logical-spec"
],
"overrides": [
{
"files": [
"**/*.scss"
],
"customSyntax": "postcss-scss"
},
{
"files": [
"**/*.vue"
],
"customSyntax": "postcss-html"
}
],
"rules": {
"liberty/use-logical-spec": true,
"selector-class-pattern": null,
"color-function-notation": null
},
"fix": true
}
================================================
FILE: .vscode/anchor-comments.code-snippets
================================================
{
"Add hand emoji": {
"prefix": "cm-hand-emoji",
"body": [
"👉"
],
"description": "Add hand emoji"
},
"Add info emoji": {
"prefix": "cm-info-emoji",
"body": [
"ℹ️"
],
"description": "Add info emoji"
},
"Add warning emoji": {
"prefix": "cm-warning-emoji",
"body": [
"❗"
],
"description": "Add warning emoji"
}
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mgmcdermott.vscode-language-babel",
"editorconfig.editorconfig",
"xabikos.javascriptsnippets",
"stylelint.vscode-stylelint",
"fabiospampinato.vscode-highlight",
"github.vscode-pull-request-github",
"vue.volar",
"antfu.iconify",
"cipchk.cssrem",
"matijao.vue-nuxt-snippets"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.formatOnSave": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.eol": "\n",
"[javascript]": {
"editor.formatOnSave": false
},
"[markdown]": {
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
},
// SCSS
"[scss]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint",
"editor.formatOnSave": false
},
// JSON
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
// Vue
"[vue]": {
"editor.formatOnSave": true
},
// Extension: Volar
"volar.preview.port": 3000,
"volar.completion.preferredTagNameCase": "pascal",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
// Extension: Stylelint
"stylelint.packageManager": "yarn",
"stylelint.validate": [
"css",
"scss",
"vue"
],
// Extension: Spell Checker
"cSpell.words": [
"Composables",
"Customizer",
"flagpack",
"Iconify",
"psudo",
"stylelint",
"touchless",
"triggerer",
"unref",
"vuetify"
],
// Extension: Comment Anchors
"commentAnchors.tags.list": [
{
"tag": "ℹ️",
"scope": "hidden",
// This color is taken from "Better Comments" Extension (?)
"highlightColor": "#3498DB",
"styleComment": true,
"isItalic": false
},
{
"tag": "👉",
"scope": "file",
// This color is taken from "Better Comments" Extension (*)
"highlightColor": "#98C379",
"styleComment": true,
"isItalic": false
},
{
"tag": "❗",
"scope": "hidden",
// This color is taken from "Better Comments" Extension (*)
"highlightColor": "#FF2D00",
"styleComment": true,
"isItalic": false
}
],
// Extension: fabiospampinato.vscode-highlight
"highlight.regexFlags": "gi",
"highlight.regexes": {
// We flaged this for enforcing logical CSS properties
"(100vh|translate|margin:|padding:|margin-left|margin-right|rotate|text-align|border-top|border-right|border-bottom|border-left|float|background-position|transform|width|height|top|left|bottom|right|float|clear|(p|m)(l|r)-|border-(start|end)-(start|end)-radius)": [
{
// "rangeBehavior": 1,
"borderWidth": "1px",
"borderColor": "tomato",
"borderStyle": "solid"
}
],
"(overflow-x:|overflow-y:)": [
{
// "rangeBehavior": 1,
"borderWidth": "1px",
"borderColor": "green",
"borderStyle": "solid"
}
]
},
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
"i18n-ally.localesPaths": [
"src/locales"
]
}
================================================
FILE: .vscode/vue-ts.code-snippets
================================================
{
"Vue TS - DefineProps": {
"prefix": "dprops",
"body": [
"defineProps<${1:Props}>()"
],
"description": "DefineProps in script setup"
},
"Vue TS - Props interface": {
"prefix": "iprops",
"body": [
"interface Props {",
" ${1}",
"}"
],
"description": "Create props interface in script setup"
}
}
================================================
FILE: .vscode/vue.code-snippets
================================================
{
"script": {
"prefix": "vue-sfc-ts",
"body": [
"<script lang=\"ts\" setup>",
"",
"</script>",
"",
"<template>",
" ",
"</template>",
"",
"<style lang=\"scss\">",
"",
"</style>",
""
],
"description": "Vue SFC Typescript"
},
"template": {
"scope": "vue",
"prefix": "template",
"body": [
"<template>",
" $1",
"</template>"
],
"description": "Create <template> block"
},
"Script setup + TS": {
"prefix": "script-setup-ts",
"body": [
"<script setup lang=\"ts\">",
"${1}",
"</script>"
],
"description": "Script setup + TS"
},
"style": {
"scope": "vue",
"prefix": "style",
"body": [
"<style lang=\"scss\">",
"$1",
"</style>"
],
"description": "Create <style> block"
},
"use composable": {
"prefix": "use-composable",
"body": [
"const { $2 } = ${1:useComposable}()"
],
"description": "We frequently uses composable in our components and writing const {} = useModule() is tedious. This snippet helps you to write it quickly."
},
"template interpolation": {
"prefix": "cc",
"body": [
"{{ ${1} }}"
],
"description": "We are just making writing template interpolation easier."
}
}
================================================
FILE: .vscode/vuetify.code-snippets
================================================
{
"Vuetify Menu -- Parent Activator": {
"prefix": "v-menu",
"body": [
"<v-btn color=\"primary\">",
" Activator",
" <v-menu activator=\"parent\">",
" <v-list>",
" <v-list-item",
" v-for=\"(item, index) in ['apple', 'banana', 'cherry']\"",
" :key=\"index\"",
" :value=\"index\"",
" >",
" <v-list-item-title>{{ item }}</v-list-item-title>",
" </v-list-item>",
" </v-list>",
" </v-menu>",
"</v-btn>"
],
"description": "We use menu component with parent activator mostly because it is compact and easy to understand."
},
"Vuetify CSS variable": {
"prefix": "v-css-var",
"body": [
"rgb(var(--v-${1:theme}))"
],
"description": "Vuetify CSS variable"
},
"Icon only button": {
"prefix": "IconBtn",
"body": [
"<IconBtn>",
" <VIcon icon=\"mdi-${1}\" />",
"</IconBtn>"
],
"description": "Icon only button"
},
"Radio Group": {
"prefix": "v-radio-grp",
"body": [
"<v-radio-group v-model=\"${1:modelValue}\">",
" <v-radio",
" v-for=\"item in ['apple', 'banana', 'cherry']\"",
" :key=\"item\"",
" :label=\"item\"",
" :value=\"item\"",
" />",
"</v-radio-group>"
],
"description": "Radio Group"
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 jxxghp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# MoviePilot-Frontend
*中文 | [English](README_EN.md)*
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目,NodeJS版本:>= `v20.12.1`。
## 特性
- 基于 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 配置参考](https://vitejs.dev/config/).
### 依赖安装
```sh
yarn
```
### 开发运行
```sh
yarn dev
```
### 编译打包
```sh
yarn build
```
### 静态运行
1. 使用 `nginx` 等Web服务器托管 `dist` 静态文件,nginx配置参考 `public/nginx.conf`。
2. 使用 `node` 命令直接运行`service.js`,默认监听 `3000` 端口,设置环境变量 `NGINX_PORT` 来调整运行端口。
```shell
node dist/service.js
```
================================================
FILE: README_EN.md
================================================
# MoviePilot-Frontend
*[中文](README.md) | English*
Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS version required: >= `v20.12.1`.
## 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).
### Configure Vite
See [Vite Configuration Reference](https://vitejs.dev/config/).
### Install Dependencies
```sh
yarn
```
### Development Server
```sh
yarn dev
```
### Build for Production
```sh
yarn build
```
### Static Deployment
1. Host the `dist` static files using a web server like `nginx`. Refer to `public/nginx.conf` for nginx configuration.
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
```
================================================
FILE: auto-imports.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const logicAnd: typeof import('@vueuse/math')['logicAnd']
const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useAbs: typeof import('@vueuse/math')['useAbs']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useAverage: typeof import('@vueuse/math')['useAverage']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useCeil: typeof import('@vueuse/math')['useCeil']
const useClamp: typeof import('@vueuse/math')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFloor: typeof import('@vueuse/math')['useFloor']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMath: typeof import('@vueuse/math')['useMath']
const useMax: typeof import('@vueuse/math')['useMax']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMin: typeof import('@vueuse/math')['useMin']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePrecision: typeof import('@vueuse/math')['usePrecision']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProjection: typeof import('@vueuse/math')['useProjection']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRound: typeof import('@vueuse/math')['useRound']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSum: typeof import('@vueuse/math')['useSum']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useTrunc: typeof import('@vueuse/math')['useTrunc']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly logicAnd: UnwrapRef<typeof import('@vueuse/math')['logicAnd']>
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAverage: UnwrapRef<typeof import('@vueuse/math')['useAverage']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFloor: UnwrapRef<typeof import('@vueuse/math')['useFloor']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMath: UnwrapRef<typeof import('@vueuse/math')['useMath']>
readonly useMax: UnwrapRef<typeof import('@vueuse/math')['useMax']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePrecision: UnwrapRef<typeof import('@vueuse/math')['usePrecision']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useTrunc: UnwrapRef<typeof import('@vueuse/math')['useTrunc']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}
================================================
FILE: components.d.ts
================================================
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
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']
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']
}
}
================================================
FILE: docs/federation-troubleshooting.md
================================================
# 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)
================================================
FILE: docs/module-federation-guide.md
================================================
# 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. 核心概念
每个 Vue 联邦插件需要提供下列标准组件(`AppPage` 为可选,用于主界面侧栏全页入口):
| 组件名称 | 暴露名 | 文件名 | 用途 |
|---------|--------|--------|------|
| Page | `./Page` | Page.vue | 插件管理中的详情弹窗 |
| Config | `./Config` | Config.vue | 插件配置页面 |
| Dashboard | `./Dashboard` | Dashboard.vue | 仪表盘小组件 |
| AppPage | `./AppPage` | AppPage.vue | 主界面侧栏独立全页(主内容区由插件完全绘制) |
| (可选) | `./AppPage{Xxx}` | 如 AppPageSettings.vue | 多 `nav_key` 时按名优先加载,见下文「多界面」 |
主应用在侧栏全页路由中按 `nav_key` 解析暴露名(如 `AppPageSettings`),再回退 `AppPage` → `Page`;`nav_key` 为 `main` 时仅尝试 `AppPage` → `Page`。
## 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',
'./AppPage': './src/components/AppPage.vue',
'./AppPageSettings': './src/components/AppPageSettings.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-hover>
<!-- 仪表板内容 -->
<template #default="{ isHovering, props: hoverProps }">
<v-card v-bind="hoverProps">
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
<v-card-text>
<!-- 组件内容 -->
</v-card-text>
<!-- 只在悬停时显示拖拽图标 -->
<div v-show="isHovering" class="absolute right-5 top-5">
<v-icon class="cursor-move">mdi-drag</v-icon>
</div>
</v-card>
</template>
</v-hover>
</div>
</template>
```
### 5.4 AppPage 组件(侧栏全页)
用于主应用左侧导航中的独立页面(路由 `#/plugin-app/:pluginId/:navKey?`),占据默认布局下的主内容区;与 `Page` 不同,不嵌在插件管理弹窗中。
主应用传入的 props:
| 属性 | 说明 |
|------|------|
| `api` | 与 `Page` 相同,用于 `bear` 认证的插件 HTTP 调用 |
| `navKey` | 与侧栏声明的 `nav_key` 一致,同一插件多入口时用于区分 |
| `pluginId` | 当前插件 ID |
```vue
<script setup lang="ts">
const props = defineProps({
api: { type: Object, default: () => ({}) },
navKey: { type: String, default: 'main' },
pluginId: { type: String, default: '' },
})
const emit = defineEmits(['action'])
</script>
<template>
<div class="pa-4">
<div class="text-h6 mb-2">侧栏全页示例({{ pluginId }} / {{ navKey }})</div>
<v-btn size="small" @click="emit('action')">通知主应用</v-btn>
</div>
</template>
```
#### 后端:注册侧栏入口
插件需为 **Vue** 渲染模式(`get_render_mode` 返回 `vue`),并实现 `get_sidebar_nav`,返回列表项字段与主应用 `GET /api/v1/plugin/sidebar_nav` 一致:
| 字段 | 说明 |
|------|------|
| `nav_key` | URL 路径段,唯一标识本入口(同一插件可多入口) |
| `title` | 侧栏显示标题 |
| `icon` | MDI 图标名,如 `mdi-rss` |
| `section` | 分组:`start` / `discovery` / `subscribe` / `organize` / `system` |
| `permission` | 可选:`subscribe` / `discovery` / `search` / `manage` / `admin`,与主应用菜单权限一致 |
| `order` | 可选:同组内排序,数值越小越靠前 |
```python
def get_sidebar_nav(self) -> List[Dict[str, Any]]:
return [
{
"nav_key": "main",
"title": "示例订阅页",
"icon": "mdi-rss",
"section": "subscribe",
"permission": "subscribe",
"order": 10,
}
]
```
#### 同一插件多个全页界面(多 `nav_key`)
在 `get_sidebar_nav` 中**返回多条**记录,每条使用不同的 `nav_key` / `title` / `section` 等,侧栏与「更多」中会出现多个入口,路由形如 `#/plugin-app/<插件ID>/<nav_key>`。
前端加载远程组件的顺序为:
| `nav_key` | 依次尝试的联邦暴露名 |
|-----------|----------------------|
| `main` 或省略 | `./AppPage` → `./Page` |
| 其它(如 `settings`、`my_tool`) | `./AppPage{PascalCase}` → `./AppPage` → `./Page` |
`PascalCase` 规则:按 `-`、`_`、空格分段后首字母大写并拼接。例如 `nav_key=settings` → 先试 `./AppPageSettings`;`my_tool` → `./AppPageMyTool`。
**两种实现方式(二选一或混用):**
1. **单文件分支**:只暴露 `./AppPage`,在组件内根据 `navKey` prop 用 `v-if` / `<component>` 切换子界面。
2. **多文件**:为某个入口单独暴露 `./AppPageSettings.vue` 等,主应用会优先加载对应模块,失败再回退到 `AppPage`。
`vite.config` 多暴露示例:
```typescript
exposes: {
'./AppPage': './src/components/AppPage.vue',
'./AppPageSettings': './src/components/AppPageSettings.vue',
// ...
}
```
## 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。
================================================
FILE: env.d.ts
================================================
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
action?: string
subject?: string
layoutWrapperClasses?: string
navActiveLink?: RouteLocationRaw
}
}
// 支持动态导入远程模块
declare module '*' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
================================================
FILE: examples/plugin-component/README.md
================================================
# MoviePilot 插件远程组件示例
这是 MoviePilot 插件远程组件的示例项目,展示了如何正确配置和开发与主应用兼容的远程组件。本示例包含 Page、Config、Dashboard、AppPage,以及可选的 `AppPageSettings`(`nav_key=settings` 时由主应用优先加载,用于演示「一插件多全页界面」)。
## 1. 开发环境准备
### 安装依赖
```bash
npm install
# 或
yarn
```
### 开发模式运行
```bash
npm run dev
# 或
yarn dev
```
## 2. 项目结构
```
plugin-component/
├── src/
│ ├── components/
│ │ ├── Page.vue # 插件详情页面组件
│ │ ├── Config.vue # 插件配置页面组件
│ │ ├── Dashboard.vue # 插件仪表板组件
│ │ ├── AppPage.vue # 侧栏全页(主内容区,nav_key=main)
│ │ └── AppPageSettings.vue # 可选第二全页(nav_key=settings)
│ ├── App.vue # 本地开发入口组件
│ └── main.js # 本地开发入口文件
├── vite.config.js # Vite和模块联邦配置
├── index.html # 本地开发HTML入口
└── package.json # 依赖配置
```
## 3. 开发指引
- [模块联邦开发指南](../../docs/module-federation-guide.md)
- [模块联邦问题排查指南](../../docs/federation-troubleshooting.md)。
================================================
FILE: examples/plugin-component/index.html
================================================
<!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>
================================================
FILE: examples/plugin-component/package.json
================================================
{
"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"
}
}
================================================
FILE: examples/plugin-component/src/App.vue
================================================
<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>
================================================
FILE: examples/plugin-component/src/components/AppPage.vue
================================================
<script setup lang="ts">
/**
* 侧栏全页:在主应用 #/plugin-app/:pluginId/:navKey 中渲染,占据主内容区。
* 需在插件后端实现 get_sidebar_nav 才会出现在侧栏。
*/
const props = defineProps({
api: {
type: Object,
default: () => ({}),
},
navKey: {
type: String,
default: 'main',
},
pluginId: {
type: String,
default: '',
},
})
const emit = defineEmits(['action'])
</script>
<template>
<div class="plugin-app-page pa-4">
<div class="text-h6 mb-2">AppPage(侧栏全页)</div>
<div class="text-body-2 text-medium-emphasis mb-4">
pluginId: {{ pluginId }} · navKey: {{ navKey }}
</div>
<v-btn size="small" variant="tonal" @click="emit('action')">action</v-btn>
</div>
</template>
================================================
FILE: examples/plugin-component/src/components/AppPageSettings.vue
================================================
<script setup lang="ts">
/**
* 示例:nav_key=settings 时主应用会优先加载 AppPageSettings,再回退 AppPage。
*/
const props = defineProps({
api: { type: Object, default: () => ({}) },
navKey: { type: String, default: 'settings' },
pluginId: { type: String, default: '' },
})
</script>
<template>
<div class="pa-4">
<div class="text-subtitle-1">Settings 子界面(AppPageSettings)</div>
<div class="text-caption text-medium-emphasis">navKey={{ navKey }} · pluginId={{ pluginId }}</div>
</div>
</template>
================================================
FILE: examples/plugin-component/src/components/Config.vue
================================================
<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>
================================================
FILE: examples/plugin-component/src/components/Dashboard.vue
================================================
<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>
================================================
FILE: examples/plugin-component/src/components/Page.vue
================================================
<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>
================================================
FILE: examples/plugin-component/src/main.js
================================================
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')
================================================
FILE: examples/plugin-component/src/vuetify/defaults.ts
================================================
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',
},
}
================================================
FILE: examples/plugin-component/src/vuetify/theme.ts
================================================
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
================================================
FILE: examples/plugin-component/vite.config.js
================================================
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',
'./AppPage': './src/components/AppPage.vue',
'./AppPageSettings': './src/components/AppPageSettings.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'
},
})
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="zh-CN" style="
overflow: hidden auto;
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>
<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, interactive-widget=resizes-content" />
<!-- 防止缩放和选择,提供原生应用体验 -->
<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="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-icon-precomposed" href="/apple-touch-icon.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" />
<!-- 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" />
<!-- 缓存控制 -->
<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 />
<style>
#app {
min-block-size: 100%;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
#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;
}
.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);
}
}
/* 超时通知样式 */
#loading-timeout {
position: absolute;
z-index: 2500;
display: none;
inset-block-end: 20px;
inset-inline-start: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 12px 24px;
border-radius: 12px;
font-size: 14px;
font-family: sans-serif;
text-align: center;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
white-space: nowrap;
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
#timeout-btn {
color: var(--initial-loader-color, #9155FD);
text-decoration: none;
font-weight: bold;
margin-inline-start: 8px;
border-bottom: 1px solid var(--initial-loader-color, #9155FD);
}
</style>
<script>
// 检测系统主题是否为深色模式
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') || 'auto'
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)
// 清除缓存处理逻辑
window.clearAndReload = async function() {
try {
// 1. 清除所有缓存
if ('caches' in window) {
const cacheNames = await caches.keys()
await Promise.all(cacheNames.map(name => caches.delete(name)))
console.log('[VersionChecker] 已清除所有缓存')
}
// 2. 注销 Service Worker
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations()
await Promise.all(registrations.map(registration => registration.unregister()))
console.log('[VersionChecker] 已注销所有 Service Worker')
}
} catch (e) {
console.error('[VersionChecker] 清除缓存时出错:', e)
} finally {
// 3. 重载页面
const url = new URL(window.location.href)
url.searchParams.set('_t', Date.now().toString())
window.location.replace(url.pathname + url.search + url.hash)
}
};
setTimeout(function() {
const timeoutEl = document.getElementById('loading-timeout');
if (timeoutEl) {
// 适配多语言
const lang = navigator.language || 'zh-CN';
const messages = {
'zh-CN': {
text: '页面加载似乎遇到了阻碍,请尝试',
btn: '清除缓存'
},
'zh-TW': {
text: '頁面載入似乎遇到了阻礙,請嘗試',
btn: '清除快取'
},
'en-US': {
text: 'Page loading seems to be blocked, please try',
btn: 'Clear Cache'
}
};
// 默认匹配前缀,如 en-GB 匹配 en-US 的逻辑
let msg = messages['zh-CN'];
if (lang.startsWith('zh-TW') || lang.startsWith('zh-HK')) {
msg = messages['zh-TW'];
} else if (lang.startsWith('en')) {
msg = messages['en-US'];
}
const textNode = document.createTextNode(msg.text + ' ');
const btnLink = document.createElement('a');
btnLink.href = 'javascript:void(0)';
btnLink.id = 'timeout-btn';
btnLink.onclick = window.clearAndReload;
btnLink.textContent = msg.btn;
timeoutEl.innerHTML = '';
timeoutEl.appendChild(textNode);
timeoutEl.appendChild(btnLink);
timeoutEl.style.display = 'block';
}
}, 15000); // 15秒后显示超时提示
</script>
</head>
<body style="margin: 0; overflow: hidden; overscroll-behavior: none; -webkit-overflow-scrolling: touch">
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
<img src="/logo.svg" alt="MoviePilot" width="160px" height="160px" />
</div>
<div class="loading">
<div class="effect-1 effects"></div>
<div class="effect-2 effects"></div>
<div class="effect-3 effects"></div>
</div>
<!-- 超时提示 - 默认隐藏 -->
<div id="loading-timeout"></div>
</div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "moviepilot",
"version": "2.10.5",
"private": true,
"type": "module",
"bin": "dist/service.js",
"scripts": {
"dev": "vite --host",
"prebuild": "npm run build:icons",
"build": "vite build",
"preview": "vite preview --port 5050",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . -c .eslintrc.js --fix --ext .ts,.js,.vue,.tsx,.jsx",
"build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js",
"postinstall": "npm run build:icons",
"pkg": "pkg . -t node18-win-x64 -o MoviePilot-Frontend.exe"
},
"pkg": {
"assets": [
"dist/**/*"
]
},
"dependencies": {
"@fullcalendar/core": "^6.1.15",
"@fullcalendar/daygrid": "^6.1.15",
"@fullcalendar/interaction": "^6.1.15",
"@fullcalendar/list": "^6.1.15",
"@fullcalendar/timegrid": "^6.1.15",
"@fullcalendar/vue3": "^6.1.15",
"@iconify/utils": "^2.2.1",
"@types/crypto-js": "^4.2.2",
"@types/js-cookie": "^3.0.6",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.42.1",
"@vue-flow/minimap": "^1.5.2",
"@vue-flow/node-resizer": "^1.4.0",
"@vue-flow/node-toolbar": "^1.1.0",
"@vue-js-cron/vuetify": "^5.0.9",
"@vueuse/core": "^12.4.0",
"@vueuse/math": "^12.4.0",
"ace-builds": "^1.37.4",
"apexcharts": "^4.0.0",
"axios": "^1.7.9",
"body-scroll-lock": "^3.1.5",
"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",
"markdown-it": "^14.1.0",
"markdown-it-link-attributes": "^4.0.1",
"mousetrap": "^1.6.5",
"nprogress": "^0.2.0",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"qrcode": "^1.5.4",
"sass": "^1.83.4",
"tailwindcss": "^ 3.4.17",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"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",
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@iconify-json/line-md": "^1.2.13",
"@iconify-json/lucide": "^1.2.85",
"@iconify-json/material-symbols": "^1.2.51",
"@iconify-json/mdi": "^1.1.52",
"@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/body-scroll-lock": "^3.1.2",
"@types/lodash-es": "^4.17.12",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-link-attributes": "^3.0.5",
"@types/mousetrap": "^1.6.15",
"@types/node": "^20.1.4",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.6",
"@types/webfontloader": "^1.6.34",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"autoprefixer": "^10.4.14",
"eslint": "^9.18.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-regex": "^1.10.0",
"eslint-plugin-sonarjs": "^3.0.1",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-vue": "^9.12.0",
"postcss": "^8.5.1",
"postcss-html": "^1.5.0",
"stylelint": "^16.13.2",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-use-logical-spec": "5.0.1",
"terser": "^5.36.0",
"type-fest": "^4.15.0",
"typescript": "^5.0.4",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"unplugin-vue-define-options": "^1.5.3",
"vite": "^5.4.11",
"vite-plugin-pages": "^0.32.1",
"vite-plugin-pwa": "^0.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",
"vue-tsc": "^2.0.10",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0"
},
"packageManager": "yarn@1.22.18"
}
================================================
FILE: postcss.config.js
================================================
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: public/nginx.conf
================================================
worker_processes auto;
events {
worker_connections 1024;
}
http {
sendfile on;
keepalive_timeout 3600;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_proxied any;
gzip_min_length 256;
gzip_vary on;
gzip_comp_level 6;
server {
include mime.types;
default_type application/octet-stream;
listen 3000;
listen [::]:3000;
server_name moviepilot;
location / {
# 主目录
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
root html;
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
# 静态资源
expires 1y;
add_header Cache-Control "public, immutable";
root html;
}
location /assets {
# 静态资源
expires 1y;
add_header Cache-Control "public";
root html;
}
location ~ ^/api/v1/system/(message|progress/) {
# SSE MIME类型设置
default_type text/event-stream;
# 禁用缓存
add_header Cache-Control no-cache;
add_header X-Accel-Buffering no;
proxy_buffering off;
proxy_cache off;
# 代理设置
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 超时设置
proxy_read_timeout 3600s;
}
location /api {
# 后端API
proxy_pass http://backend_api;
rewrite ^.+mock-server/?(.*)$ /$1 break;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_redirect off;
proxy_set_header Connection "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
# 超时设置
proxy_read_timeout 600s;
}
location /cookiecloud {
# 后端cookiecloud地址
proxy_pass http://backend_api;
rewrite ^.+mock-server/?(.*)$ /$1 break;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_redirect off;
proxy_set_header Connection "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
# 超时设置
proxy_read_timeout 600s;
}
}
upstream backend_api {
# 后端API的地址和端口
server 127.0.0.1:3001;
# 可以添加更多后端服务器作为负载均衡
}
}
================================================
FILE: public/offline.html
================================================
<!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>
================================================
FILE: public/robots.txt
================================================
User-agent: *
Disallow: /
================================================
FILE: public/service.js
================================================
const path = require('node:path')
const express = require('express')
const proxy = require('express-http-proxy')
const app = express()
const port = process.env.NGINX_PORT || 3000
// 后端 API 地址
const proxyConfig = {
URL: '127.0.0.1',
PORT: process.env.PORT || 3001
}
// 静态文件服务目录
app.use(express.static(__dirname))
// 配置代理中间件将请求转发给后端API
app.use(
'/api',
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /api 前缀
proxyReqPathResolver: (req) => {
return `/api${req.url}`
}
})
);
// 配置代理中间件将CookieCloud请求转发给后端API
app.use(
'/cookiecloud',
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /cookiecloud 前缀
proxyReqPathResolver: (req) => {
return `/cookiecloud${req.url}`
}
})
);
// 处理根路径的请求
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'))
})
// 处理所有其他请求,重定向到前端入口文件
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'))
})
app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})
================================================
FILE: shims.d.ts
================================================
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'vue-prism-component' {
import { ComponentOptions } from 'vue'
const component: ComponentOptions
export default component
}
declare module 'vue-shepherd';
declare module 'colorthief';
================================================
FILE: src/@core/components/ConfirmDialog.vue
================================================
<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>
================================================
FILE: src/@core/components/DialogCloseBtn.vue
================================================
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
// 是否显示
innerClass: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['click', 'update:modelValue'])
// 按钮点击
function onClick() {
emit('update:modelValue', false)
emit('click')
}
</script>
<template>
<IconBtn
:class="props.innerClass ? props.innerClass : 'absolute right-3 top-3 z-10'"
@click.stop="onClick"
>
<VIcon icon="mdi-close" />
</IconBtn>
</template>
================================================
FILE: src/@core/components/ErrorHeader.vue
================================================
<script setup lang="ts">
interface Props {
errorCode?: string
errorTitle?: string
errorDescription?: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="text-center mb-4">
<!-- 👉 Title and subtitle -->
<h1
v-if="props.errorCode"
class="text-h1 font-weight-medium"
>
{{ props.errorCode }}
</h1>
<h5
v-if="props.errorTitle"
class="text-h5 font-weight-medium mb-3"
>
{{ props.errorTitle }}
</h5>
<p v-if="props.errorDescription">
{{ props.errorDescription }}
</p>
</div>
</template>
================================================
FILE: src/@core/components/ExistIcon.vue
================================================
<template>
<div class="absolute top-0 right-0 flex items-center justify-between p-2">
<div class="pointer-events-none z-40 flex items-center">
<div
class="relative inline-flex whitespace-nowrap rounded-full border-gray-700 font-semibold leading-5 ring-gray-700"
>
<div
class="rounded-full bg-opacity-80 w-5 border p-0 bg-green-500 border-green-400 ring-green-400 text-green-100"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
</div>
</div>
</template>
================================================
FILE: src/@core/components/LoadingBanner.vue
================================================
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
text: String,
})
</script>
<template>
<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>
================================================
FILE: src/@core/components/MoreBtn.vue
================================================
<script lang="ts" setup>
interface Props {
menuList?: unknown[]
itemProps?: boolean
}
const props = defineProps<Props>()
</script>
<template>
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu
v-if="props.menuList"
activator="parent"
close-on-content-click
>
<VList
:items="props.menuList"
:item-props="props.itemProps"
/>
</VMenu>
</IconBtn>
</template>
================================================
FILE: src/@core/components/PageContentTitle.vue
================================================
<script setup lang="ts">
defineProps({
// 标题
title: String,
})
</script>
<template>
<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"
data-testid="page-header"
>
<span class="text-moviepilot">{{ title }}</span>
</h2>
</div>
</div>
</template>
================================================
FILE: src/@core/components/ScrollToTopBtn.vue
================================================
<script lang="ts" setup>
// 控制回到顶部按钮的可见性
const showScrollToTop = ref(false)
const scrollThreshold = 200 // 滚动多少像素后显示按钮
// 滚动事件处理函数
const handleScroll = () => {
showScrollToTop.value = window.scrollY > scrollThreshold
}
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
onMounted(async () => {
// Add scroll event listener
window.addEventListener('scroll', handleScroll)
// Initial check for scroll-to-top
handleScroll()
})
onUnmounted(() => {
// Remove scroll event listener
window.removeEventListener('scroll', handleScroll)
})
</script>
<template>
<div class="global-action-buttons d-none d-sm-block">
<Transition name="scroll-fade">
<button v-show="showScrollToTop" class="global-action-button" @click="scrollToTop">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7 14L12 9L17 14"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</Transition>
</div>
</template>
<style lang="scss" scoped>
/* Global Action Button Styles (FAB) */
.global-action-buttons {
position: fixed;
z-index: 100;
display: flex;
flex-direction: column;
gap: 16px;
inset-block-end: 2rem;
inset-inline-end: 2rem;
}
.global-action-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
border-radius: 50%;
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-background), 0.8);
block-size: 44px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 8%);
color: rgb(var(--v-theme-on-surface));
cursor: pointer;
inline-size: 44px;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
&:hover {
background-color: rgba(var(--v-theme-background), 0.95);
color: rgb(var(--v-theme-primary));
transform: translateY(-4px);
}
svg {
block-size: 20px;
inline-size: 20px;
transition: all 0.3s ease;
}
}
</style>
================================================
FILE: src/@core/components/StatIcon.vue
================================================
<script lang="ts" setup>
interface Props {
color?: string
message?: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="absolute top-2 right-2 flex items-center justify-between p-2">
<VBadge :color="props.color" bordered>
<template #badge>
<VIcon icon="mdi-pulse"></VIcon>
</template>
</VBadge>
</div>
</template>
================================================
FILE: src/@core/libs/apex-chart/apexCharConfig.ts
================================================
import type { ThemeInstance } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
// 👉 Colors variables
function colorVariables(themeColors: ThemeInstance['themes']['value']['colors']) {
const themeSecondaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['medium-emphasis-opacity']})`
const themeDisabledTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['disabled-opacity']})`
const themeBorderColor = `rgba(${hexToRgb(String(themeColors.variables['border-color']))},${themeColors.variables['border-opacity']})`
const themePrimaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['high-emphasis-opacity']})`
return { themeSecondaryTextColor, themeDisabledTextColor, themeBorderColor, themePrimaryTextColor }
}
export function getScatterChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const scatterColors = {
series1: '#ff9f43',
series2: '#7367f0',
series3: '#28c76f',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
zoom: {
type: 'xy',
enabled: true,
},
},
legend: {
position: 'top',
horizontalAlign: 'left',
markers: { offsetX: -3 },
labels: { colors: themeSecondaryTextColor },
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
colors: [scatterColors.series1, scatterColors.series2, scatterColors.series3],
grid: {
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor },
},
},
xaxis: {
tickAmount: 10,
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
formatter: (val: string) => parseFloat(val).toFixed(1),
},
},
}
}
export function getLineChartSimpleConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
zoom: { enabled: false },
toolbar: { show: false },
},
colors: ['#ff9f43'],
stroke: { curve: 'straight' },
dataLabels: { enabled: false },
markers: {
strokeWidth: 7,
strokeOpacity: 1,
colors: ['#ff9f43'],
strokeColors: ['#fff'],
},
grid: {
padding: { top: -10 },
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
tooltip: {
custom(data: any) {
return `<div class='bar-chart pa-2'>
<span>${data.series[data.seriesIndex][data.dataPointIndex]}%</span>
</div>`
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
categories: [
'7/12',
'8/12',
'9/12',
'10/12',
'11/12',
'12/12',
'13/12',
'14/12',
'15/12',
'16/12',
'17/12',
'18/12',
'19/12',
'20/12',
'21/12',
],
},
}
}
export function getBarChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
colors: ['#00cfe8'],
dataLabels: { enabled: false },
plotOptions: {
bar: {
borderRadius: 8,
barHeight: '30%',
horizontal: true,
startingShape: 'rounded',
},
},
grid: {
borderColor: themeBorderColor,
xaxis: {
lines: { show: false },
},
padding: {
top: -10,
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
categories: ['MON, 11', 'THU, 14', 'FRI, 15', 'MON, 18', 'WED, 20', 'FRI, 21', 'MON, 23'],
labels: {
style: { colors: themeDisabledTextColor },
},
},
}
}
export function getCandlestickChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const candlestickColors = {
series1: '#28c76f',
series2: '#ea5455',
}
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
plotOptions: {
bar: { columnWidth: '40%' },
candlestick: {
colors: {
upward: candlestickColors.series1,
downward: candlestickColors.series2,
},
},
},
grid: {
padding: { top: -10 },
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
tooltip: { enabled: true },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
},
xaxis: {
type: 'datetime',
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
},
}
}
export function getRadialBarChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const radialBarColors = {
series1: '#fdd835',
series2: '#32baff',
series3: '#00d4bd',
series4: '#7367f0',
series5: '#FFA1A1',
}
const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors)
return {
stroke: { lineCap: 'round' },
labels: ['Comments', 'Replies', 'Shares'],
legend: {
show: true,
position: 'bottom',
labels: {
colors: themeSecondaryTextColor,
},
markers: {
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
colors: [radialBarColors.series1, radialBarColors.series2, radialBarColors.series4],
plotOptions: {
radialBar: {
hollow: { size: '30%' },
track: {
margin: 15,
background: themeColors.colors['grey-100'],
},
dataLabels: {
name: {
fontSize: '2rem',
},
value: {
fontSize: '1rem',
color: themeSecondaryTextColor,
},
total: {
show: true,
fontWeight: 400,
label: 'Comments',
fontSize: '1.125rem',
color: themePrimaryTextColor,
formatter(w: { globals: { seriesTotals: any[]; series: string | any[] } }) {
const totalValue
= w.globals.seriesTotals.reduce((a: number, b: number) => {
return a + b
}, 0) / w.globals.series.length
if (totalValue % 1 === 0)
return `${totalValue}%`
else
return `${totalValue.toFixed(2)}%`
},
},
},
},
},
grid: {
padding: {
top: -30,
bottom: -25,
},
},
}
}
export function getDonutChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const donutColors = {
series1: '#fdd835',
series2: '#00d4bd',
series3: '#826bf8',
series4: '#32baff',
series5: '#ffa1a1',
}
const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors)
return {
stroke: { width: 0 },
labels: ['Operational', 'Networking', 'Hiring', 'R&D'],
colors: [donutColors.series1, donutColors.series5, donutColors.series3, donutColors.series2],
dataLabels: {
enabled: true,
formatter: (val: string) => `${parseInt(val, 10)}%`,
},
legend: {
position: 'bottom',
markers: { offsetX: -3 },
labels: { colors: themeSecondaryTextColor },
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
fontSize: '1.5rem',
},
value: {
fontSize: '1.5rem',
color: themeSecondaryTextColor,
formatter: (val: string) => `${parseInt(val, 10)}`,
},
total: {
show: true,
fontSize: '1.5rem',
label: 'Operational',
formatter: () => '31%',
color: themePrimaryTextColor,
},
},
},
},
},
responsive: [
{
breakpoint: 992,
options: {
chart: {
height: 380,
},
legend: {
position: 'bottom',
},
},
},
{
breakpoint: 576,
options: {
chart: {
height: 320,
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
fontSize: '1rem',
},
value: {
fontSize: '1rem',
},
total: {
fontSize: '1rem',
},
},
},
},
},
},
},
],
}
}
export function getAreaChartSplineConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const areaColors = {
series3: '#e0cffe',
series2: '#b992fe',
series1: '#ab7efd',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
tooltip: { shared: false },
dataLabels: { enabled: false },
stroke: {
show: false,
curve: 'straight',
},
legend: {
position: 'top',
horizontalAlign: 'left',
labels: { colors: themeSecondaryTextColor },
markers: {
offsetY: 1,
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
colors: [areaColors.series3, areaColors.series2, areaColors.series1],
fill: {
opacity: 1,
type: 'solid',
},
grid: {
show: true,
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
categories: [
'7/12',
'8/12',
'9/12',
'10/12',
'11/12',
'12/12',
'13/12',
'14/12',
'15/12',
'16/12',
'17/12',
'18/12',
'19/12',
],
},
}
}
export function getColumnChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const columnColors = {
series1: '#826af9',
series2: '#d2b0ff',
bg: '#f8d3ff',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
offsetX: -10,
stacked: true,
parentHeightOffset: 0,
toolbar: { show: false },
},
fill: { opacity: 1 },
dataLabels: { enabled: false },
colors: [columnColors.series1, columnColors.series2],
legend: {
position: 'top',
horizontalAlign: 'left',
labels: { colors: themeSecondaryTextColor },
markers: {
offsetY: 1,
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
stroke: {
show: true,
colors: ['transparent'],
},
plotOptions: {
bar: {
columnWidth: '15%',
colors: {
backgroundBarRadius: 10,
backgroundBarColors: [columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg],
},
},
},
grid: {
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
categories: ['7/12', '8/12', '9/12', '10/12', '11/12', '12/12', '13/12', '14/12', '15/12'],
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
},
responsive: [
{
breakpoint: 600,
options: {
plotOptions: {
bar: {
columnWidth: '35%',
},
},
},
},
],
}
}
export function getHeatMapChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const { themeSecondaryTextColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
dataLabels: { enabled: false },
stroke: {
colors: [themeColors.colors.surface],
},
legend: {
position: 'bottom',
labels: {
colors: themeSecondaryTextColor,
},
markers: {
offsetY: 0,
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
plotOptions: {
heatmap: {
enableShades: false,
colorScale: {
ranges: [
{ to: 10, from: 0, name: '0-10', color: '#b9b3f8' },
{ to: 20, from: 11, name: '10-20', color: '#aba4f6' },
{ to: 30, from: 21, name: '20-30', color: '#9d95f5' },
{ to: 40, from: 31, name: '30-40', color: '#8f85f3' },
{ to: 50, from: 41, name: '40-50', color: '#8176f2' },
{ to: 60, from: 51, name: '50-60', color: '#7367f0' },
],
},
},
},
grid: {
padding: { top: -20 },
},
yaxis: {
labels: {
style: {
colors: themeDisabledTextColor,
},
},
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
}
}
export function getRadarChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
const radarColors = {
series1: '#9b88fa',
series2: '#ffa1a1',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
dropShadow: {
top: 1,
blur: 8,
left: 1,
opacity: 0.2,
enabled: false,
},
},
markers: { size: 0 },
fill: { opacity: [1, 0.8] },
colors: [radarColors.series1, radarColors.series2],
stroke: {
width: 0,
show: false,
},
legend: {
labels: {
colors: themeSecondaryTextColor,
},
markers: {
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
plotOptions: {
radar: {
polygons: {
strokeColors: themeBorderColor,
connectorColors: themeBorderColor,
},
},
},
grid: {
show: false,
padding: {
top: -20,
bottom: -20,
},
},
yaxis: { show: false },
xaxis: {
categories: ['Battery', 'Brand', 'Camera', 'Memory', 'Storage', 'Display', 'OS', 'Price'],
labels: {
style: {
colors: [
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
],
},
},
},
}
}
================================================
FILE: src/@core/scss/README.md
================================================
# SCSS结构说明
## 目录整合
本项目SCSS文件已完成整合:
- 主入口文件:`src/@core/scss/index.scss`
- 实际功能文件位于:`src/@core/scss/template/index.scss`
## 整合内容
- 整合了原`src/@core/scss/base`和`src/@core/scss/template`目录的功能
- 统一使用`template`目录作为SCSS样式的主要引用点
- 保留原有引用结构以保证向后兼容性
## 整合进度
已完成:
- ✅ 主入口文件引用更新
- ✅ mixins文件合并
- ✅ placeholders目录下文件转移
- ✅ perfect-scrollbar文件整合
- ✅ vuetify相关文件整合
- ✅ default-layout-w-vertical-nav文件整合
- ✅ 移除了template/index.scss中对base目录组件的依赖
- ✅ 修复了components.scss中对base/mixins的引用
- ✅ 修复了variables.scss中对base/variables的引用
- ✅ 修复了apex-chart.scss和full-calendar.scss的linter错误
- ✅ 整合并移除了对vuetify/variables的依赖
- ✅ 修复了SCSS变量名冲突问题
- ✅ 修复了SASS模块重复加载配置问题
- ✅ 修复了导入路径问题(misc、utils等模块的引用路径)
待完成:
- ⬜ 最终测试确保无样式问题
- ⬜ 清理冗余文件
## 使用方式
在项目中引用SCSS时,应使用:
```scss
@use "@core/scss";
```
这将自动加载所有必要的样式文件。
## 注意事项
此次整合已将所有功能文件整合到template目录,不再依赖base目录的代码。现在可以安全地从外部引用template目录下的文件,但需要进行最终测试以确保样式正常工作。
测试无误后,可以考虑完全删除base目录,以简化项目结构。
## 最近修复
在最近的更新中,我们修复了以下问题:
1. 解决了变量名冲突问题,通过使用命名空间(如`layouts-vars`)来引用外部模块变量
2. 修复了SASS模块重复配置问题,将多处的`@forward...with`配置合并到了template/_variables.scss文件中
3. 统一使用命名空间引用模块,避免后续出现冲突
4. 修复了`_default-layout-w-vertical-nav.scss`中导入路径错误,将`@use "misc"`修改为`@use "../misc"`
================================================
FILE: src/@core/scss/_components.scss
================================================
@use "mixins";
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@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);
}
// 👉 Avatar group
.v-avatar-group {
display: flex;
align-items: center;
> * {
&:not(:first-child) {
margin-inline-start: -0.8rem;
}
transition: transform 0.25s ease, box-shadow 0.15s ease;
&:hover {
z-index: 2;
transform: translateY(-5px) scale(1.05);
@include mixins_elevation.elevation(3);
}
}
> .v-avatar {
border: 2px solid rgb(var(--v-theme-surface));
}
}
// 👉 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 {
@extend %style-scroll-bar;
}
}
@media (width >= 576px) {
.v-dialog {
&.v-dialog-sm,
&.v-dialog-lg,
&.v-dialog-xl {
inline-size: 565px !important;
}
}
}
@media (width >= 992px) {
.v-dialog {
&.v-dialog-lg,
&.v-dialog-xl {
inline-size: 865px !important;
}
}
}
@media (width >= 1200px) {
.v-dialog.v-dialog-xl,
.v-dialog.v-dialog-xl .v-overlay__content > .v-card {
inline-size: 1165px !important;
}
}
// 👉 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;
min-inline-size: 8.125rem;
transition: none;
.v-tab__slider {
visibility: hidden;
}
}
.v-slide-group__content {
transition: none;
}
}
// loop for all colors bg
@each $color-name in variables.$theme-colors-name {
.v-tabs.v-tabs-pill {
.v-slide-group-item--active.v-tab--selected.text-#{$color-name} {
background-color: rgb(var(--v-theme-#{$color-name}));
color: rgb(var(--v-theme-on-#{$color-name})) !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}));
}
}
}
}
}
// ℹ️ We are make even width of all v-timeline body
.v-timeline--vertical.v-timeline {
.v-timeline-item {
.v-timeline-item__body {
justify-self: stretch !important;
}
}
}
// 👉 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.v-input--horizontal .v-slider-track__fill {
block-size: var(--v-slider-track-size);
}
.v-slider.v-input--vertical .v-slider-track__fill {
inline-size: var(--v-slider-track-size);
}
.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);
}
================================================
FILE: src/@core/scss/_dark.scss
================================================
@use "@configured-variables" as variables;
// ————————————————————————————————————
// Perfect Scrollbar
// ————————————————————————————————————
.v-application.v-theme--dark {
.ps__rail-y,
.ps__rail-x {
background-color: transparent !important;
}
.ps__thumb-y {
background-color: variables.$plugin-ps-thumb-y-dark;
}
}
================================================
FILE: src/@core/scss/_default-layout-w-vertical-nav.scss
================================================
@use "@configured-variables" as variables;
@use "placeholders" as *;
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "misc";
@use "mixins";
$header: ".layout-navbar";
@if variables.$layout-vertical-nav-navbar-is-contained {
$header: ".layout-navbar .navbar-content-container";
}
.layout-wrapper.layout-nav-type-vertical {
// SECTION Layout Navbar
// 👉 Elevated navbar
@if variables.$vertical-nav-navbar-style == "elevated" {
// Add transition
#{$header} {
transition: padding 0.2s ease;
}
// If navbar is contained => Add border radius to header
@if variables.$layout-vertical-nav-navbar-is-contained {
// #{$header} {
// border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
// }
}
// Scrolled styles for sticky navbar
@at-root {
/* ℹ️ Only apply scrolled styles when window is actually scrolled,
not when dialog is opened without scroll
*/
&.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;
}
.navbar-blur#{$header} {
@extend %blurry-bg;
}
}
}
}
// 👉 Floating navbar
@else if variables.$vertical-nav-navbar-style == "floating" {
// ℹ️ Regardless of navbar is contained or not => Apply overlay to .layout-navbar
.layout-navbar {
&.navbar-blur {
@extend %default-layout-vertical-nav-floating-navbar-overlay;
}
}
&:not(.layout-navbar-fixed) {
#{$header} {
margin-block-start: variables.$vertical-nav-floating-navbar-top;
}
}
#{$header} {
@if variables.$layout-vertical-nav-navbar-is-contained {
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
}
background-color: rgb(var(--v-theme-surface));
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
}
.navbar-blur#{$header} {
@extend %blurry-bg;
}
}
// !SECTION
// 👉 Layout footer
.layout-footer {
$ele-layout-footer: &;
.footer-content-container {
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness 0 0;
// Sticky footer
@at-root {
// ℹ️ .layout-footer-sticky#{$ele-layout-footer} => .layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer
.layout-footer-sticky#{$ele-layout-footer} {
.footer-content-container {
background-color: rgb(var(--v-theme-surface));
padding-block: 0;
padding-inline: 1.2rem;
@include mixins.elevation(3);
}
}
}
}
}
}
================================================
FILE: src/@core/scss/_default-layout.scss
================================================
@use "placeholders";
@use "variables" as core-vars;
.layout-navbar {
@if core-vars.$navbar-high-emphasis-text {
@extend %layout-navbar;
}
}
================================================
FILE: src/@core/scss/_misc.scss
================================================
// ℹ️ scrollable-content allows creating fixed header and scrollable content for VNavigationDrawer (Used when perfect scrollbar is used)
.scrollable-content {
&.v-navigation-drawer {
.v-navigation-drawer__content {
display: flex;
overflow: hidden;
flex-direction: column;
}
}
}
// ℹ️ adding styling for code tag
code {
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));
}
}
}
}
================================================
FILE: src/@core/scss/_mixins.scss
================================================
@use "sass:map";
@use "vuetify/lib/styles/settings/_index.sass" as vuetify_settings;
@use "@styles/variables/_vuetify.scss" as vuetify;
@mixin themed($property, $light-value, $dark-value) {
@at-root {
.v-theme {
&--light {
#{$property}: $light-value;
}
&--dark {
#{$property}: $dark-value;
}
}
}
}
// ℹ️ 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%;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
}
}
// ——— Light background generator ——————— //
// ℹ️ With this you have to give text color to the component you want light bg
// e.g. class="avatar-initial text-primary" for primary light bg
@mixin light-bg-provider($component, $inner-selector: "", $opacity: 0.12) {
.#{$component}.#{$component}-light-bg #{$inner-selector} {
background-color: transparent !important;
&.bg-static-white {
background-color: white !important;
}
&::before {
position: absolute;
border-radius: inherit;
background-color: currentcolor;
content: "";
inset: 0;
opacity: $opacity;
pointer-events: none;
}
}
}
@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 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;
}
}
================================================
FILE: src/@core/scss/_utilities.scss
================================================
.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;
}
}
}
================================================
FILE: src/@core/scss/_utils.scss
================================================
@use "sass:map";
@use "sass:list";
@use "sass:string";
// Thanks: https://css-tricks.com/snippets/sass/deep-getset-maps/
@function map-deep-get($map, $keys...) {
@each $key in $keys {
$map: map.get($map, $key);
}
@return $map;
}
@function map-deep-set($map, $keys, $value) {
$maps: ($map,);
$result: null;
// If the last key is a map already
// Warn the user we will be overriding it with $value
@if type-of(nth($keys, -1)) == "map" {
@warn "The last key you specified is a map; it will be overrided with `#{$value}`.";
}
// If $keys is a single key
// Just merge and return
@if length($keys) == 1 {
@return map-merge($map, ($keys: $value));
}
// Loop from the first to the second to last key from $keys
// Store the associated map to this key in the $maps list
// If the key doesn't exist, throw an error
@for $i from 1 through length($keys) - 1 {
$current-key: list.nth($keys, $i);
$current-map: list.nth($maps, -1);
$current-get: map.get($current-map, $current-key);
@if not $current-get {
@error "Key `#{$key}` doesn't exist at current level in map.";
}
$maps: list.append($maps, $current-get);
}
// Loop from the last map to the first one
// Merge it with the previous one
@for $i from length($maps) through 1 {
$current-map: list.nth($maps, $i);
$current-key: list.nth($keys, $i);
$current-val: if($i == list.length($maps), $value, $result);
$result: map.map-merge($current-map, ($current-key: $current-val));
}
// Return result
@return $result;
}
// font size utility classes
// font size
$font-sizes: (
"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 line-height
$font-line-height: (
"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
);
@each $name, $size in $font-sizes {
.text-#{$name} {
font-size: $size;
line-height: map.get($font-line-height, $name);
}
}
// truncate utility class
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// gap utility class
$gap: (
"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
);
@each $name, $size in $gap {
.gap-#{$name} {
gap: $size;
}
.gap-x-#{$name} {
column-gap: $size;
}
.gap-y-#{$name} {
gitextract_ghdmazve/ ├── .editorconfig ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── discussion.yml │ │ ├── feature_request.yml │ │ └── rfc.yml │ └── workflows/ │ └── build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .stylelintrc.json ├── .vscode/ │ ├── anchor-comments.code-snippets │ ├── extensions.json │ ├── settings.json │ ├── vue-ts.code-snippets │ ├── vue.code-snippets │ └── vuetify.code-snippets ├── LICENSE ├── README.md ├── README_EN.md ├── auto-imports.d.ts ├── components.d.ts ├── docs/ │ ├── federation-troubleshooting.md │ └── module-federation-guide.md ├── env.d.ts ├── examples/ │ └── plugin-component/ │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── components/ │ │ │ ├── AppPage.vue │ │ │ ├── AppPageSettings.vue │ │ │ ├── Config.vue │ │ │ ├── Dashboard.vue │ │ │ └── Page.vue │ │ ├── main.js │ │ └── vuetify/ │ │ ├── defaults.ts │ │ └── theme.ts │ └── vite.config.js ├── index.html ├── package.json ├── postcss.config.js ├── public/ │ ├── nginx.conf │ ├── offline.html │ ├── robots.txt │ └── service.js ├── shims.d.ts ├── src/ │ ├── @core/ │ │ ├── components/ │ │ │ ├── ConfirmDialog.vue │ │ │ ├── DialogCloseBtn.vue │ │ │ ├── ErrorHeader.vue │ │ │ ├── ExistIcon.vue │ │ │ ├── LoadingBanner.vue │ │ │ ├── MoreBtn.vue │ │ │ ├── PageContentTitle.vue │ │ │ ├── ScrollToTopBtn.vue │ │ │ └── StatIcon.vue │ │ ├── libs/ │ │ │ └── apex-chart/ │ │ │ └── apexCharConfig.ts │ │ ├── scss/ │ │ │ ├── README.md │ │ │ ├── _components.scss │ │ │ ├── _dark.scss │ │ │ ├── _default-layout-w-vertical-nav.scss │ │ │ ├── _default-layout.scss │ │ │ ├── _misc.scss │ │ │ ├── _mixins.scss │ │ │ ├── _utilities.scss │ │ │ ├── _utils.scss │ │ │ ├── _variables.scss │ │ │ ├── _vertical-nav.scss │ │ │ ├── index.scss │ │ │ ├── libs/ │ │ │ │ ├── apex-chart.scss │ │ │ │ ├── full-calendar.scss │ │ │ │ ├── perfect-scrollbar.scss │ │ │ │ └── vuetify/ │ │ │ │ ├── _overrides.scss │ │ │ │ ├── _variables.scss │ │ │ │ └── index.scss │ │ │ ├── pages/ │ │ │ │ ├── misc.scss │ │ │ │ └── page-auth.scss │ │ │ └── placeholders/ │ │ │ ├── _default-layout.scss │ │ │ ├── _index.scss │ │ │ ├── _nav.scss │ │ │ └── _vertical-nav.scss │ │ └── utils/ │ │ ├── compatibility.ts │ │ ├── dom.ts │ │ ├── formatters.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── navigator.ts │ │ ├── theme.ts │ │ └── workflow.ts │ ├── @iconify/ │ │ ├── build-icons.ts │ │ ├── tsconfig.json │ │ └── tsconfig.tsbuildinfo │ ├── @layouts/ │ │ ├── components/ │ │ │ ├── VerticalNav.vue │ │ │ ├── VerticalNavLayout.vue │ │ │ ├── VerticalNavLink.vue │ │ │ └── VerticalNavSectionTitle.vue │ │ ├── components.ts │ │ ├── index.ts │ │ ├── styles/ │ │ │ ├── _classes.scss │ │ │ ├── _default-layout.scss │ │ │ ├── _global.scss │ │ │ ├── _mixins.scss │ │ │ ├── _placeholders.scss │ │ │ ├── _rtl.scss │ │ │ ├── _variables.scss │ │ │ └── index.scss │ │ ├── types.d.ts │ │ └── utils.ts │ ├── @validators/ │ │ └── index.ts │ ├── App.vue │ ├── ace-config.ts │ ├── api/ │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── nprogress.ts │ │ └── types.ts │ ├── components/ │ │ ├── FileBrowser.vue │ │ ├── NoDataFound.vue │ │ ├── PWAInstallPrompt.vue │ │ ├── cards/ │ │ │ ├── BackdropCard.vue │ │ │ ├── CustomRuleCard.vue │ │ │ ├── DirectoryCard.vue │ │ │ ├── DownloaderCard.vue │ │ │ ├── DownloadingCard.vue │ │ │ ├── FilterRuleCard.vue │ │ │ ├── FilterRuleGroupCard.vue │ │ │ ├── LibraryCard.vue │ │ │ ├── MediaCard.vue │ │ │ ├── MediaInfoCard.vue │ │ │ ├── MediaServerCard.vue │ │ │ ├── MessageCard.vue │ │ │ ├── NotificationChannelCard.vue │ │ │ ├── PersonCard.vue │ │ │ ├── PluginAppCard.vue │ │ │ ├── PluginCard.vue │ │ │ ├── PluginFolderCard.vue │ │ │ ├── PluginMixedSortCard.vue │ │ │ ├── PosterCard.vue │ │ │ ├── SiteCard.vue │ │ │ ├── StorageCard.vue │ │ │ ├── SubscribeCard.vue │ │ │ ├── SubscribeShareCard.vue │ │ │ ├── TorrentCard.vue │ │ │ ├── TorrentItem.vue │ │ │ ├── UserCard.vue │ │ │ ├── WorkflowShareCard.vue │ │ │ └── WorkflowTaskCard.vue │ │ ├── dialog/ │ │ │ ├── AboutDialog.vue │ │ │ ├── AddDownloadDialog.vue │ │ │ ├── AlistConfigDialog.vue │ │ │ ├── AliyunAuthDialog.vue │ │ │ ├── CategoryEditDialog.vue │ │ │ ├── ForkSubscribeDialog.vue │ │ │ ├── ForkWorkflowDialog.vue │ │ │ ├── ImportCodeDialog.vue │ │ │ ├── MediaInfoDialog.vue │ │ │ ├── OTPAuthDialog.vue │ │ │ ├── PasskeyDialog.vue │ │ │ ├── PluginConfigDialog.vue │ │ │ ├── PluginDataDialog.vue │ │ │ ├── PluginMarketSettingDialog.vue │ │ │ ├── ProgressDialog.vue │ │ │ ├── RcloneConfigDialog.vue │ │ │ ├── ReorganizeDialog.vue │ │ │ ├── SearchBarDialog.vue │ │ │ ├── SearchSiteDialog.vue │ │ │ ├── SiteAddEditDialog.vue │ │ │ ├── SiteCookieUpdateDialog.vue │ │ │ ├── SiteImportDialog.vue │ │ │ ├── SiteResourceDialog.vue │ │ │ ├── SiteStatisticsDialog.vue │ │ │ ├── SiteUserDataDialog.vue │ │ │ ├── SmbConfigDialog.vue │ │ │ ├── SubscribeEditDialog.vue │ │ │ ├── SubscribeFilesDialog.vue │ │ │ ├── SubscribeHistoryDialog.vue │ │ │ ├── SubscribeSeasonDialog.vue │ │ │ ├── SubscribeShareDialog.vue │ │ │ ├── SubscribeShareStatisticsDialog.vue │ │ │ ├── TransferQueueDialog.vue │ │ │ ├── U115AuthDialog.vue │ │ │ ├── UserAddEditDialog.vue │ │ │ ├── UserAuthDialog.vue │ │ │ ├── WorkflowActionsDialog.vue │ │ │ ├── WorkflowAddEditDialog.vue │ │ │ └── WorkflowShareDialog.vue │ │ ├── field/ │ │ │ ├── CronField.vue │ │ │ └── PathField.vue │ │ ├── filebrowser/ │ │ │ ├── FileList.vue │ │ │ ├── FileNavigator.vue │ │ │ └── FileToolbar.vue │ │ ├── filter/ │ │ │ └── TorrentFilterBar.vue │ │ ├── input/ │ │ │ ├── CronInput.vue │ │ │ └── PathInput.vue │ │ ├── misc/ │ │ │ ├── DashboardElement.vue │ │ │ ├── FilterOption.vue │ │ │ ├── MediaIdSelector.vue │ │ │ └── VersionHistory.vue │ │ ├── render/ │ │ │ ├── DashboardRender.vue │ │ │ ├── FormRender.vue │ │ │ └── PageRender.vue │ │ ├── slide/ │ │ │ ├── SlideView.vue │ │ │ └── SlideViewTitle.vue │ │ ├── toast/ │ │ │ └── VersionUpdateToast.vue │ │ └── workflow/ │ │ ├── AddDownloadAction.vue │ │ ├── AddSubscribeAction.vue │ │ ├── FetchDownloadsAction.vue │ │ ├── FetchMediasAction.vue │ │ ├── FetchRssAction.vue │ │ ├── FetchTorrentsAction.vue │ │ ├── FilterMediasAction.vue │ │ ├── FilterTorrentsAction.vue │ │ ├── InvokePluginAction.vue │ │ ├── NoteAction.vue │ │ ├── ScanFileAction.vue │ │ ├── ScrapeFileAction.vue │ │ ├── SendEventAction.vue │ │ ├── SendMessageAction.vue │ │ └── TransferFileAction.vue │ ├── composables/ │ │ ├── useAvailableHeight.ts │ │ ├── useBackgroundOptimization.ts │ │ ├── useCacheManager.ts │ │ ├── useConfirm.ts │ │ ├── useDynamicButton.ts │ │ ├── useDynamicHeaderTab.ts │ │ ├── useInfiniteScroll.ts │ │ ├── useOfflineStatus.ts │ │ ├── usePWA.ts │ │ ├── usePWAInstall.ts │ │ ├── usePullDownGesture.ts │ │ ├── useRecentPlugins.ts │ │ ├── useSetupWizard.ts │ │ ├── useStateRestore.ts │ │ ├── useTorrentFilter.ts │ │ └── useVersionChecker.ts │ ├── layouts/ │ │ ├── blank.vue │ │ ├── components/ │ │ │ ├── DefaultLayout.vue │ │ │ ├── DropzoneBackground.vue │ │ │ ├── Footer.vue │ │ │ ├── HeaderTab.vue │ │ │ ├── OfflinePage.vue │ │ │ ├── QuickAccess.vue │ │ │ ├── SearchBar.vue │ │ │ ├── ShortcutBar.vue │ │ │ ├── UserNotification.vue │ │ │ ├── UserProfile.vue │ │ │ └── WorkflowSidebar.vue │ │ └── default.vue │ ├── locales/ │ │ ├── en-US.ts │ │ ├── zh-CN.ts │ │ └── zh-TW.ts │ ├── main.ts │ ├── pages/ │ │ ├── [...all].vue │ │ ├── appcenter.vue │ │ ├── browse.vue │ │ ├── calendar.vue │ │ ├── credits.vue │ │ ├── dashboard.vue │ │ ├── discover.vue │ │ ├── downloading.vue │ │ ├── filemanager.vue │ │ ├── history.vue │ │ ├── login.vue │ │ ├── media.vue │ │ ├── person.vue │ │ ├── plugin-app.vue │ │ ├── plugin.vue │ │ ├── profile.vue │ │ ├── recommend.vue │ │ ├── resource.vue │ │ ├── setting.vue │ │ ├── setup.vue │ │ ├── site.vue │ │ ├── subscribe-share.vue │ │ ├── subscribe.vue │ │ ├── user.vue │ │ └── workflow.vue │ ├── plugins/ │ │ ├── i18n.ts │ │ ├── stateRestore.ts │ │ ├── vuetify/ │ │ │ ├── defaults.ts │ │ │ ├── icons.ts │ │ │ ├── index.ts │ │ │ └── theme.ts │ │ └── webfontloader.ts │ ├── router/ │ │ ├── i18n-menu.ts │ │ └── index.ts │ ├── service-worker.ts │ ├── stores/ │ │ ├── auth.ts │ │ ├── global.ts │ │ ├── index.ts │ │ ├── pluginSidebarNav.ts │ │ ├── types.ts │ │ └── user.ts │ ├── styles/ │ │ ├── common.scss │ │ ├── main.scss │ │ ├── themes/ │ │ │ └── transparent.scss │ │ └── variables/ │ │ ├── _template.scss │ │ └── _vuetify.scss │ ├── types/ │ │ ├── global.d.ts │ │ ├── i18n.ts │ │ ├── service-worker-sync.d.ts │ │ └── workbox-precaching.d.ts │ ├── utils/ │ │ ├── appDeepLink.ts │ │ ├── backgroundManager.ts │ │ ├── badge.ts │ │ ├── colorUtils.ts │ │ ├── federationLoader.ts │ │ ├── globalSetting.ts │ │ ├── imageUtils.ts │ │ ├── loadingStateManager.ts │ │ ├── permission.ts │ │ ├── pluginSidebarNav.ts │ │ ├── requestOptimizer.ts │ │ ├── sseManager.ts │ │ └── themeManager.ts │ └── views/ │ ├── dashboard/ │ │ ├── AnalyticsCpu.vue │ │ ├── AnalyticsMediaStatistic.vue │ │ ├── AnalyticsMemory.vue │ │ ├── AnalyticsNetwork.vue │ │ ├── AnalyticsProcesses.vue │ │ ├── AnalyticsScheduler.vue │ │ ├── AnalyticsSpeed.vue │ │ ├── AnalyticsStorage.vue │ │ ├── AnalyticsWeeklyOverview.vue │ │ ├── MediaServerLatest.vue │ │ ├── MediaServerLibrary.vue │ │ └── MediaServerPlaying.vue │ ├── discover/ │ │ ├── BangumiView.vue │ │ ├── DoubanView.vue │ │ ├── ExtraSourceView.vue │ │ ├── MediaCardListView.vue │ │ ├── MediaCardSlideView.vue │ │ ├── MediaDetailView.vue │ │ ├── PersonCardListView.vue │ │ ├── PersonCardSlideView.vue │ │ ├── PersonDetailView.vue │ │ └── TheMovieDbView.vue │ ├── plugin/ │ │ └── PluginCardListView.vue │ ├── reorganize/ │ │ ├── DownloadingListView.vue │ │ ├── FileBrowserView.vue │ │ └── TransferHistoryView.vue │ ├── setting/ │ │ ├── AccountSettingDirectory.vue │ │ ├── AccountSettingNotification.vue │ │ ├── AccountSettingRule.vue │ │ ├── AccountSettingSearch.vue │ │ ├── AccountSettingSite.vue │ │ ├── AccountSettingSubscribe.vue │ │ └── AccountSettingSystem.vue │ ├── setup/ │ │ ├── AgentSettingsStep.vue │ │ ├── BasicSettingsStep.vue │ │ ├── ConnectivityTest.vue │ │ ├── DownloaderSettingsStep.vue │ │ ├── MediaServerSettingsStep.vue │ │ ├── NotificationSettingsStep.vue │ │ ├── PreferencesSettingsStep.vue │ │ ├── SiteAuthSettingsStep.vue │ │ └── StorageSettingsStep.vue │ ├── site/ │ │ └── SiteCardListView.vue │ ├── subscribe/ │ │ ├── FullCalendarView.vue │ │ ├── SubscribeListView.vue │ │ ├── SubscribePopularView.vue │ │ └── SubscribeShareView.vue │ ├── system/ │ │ ├── CacheView.vue │ │ ├── LoggingView.vue │ │ ├── MessageView.vue │ │ ├── ModuleTestView.vue │ │ ├── NameTestView.vue │ │ ├── NetTestView.vue │ │ ├── RuleTestView.vue │ │ ├── ServiceView.vue │ │ └── WordsView.vue │ ├── user/ │ │ ├── UserListView.vue │ │ └── UserProfileView.vue │ └── workflow/ │ ├── WorkflowListView.vue │ └── WorkflowShareView.vue ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts
SYMBOL INDEX (390 symbols across 63 files)
FILE: auto-imports.d.ts
type GlobalComponents (line 338) | interface GlobalComponents {}
type ComponentCustomProperties (line 339) | interface ComponentCustomProperties {
FILE: components.d.ts
type GlobalComponents (line 10) | interface GlobalComponents {
FILE: env.d.ts
type RouteMeta (line 4) | interface RouteMeta {
FILE: examples/plugin-component/vite.config.js
method Root (line 62) | Root(root) {
FILE: src/@core/libs/apex-chart/apexCharConfig.ts
function colorVariables (line 5) | function colorVariables(themeColors: ThemeInstance['themes']['value']['c...
function getScatterChartConfig (line 14) | function getScatterChartConfig(themeColors: ThemeInstance['themes']['val...
function getLineChartSimpleConfig (line 70) | function getLineChartSimpleConfig(themeColors: ThemeInstance['themes']['...
function getBarChartConfig (line 140) | function getBarChartConfig(themeColors: ThemeInstance['themes']['value']...
function getCandlestickChartConfig (line 183) | function getCandlestickChartConfig(themeColors: ThemeInstance['themes'][...
function getRadialBarChartConfig (line 234) | function getRadialBarChartConfig(themeColors: ThemeInstance['themes']['v...
function getDonutChartConfig (line 310) | function getDonutChartConfig(themeColors: ThemeInstance['themes']['value...
function getAreaChartSplineConfig (line 404) | function getAreaChartSplineConfig(themeColors: ThemeInstance['themes']['...
function getColumnChartConfig (line 485) | function getColumnChartConfig(themeColors: ThemeInstance['themes']['valu...
function getHeatMapChartConfig (line 571) | function getHeatMapChartConfig(themeColors: ThemeInstance['themes']['val...
function getRadarChartConfig (line 630) | function getRadarChartConfig(themeColors: ThemeInstance['themes']['value...
FILE: src/@core/utils/dom.ts
function removeEl (line 1) | function removeEl(selector: string) {
function useDefer (line 8) | function useDefer(maxFrameCount = 1) {
function ensureRenderComplete (line 22) | function ensureRenderComplete(callback: () => void) {
FILE: src/@core/utils/formatters.ts
function avatarText (line 10) | function avatarText(value: string) {
function kFormatter (line 18) | function kFormatter(num: number) {
function formatDownloadCount (line 27) | function formatDownloadCount(num: number): string {
function formatDate (line 40) | function formatDate(
function formatDateToMonthShort (line 55) | function formatDateToMonthShort(value: string, toTimeForCurrentDay = tru...
function formatFileSize (line 70) | function formatFileSize(bytes: number, decimals = 2, prefix = false) {
function formatSeconds (line 92) | function formatSeconds(seconds: number) {
function parseDate (line 109) | function parseDate(dateString: string): Date | null {
function formatBytes (line 117) | function formatBytes(bytes: number, decimals = 2) {
function formatEp (line 130) | function formatEp(nums: number[]): string {
function formatDateDifference (line 159) | function formatDateDifference(dateString: string): string {
function formatRating (line 165) | function formatRating(rating: number): string {
FILE: src/@core/utils/image.ts
function rgbStringToHex (line 4) | function rgbStringToHex(rgbArray: number[]): string {
function getDominantColor (line 18) | async function getDominantColor(image: HTMLImageElement): Promise<string> {
function preloadImage (line 25) | async function preloadImage(url: string): Promise<boolean> {
FILE: src/@core/utils/index.ts
function isEmpty (line 2) | function isEmpty(value: unknown): boolean {
function isNullOrUndefined (line 9) | function isNullOrUndefined(value: unknown): value is undefined | null {
function isEmptyArray (line 14) | function isEmptyArray(arr: unknown): boolean {
function isObject (line 19) | function isObject(obj: unknown): obj is Record<string, unknown> {
function isToday (line 23) | function isToday(date: Date) {
function isContained (line 36) | function isContained(subArray: any[], mainArray: any[]): boolean {
function isIntersected (line 41) | function isIntersected(array1: any[], array2: any[]): boolean {
function isNullOrEmptyObject (line 45) | function isNullOrEmptyObject(obj: any): boolean {
function checkPrefersColorSchemeIsDark (line 54) | function checkPrefersColorSchemeIsDark(): boolean {
function getQueryValue (line 63) | function getQueryValue(key: string, url = window.location.href): string {
FILE: src/@core/utils/navigator.ts
function getClipboardContent (line 4) | async function getClipboardContent() {
function copyToClipboard (line 19) | async function copyToClipboard(content: string) {
function urlBase64ToUint8Array (line 25) | function urlBase64ToUint8Array(base64String: string) {
function bufferToBase64Url (line 39) | function bufferToBase64Url(buffer: ArrayBuffer): string {
function base64UrlToUint8Array (line 47) | function base64UrlToUint8Array(base64Url: string): Uint8Array {
FILE: src/@core/utils/theme.ts
function saveLocalTheme (line 1) | function saveLocalTheme(name: string, theme: any) {
FILE: src/@core/utils/workflow.ts
function getId (line 8) | function getId() {
function useDragAndDrop (line 26) | function useDragAndDrop() {
FILE: src/@iconify/build-icons.ts
type BundleScriptCustomSVGConfig (line 38) | interface BundleScriptCustomSVGConfig {
type BundleScriptCustomJSONConfig (line 50) | interface BundleScriptCustomJSONConfig {
type BundleScriptConfig (line 59) | interface BundleScriptConfig {
function removeMetaData (line 286) | function removeMetaData(iconSet: IconifyJSON) {
function organizeIconsList (line 304) | function organizeIconsList(icons: string[]): Record<string, string[]> {
FILE: src/@layouts/types.d.ts
type UserConfig (line 5) | interface UserConfig {
type Config (line 45) | interface Config {
type AclProperties (line 81) | interface AclProperties {
type NavSectionTitle (line 87) | interface NavSectionTitle extends Partial<AclProperties> {
type ATagTargetAttrValues (line 92) | type ATagTargetAttrValues = '_blank' | '_self' | '_parent' | '_top' | 'f...
type ATagRelAttrValues (line 93) | type ATagRelAttrValues =
type NavLinkProps (line 108) | interface NavLinkProps {
type NavLink (line 115) | interface NavLink extends NavLinkProps, Partial<AclProperties> {
type NavMenu (line 124) | interface NavMenu extends NavLink {
type NavGroup (line 132) | interface NavGroup extends Partial<AclProperties> {
type VerticalNavItems (line 141) | type VerticalNavItems = (NavLink | NavGroup | NavSectionTitle)[]
type HorizontalNavItems (line 142) | type HorizontalNavItems = (NavLink | NavGroup)[]
type I18nLanguage (line 146) | interface I18nLanguage {
type Notification (line 153) | type Notification = {
type ThemeSwitcherTheme (line 166) | interface ThemeSwitcherTheme {
FILE: src/@layouts/utils.ts
function hexToRgb (line 27) | function hexToRgb(hex: string) {
FILE: src/@validators/index.ts
type ValidationRule (line 1) | type ValidationRule = (value: any) => string | boolean
FILE: src/ace-config.ts
function registerJinja2Mode (line 51) | function registerJinja2Mode() {
FILE: src/api/index.ts
type Window (line 14) | interface Window {
FILE: src/api/nprogress.ts
function configureNProgress (line 4) | function configureNProgress() {
function startNProgress (line 10) | function startNProgress() {
function doneNProgress (line 14) | function doneNProgress() {
FILE: src/api/types.ts
type Subscribe (line 2) | interface Subscribe {
type SubscribeShare (line 86) | interface SubscribeShare {
type WorkflowShare (line 148) | interface WorkflowShare {
type TransferHistory (line 184) | interface TransferHistory {
type MediaInfo (line 232) | interface MediaInfo {
type MediaSeason (line 338) | interface MediaSeason {
type TmdbSeason (line 356) | interface TmdbSeason {
type MediaRelease (line 374) | interface MediaRelease {
type TmdbEpisode (line 386) | interface TmdbEpisode {
type Person (line 410) | interface Person {
type Site (line 467) | interface Site {
type SiteStatistic (line 513) | interface SiteStatistic {
type SiteUserData (line 531) | interface SiteUserData {
type DownloadingInfo (line 573) | interface DownloadingInfo {
type NotExistMediaInfo (line 605) | interface NotExistMediaInfo {
type Plugin (line 617) | interface Plugin {
type PluginSidebarNavItem (line 660) | interface PluginSidebarNavItem {
type RenderProps (line 671) | interface RenderProps {
type DashboardItem (line 682) | interface DashboardItem {
type TorrentInfo (line 700) | interface TorrentInfo {
type MetaInfo (line 758) | interface MetaInfo {
type Context (line 840) | interface Context {
type User (line 850) | interface User {
type PassKey (line 876) | interface PassKey {
type Storage (line 886) | interface Storage {
type MediaStatistic (line 894) | interface MediaStatistic {
type Process (line 906) | interface Process {
type DownloaderInfo (line 924) | interface DownloaderInfo {
type ScheduleInfo (line 938) | interface ScheduleInfo {
type NotificationSwitch (line 952) | interface NotificationSwitch {
type EndPoints (line 965) | interface EndPoints {
type FileItem (line 981) | interface FileItem {
type MediaServerPlayItem (line 1013) | interface MediaServerPlayItem {
type MediaServerLibrary (line 1035) | interface MediaServerLibrary {
type Message (line 1059) | interface Message {
type SystemNotification (line 1083) | interface SystemNotification {
type DownloaderConf (line 1097) | interface DownloaderConf {
type NotificationConf (line 1113) | interface NotificationConf {
type NotificationSwitchConf (line 1127) | interface NotificationSwitchConf {
type StorageConf (line 1135) | interface StorageConf {
type MediaServerConf (line 1145) | interface MediaServerConf {
type TransferDirectoryConf (line 1159) | interface TransferDirectoryConf {
type CustomRule (line 1201) | interface CustomRule {
type FilterRuleGroup (line 1219) | interface FilterRuleGroup {
type SubscribeDownloadFileInfo (line 1231) | interface SubscribeDownloadFileInfo {
type SubscribeLibraryFileInfo (line 1245) | interface SubscribeLibraryFileInfo {
type SubscribeEpisodeInfo (line 1253) | interface SubscribeEpisodeInfo {
type SubscrbieInfo (line 1267) | interface SubscrbieInfo {
type TransferForm (line 1275) | interface TransferForm {
type TransferQueue (line 1317) | interface TransferQueue {
type DiscoverSource (line 1334) | interface DiscoverSource {
type RecommendSource (line 1350) | interface RecommendSource {
type SiteCategory (line 1360) | interface SiteCategory {
type Workflow (line 1367) | interface Workflow {
type TorrentCacheItem (line 1399) | interface TorrentCacheItem {
type TorrentCacheData (line 1435) | interface TorrentCacheData {
type SubscribeShareStatistics (line 1445) | interface SubscribeShareStatistics {
type ApiResponse (line 1455) | interface ApiResponse<T = any> {
type CategoryRule (line 1462) | interface CategoryRule {
type CategoryConfig (line 1471) | interface CategoryConfig {
FILE: src/composables/useAvailableHeight.ts
function useAvailableHeight (line 15) | function useAvailableHeight(
FILE: src/composables/useBackgroundOptimization.ts
function useBackgroundOptimization (line 9) | function useBackgroundOptimization() {
FILE: src/composables/useCacheManager.ts
type CacheInfo (line 1) | interface CacheInfo {
function useCacheManager (line 7) | function useCacheManager() {
FILE: src/composables/useConfirm.ts
type ConfirmOptions (line 8) | interface ConfirmOptions {
function createConfirmDialog (line 20) | async function createConfirmDialog(options: ConfirmOptions = {}) {
function useConfirm (line 79) | function useConfirm() {
FILE: src/composables/useDynamicButton.ts
type Window (line 18) | interface Window {
type MaybeRefValue (line 24) | type MaybeRefValue<T> = T | Ref<T> | ComputedRef<T>
type DynamicButtonMenuItem (line 26) | interface DynamicButtonMenuItem {
function resolveMaybeRef (line 37) | function resolveMaybeRef<T>(value: MaybeRefValue<T> | undefined, fallbac...
function useDynamicButton (line 56) | function useDynamicButton(options: {
FILE: src/composables/useDynamicHeaderTab.ts
type DynamicHeaderTabButton (line 5) | interface DynamicHeaderTabButton {
type DynamicHeaderTabItem (line 16) | interface DynamicHeaderTabItem {
type DynamicHeaderTabConfig (line 22) | interface DynamicHeaderTabConfig {
function useDynamicHeaderTab (line 30) | function useDynamicHeaderTab() {
FILE: src/composables/useInfiniteScroll.ts
type InfiniteScrollStatus (line 3) | type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error'
function useInfiniteScroll (line 11) | function useInfiniteScroll<T>(
FILE: src/composables/useOfflineStatus.ts
constant MAX_CONSECUTIVE_ERRORS (line 8) | const MAX_CONSECUTIVE_ERRORS = 3
function useGlobalOfflineStatus (line 11) | function useGlobalOfflineStatus() {
function useOfflineStatus (line 70) | function useOfflineStatus(initialMessage?: string) {
FILE: src/composables/usePWA.ts
type UIMode (line 16) | type UIMode = 'auto' | 'desktop' | 'app'
function setUIMode (line 20) | function setUIMode(mode: UIMode) {
function initializePWAGlobally (line 26) | async function initializePWAGlobally() {
function usePWA (line 54) | function usePWA() {
FILE: src/composables/usePWAInstall.ts
type BeforeInstallPromptEvent (line 1) | interface BeforeInstallPromptEvent extends Event {
type WindowEventMap (line 11) | interface WindowEventMap {
function usePWAInstall (line 16) | function usePWAInstall() {
FILE: src/composables/usePullDownGesture.ts
type PullDownConfig (line 6) | interface PullDownConfig {
type PullDownOptions (line 17) | interface PullDownOptions {
constant DEFAULT_CONFIG (line 28) | const DEFAULT_CONFIG: PullDownConfig = {
function usePullDownGesture (line 38) | function usePullDownGesture(options: PullDownOptions = {}) {
FILE: src/composables/useRecentPlugins.ts
constant RECENT_PLUGINS_KEY (line 3) | const RECENT_PLUGINS_KEY = 'moviepilot_recent_plugins'
constant MAX_RECENT_PLUGINS (line 4) | const MAX_RECENT_PLUGINS = 3
type RecentPlugin (line 6) | interface RecentPlugin {
function pluginToRecentPlugin (line 17) | function pluginToRecentPlugin(plugin: Plugin): RecentPlugin {
function recentPluginToPlugin (line 30) | function recentPluginToPlugin(recentPlugin: RecentPlugin): Plugin {
function useRecentPlugins (line 41) | function useRecentPlugins() {
FILE: src/composables/useSetupWizard.ts
type WizardData (line 9) | interface WizardData {
type ConnectivityTestState (line 85) | interface ConnectivityTestState {
type ValidationErrorState (line 93) | interface ValidationErrorState {
function normalizeThinkingLevelValue (line 125) | function normalizeThinkingLevelValue(value?: unknown) {
function resolveThinkingLevelValue (line 142) | function resolveThinkingLevelValue(data?: Record<string, any>) {
function useSetupWizard (line 285) | function useSetupWizard() {
FILE: src/composables/useStateRestore.ts
function useTabStateRestore (line 18) | function useTabStateRestore(defaultTab?: string) {
function useRouteStateRestore (line 74) | function useRouteStateRestore() {
function useStateRestore (line 133) | function useStateRestore() {
function usePageStateRestore (line 169) | function usePageStateRestore(defaultTab?: string) {
FILE: src/composables/useTorrentFilter.ts
type SearchTorrent (line 6) | interface SearchTorrent extends Context {
type GroupedItem (line 10) | interface GroupedItem {
type FilterState (line 16) | interface FilterState {
function useTorrentFilter (line 24) | function useTorrentFilter() {
FILE: src/composables/useVersionChecker.ts
function useVersionChecker (line 67) | function useVersionChecker() {
FILE: src/plugins/i18n.ts
function getBrowserLocale (line 27) | function getBrowserLocale(): SupportedLocale | null {
function setI18nLanguage (line 48) | async function setI18nLanguage(locale: SupportedLocale) {
function getCurrentLocale (line 62) | function getCurrentLocale(): SupportedLocale {
FILE: src/plugins/stateRestore.ts
class RouteStateManager (line 12) | class RouteStateManager {
method saveCurrentRoute (line 16) | saveCurrentRoute() {
method restoreRoute (line 27) | restoreRoute() {
method clearRoute (line 46) | clearRoute() {
method init (line 51) | init() {
class TabStateManager (line 73) | class TabStateManager {
method saveTabState (line 77) | saveTabState(routePath: string, activeTab: string) {
method getTabState (line 91) | getTabState(routePath: string): string | null {
method getAllTabStates (line 112) | private getAllTabStates(): Record<string, any> {
method clearTabState (line 122) | clearTabState(routePath?: string) {
class StateRestore (line 137) | class StateRestore {
method init (line 142) | init() {
method setupAutoRestore (line 148) | private setupAutoRestore() {
method checkAndRestoreRoute (line 169) | private checkAndRestoreRoute() {
method clearAllStates (line 190) | clearAllStates() {
method install (line 203) | install(app: App) {
FILE: src/router/i18n-menu.ts
function getNavMenus (line 5) | function getNavMenus(t: Composer['t']) {
function getSettingTabs (line 150) | function getSettingTabs(t: Composer['t']) {
function getSubscribeMovieTabs (line 198) | function getSubscribeMovieTabs(t: Composer['t']) {
function getSubscribeTvTabs (line 214) | function getSubscribeTvTabs(t: Composer['t']) {
function getPluginTabs (line 235) | function getPluginTabs(t: Composer['t']) {
function getDiscoverTabs (line 251) | function getDiscoverTabs(t: Composer['t']) {
function getWorkflowTabs (line 272) | function getWorkflowTabs(t: Composer['t']) {
type PluginSidebarSection (line 288) | type PluginSidebarSection = 'start' | 'discovery' | 'subscribe' | 'organ...
function pluginSidebarSectionToHeaderKey (line 293) | function pluginSidebarSectionToHeaderKey(section: string, t: Composer['t...
FILE: src/router/index.ts
method scrollBehavior (line 12) | scrollBehavior(to: any, from: any, savedPosition: any) {
FILE: src/service-worker.ts
constant RESOURCE_VERSION (line 14) | const RESOURCE_VERSION = 'V2'
constant CACHE_VERSION (line 26) | const CACHE_VERSION = `${appVersion}-${buildTime}`
constant UNREAD_COUNT_KEY (line 63) | const UNREAD_COUNT_KEY = 'mp_unread_count'
function cleanupRuntimeCaches (line 183) | async function cleanupRuntimeCaches(onlyOld: boolean = false) {
function openDB (line 219) | async function openDB(): Promise<IDBDatabase> {
function get (line 233) | async function get(key: string, storeName: string = 'badge'): Promise<an...
function set (line 252) | async function set(key: string, value: any, storeName: string = 'badge')...
function getStoredUnreadCount (line 272) | async function getStoredUnreadCount(): Promise<number> {
function setStoredUnreadCount (line 277) | async function setStoredUnreadCount(count: number): Promise<void> {
function updateBadge (line 281) | async function updateBadge(count: number) {
function clearBadge (line 295) | async function clearBadge() {
function monitorCacheSize (line 307) | async function monitorCacheSize() {
FILE: src/stores/auth.ts
method setToken (line 16) | setToken(token: string | null) {
method clearToken (line 19) | clearToken() {
method setRemember (line 22) | setRemember(remember: boolean) {
method setOriginalPath (line 25) | setOriginalPath(originalPath: string | null) {
method login (line 28) | login(payload: authState) {
method logout (line 32) | logout() {
FILE: src/stores/global.ts
method initialize (line 15) | async initialize() {
method loadUserSettings (line 45) | async loadUserSettings() {
method setData (line 57) | setData(data: { [key: string]: any }) {
method get (line 62) | get(key: string) {
method reset (line 66) | reset() {
FILE: src/stores/pluginSidebarNav.ts
method ensureSidebarNav (line 22) | async ensureSidebarNav(force = false): Promise<void> {
method reset (line 43) | reset() {
FILE: src/stores/types.ts
type authState (line 1) | interface authState {
type userState (line 10) | interface userState {
type globalSettingsState (line 27) | interface globalSettingsState {
FILE: src/stores/user.ts
method setSuperUser (line 20) | setSuperUser(superUser: boolean) {
method setUserID (line 23) | setUserID(userID: number) {
method setUserName (line 26) | setUserName(userName: string) {
method setAvatar (line 29) | setAvatar(avatar: string) {
method setLevel (line 32) | setLevel(level: number) {
method setPermissions (line 35) | setPermissions(permissions: object) {
method setWizard (line 38) | setWizard(wizard: boolean) {
method loginUser (line 41) | loginUser(payload: userState) {
method reset (line 50) | reset() {
FILE: src/types/global.d.ts
type Navigator (line 6) | interface Navigator {
FILE: src/types/i18n.ts
type LocaleInfo (line 1) | interface LocaleInfo {
constant SUPPORTED_LOCALES (line 7) | const SUPPORTED_LOCALES: Record<string, LocaleInfo> = {
type SupportedLocale (line 25) | type SupportedLocale = keyof typeof SUPPORTED_LOCALES
FILE: src/types/service-worker-sync.d.ts
type SyncManager (line 7) | interface SyncManager {
type ServiceWorkerRegistration (line 17) | interface ServiceWorkerRegistration {
type SyncEvent (line 27) | interface SyncEvent extends ExtendableEvent {
type ServiceWorkerGlobalScopeEventMap (line 35) | interface ServiceWorkerGlobalScopeEventMap {
FILE: src/types/workbox-precaching.d.ts
type ManifestEntry (line 8) | interface ManifestEntry {
FILE: src/utils/appDeepLink.ts
type AppType (line 15) | type AppType = 'plex' | 'jellyfin' | 'emby' | 'trimemedia' | 'douban'
type DeepLinkConfig (line 18) | interface DeepLinkConfig {
constant DEEP_LINK_CONFIGS (line 25) | const DEEP_LINK_CONFIGS: Record<AppType, DeepLinkConfig> = {
type DoubanAppParams (line 54) | interface DoubanAppParams {
function openApp (line 67) | async function openApp(appType: AppType, params: string | DoubanAppParam...
function getWebUrl (line 108) | function getWebUrl(appType: AppType, params: string | DoubanAppParams, f...
function buildDeepLinkUrl (line 127) | function buildDeepLinkUrl(appType: AppType, params: string | DoubanAppPa...
function buildPlexDeepLink (line 163) | function buildPlexDeepLink(playUrl: string): string {
function buildJellyfinDeepLink (line 301) | function buildJellyfinDeepLink(playUrl: string): string {
function buildEmbyDeepLink (line 397) | function buildEmbyDeepLink(playUrl: string): string {
function buildTrimemediaDeepLink (line 507) | function buildTrimemediaDeepLink(playUrl: string): string {
function buildDoubanDeepLink (line 560) | function buildDoubanDeepLink(params: DoubanAppParams): string {
function attemptAppLaunch (line 582) | async function attemptAppLaunch(appUrl: string, timeout: number): Promis...
function openMediaServerWithAutoDetect (line 627) | async function openMediaServerWithAutoDetect(
function openDoubanApp (line 671) | async function openDoubanApp(
function getAppDownloadUrl (line 693) | function getAppDownloadUrl(appType: AppType): string {
function checkAppInstalled (line 715) | function checkAppInstalled(appType: AppType): boolean {
FILE: src/utils/backgroundManager.ts
class BackgroundManager (line 5) | class BackgroundManager {
method constructor (line 19) | constructor() {
method setupVisibilityListener (line 24) | private setupVisibilityListener() {
method setupActivityTracking (line 44) | private setupActivityTracking() {
method addTimer (line 69) | addTimer(
method removeTimer (line 117) | removeTimer(id: string) {
method pauseAllTimers (line 131) | private pauseAllTimers() {
method resumeAllTimers (line 144) | private resumeAllTimers() {
method getTimerStatus (line 168) | getTimerStatus(id: string): 'running' | 'paused' | 'not-found' {
method getTimersInfo (line 177) | getTimersInfo(): Array<{
method isUserActive (line 196) | isUserActive(maxInactiveTime = 5 * 60 * 1000): boolean {
method getLastActivityTime (line 203) | getLastActivityTime(): number {
method getStatus (line 210) | getStatus(): {
method destroy (line 229) | destroy() {
function addBackgroundTimer (line 258) | function addBackgroundTimer(
function removeBackgroundTimer (line 270) | function removeBackgroundTimer(id: string) {
function getBackgroundTimerStatus (line 274) | function getBackgroundTimerStatus(id: string) {
FILE: src/utils/badge.ts
type UnreadMessageEvent (line 6) | interface UnreadMessageEvent extends CustomEvent {
function emitUnreadMessageEvent (line 11) | function emitUnreadMessageEvent(count: number) {
function onUnreadMessage (line 17) | function onUnreadMessage(callback: (count: number) => void) {
function waitForServiceWorker (line 27) | async function waitForServiceWorker(): Promise<ServiceWorker | null> {
function checkUnreadOnStartup (line 72) | async function checkUnreadOnStartup(): Promise<number> {
function checkAndEmitUnreadMessages (line 88) | async function checkAndEmitUnreadMessages() {
function clearAppBadge (line 100) | async function clearAppBadge(): Promise<boolean> {
function updateAppBadge (line 128) | async function updateAppBadge(count: number): Promise<boolean> {
function getUnreadCount (line 160) | async function getUnreadCount(): Promise<number> {
function supportsBadgeAPI (line 182) | function supportsBadgeAPI(): boolean {
FILE: src/utils/colorUtils.ts
constant COLORS (line 2) | const COLORS = [
function generateRandomColor (line 88) | function generateRandomColor(): string {
function getItemColor (line 97) | function getItemColor(itemKey: string): string {
function initializeItemColors (line 109) | function initializeItemColors<T>(items: T[], keyExtractor: (item: T) => ...
function clearColorCache (line 119) | function clearColorCache(): void {
function getAllColors (line 127) | function getAllColors(): string[] {
function getColorCount (line 135) | function getColorCount(): number {
FILE: src/utils/federationLoader.ts
type RemoteModule (line 13) | interface RemoteModule {
function fetchSingleRemoteModule (line 22) | async function fetchSingleRemoteModule(id: string): Promise<RemoteModule...
function navKeyToPascalSegment (line 35) | function navKeyToPascalSegment(navKey: string): string {
function loadRemoteAppPageComponent (line 56) | async function loadRemoteAppPageComponent(id: string, navKey: string = '...
function loadRemoteComponent (line 89) | async function loadRemoteComponent(id: string, componentName: string = '...
function fetchRemoteModules (line 118) | async function fetchRemoteModules(): Promise<RemoteModule[]> {
function injectRemoteModule (line 134) | function injectRemoteModule(module: RemoteModule): void {
function loadRemoteComponents (line 158) | async function loadRemoteComponents(): Promise<void> {
FILE: src/utils/globalSetting.ts
function fetchGlobalSettings (line 6) | async function fetchGlobalSettings() {
FILE: src/utils/imageUtils.ts
function getLogoUrl (line 73) | function getLogoUrl(logoName: string): string {
function getAvailableLogos (line 81) | function getAvailableLogos(): string[] {
function hasLogo (line 90) | function hasLogo(logoName: string): boolean {
FILE: src/utils/loadingStateManager.ts
class PWALoadingStateManager (line 5) | class PWALoadingStateManager {
method setLoadingState (line 14) | setLoadingState(key: string, loading: boolean): void {
method isAnyLoading (line 28) | isAnyLoading(): boolean {
method waitForAllComplete (line 35) | waitForAllComplete(): Promise<void> {
method addListener (line 58) | addListener(listener: (isLoading: boolean) => void): void {
method removeListener (line 66) | removeListener(listener: (isLoading: boolean) => void): void {
method notifyListeners (line 74) | private notifyListeners(isLoading: boolean): void {
method getLoadingStates (line 87) | getLoadingStates(): Record<string, boolean> {
method reset (line 94) | reset(): void {
FILE: src/utils/permission.ts
type UserPermissions (line 2) | interface UserPermissions {
constant DEFAULT_PERMISSIONS (line 10) | const DEFAULT_PERMISSIONS: UserPermissions = {
constant ADMIN_PERMISSIONS (line 18) | const ADMIN_PERMISSIONS: UserPermissions = {
function hasPermission (line 26) | function hasPermission(userPermissions: any, permission: keyof UserPermi...
function hasAnyPermission (line 38) | function hasAnyPermission(userPermissions: any, permissionList: (keyof U...
function hasAllPermissions (line 43) | function hasAllPermissions(userPermissions: any, permissionList: (keyof ...
function filterMenusByPermission (line 48) | function filterMenusByPermission(menus: any[], userPermissions: any): an...
FILE: src/utils/pluginSidebarNav.ts
type PluginNavMenuEntry (line 7) | type PluginNavMenuEntry = {
function navMenuFromPluginSidebarItem (line 15) | function navMenuFromPluginSidebarItem(
function filterPluginSidebarNavEntries (line 39) | function filterPluginSidebarNavEntries(
FILE: src/utils/requestOptimizer.ts
function setNavigatingState (line 8) | function setNavigatingState(navigating: boolean) {
function abortAllActiveRequests (line 19) | function abortAllActiveRequests() {
function cleanupController (line 29) | function cleanupController(controller: AbortController) {
function initializeRequestOptimizer (line 34) | function initializeRequestOptimizer(axiosInstance: any) {
function getActiveRequestsCount (line 91) | function getActiveRequestsCount() {
function abortAllRequests (line 96) | function abortAllRequests() {
FILE: src/utils/sseManager.ts
class SSEManager (line 5) | class SSEManager {
method constructor (line 20) | constructor(url: string, options: Partial<typeof SSEManager.prototype....
method setupVisibilityListener (line 32) | private setupVisibilityListener() {
method handleBackground (line 47) | private handleBackground() {
method handleForeground (line 63) | private handleForeground() {
method reconnectSSE (line 78) | private reconnectSSE(attemptCount = 0) {
method addMessageListener (line 150) | addMessageListener(id: string, listener: (event: MessageEvent) => void) {
method removeMessageListener (line 162) | removeMessageListener(id: string) {
method close (line 174) | close() {
method readyState (line 198) | get readyState(): number {
method connectionUrl (line 205) | get connectionUrl(): string {
method forceReconnect (line 212) | forceReconnect() {
method hasActiveListeners (line 222) | get hasActiveListeners(): boolean {
method currentReconnectAttempts (line 229) | get currentReconnectAttempts(): number {
method hasReachedMaxAttempts (line 236) | get hasReachedMaxAttempts(): boolean {
class SSEManagerSingleton (line 244) | class SSEManagerSingleton {
method getManager (line 253) | getManager(url: string, options?: ConstructorParameters<typeof SSEMana...
method getIndependentManager (line 269) | getIndependentManager(
method closeManager (line 285) | closeManager(url: string) {
method closeAllManagers (line 296) | closeAllManagers() {
FILE: src/utils/themeManager.ts
type ThemeConfig (line 2) | interface ThemeConfig {
class ThemeManager (line 8) | class ThemeManager {
method constructor (line 13) | constructor() {
method registerTheme (line 27) | registerTheme(name: string, cssPath: string): void {
method getCurrentTheme (line 38) | getCurrentTheme(): string {
method setTheme (line 45) | async setTheme(themeName: string): Promise<void> {
method loadThemeCSS (line 73) | private async loadThemeCSS(themeName: string, cssPath: string): Promis...
method applyTheme (line 117) | private applyTheme(themeName: string): void {
method unloadTheme (line 135) | unloadTheme(themeName: string): void {
method unloadOtherThemes (line 157) | unloadOtherThemes(): void {
method getAvailableThemes (line 168) | getAvailableThemes(): string[] {
method isThemeLoaded (line 175) | isThemeLoaded(themeName: string): boolean {
method dispatchThemeChangeEvent (line 182) | private dispatchThemeChangeEvent(themeName: string): void {
method onThemeChange (line 192) | onThemeChange(callback: (theme: string) => void): void {
method offThemeChange (line 201) | offThemeChange(callback: (theme: string) => void): void {
Condensed preview — 377 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,873K chars).
[
{
"path": ".editorconfig",
"chars": 751,
"preview": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines wit"
},
{
"path": ".eslintrc.js",
"chars": 1842,
"preview": "module.exports = {\n env: {\n browser: true,\n es2021: true,\n },\n extends: ['@antfu/eslint-config-vue', 'plugin:so"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1421,
"preview": "name: 问题反馈\ndescription: File a bug report\ntitle: \"[错误报告]:请在此处简单描述你的问题\"\nlabels: [\"bug\"]\nbody:\n - type: markdown\n attr"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 209,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Telegram 频道\n url: https://t.me/moviepilot_channel\n about: "
},
{
"path": ".github/ISSUE_TEMPLATE/discussion.yml",
"chars": 628,
"preview": "name: 项目讨论\ndescription: discussion\ntitle: \"[Discussion]: \"\nlabels: [\"discussion\"]\nbody:\n - type: markdown\n attribute"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 636,
"preview": "name: 功能改进\ndescription: Feature Request\ntitle: \"[Feature Request]: \"\nlabels: [\"feature request\"]\nbody:\n - type: markdow"
},
{
"path": ".github/ISSUE_TEMPLATE/rfc.yml",
"chars": 1344,
"preview": "name: 功能提案\ndescription: Request for Comments\ntitle: '[RFC]'\nlabels: ['RFC']\nbody:\n - type: markdown\n attributes:\n "
},
{
"path": ".github/workflows/build.yml",
"chars": 1684,
"preview": "name: Build Moviepilot-Frontend v2\n\non:\n workflow_dispatch:\n push:\n branches:\n - v2\n paths:\n - 'packag"
},
{
"path": ".gitignore",
"chars": 451,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Stor"
},
{
"path": ".prettierignore",
"chars": 17,
"preview": "dist\nnode_modules"
},
{
"path": ".prettierrc.json",
"chars": 472,
"preview": "{\n \"arrowParens\": \"avoid\",\n \"bracketSpacing\": true,\n \"htmlWhitespaceSensitivity\": \"css\",\n \"insertPragma\": false,\n \""
},
{
"path": ".stylelintrc.json",
"chars": 617,
"preview": "{\n \"extends\": [\n \"stylelint-config-standard-scss\",\n \"stylelint-config-idiomatic-order\"\n ],\n \"plug"
},
{
"path": ".vscode/anchor-comments.code-snippets",
"chars": 466,
"preview": "{\n \"Add hand emoji\": {\n \"prefix\": \"cm-hand-emoji\",\n \"body\": [\n \"👉\"\n ],\n \"descr"
},
{
"path": ".vscode/extensions.json",
"chars": 405,
"preview": "{\n \"recommendations\": [\n \"dbaeumer.vscode-eslint\",\n \"esbenp.prettier-vscode\",\n \"mgmcdermott.vscode-language-ba"
},
{
"path": ".vscode/settings.json",
"chars": 2999,
"preview": "{\n \"editor.formatOnSave\": true,\n \"javascript.updateImportsOnFileMove.enabled\": \"always\",\n \"editor.defaultFormatter\": "
},
{
"path": ".vscode/vue-ts.code-snippets",
"chars": 423,
"preview": "{\n \"Vue TS - DefineProps\": {\n \"prefix\": \"dprops\",\n \"body\": [\n \"defineProps<${1:Props}>()\"\n "
},
{
"path": ".vscode/vue.code-snippets",
"chars": 1602,
"preview": "{\n \"script\": {\n \"prefix\": \"vue-sfc-ts\",\n \"body\": [\n \"<script lang=\\\"ts\\\" setup>\",\n "
},
{
"path": ".vscode/vuetify.code-snippets",
"chars": 1383,
"preview": "{\n \"Vuetify Menu -- Parent Activator\": {\n \"prefix\": \"v-menu\",\n \"body\": [\n \"<v-btn color=\\\"primary\\\">\",\n "
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2023 jxxghp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "README.md",
"chars": 1016,
"preview": "# MoviePilot-Frontend\n\n*中文 | [English](README_EN.md)*\n\n[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目,NodeJS版本"
},
{
"path": "README_EN.md",
"chars": 1575,
"preview": "# MoviePilot-Frontend\n\n*[中文](README.md) | English*\n\nFrontend project for [MoviePilot](https://github.com/jxxghp/MoviePil"
},
{
"path": "auto-imports.d.ts",
"chars": 48690,
"preview": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin"
},
{
"path": "components.d.ts",
"chars": 1144,
"preview": "/* eslint-disable */\n// @ts-nocheck\n// Generated by unplugin-vue-components\n// Read more: https://github.com/vuejs/core/"
},
{
"path": "docs/federation-troubleshooting.md",
"chars": 1796,
"preview": "# MoviePilot 模块联邦问题排查指南\n\n本文档提供了针对 MoviePilot 项目中使用模块联邦时可能遇到的常见问题及解决方案。\n\n## 远程组件注册机制\n\nMoviePilot 使用自动注册机制来加载远程组件:\n\n1. 对于使"
},
{
"path": "docs/module-federation-guide.md",
"chars": 9901,
"preview": "# MoviePilot前端远程模块开发指南\n\n## 1. 概述\n\nMoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态加载和集成。本文档详细说明如何开发符合要求的远程模块,以便在MoviePilot"
},
{
"path": "env.d.ts",
"chars": 346,
"preview": "import 'vue-router'\n\ndeclare module 'vue-router' {\n interface RouteMeta {\n action?: string\n subject?: string\n "
},
{
"path": "examples/plugin-component/README.md",
"chars": 895,
"preview": "# MoviePilot 插件远程组件示例\n\n这是 MoviePilot 插件远程组件的示例项目,展示了如何正确配置和开发与主应用兼容的远程组件。本示例包含 Page、Config、Dashboard、AppPage,以及可选的 `AppP"
},
{
"path": "examples/plugin-component/index.html",
"chars": 619,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-wid"
},
{
"path": "examples/plugin-component/package.json",
"chars": 491,
"preview": "{\n \"name\": \"moviepilot-plugin-component\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n "
},
{
"path": "examples/plugin-component/src/App.vue",
"chars": 3337,
"preview": "<template>\n <div class=\"app-container\">\n <v-app>\n <v-app-bar color=\"primary\" app>\n <v-app-bar-title>Movi"
},
{
"path": "examples/plugin-component/src/components/AppPage.vue",
"chars": 692,
"preview": "<script setup lang=\"ts\">\n/**\n * 侧栏全页:在主应用 #/plugin-app/:pluginId/:navKey 中渲染,占据主内容区。\n * 需在插件后端实现 get_sidebar_nav 才会出现在侧栏"
},
{
"path": "examples/plugin-component/src/components/AppPageSettings.vue",
"chars": 500,
"preview": "<script setup lang=\"ts\">\n/**\n * 示例:nav_key=settings 时主应用会优先加载 AppPageSettings,再回退 AppPage。\n */\nconst props = defineProps"
},
{
"path": "examples/plugin-component/src/components/Config.vue",
"chars": 5906,
"preview": "<template>\n <div class=\"plugin-config\">\n <v-card>\n <v-card-item>\n <v-card-title>插件配置</v-card-title>\n "
},
{
"path": "examples/plugin-component/src/components/Dashboard.vue",
"chars": 7806,
"preview": "<template>\n <div class=\"dashboard-widget\">\n <v-card v-if=\"!config?.attrs?.border\" flat>\n <v-card-text class=\"pa"
},
{
"path": "examples/plugin-component/src/components/Page.vue",
"chars": 4519,
"preview": "<template>\n <div class=\"plugin-page\">\n <v-card>\n <v-card-item>\n <v-card-title>{{ title }}</v-card-title>"
},
{
"path": "examples/plugin-component/src/main.js",
"chars": 489,
"preview": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport { createVuetify } from 'vuetify'\nimport * as componen"
},
{
"path": "examples/plugin-component/src/vuetify/defaults.ts",
"chars": 2713,
"preview": "export default {\n IconBtn: {\n icon: true,\n color: 'default',\n variant: 'text',\n VIcon: {\n size: 24,\n "
},
{
"path": "examples/plugin-component/src/vuetify/theme.ts",
"chars": 7135,
"preview": "import type { VuetifyOptions } from 'vuetify'\n\nconst theme: VuetifyOptions['theme'] = {\n defaultTheme: 'light',\n theme"
},
{
"path": "examples/plugin-component/vite.config.js",
"chars": 2004,
"preview": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport federation from '@originjs/vite-plugin-f"
},
{
"path": "index.html",
"chars": 11224,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\" style=\"\n overflow: hidden auto;\n min-block-size: 100vh;\n min-block-size: 100"
},
{
"path": "package.json",
"chars": 4290,
"preview": "{\n \"name\": \"moviepilot\",\n \"version\": \"2.10.5\",\n \"private\": true,\n \"type\": \"module\",\n \"bin\": \"dist/service.js\",\n \"s"
},
{
"path": "postcss.config.js",
"chars": 80,
"preview": "export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "public/nginx.conf",
"chars": 3228,
"preview": "worker_processes auto;\n\nevents {\n worker_connections 1024;\n}\n\n\nhttp {\n\n sendfile on;\n\n keepalive_timeout 3600;\n"
},
{
"path": "public/offline.html",
"chars": 5291,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width,"
},
{
"path": "public/robots.txt",
"chars": 26,
"preview": "User-agent: *\nDisallow: /\n"
},
{
"path": "public/service.js",
"chars": 1027,
"preview": "const path = require('node:path')\nconst express = require('express')\nconst proxy = require('express-http-proxy')\n\nconst "
},
{
"path": "shims.d.ts",
"chars": 357,
"preview": "declare module '*.vue' {\n import type { DefineComponent } from 'vue'\n \n const component: DefineComponent<{}, {}, any>"
},
{
"path": "src/@core/components/ConfirmDialog.vue",
"chars": 2004,
"preview": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\ninterface Props {\n modelValue: boolean\n type?: 'info' | 'warn"
},
{
"path": "src/@core/components/DialogCloseBtn.vue",
"chars": 451,
"preview": "<script lang=\"ts\" setup>\n// 定义输入参数\nconst props = defineProps({\n // 是否显示\n innerClass: String,\n})\n// 定义触发的自定义事件\nconst em"
},
{
"path": "src/@core/components/ErrorHeader.vue",
"chars": 595,
"preview": "<script setup lang=\"ts\">\ninterface Props {\n errorCode?: string\n errorTitle?: string\n errorDescription?: string\n}\n\ncon"
},
{
"path": "src/@core/components/ExistIcon.vue",
"chars": 890,
"preview": "<template>\n <div class=\"absolute top-0 right-0 flex items-center justify-between p-2\">\n <div class=\"pointer-events-n"
},
{
"path": "src/@core/components/LoadingBanner.vue",
"chars": 1600,
"preview": "<script lang=\"ts\" setup>\n// 定义输入参数\nconst props = defineProps({\n text: String,\n})\n</script>\n\n<template>\n <div class=\"w-"
},
{
"path": "src/@core/components/MoreBtn.vue",
"chars": 429,
"preview": "<script lang=\"ts\" setup>\ninterface Props {\n menuList?: unknown[]\n itemProps?: boolean\n}\n\nconst props = defineProps<Pro"
},
{
"path": "src/@core/components/PageContentTitle.vue",
"chars": 487,
"preview": "<script setup lang=\"ts\">\ndefineProps({\n // 标题\n title: String,\n})\n</script>\n<template>\n <div v-if=\"title\" class=\"my-3 "
},
{
"path": "src/@core/components/ScrollToTopBtn.vue",
"chars": 2103,
"preview": "<script lang=\"ts\" setup>\n// 控制回到顶部按钮的可见性\nconst showScrollToTop = ref(false)\nconst scrollThreshold = 200 // 滚动多少像素后显示按钮\n\n"
},
{
"path": "src/@core/components/StatIcon.vue",
"chars": 377,
"preview": "<script lang=\"ts\" setup>\ninterface Props {\n color?: string\n message?: string\n}\n\nconst props = defineProps<Props>()\n</s"
},
{
"path": "src/@core/libs/apex-chart/apexCharConfig.ts",
"chars": 16758,
"preview": "import type { ThemeInstance } from 'vuetify'\nimport { hexToRgb } from '@layouts/utils'\n\n// 👉 Colors variables\nfunction c"
},
{
"path": "src/@core/scss/README.md",
"chars": 1169,
"preview": "# SCSS结构说明\n\n## 目录整合\n\n本项目SCSS文件已完成整合:\n- 主入口文件:`src/@core/scss/index.scss`\n- 实际功能文件位于:`src/@core/scss/template/index.scss`"
},
{
"path": "src/@core/scss/_components.scss",
"chars": 5977,
"preview": "@use \"mixins\";\n@use \"vuetify/lib/styles/tools/_elevation\" as mixins_elevation;\n@use \"@layouts/styles/_placeholders\";\n@us"
},
{
"path": "src/@core/scss/_dark.scss",
"chars": 339,
"preview": "@use \"@configured-variables\" as variables;\n\n// ————————————————————————————————————\n// Perfect Scrollbar\n// ————————————"
},
{
"path": "src/@core/scss/_default-layout-w-vertical-nav.scss",
"chars": 3579,
"preview": "@use \"@configured-variables\" as variables;\n@use \"placeholders\" as *;\n@use \"vuetify/lib/styles/tools/_elevation\" as mixin"
},
{
"path": "src/@core/scss/_default-layout.scss",
"chars": 149,
"preview": "@use \"placeholders\";\n@use \"variables\" as core-vars;\n\n.layout-navbar {\n @if core-vars.$navbar-high-emphasis-text {\n @"
},
{
"path": "src/@core/scss/_misc.scss",
"chars": 1878,
"preview": "// ℹ️ scrollable-content allows creating fixed header and scrollable content for VNavigationDrawer (Used when perfect sc"
},
{
"path": "src/@core/scss/_mixins.scss",
"chars": 3264,
"preview": "@use \"sass:map\";\n@use \"vuetify/lib/styles/settings/_index.sass\" as vuetify_settings;\n@use \"@styles/variables/_vuetify.sc"
},
{
"path": "src/@core/scss/_utilities.scss",
"chars": 500,
"preview": ".bg-var-theme-background {\n background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)) !important;\n}\n\n//"
},
{
"path": "src/@core/scss/_utils.scss",
"chars": 3851,
"preview": "@use \"sass:map\";\n@use \"sass:list\";\n@use \"sass:string\";\n\n// Thanks: https://css-tricks.com/snippets/sass/deep-getset-maps"
},
{
"path": "src/@core/scss/_variables.scss",
"chars": 6248,
"preview": "/*\n TODO: Add docs on when to use placeholder vs when to use SASS variable\n\n Placeholder\n - When we want to keep cu"
},
{
"path": "src/@core/scss/_vertical-nav.scss",
"chars": 3966,
"preview": "@use \"./placeholders\";\n@use \"@configured-variables\" as variables;\n@use \"./mixins\" as mixins;\n@use \"vuetify/lib/styles/to"
},
{
"path": "src/@core/scss/index.scss",
"chars": 620,
"preview": "@use \"sass:map\";\n\n// 基础变量和配置\n@use \"variables\";\n@use \"mixins\";\n@use \"utils\";\n\n// 布局相关\n@use \"default-layout\";\n@use \"vertic"
},
{
"path": "src/@core/scss/libs/apex-chart.scss",
"chars": 2467,
"preview": "@use \"@configured-variables\" as variables;\n@use \"../mixins\";\n\n// 👉 Apex chart\n.apexcharts-canvas {\n // For RTL alignmen"
},
{
"path": "src/@core/scss/libs/full-calendar.scss",
"chars": 5932,
"preview": "@use \"../mixins\";\n@use \"@configured-variables\" as variables;\n\n.v-application .fc {\n --fc-today-bg-color: rgba(var(--v-t"
},
{
"path": "src/@core/scss/libs/perfect-scrollbar.scss",
"chars": 811,
"preview": "$ps-size: 0.25rem;\n$ps-hover-size: 0.375rem;\n$ps-track-size: 0.5rem;\n\n.ps__thumb-x,\n.ps__thumb-y {\n background-color: r"
},
{
"path": "src/@core/scss/libs/vuetify/_overrides.scss",
"chars": 6171,
"preview": "@use \"@configured-variables\" as variables;\n@use \"../../utils\";\n\n// 👉 Application\n// ℹ️ We need accurate vh in mobile dev"
},
{
"path": "src/@core/scss/libs/vuetify/_variables.scss",
"chars": 9670,
"preview": "$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);\n$shadow-key-penumbra-opacity-custom: var(--v-shadow"
},
{
"path": "src/@core/scss/libs/vuetify/index.scss",
"chars": 36,
"preview": "@use \"variables\";\n@use \"overrides\";\n"
},
{
"path": "src/@core/scss/pages/misc.scss",
"chars": 329,
"preview": ".misc-wrapper {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: "
},
{
"path": "src/@core/scss/pages/page-auth.scss",
"chars": 672,
"preview": ".auth-wrapper {\n min-block-size: 100%;\n min-block-size: 100vh;\n min-block-size: 100dvh;\n}\n\n.auth-footer-mask {\n posi"
},
{
"path": "src/@core/scss/placeholders/_default-layout.scss",
"chars": 712,
"preview": "%layout-navbar {\n color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));\n}\n\n// Vertical nav scrolled s"
},
{
"path": "src/@core/scss/placeholders/_index.scss",
"chars": 68,
"preview": "@forward \"vertical-nav\";\n@forward \"nav\";\n@forward \"default-layout\";\n"
},
{
"path": "src/@core/scss/placeholders/_nav.scss",
"chars": 764,
"preview": "@use \"vuetify/lib/styles/tools/_elevation\" as mixins_elevation;\n\n// ℹ️ This is common style that needs to be applied to "
},
{
"path": "src/@core/scss/placeholders/_vertical-nav.scss",
"chars": 2535,
"preview": "@use \"../mixins\";\n@use \"@configured-variables\" as variables;\n@use \"vuetify/lib/styles/tools/states\" as vuetifyStates;\n@u"
},
{
"path": "src/@core/utils/compatibility.ts",
"chars": 297,
"preview": "/**\n * 浏览器兼容性处理\n */\n\n/**\n * 修复低版本Safari等浏览器数组不支持at函数的问题\n */\n;(function fixArrayAt() {\n if (!Array.prototype.at) {\n A"
},
{
"path": "src/@core/utils/dom.ts",
"chars": 658,
"preview": "export function removeEl(selector: string) {\n if (selector) {\n const el = document.querySelector(selector)\n el?.p"
},
{
"path": "src/@core/utils/formatters.ts",
"chars": 5268,
"preview": "import dayjs from 'dayjs'\nimport relativeTime from 'dayjs/plugin/relativeTime'\nimport ZH_CN from 'dayjs/locale/zh-cn'\n\ni"
},
{
"path": "src/@core/utils/image.ts",
"chars": 1106,
"preview": "import ColorThief from 'colorthief'\n\n// 将 RGB 转换为十六进制\nfunction rgbStringToHex(rgbArray: number[]): string {\n if (rgbArr"
},
{
"path": "src/@core/utils/index.ts",
"chars": 1979,
"preview": "// 👉 IsEmpty\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined || value ==="
},
{
"path": "src/@core/utils/navigator.ts",
"chars": 3254,
"preview": "import copy from 'copy-to-clipboard'\n\n// 请求和获取剪贴板内容\nexport async function getClipboardContent() {\n if (navigator.clipbo"
},
{
"path": "src/@core/utils/theme.ts",
"chars": 294,
"preview": "export function saveLocalTheme(name: string, theme: any) {\n // 存储主题到本地\n localStorage.setItem('theme', name)\n localSto"
},
{
"path": "src/@core/utils/workflow.ts",
"chars": 2876,
"preview": "import { useVueFlow } from '@vue-flow/core'\nimport { ref, watch } from 'vue'\nimport { cloneDeep } from 'lodash-es'\n\n/**\n"
},
{
"path": "src/@iconify/build-icons.ts",
"chars": 7997,
"preview": "/**\n * This is an advanced example for creating icon bundles for Iconify SVG Framework.\n *\n * It creates a bundle from:\n"
},
{
"path": "src/@iconify/tsconfig.json",
"chars": 344,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"Node16\",\n \"declaration\": false,\n \"declarationMap\":"
},
{
"path": "src/@iconify/tsconfig.tsbuildinfo",
"chars": 47,
"preview": "{\"root\":[\"./build-icons.ts\"],\"version\":\"5.8.3\"}"
},
{
"path": "src/@layouts/components/VerticalNav.vue",
"chars": 3469,
"preview": "<script lang=\"ts\" setup>\nimport type { Component } from 'vue'\nimport { useDisplay } from 'vuetify'\nimport logo from '@im"
},
{
"path": "src/@layouts/components/VerticalNavLayout.vue",
"chars": 6973,
"preview": "<script lang=\"ts\">\nimport { Transition } from 'vue'\nimport { useDisplay } from 'vuetify'\nimport VerticalNav from '@layou"
},
{
"path": "src/@layouts/components/VerticalNavLink.vue",
"chars": 733,
"preview": "<script lang=\"ts\" setup>\nimport type { NavLink } from '@layouts/types'\n\ndefineProps<{\n item: NavLink\n}>()\n</script>\n\n<t"
},
{
"path": "src/@layouts/components/VerticalNavSectionTitle.vue",
"chars": 559,
"preview": "<script lang=\"ts\" setup>\nimport type { NavSectionTitle } from '@layouts/types'\n\ndefineProps<{\n item: NavSectionTitle\n}>"
},
{
"path": "src/@layouts/components.ts",
"chars": 254,
"preview": "export { default as VerticalNavLayout } from './components/VerticalNavLayout.vue'\nexport { default as VerticalNavLink } "
},
{
"path": "src/@layouts/index.ts",
"chars": 29,
"preview": "export * from './components'\n"
},
{
"path": "src/@layouts/styles/_classes.scss",
"chars": 39,
"preview": ".cursor-pointer {\n cursor: pointer;\n}\n"
},
{
"path": "src/@layouts/styles/_default-layout.scss",
"chars": 1902,
"preview": "// These are styles which are both common in layout w/ vertical nav & horizontal nav\n@use \"@layouts/styles/rtl\";\n@use \"@"
},
{
"path": "src/@layouts/styles/_global.scss",
"chars": 191,
"preview": "*,\n::before,\n::after {\n box-sizing: inherit;\n background-repeat: no-repeat;\n}\n\nhtml {\n box-sizing: border-box;\n min-"
},
{
"path": "src/@layouts/styles/_mixins.scss",
"chars": 510,
"preview": "@use \"placeholders\";\n@use \"@configured-variables\" as variables;\n\n@mixin rtl {\n @if variables.$enable-rtl-styles {\n ["
},
{
"path": "src/@layouts/styles/_placeholders.scss",
"chars": 1129,
"preview": "// placeholders\n@use \"@configured-variables\" as variables;\n\n%boxed-content {\n @at-root #{&}-spacing {\n // TODO: Use "
},
{
"path": "src/@layouts/styles/_rtl.scss",
"chars": 119,
"preview": "@use \"./mixins\";\n\n.layout-vertical-nav .nav-group-arrow {\n @include mixins.rtl {\n transform: rotate(180deg);\n }\n}\n"
},
{
"path": "src/@layouts/styles/_variables.scss",
"chars": 780,
"preview": "// @use \"@styles/style.scss\";\n\n// 👉 Vertical nav\n$layout-vertical-nav-z-index: 12 !default;\n$layout-vertical-nav-width: "
},
{
"path": "src/@layouts/styles/index.scss",
"chars": 33,
"preview": "@use \"_global\";\n@use \"_classes\";\n"
},
{
"path": "src/@layouts/types.d.ts",
"chars": 4343,
"preview": "import type { Component, Ref, VNode } from 'vue'\nimport type { RouteLocationRaw } from 'vue-router'\nimport { ContentWidt"
},
{
"path": "src/@layouts/utils.ts",
"chars": 1208,
"preview": "import type { NavLink, NavLinkProps } from '@layouts/types'\n\n/**\n * Return nav link props to use\n * @param {Object, Stri"
},
{
"path": "src/@validators/index.ts",
"chars": 249,
"preview": "type ValidationRule = (value: any) => string | boolean\n\n// 必输校验\nexport const requiredValidator: ValidationRule = (value:"
},
{
"path": "src/App.vue",
"chars": 9281,
"preview": "<script lang=\"ts\" setup>\nimport { useTheme } from 'vuetify'\nimport { checkPrefersColorSchemeIsDark } from '@/@core/utils"
},
{
"path": "src/ace-config.ts",
"chars": 19300,
"preview": "import ace from 'ace-builds'\n\nimport modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'\n\nimport modeJavascriptUr"
},
{
"path": "src/api/constants.ts",
"chars": 9687,
"preview": "import i18n from '@/plugins/i18n'\n\nexport const storageAttributes = [\n {\n type: 'local',\n icon: 'mdi-folder-multi"
},
{
"path": "src/api/index.ts",
"chars": 2411,
"preview": "import axios from 'axios'\nimport router from '@/router'\nimport { useAuthStore } from '@/stores'\nimport { initializeReque"
},
{
"path": "src/api/nprogress.ts",
"chars": 276,
"preview": "import NProgress from 'nprogress'\nimport 'nprogress/nprogress.css'\n\nexport function configureNProgress() {\n NProgress.c"
},
{
"path": "src/api/types.ts",
"chars": 23198,
"preview": "// 订阅\nexport interface Subscribe {\n // 订阅ID\n id: number\n // 订阅名称\n name: string\n // 订阅年份\n year: string\n // 订阅类型 电影"
},
{
"path": "src/components/FileBrowser.vue",
"chars": 10233,
"preview": "<script lang=\"ts\" setup>\nimport FileList from './filebrowser/FileList.vue'\nimport FileToolbar from './filebrowser/FileTo"
},
{
"path": "src/components/NoDataFound.vue",
"chars": 1881,
"preview": "<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\nimport page404 from '@images/pages/404.svg'\n\n// 国际化\nconst { "
},
{
"path": "src/components/PWAInstallPrompt.vue",
"chars": 5305,
"preview": "<script setup lang=\"ts\">\nimport { usePWAInstall } from '@/composables/usePWAInstall'\nimport { useAuthStore } from '@/sto"
},
{
"path": "src/components/cards/BackdropCard.vue",
"chars": 2705,
"preview": "<script lang=\"ts\" setup>\nimport type { MediaServerPlayItem } from '@/api/types'\nimport noImage from '@images/no-image.jp"
},
{
"path": "src/components/cards/CustomRuleCard.vue",
"chars": 7175,
"preview": "<script lang=\"ts\" setup>\nimport { CustomRule } from '@/api/types'\nimport { useToast } from 'vue-toastification'\nimport f"
},
{
"path": "src/components/cards/DirectoryCard.vue",
"chars": 11171,
"preview": "<script lang=\"ts\" setup>\nimport type { StorageConf, TransferDirectoryConf } from '@/api/types'\nimport api from '@/api'\ni"
},
{
"path": "src/components/cards/DownloaderCard.vue",
"chars": 20190,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { formatFileSize } from '@/@core/utils/formatters'\nimport { Down"
},
{
"path": "src/components/cards/DownloadingCard.vue",
"chars": 3303,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport type { DownloadingInfo } from '@/api/types'\nimport { formatFileS"
},
{
"path": "src/components/cards/FilterRuleCard.vue",
"chars": 1725,
"preview": "<script lang=\"ts\" setup>\nimport { innerFilterRules } from '@/api/constants'\nimport { CustomRule } from '@/api/types'\nimp"
},
{
"path": "src/components/cards/FilterRuleGroupCard.vue",
"chars": 9439,
"preview": "<script lang=\"ts\" setup>\nimport draggable from 'vuedraggable'\nimport { copyToClipboard } from '@/@core/utils/navigator'\n"
},
{
"path": "src/components/cards/LibraryCard.vue",
"chars": 5739,
"preview": "<script lang=\"ts\" setup>\nimport type { MediaServerLibrary } from '@/api/types'\nimport plex from '@images/misc/plex.png'\n"
},
{
"path": "src/components/cards/MediaCard.vue",
"chars": 15550,
"preview": "<script lang=\"ts\" setup>\nimport noImage from '@images/no-image.jpeg'\nimport { getLogoUrl } from '@/utils/imageUtils'\nimp"
},
{
"path": "src/components/cards/MediaInfoCard.vue",
"chars": 5388,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport type { Context } from '@/api/types'\nimport { isNullO"
},
{
"path": "src/components/cards/MediaServerCard.vue",
"chars": 21422,
"preview": "<script setup lang=\"ts\">\nimport { MediaServerConf, MediaServerLibrary, MediaStatistic } from '@/api/types'\nimport { useT"
},
{
"path": "src/components/cards/MessageCard.vue",
"chars": 5545,
"preview": "<script lang=\"ts\" setup>\nimport MarkdownIt from 'markdown-it'\nimport mdLinkAttributes from 'markdown-it-link-attributes'"
},
{
"path": "src/components/cards/NotificationChannelCard.vue",
"chars": 26045,
"preview": "<script setup lang=\"ts\">\nimport { NotificationConf } from '@/api/types'\nimport { getLogoUrl } from '@/utils/imageUtils'\n"
},
{
"path": "src/components/cards/PersonCard.vue",
"chars": 3749,
"preview": "<script lang=\"ts\" setup>\nimport personIcon from '@images/misc/person-icon.png'\nimport type { Person } from '@/api/types'"
},
{
"path": "src/components/cards/PluginAppCard.vue",
"chars": 11411,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport VersionHistory from '../misc/VersionHistor"
},
{
"path": "src/components/cards/PluginCard.vue",
"chars": 20463,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport { useConfirm } from '@/composables/useConf"
},
{
"path": "src/components/cards/PluginFolderCard.vue",
"chars": 17535,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport { useConfirm } from '@/composables/useConf"
},
{
"path": "src/components/cards/PluginMixedSortCard.vue",
"chars": 4483,
"preview": "<script lang=\"ts\" setup>\nimport PluginCard from './PluginCard.vue'\nimport PluginFolderCard from './PluginFolderCard.vue'"
},
{
"path": "src/components/cards/PosterCard.vue",
"chars": 3070,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport type { MediaServerPlayItem } from '@/api/types'\nimpo"
},
{
"path": "src/components/cards/SiteCard.vue",
"chars": 17088,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport { getLogoUrl } from '@/utils/imageUtils'\nimport { us"
},
{
"path": "src/components/cards/StorageCard.vue",
"chars": 6999,
"preview": "<script setup lang=\"ts\">\nimport { StorageConf } from '@/api/types'\nimport { formatBytes } from '@core/utils/formatters'\n"
},
{
"path": "src/components/cards/SubscribeCard.vue",
"chars": 13450,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport { useConfirm } from '@/composables/useConf"
},
{
"path": "src/components/cards/SubscribeShareCard.vue",
"chars": 6203,
"preview": "<script lang=\"ts\" setup>\nimport { formatDateDifference } from '@/@core/utils/formatters'\nimport type { SubscribeShare } "
},
{
"path": "src/components/cards/TorrentCard.vue",
"chars": 14259,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport { formatFileSize, formatDateDifference } from '@/@co"
},
{
"path": "src/components/cards/TorrentItem.vue",
"chars": 9377,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport { formatFileSize, formatDateDifference } from '@/@co"
},
{
"path": "src/components/cards/UserCard.vue",
"chars": 11954,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { Subscribe, User } from '@/api/types'\nimport { useUserStore } f"
},
{
"path": "src/components/cards/WorkflowShareCard.vue",
"chars": 4883,
"preview": "<script lang=\"ts\" setup>\nimport { formatDateDifference } from '@/@core/utils/formatters'\nimport type { WorkflowShare } f"
},
{
"path": "src/components/cards/WorkflowTaskCard.vue",
"chars": 13069,
"preview": "<script lang=\"ts\" setup>\nimport { Workflow } from '@/api/types'\nimport { useToast } from 'vue-toastification'\nimport { u"
},
{
"path": "src/components/dialog/AboutDialog.vue",
"chars": 17849,
"preview": "<script lang=\"ts\" setup>\nimport { formatDateDifference } from '@/@core/utils/formatters'\nimport api from '@/api'\nimport "
},
{
"path": "src/components/dialog/AddDownloadDialog.vue",
"chars": 9335,
"preview": "<script setup lang=\"ts\">\nimport { useToast } from 'vue-toastification'\nimport api from '@/api'\nimport { doneNProgress, s"
},
{
"path": "src/components/dialog/AlistConfigDialog.vue",
"chars": 3971,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport { useI18n } from 'vue-i18n'\nimport { useDisplay } from 'vuetify'"
},
{
"path": "src/components/dialog/AliyunAuthDialog.vue",
"chars": 3845,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport { useI18n } from 'vue-i18n'\nimport { useDisplay } from 'vuetify'"
},
{
"path": "src/components/dialog/CategoryEditDialog.vue",
"chars": 21706,
"preview": "<script setup lang=\"ts\">\nimport draggable from 'vuedraggable'\nimport api from '@/api'\nimport type { CategoryConfig } fro"
},
{
"path": "src/components/dialog/ForkSubscribeDialog.vue",
"chars": 8976,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { doneNProgress, startNProgress } from '@/api/nprogress'\nimport "
},
{
"path": "src/components/dialog/ForkWorkflowDialog.vue",
"chars": 10347,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { doneNProgress, startNProgress } from '@/api/nprogress'\nimport "
},
{
"path": "src/components/dialog/ImportCodeDialog.vue",
"chars": 1052,
"preview": "<script lang=\"ts\" setup>\nimport { useI18n } from 'vue-i18n'\n\n// 多语言支持\nconst { t } = useI18n()\n\n// 输入参数\nconst props = def"
},
{
"path": "src/components/dialog/MediaInfoDialog.vue",
"chars": 530,
"preview": "<script setup lang=\"ts\">\nimport { Context } from '@/api/types'\nimport MediaInfoCard from '../cards/MediaInfoCard.vue'\nim"
},
{
"path": "src/components/dialog/OTPAuthDialog.vue",
"chars": 6852,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport QRCode from 'qrcode'\nimport { useDisplay }"
},
{
"path": "src/components/dialog/PasskeyDialog.vue",
"chars": 9867,
"preview": "<script lang=\"ts\" setup>\nimport { bufferToBase64Url, base64UrlToUint8Array } from '@/@core/utils/navigator'\nimport { use"
},
{
"path": "src/components/dialog/PluginConfigDialog.vue",
"chars": 5048,
"preview": "<script setup lang=\"ts\">\nimport { useDisplay } from 'vuetify'\nimport type { Plugin } from '@/api/types'\nimport { isNullO"
},
{
"path": "src/components/dialog/PluginDataDialog.vue",
"chars": 3971,
"preview": "<script setup lang=\"ts\">\nimport { useDisplay } from 'vuetify'\nimport type { Plugin } from '@/api/types'\nimport PageRende"
},
{
"path": "src/components/dialog/PluginMarketSettingDialog.vue",
"chars": 7365,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport draggable from 'vuedraggable'\nimport { useToast } from 'vue-toas"
},
{
"path": "src/components/dialog/ProgressDialog.vue",
"chars": 537,
"preview": "<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\n\nconst props = defineProps({\n valu"
},
{
"path": "src/components/dialog/RcloneConfigDialog.vue",
"chars": 2406,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport { useI18n } from 'vue-i18n'\nimport { useDisplay } from 'vuetify'"
},
{
"path": "src/components/dialog/ReorganizeDialog.vue",
"chars": 16907,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport MediaIdSelector from '../misc/MediaIdSelec"
},
{
"path": "src/components/dialog/SearchBarDialog.vue",
"chars": 24843,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport type { Site, Plugin, Subscribe } from '@/api/types'\nimport { get"
},
{
"path": "src/components/dialog/SearchSiteDialog.vue",
"chars": 6241,
"preview": "<script setup lang=\"ts\">\nimport type { Site } from '@/api/types'\nimport { useI18n } from 'vue-i18n'\n\n// 多语言支持\nconst { t "
},
{
"path": "src/components/dialog/SiteAddEditDialog.vue",
"chars": 11185,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport type { DownloaderConf, Site } from '@/api/"
},
{
"path": "src/components/dialog/SiteCookieUpdateDialog.vue",
"chars": 3396,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { Site } from '@/api/types'\nimport { requiredValidator } from '@"
},
{
"path": "src/components/dialog/SiteImportDialog.vue",
"chars": 12489,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport type { Site } from '@/api/types'\nimport { "
},
{
"path": "src/components/dialog/SiteResourceDialog.vue",
"chars": 23205,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport type { Site, TorrentInfo, SiteCategory } from '@/api/types'\nimpo"
},
{
"path": "src/components/dialog/SiteStatisticsDialog.vue",
"chars": 14335,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport api from '@/api'\nimport type { Site, SiteStatistic }"
},
{
"path": "src/components/dialog/SiteUserDataDialog.vue",
"chars": 15976,
"preview": "<script lang=\"ts\" setup>\nimport type { Site, SiteUserData } from '@/api/types'\nimport api from '@/api'\nimport { useDispl"
},
{
"path": "src/components/dialog/SmbConfigDialog.vue",
"chars": 3544,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport { useI18n } from 'vue-i18n'\nimport { useDisplay } from 'vuetify'"
},
{
"path": "src/components/dialog/SubscribeEditDialog.vue",
"chars": 18160,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport { numberValidator } from '@/@validators'\ni"
},
{
"path": "src/components/dialog/SubscribeFilesDialog.vue",
"chars": 9967,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { SubscrbieInfo } from '@/api/types'\nimport { useDisplay } from "
},
{
"path": "src/components/dialog/SubscribeHistoryDialog.vue",
"chars": 6232,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport { Subscribe } from '@/api/types'\nimport { formatDateDifference }"
},
{
"path": "src/components/dialog/SubscribeSeasonDialog.vue",
"chars": 8639,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport { MediaInfo, MediaSeason, NotExistMediaInfo } from '@/api/types'"
},
{
"path": "src/components/dialog/SubscribeShareDialog.vue",
"chars": 3665,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport { requiredValidator } from '@/@validators'"
},
{
"path": "src/components/dialog/SubscribeShareStatisticsDialog.vue",
"chars": 17117,
"preview": "<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue'\nimport api from '@/api'\nimport type { SubscribeS"
},
{
"path": "src/components/dialog/TransferQueueDialog.vue",
"chars": 9971,
"preview": "<script lang=\"ts\" setup>\nimport { ref, computed, watch, onMounted, onUnmounted } from 'vue'\nimport { formatFileSize } fr"
},
{
"path": "src/components/dialog/U115AuthDialog.vue",
"chars": 5542,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport { useI18n } from 'vue-i18n'\nimport { useDisplay } from 'vuetify'"
},
{
"path": "src/components/dialog/UserAddEditDialog.vue",
"chars": 19764,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport type { User } from '@/api/types'\nimport { "
},
{
"path": "src/components/dialog/UserAuthDialog.vue",
"chars": 4393,
"preview": "<script lang=\"ts\" setup>\nimport { isNullOrEmptyObject } from '@/@core/utils'\nimport api from '@/api'\nimport { useToast }"
},
{
"path": "src/components/dialog/WorkflowActionsDialog.vue",
"chars": 9391,
"preview": "<script lang=\"ts\" setup>\nimport { ref } from 'vue'\nimport { VueFlow, useVueFlow, type Connection, type GraphNode } from "
},
{
"path": "src/components/dialog/WorkflowAddEditDialog.vue",
"chars": 7759,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport type { Workflow } from '@/api/types'\nimpor"
},
{
"path": "src/components/dialog/WorkflowShareDialog.vue",
"chars": 3985,
"preview": "<script lang=\"ts\" setup>\nimport { useToast } from 'vue-toastification'\nimport { requiredValidator } from '@/@validators'"
},
{
"path": "src/components/field/CronField.vue",
"chars": 975,
"preview": "<script setup lang=\"ts\">\nimport CronInput from '@/components/input/CronInput.vue'\n\nconst attrs = useAttrs()\n\nconst props"
},
{
"path": "src/components/field/PathField.vue",
"chars": 1032,
"preview": "<script setup lang=\"ts\">\nimport PathInput from '@/components/input/PathInput.vue'\n\nconst attrs = useAttrs()\n\nconst props"
},
{
"path": "src/components/filebrowser/FileList.vue",
"chars": 22451,
"preview": "<script lang=\"ts\" setup>\nimport type { AxiosRequestConfig, AxiosInstance } from 'axios'\nimport type { PropType } from 'v"
},
{
"path": "src/components/filebrowser/FileNavigator.vue",
"chars": 12120,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport type { FileItem } from '@/api/types'\nimport { useDis"
},
{
"path": "src/components/filebrowser/FileToolbar.vue",
"chars": 5490,
"preview": "<script lang=\"ts\" setup>\nimport type { AxiosRequestConfig, AxiosInstance } from 'axios'\nimport type { EndPoints, FileIte"
},
{
"path": "src/components/filter/TorrentFilterBar.vue",
"chars": 23488,
"preview": "<script lang=\"ts\" setup>\nimport { useI18n } from 'vue-i18n'\nimport { useDisplay } from 'vuetify'\nimport { useEventListen"
},
{
"path": "src/components/input/CronInput.vue",
"chars": 2009,
"preview": "<script setup lang=\"ts\">\nconst props = defineProps({\n modelValue: {\n type: String,\n default: '* * * * *',\n },\n})"
},
{
"path": "src/components/input/PathInput.vue",
"chars": 3968,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { FileItem } from '@/api/types'\n\nconst props = defineProps({\n m"
},
{
"path": "src/components/misc/DashboardElement.vue",
"chars": 4851,
"preview": "<script setup lang=\"ts\">\nimport api from '@/api'\nimport { DashboardItem } from '@/api/types'\nimport AnalyticsMediaStatis"
},
{
"path": "src/components/misc/FilterOption.vue",
"chars": 172,
"preview": "<script lang=\"ts\" setup>\ndefineProps<{ title: string }>()\n</script>\n<template>\n <VListSubheader>{{ title }}</VListSubhe"
},
{
"path": "src/components/misc/MediaIdSelector.vue",
"chars": 3141,
"preview": "<script lang=\"ts\" setup>\nimport api from '@/api'\nimport type { MediaInfo } from '@/api/types'\n\n// 定义输入变量\nconst props = d"
},
{
"path": "src/components/misc/VersionHistory.vue",
"chars": 2203,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue'\nimport MarkdownIt from 'markdown-it'\nimport mdLinkAttribute"
},
{
"path": "src/components/render/DashboardRender.vue",
"chars": 1170,
"preview": "<script lang=\"ts\" setup>\nimport { RenderProps } from '@/api/types'\nimport { type PropType } from 'vue'\n\n// 输入参数\nconst el"
},
{
"path": "src/components/render/FormRender.vue",
"chars": 4626,
"preview": "<script setup lang=\"ts\">\nimport { RenderProps } from '@/api/types'\n\n// 定义 props\ndefineProps<{\n config: RenderProps // J"
},
{
"path": "src/components/render/PageRender.vue",
"chars": 1875,
"preview": "<script lang=\"ts\" setup>\nimport { isNullOrEmptyObject } from '@/@core/utils'\nimport api from '@/api'\nimport { type PropT"
}
]
// ... and 177 more files (download for full content)
About this extraction
This page contains the full source code of the jxxghp/MoviePilot-Frontend GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 377 files (2.4 MB), approximately 657.2k tokens, and a symbol index with 390 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.