Compare commits

..

22 Commits
v2.6.3 ... main

Author SHA1 Message Date
jxxghp
6fdbc8104c v1.9.17 2024-09-18 17:37:57 +08:00
jxxghp
433c14679c 更新 package.json 2024-09-08 13:09:25 +08:00
jxxghp
fcaa4476f0 更新 package.json 2024-09-05 06:59:13 +08:00
jxxghp
85c5c3058c 更新 AccountSettingDirectory.vue 2024-09-05 06:57:32 +08:00
jxxghp
035122a08e Merge pull request #172 from flowclouds/main 2024-08-16 12:07:18 +08:00
jxxghp
0a76875f8e chore: update package.json version to 1.9.14-2 2024-08-16 10:10:41 +08:00
lj
218eac54ce fix: 优化代码移除相关警告 2024-08-15 15:52:32 +08:00
jxxghp
84deeff4f5 chore: update package.json version to 1.9.14-1 2024-08-12 18:13:47 +08:00
jxxghp
0c72d026f6 chore: update package.json version to 1.9.14 2024-08-08 14:37:40 +08:00
jxxghp
aec9ea83c5 更新 package.json 2024-07-30 06:33:53 +08:00
jxxghp
effd13aedd Merge pull request #171 from InfinityPacer/main 2024-07-22 22:12:19 +08:00
InfinityPacer
42b43d65d7 fix(FilterRuleCard): 移除规则中的空白字符并保留前后的空格 2024-07-22 21:47:42 +08:00
jxxghp
c501d824dd Merge pull request #170 from InfinityPacer/main 2024-07-17 06:53:43 +08:00
InfinityPacer
384ac2faf1 feat: ace-editor support python 2024-07-17 01:48:18 +08:00
jxxghp
dd2c4dd24b v1.9.12 2024-07-16 07:56:07 +08:00
jxxghp
356ffddb1c release 2024-07-09 08:00:11 +08:00
jxxghp
de69be7c4e Merge pull request #168 from s0urcelab/main 2024-07-09 06:28:30 +08:00
s0urce
e962f555ae fix: issue #167 2024-07-09 02:56:48 +08:00
jxxghp
1987246585 Merge pull request #166 from JavaZeroo/main 2024-07-08 19:05:23 +08:00
JavaZero
393264f66b 实现根据操作系统动态显示不同的搜索快捷键提示 2024-07-08 13:51:00 +08:00
jxxghp
9b50020b3b Merge pull request #165 from BrettDean/main 2024-07-05 15:26:31 +08:00
Dean
5e5545fe01 优化"最近入库"数量显示 2024-07-05 15:19:50 +08:00
363 changed files with 16528 additions and 72448 deletions

View File

@@ -1,2 +1,2 @@
VITE_API_BASE_URL=/api/v1/
VITE_API_BASE_URL=http://localhost:3001/api/v1/
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM

View File

@@ -1,45 +0,0 @@
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

View File

@@ -1,12 +1,12 @@
name: Build Moviepilot-Frontend v2
name: Build Moviepilot-Frontend
on:
workflow_dispatch:
push:
branches:
- v2
- main
paths:
- 'package.json'
- package.json
jobs:
build:
@@ -42,22 +42,13 @@ jobs:
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
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ env.frontend_version }}
name: ${{ env.frontend_version }}
draft: false
prerelease: false
make_latest: false
files: |
dist.zip
env:

10
.vscode/settings.json vendored
View File

@@ -11,8 +11,7 @@
},
// SCSS
"[scss]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint",
"editor.formatOnSave": false
"editor.defaultFormatter": "stylelint.vscode-stylelint"
},
// JSON
"[json]": {
@@ -106,8 +105,5 @@
}
]
},
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
"i18n-ally.localesPaths": [
"src/locales"
]
}
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
}

View File

@@ -1,37 +1,16 @@
# 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设置
## 推荐的IDE设置
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (并禁用 Vetur).
### 配置Vite
## 配置Vite
请参阅 [Vite 配置参考](https://vitejs.dev/config/).
### 依赖安装
## 依赖安装
```sh
yarn

View File

@@ -1,59 +0,0 @@
# 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
```

375
auto-imports.d.ts vendored
View File

@@ -3,11 +3,9 @@
// @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']
@@ -22,12 +20,13 @@ declare global {
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 createLogger: typeof import('vuex')['createLogger']
const createNamespacedHelpers: typeof import('vuex')['createNamespacedHelpers']
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 createStore: typeof import('vuex')['createStore']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
@@ -35,11 +34,9 @@ declare global {
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
@@ -55,11 +52,10 @@ declare global {
const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('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 mapActions: typeof import('vuex')['mapActions']
const mapGetters: typeof import('vuex')['mapGetters']
const mapMutations: typeof import('vuex')['mapMutations']
const mapState: typeof import('vuex')['mapState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
@@ -70,7 +66,6 @@ declare global {
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
@@ -82,7 +77,6 @@ declare global {
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
@@ -102,12 +96,9 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
@@ -160,7 +151,6 @@ declare global {
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']
@@ -200,8 +190,6 @@ declare global {
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']
@@ -221,7 +209,6 @@ declare global {
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMin: typeof import('@vueuse/math')['useMin']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
@@ -247,7 +234,6 @@ declare global {
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProjection: typeof import('@vueuse/math')['useProjection']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
@@ -256,7 +242,6 @@ declare global {
const useRound: typeof import('@vueuse/math')['useRound']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
@@ -271,11 +256,11 @@ declare global {
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStore: typeof import('vuex')['useStore']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSum: typeof import('@vueuse/math')['useSum']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
@@ -328,17 +313,15 @@ declare global {
// 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'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, 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']>
@@ -353,12 +336,13 @@ declare module 'vue' {
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createLogger: UnwrapRef<typeof import('vuex')['createLogger']>
readonly createNamespacedHelpers: UnwrapRef<typeof import('vuex')['createNamespacedHelpers']>
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createStore: UnwrapRef<typeof import('vuex')['createStore']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
@@ -366,11 +350,9 @@ declare module 'vue' {
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
@@ -386,11 +368,10 @@ declare module 'vue' {
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('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 mapActions: UnwrapRef<typeof import('vuex')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('vuex')['mapGetters']>
readonly mapMutations: UnwrapRef<typeof import('vuex')['mapMutations']>
readonly mapState: UnwrapRef<typeof import('vuex')['mapState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
@@ -401,7 +382,6 @@ declare module 'vue' {
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
@@ -413,7 +393,6 @@ declare module 'vue' {
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
@@ -433,12 +412,9 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
@@ -491,7 +467,6 @@ declare module 'vue' {
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']>
@@ -531,8 +506,6 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly 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']>
@@ -552,7 +525,6 @@ declare module 'vue' {
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
@@ -578,7 +550,6 @@ declare module 'vue' {
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
@@ -587,7 +558,6 @@ declare module 'vue' {
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
@@ -602,11 +572,11 @@ declare module 'vue' {
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStore: UnwrapRef<typeof import('vuex')['useStore']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
@@ -656,4 +626,313 @@ declare module 'vue' {
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}
}
declare module '@vue/runtime-core' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createLogger: UnwrapRef<typeof import('vuex')['createLogger']>
readonly createNamespacedHelpers: UnwrapRef<typeof import('vuex')['createNamespacedHelpers']>
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createStore: UnwrapRef<typeof import('vuex')['createStore']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly logicAnd: UnwrapRef<typeof import('@vueuse/math')['logicAnd']>
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('vuex')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('vuex')['mapGetters']>
readonly mapMutations: UnwrapRef<typeof import('vuex')['mapMutations']>
readonly mapState: UnwrapRef<typeof import('vuex')['mapState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAverage: UnwrapRef<typeof import('@vueuse/math')['useAverage']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFloor: UnwrapRef<typeof import('@vueuse/math')['useFloor']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMath: UnwrapRef<typeof import('@vueuse/math')['useMath']>
readonly useMax: UnwrapRef<typeof import('@vueuse/math')['useMax']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePrecision: UnwrapRef<typeof import('@vueuse/math')['usePrecision']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStore: UnwrapRef<typeof import('vuex')['useStore']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useTrunc: UnwrapRef<typeof import('@vueuse/math')['useTrunc']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}

7
components.d.ts vendored
View File

@@ -1,23 +1,20 @@
/* eslint-disable */
/* prettier-ignore */
// @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']
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
}
}

View File

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

View File

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

7
env.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,232 +1,42 @@
<!DOCTYPE html>
<html lang="zh-CN" style="
overflow: hidden auto;
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
background: var(--initial-loader-bg, #fff);
">
<html lang="en">
<head>
<title>MoviePilot</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="expires" content="0">
<meta charset="UTF-8" />
<!-- 核心viewport设置 - 针对PWA优化 -->
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
<!-- 防止缩放和选择,提供原生应用体验 -->
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
<!-- 基础信息 -->
<meta name="description" content="MoviePilot - 智能影视媒体库管理工具" />
<meta name="author" content="MoviePilot" />
<meta name="keywords" content="MoviePilot,影视,媒体库,管理" />
<!-- 安全和隐私 -->
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
<title>MoviePilot</title>
<meta name="Robots" content="noindex,nofollow,noarchive" />
<meta name="referrer" content="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 优化 -->
<meta name="referrer" content="origin" />
<link rel="icon" type="image/png" href="/logo.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
<link rel="apple-touch-startup-image" href="/splash/apple-splash.png" />
<!-- iOS Safari 全屏模式 -->
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
<!-- 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="description" content="MoviePilot" />
<meta name="format-detection" content="telephone=no" />
<meta name="referrer" content="never" />
<meta name="msapplication-TileColor" content="#7D34FD" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
<meta name="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 />
<!-- 预加载关键资源 -->
<link rel="preload" href="/logo.png" as="image" />
<link rel="modulepreload" href="/src/main.ts" />
<!-- 内联关键CSS -->
<style>
/* 关键路径CSS - 从loader.css内联 */
#loading-bg {
position: fixed;
z-index: 99999;
display: block;
background: var(--initial-loader-bg, #fff);
block-size: 100vh;
inline-size: 100vw;
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
}
.loading-logo {
position: absolute;
inset-block-start: 35%;
inset-inline-start: calc(50% - 5rem);
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
}
/* 添加logo完成动画 - 放大虚化效果 */
.loading-complete .loading-logo {
filter: blur(10px);
opacity: 0;
transform: scale(1.5);
}
/* 添加加载背景消失动画 - 放大虚化效果 */
.loading-complete {
filter: blur(15px);
opacity: 0;
transform: scale(1.2);
}
.loading {
position: absolute;
box-sizing: border-box;
border: 3px solid transparent;
border-radius: 50%;
block-size: 55px;
inline-size: 55px;
inset-block-start: 80%;
inset-inline-start: calc(50% - 27.5px);
transition: opacity 0.6s ease;
}
/* 完成时隐藏加载动画 */
.loading-complete .loading {
opacity: 0;
}
.loading .effect-1,
.loading .effect-2,
.loading .effect-3 {
position: absolute;
box-sizing: border-box;
border: 3px solid transparent;
border-radius: 50%;
block-size: 100%;
border-inline-start: 3px solid var(--initial-loader-color, #eee);
inline-size: 100%;
}
.loading .effect-1 {
animation: rotate 1s ease infinite;
}
.loading .effect-2 {
animation: rotate-opacity 1s ease infinite 0.1s;
}
.loading .effect-3 {
animation: rotate-opacity 1s ease infinite 0.2s;
}
.loading .effects {
transition: all 0.3s ease;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(1turn);
}
}
@keyframes rotate-opacity {
0% {
opacity: 0.1;
transform: rotate(0deg);
}
100% {
opacity: 1;
transform: rotate(1turn);
}
}
</style>
<!-- 初始化脚本 -->
<script>
// 主题色彩初始化
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
// 状态栏适配
if (window.navigator.standalone) {
document.documentElement.style.setProperty('--status-bar-height', '20px')
}
// 安全区域适配
function updateSafeArea() {
const safeAreaTop = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)')
const safeAreaBottom = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-bottom)')
if (safeAreaTop) document.documentElement.style.setProperty('--safe-area-top', safeAreaTop)
if (safeAreaBottom) document.documentElement.style.setProperty('--safe-area-bottom', safeAreaBottom)
}
updateSafeArea()
window.addEventListener('resize', updateSafeArea)
window.addEventListener('orientationchange', updateSafeArea)
</script>
<link rel="stylesheet" type="text/css" href="/loader.css" />
<link rel="preload" href="index.js" as="script">
</head>
<body style="margin: 0; overflow: hidden; overscroll-behavior: none; -webkit-overflow-scrolling: touch;">
<body>
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
<svg width="160px" height="160px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
<svg width="10rem" height="10rem" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
<g transform="matrix(1,0,0,1,-2606,-236)">
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
@@ -337,6 +147,16 @@
</div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
if (loaderColor)
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
if (primaryColor)
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
</script>
</body>
</html>

16626
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
{
"name": "moviepilot",
"version": "2.6.3",
"version": "1.9.17",
"private": true,
"type": "module",
"bin": "dist/service.js",
"scripts": {
"dev": "vite --host",
@@ -20,98 +19,86 @@
]
},
"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/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",
"colorthief": "^2.6.0",
"copy-to-clipboard": "^3.3.3",
"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",
"@fullcalendar/core": "^6.1.8",
"@fullcalendar/daygrid": "^6.1.8",
"@fullcalendar/interaction": "^6.1.7",
"@fullcalendar/list": "^6.1.7",
"@fullcalendar/timegrid": "^6.1.7",
"@fullcalendar/vue3": "^6.1.8",
"@iconify/utils": "^2.1.22",
"@vueuse/core": "^10.1.2",
"@vueuse/math": "^10.1.2",
"ace-builds": "^1.32.6",
"apexcharts-clevision": "^3.28.5",
"axios": "1.6.8",
"colorthief": "^2.4.0",
"dayjs": "^1.11.10",
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
"nprogress": "^0.2.0",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"qrcode.vue": "^3.6.0",
"sass": "^1.83.4",
"tailwindcss": "^ 3.4.17",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-toastification": "^2.0.0-rc.5",
"qrcode.vue": "^3.4.1",
"sass": "^1.59.3",
"tailwindcss": "^3.3.2",
"unplugin-vue-define-options": "^1.3.5",
"vue": "^3.3.2",
"vue-router": "^4.2.0",
"vue-toast-notification": "^3",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.8.0",
"vue3-apexcharts": "^1.4.1",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "3.7.3",
"vuetify": "3.6.8",
"vuetify-use-dialog": "^0.6.11",
"vuex": "^4.1.0",
"vuex-persistedstate": "^4.1.0",
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@antfu/eslint-config-vue": "^0.43.1",
"@iconify-json/mdi": "^1.1.52",
"@iconify/tools": "^4.0.4",
"@iconify/vue": "^4.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@originjs/vite-plugin-federation": "^1.4.1",
"@iconify/vue": "4.1.1",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/lodash": "^4.14.197",
"@types/node": "^20.1.4",
"@types/nprogress": "^0.2.3",
"@types/webfontloader": "^1.6.34",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^9.18.0",
"eslint": "^9.0.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-promise": "^6.0.1",
"eslint-plugin-regex": "^1.10.0",
"eslint-plugin-sonarjs": "^3.0.1",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-sonarjs": "^0.25.1",
"eslint-plugin-unicorn": "^52.0.0",
"eslint-plugin-vue": "^9.12.0",
"postcss": "^8.5.1",
"postcss": "8",
"postcss-html": "^1.5.0",
"stylelint": "^16.13.2",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint": "16.3.1",
"stylelint-config-idiomatic-order": "10.0.0",
"stylelint-config-standard-scss": "13.1.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",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.8",
"vite-plugin-pages": "^0.32.1",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-pwa": "^0.20.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"
"vite-plugin-vuetify": "2.0.3",
"vue-shepherd": "^3.0.0",
"vue-tsc": "^2.0.10"
},
"packageManager": "yarn@1.22.18"
"packageManager": "yarn@1.22.18",
"resolutions": {
"postcss": "8"
}
}

View File

@@ -1,4 +1,4 @@
export default {
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

85
public/loader.css Normal file
View File

@@ -0,0 +1,85 @@
body {
margin: 0;
}
html {
overflow: hidden auto;
background: var(--initial-loader-bg, #fff);
min-block-size: calc(100% + env(safe-area-inset-top));
}
#loading-bg {
position: absolute;
z-index: 999;
display: block;
background: var(--initial-loader-bg, #fff);
block-size: 100vh;
inline-size: 100vw;
}
.loading-logo {
position: absolute;
inset-block-start: 35%;
inset-inline-start: calc(50% - 5rem);
}
.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);
}
.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);
}
}

View File

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

View File

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

View File

@@ -1,13 +1,14 @@
<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">
<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 shadow-md 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"

View File

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

View File

@@ -1,18 +0,0 @@
<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>

View File

@@ -1,85 +0,0 @@
<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>

View File

@@ -8,9 +8,9 @@ const props = defineProps<Props>()
</script>
<template>
<div class="absolute top-2 right-2 flex items-center justify-between p-2">
<div class="absolute top-2 right-2 flex items-center justify-between p-2 shadow">
<VBadge :color="props.color" bordered>
<template #badge>
<template #badge>
<VIcon icon="mdi-pulse"></VIcon>
</template>
</VBadge>

View File

@@ -0,0 +1,252 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useDisplay, useTheme } from 'vuetify'
import type { ThemeSwitcherTheme } from '@layouts/types'
import api from '@/api'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { useToast } from 'vue-toast-notification'
import { VAceEditor } from 'vue3-ace-editor'
// 显示器宽度
const display = useDisplay()
const props = defineProps<{
themes: ThemeSwitcherTheme[]
}>()
const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
const { state: currentThemeName, next: getNextThemeName } = useCycleList(
props.themes.map(t => t.name),
{ initialValue: savedTheme.value },
)
const $toast = useToast()
// 自定义CSS弹窗
const cssDialog = ref(false)
// 自定义 CSS
const customCSS = ref('')
// 编辑器主题
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
// 主题切换动画
function themeTransition() {
const x = performance.now()
for (let i = 0; i++ < 1e7; (i << 9) & ((9 % 9) * 9 + 9));
const cost = performance.now() - x
if (cost > 10) return
const el: HTMLElement = document.querySelector('[data-v-app]')!
const children = el.querySelectorAll('*') as NodeListOf<HTMLElement>
children.forEach(el => {
if (hasScrollbar(el)) {
el.dataset.scrollX = String(el.scrollLeft)
el.dataset.scrollY = String(el.scrollTop)
}
})
const copy = el.cloneNode(true) as HTMLElement
copy.classList.add('app-copy')
const rect = el.getBoundingClientRect()
copy.style.top = `${rect.top}px`
copy.style.left = `${rect.left}px`
copy.style.width = `${rect.width}px`
copy.style.height = `${rect.height}px`
const targetEl = document.activeElement as HTMLElement
const targetRect = targetEl.getBoundingClientRect()
const left = targetRect.left + targetRect.width / 2 + window.scrollX
const top = targetRect.top + targetRect.height / 2 + window.scrollY
el.style.setProperty('--clip-pos', `${left}px ${top}px`)
el.style.removeProperty('--clip-size')
nextTick(() => {
el.classList.add('app-transition')
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.style.setProperty('--clip-size', `${Math.hypot(window.innerWidth, window.innerHeight)}px`)
})
})
})
document.body.append(copy)
;(copy.querySelectorAll('[data-scroll-x], [data-scroll-y]') as NodeListOf<HTMLElement>).forEach(el => {
el.scrollLeft = +el.dataset.scrollX!
el.scrollTop = +el.dataset.scrollY!
})
function onTransitionend(e: TransitionEvent) {
if (e.target === e.currentTarget) {
copy.remove()
el.removeEventListener('transitionend', onTransitionend)
el.removeEventListener('transitioncancel', onTransitionend)
el.classList.remove('app-transition')
el.style.removeProperty('--clip-size')
el.style.removeProperty('--clip-pos')
}
}
el.addEventListener('transitionend', onTransitionend)
el.addEventListener('transitioncancel', onTransitionend)
}
// 更新主题
function updateTheme() {
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
globalTheme.name.value = theme
savedTheme.value = theme
themeTransition()
// 保存主题到本地
localStorage.setItem('theme', theme)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
}
// 切换主题
function changeTheme(theme: string) {
let nextTheme = theme
if (!theme) nextTheme = getNextThemeName()
currentThemeName.value = nextTheme
// 保存主题到服务端
try {
api.post('/user/config/theme', nextTheme, {
headers: {
'Content-Type': 'text/plain',
},
})
} catch (e) {
console.error('保存主题到服务端失败')
}
}
// 是否有滚动条
function hasScrollbar(el?: Element | null) {
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false
const style = window.getComputedStyle(el)
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
}
// 监听系统主题变化
try {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
} catch (e) {
console.error('当前设备不支持监听系统主题变化')
}
// 查询当前主题的图标
const getThemeIcon = computed(() => {
const theme = props.themes.find(t => t.name === currentThemeName.value)
return theme?.icon ?? 'mdi-circle'
})
// 监听设置主题变化
watch(
() => currentThemeName.value,
() => updateTheme(),
)
// 获取自定义 CSS
async function getCustomCSS() {
try {
const result: { [key: string]: any } = await api.get('system/setting/UserCustomCSS')
if (result && result.success && result.data?.value) {
customCSS.value = result.data?.value ?? ''
if (customCSS.value) {
const style = document.createElement('style')
style.innerHTML = result.data?.value ?? ''
document.head.appendChild(style)
}
}
} catch (error) {
console.error(error)
}
}
// 保存自定义 CSS
async function saveCustomCSS() {
cssDialog.value = false
try {
const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', customCSS.value, {
headers: {
'Content-Type': 'text/plain',
},
})
if (result.success) $toast.success('自定义CSS保存成功')
} catch (e) {
console.error('保存自定义 CSS 到服务端失败')
}
}
onMounted(() => {
getCustomCSS()
})
</script>
<template>
<VMenu v-if="props.themes">
<template v-slot:activator="{ props }">
<IconBtn v-bind="props">
<VIcon :icon="getThemeIcon" />
</IconBtn>
</template>
<VList>
<VListItem v-for="theme in props.themes" :key="theme.name" @click="changeTheme(theme.name)">
<template #prepend>
<VIcon :icon="theme.icon" />
</template>
<VListItemTitle>{{ theme.title }}</VListItemTitle>
</VListItem>
<VListItem @click="cssDialog = true">
<template #prepend>
<VIcon icon="mdi-palette" />
</template>
<VListItemTitle>自定义</VListItemTitle>
</VListItem>
</VList>
</VMenu>
<!-- 自定义 CSS -- -->
<VDialog v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard title="自定义主题风格">
<DialogCloseBtn @click="cssDialog = false" />
<VDivider />
<VAceEditor
v-model:value="customCSS"
lang="css"
:theme="editorTheme"
style="block-size: 100%; min-block-size: 30rem"
/>
<VDivider />
<VCardText class="text-center">
<VBtn @click="saveCustomCSS" class="w-1/2">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="sass">
// Theme transition
.app-copy
position: fixed !important
z-index: -1 !important
pointer-events: none !important
contain: size style !important
overflow: clip !important
.app-transition
--clip-size: 0
--clip-pos: 0 0
clip-path: circle(var(--clip-size) at var(--clip-pos))
transition: clip-path .35s ease-out
</style>

View File

@@ -1,58 +0,0 @@
# 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"`

View File

@@ -1,7 +1,7 @@
@use "@configured-variables" as variables;
// ————————————————————————————————————
// Perfect Scrollbar
//* ——— Perfect Scrollbar
// ————————————————————————————————————
.v-application.v-theme--dark {

View File

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

View File

@@ -10,19 +10,17 @@
*/
@use "sass:map";
// 使用模板中的变量,不再进行配置
@use "@layouts/styles/variables" as layouts-vars;
// @forward "@layouts/styles/variables";
@use "utils";
// 👉 Default layout
$navbar-high-emphasis-text: true !default;
// 移除@forward配置已合并到template/_variables.scss
// @forward "@layouts/styles/variables" with (
// $layout-vertical-nav-collapsed-width: 68px !default,
// );
// @use "@layouts/styles/variables" as *;
@forward "@layouts/styles/variables" with (
$layout-vertical-nav-collapsed-width: 68px !default,
);
@use "@layouts/styles/variables" as *;
$theme-colors-name: (
"primary",
@@ -57,7 +55,7 @@ $vertical-nav-horizontal-padding: 1.375rem 1rem !default;
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding);
// Vertical nav header height. Mostly we will align it with navbar height;
$vertical-nav-header-height: layouts-vars.$layout-vertical-nav-navbar-height !default;
$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default;
$vertical-nav-navbar-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
// Vertical nav header padding

View File

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

View File

@@ -0,0 +1,157 @@
@use "mixins";
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@layouts/styles/placeholders";
@use "@configured-variables" as variables;
// 👉 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(3);
}
}
> .v-avatar {
border: 2px solid rgb(var(--v-theme-surface));
transition: transform 0.15s ease;
}
}
// 👉 Button outline with default color border color
.v-alert--variant-outlined,
.v-avatar--variant-outlined,
.v-btn.v-btn--variant-outlined,
.v-card--variant-outlined,
.v-chip--variant-outlined,
.v-list-item--variant-outlined {
&:not([class*="text-"]) {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
&.text-default {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
}
// 👉 Custom Input
.v-label.custom-input {
padding: 1rem;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
opacity: 1;
white-space: normal;
&:hover {
border-color: rgba(var(--v-border-color), 0.25);
}
&.active {
border-color: rgb(var(--v-theme-primary));
.v-icon {
color: rgb(var(--v-theme-primary)) !important;
}
}
}
// Dialog responsive width
.v-dialog {
// dialog custom close btn
.v-dialog-close-btn {
position: absolute;
z-index: 1;
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
inset-block-start: 0.9375rem;
inset-inline-end: 0.9375rem;
.v-btn__overlay {
display: none;
}
}
.v-card {
@extend %style-scroll-bar;
}
}
@media (min-width: 600px) {
.v-dialog {
&.v-dialog-sm,
&.v-dialog-lg,
&.v-dialog-xl {
.v-overlay__content {
inline-size: 565px !important;
}
}
}
}
@media (min-width: 960px) {
.v-dialog {
&.v-dialog-lg,
&.v-dialog-xl {
.v-overlay__content {
inline-size: 865px !important;
}
}
}
}
@media (min-width: 1264px) {
.v-dialog.v-dialog-xl {
.v-overlay__content {
inline-size: 1165px !important;
}
}
}
// v-tab with pill support
.v-tabs.v-tabs-pill {
.v-tab.v-btn {
border-radius: 0.25rem !important;
transition: none;
.v-tab__slider {
visibility: hidden;
}
}
}
// 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;
}
}
}
// 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;
}
}
}
// 👉 Textarea
.v-textarea .v-field__input {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-mask-image: none !important;
mask-image: none !important;
}

View File

@@ -0,0 +1,16 @@
@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;
}
}

View File

@@ -1,8 +1,9 @@
@use "@configured-variables" as variables;
@use "placeholders" as *;
@use "@core/scss/base/placeholders" as *;
@use "@core/scss/template/placeholders" as *;
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "../misc";
@use "mixins";
@use "misc";
@use "@core/scss/base/mixins";
$header: ".layout-navbar";
@@ -16,14 +17,14 @@ $header: ".layout-navbar";
@if variables.$vertical-nav-navbar-style == "elevated" {
// Add transition
#{$header} {
transition: padding 0.2s ease;
transition: padding 0.2s ease, background-color 0.18s 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;
// }
#{$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
@@ -31,7 +32,7 @@ $header: ".layout-navbar";
/* This html selector with not selector is required when:
dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and out style broke
*/
html.v-overlay-scroll-blocked .layout-navbar-fixed,
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-fixed,
&.window-scrolled.layout-navbar-fixed {
#{$header} {
@@ -100,4 +101,4 @@ $header: ".layout-navbar";
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
@use "@core/scss/base/placeholders";
@use "@core/scss/base/variables";
.layout-vertical-nav,
.layout-horizontal-nav {
ol,
ul {
list-style: none;
}
}
.layout-navbar {
@if variables.$navbar-high-emphasis-text {
@extend %layout-navbar;
}
}

View File

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

View File

@@ -0,0 +1,63 @@
@use "@configured-variables" as variables;
/* This styles extends the existing layout package's styles for handling cases that aren't related to layouts package */
/*
When we use v-layout as immediate first child of `.page-content-container`, it adds display:flex and page doesn't get contained height
*/
// .layout-wrapper.layout-nav-type-vertical {
// &.layout-content-height-fixed {
// .page-content-container {
// > .v-layout:first-child > :not(.v-navigation-drawer):first-child {
// flex-grow: 1;
// block-size: 100%;
// }
// }
// }
// }
.layout-wrapper.layout-nav-type-vertical {
&.layout-content-height-fixed {
.page-content-container {
> .v-layout:first-child {
overflow: hidden;
min-block-size: 100%;
> .v-main {
// overflow-y: auto;
.v-main__wrap > :first-child {
block-size: 100%;
overflow-y: auto;
}
}
}
}
}
}
// Let div/v-layout take full height. E.g. Email App
.layout-wrapper.layout-nav-type-horizontal {
&.layout-content-height-fixed {
> .layout-page-content {
// display: flex;
}
}
}
// 👉 Floating navbar styles
@if variables.$vertical-nav-navbar-style == "floating" {
// Add spacing above navbar if navbar is floating (was in %layout-navbar-fixed placeholder)
.layout-wrapper.layout-nav-type-vertical.layout-navbar-fixed {
.layout-navbar {
inset-block-start: variables.$vertical-nav-floating-navbar-top;
}
/*
If it's floating navbar
Add `vertical-nav-floating-navbar-top` as margin top to .layout-page-content
*/
.layout-page-content {
margin-block-start: variables.$vertical-nav-floating-navbar-top;
}
}
}

View File

@@ -0,0 +1,20 @@
// 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;
color: rgb(var(--v-code-color));
font-size: 90%;
font-weight: 400;
padding-block: 0.2em;
padding-inline: 0.4em;
}

View File

@@ -0,0 +1,77 @@
@use "sass:map";
@use "@styles/variables/_vuetify.scss";
@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);
}
// 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;
}
}
@mixin bordered-skin($component, $border-property: "border", $important: false) {
#{$component} {
// background-color: rgb(var(--v-theme-background));
box-shadow: none !important;
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
}
}
// Inspired from vuetify's active-states mixin
// focus => 0.12 & selected => 0.08
@mixin selected-states($selector) {
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected")} * var(--v-theme-overlay-multiplier));
// }
// &:hover
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "hover")} * var(--v-theme-overlay-multiplier));
// }
// &:focus-visible
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier));
// }
// @supports not selector(:focus-visible) {
// &:focus {
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier));
// }
// }
// }
#{$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));
}
}
}
}

View File

@@ -0,0 +1,141 @@
@use "@configured-variables" as variables;
@use "@layouts/styles/mixins" as layoutsMixins;
// 👉 Demo spacers
// TODO: Use vuetify SCSS variable here
$card-spacer-content: 16px;
.demo-space-x {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-block-start: -$card-spacer-content;
& > * {
margin-block-start: $card-spacer-content;
margin-inline-end: $card-spacer-content;
}
}
.demo-space-y {
& > * {
margin-block-end: $card-spacer-content;
&:last-child {
margin-block-end: 0;
}
}
}
// 👉 Card match height
.match-height.v-row {
.v-card {
block-size: 100%;
}
}
// 👉 Whitespace
.whitespace-no-wrap {
white-space: nowrap;
}
// 👉 Colors
/*
Vuetify is applying `.text-white` class to badge icon but don't provide its styles
Moreover, we also use this class in some places
In vuetify 2 with `$color-pack: false` SCSS var config this class was getting generated but this is not the case in v3
We also need !important to get correct color in badge icon
*/
.text-white {
color: #fff !important;
}
.bg-var-theme-background {
background-color: rgba(var(--v-theme-background), var(--v-hover-opacity)) !important;
}
// [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })],
@each $color-name in variables.$theme-colors-name {
.bg-light-#{$color-name} {
background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important;
}
}
// 👉 clamp text
.clamp-text {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
}
.leading-normal {
line-height: normal !important;
}
// 👉 for rtl only
.flip-in-rtl {
@include layoutsMixins.rtl {
transform: scaleX(-1);
}
}
// 👉 Carousel
.carousel-delimiter-top-end {
.v-carousel__controls {
justify-content: end;
block-size: 40px;
inset-block-start: 0;
padding-inline: 1rem;
.v-btn--icon.v-btn--density-default {
block-size: calc(var(--v-btn-height) + -10px);
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
inline-size: calc(var(--v-btn-height) + -10px);
&.v-btn--active {
color: #fff;
}
.v-btn__overlay {
opacity: 0;
}
}
}
@each $color-name in variables.$theme-colors-name {
&.dots-active-#{$color-name} {
.v-carousel__controls {
.v-btn--active {
color: rgb(var(--v-theme-#{$color-name})) !important;
}
}
}
}
}
.v-timeline-item {
.app-timeline-title {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 16px;
font-weight: 500;
line-height: 1.3125rem;
}
.app-timeline-meta {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
font-size: 12px;
line-height: 0.875rem;
}
.app-timeline-text {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
font-size: 14px;
line-height: 1.25rem;
}
}

View File

@@ -0,0 +1,90 @@
@use "sass:map";
@use "sass:list";
@use "@configured-variables" as variables;
// 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
@each $name, $size in variables.$font-sizes {
.text-#{$name} {
font-size: $size;
line-height: map.get(variables.$font-line-height, $name);
}
}
// truncate utility class
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// gap utility class
@each $name, $size in variables.$gap {
.gap-#{$name} {
gap: $size;
}
.gap-x-#{$name} {
column-gap: $size;
}
.gap-y-#{$name} {
row-gap: $size;
}
}
.list-none {
list-style-type: none;
}

View File

@@ -0,0 +1,197 @@
@use "vuetify/lib/styles/tools/functions" as *;
/*
TODO: Add docs on when to use placeholder vs when to use SASS variable
Placeholder
- When we want to keep customization to our self between templates use it
Variables
- When we want to allow customization from both user and our side
- You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header)
*/
@forward "@layouts/styles/variables" with (
// Adjust z-index so vertical nav & overlay stays on top of v-layout in v-main. E.g. Email app
$layout-vertical-nav-z-index: 1004,
$layout-overlay-z-index: 1003,
);
@use "@layouts/styles/variables" as *;
// 👉 Default layout
$navbar-high-emphasis-text: true !default;
// @forward "@layouts/styles/variables" with (
// $layout-vertical-nav-width: 350px !default,
// );
$theme-colors-name: (
"primary",
"secondary",
"error",
"info",
"success",
"warning"
) !default;
// 👉 Default layout with vertical nav
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
// 👉 Vertical nav
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
// This is used to keep consistency between nav items and nav header left & right margin
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 1rem !default;
$vertical-nav-horizontal-padding: 0.75rem !default;
// Vertical nav header height. Mostly we will align it with navbar height;
$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default;
$vertical-nav-navbar-elevation: 3 !default;
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
$vertical-nav-floating-navbar-top: 1rem !default;
// Vertical nav header padding
$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default;
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
// Move logo when vertical nav is mini (collapsed but not hovered)
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
// Space between logo and title
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
// Section title margin top (when its not first child)
$vertical-nav-section-title-mt: 1.5rem !default;
// Section title margin bottom
$vertical-nav-section-title-mb: 0.5rem !default;
// Vertical nav icons
$vertical-nav-items-icon-size: 1.5rem !default;
$vertical-nav-items-nested-icon-size: 0.9rem !default;
$vertical-nav-items-icon-margin-inline-end: 0.5rem !default;
// Transition duration for nav group arrow
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
// Timing function for nav group arrow
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
// 👉 Horizontal nav
/*
❗ Heads up
==================
Here we assume we will always use shorthand property which will apply same padding on four side
This is because this have been used as value of top property by `.popper-content`
*/
$horizontal-nav-padding: 0.6875rem !default;
// Gap between top level horizontal nav items
$horizontal-nav-top-level-items-gap: 4px !default;
// Horizontal nav icons
$horizontal-nav-items-icon-size: 1.5rem !default;
$horizontal-nav-third-level-icon-size: 0.9rem !default;
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
// We used SCSS variable because we want to allow users to update max height of popper content
// 120px is combined height of navbar & horizontal nav
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
// This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
// 👉 Plugins
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
// 👉 Vuetify
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
$vuetify-reduce-default-compact-button-icon-size: true !default;
// 👉 Custom variables
// for utility classes
$font-sizes: () !default;
$font-sizes: map-deep-merge(
(
"xs": 0.75rem,
"sm": 0.875rem,
"base": 1rem,
"lg": 1.125rem,
"xl": 1.25rem,
"2xl": 1.5rem,
"3xl": 1.875rem,
"4xl": 2.25rem,
"5xl": 3rem,
"6xl": 3.75rem,
"7xl": 4.5rem,
"8xl": 6rem,
"9xl": 8rem
),
$font-sizes
);
// line height
$font-line-height: () !default;
$font-line-height: map-deep-merge(
(
"xs": 1rem,
"sm": 1.25rem,
"base": 1.5rem,
"lg": 1.75rem,
"xl": 1.75rem,
"2xl": 2rem,
"3xl": 2.25rem,
"4xl": 2.5rem,
"5xl": 1,
"6xl": 1,
"7xl": 1,
"8xl": 1,
"9xl": 1
),
$font-line-height
);
// gap utility class
$gap: () !default;
$gap: map-deep-merge(
(
"0": 0,
"1": 0.25rem,
"2": 0.5rem,
"3": 0.75rem,
"4": 1rem,
"5": 1.25rem,
"6":1.5rem,
"7": 1.75rem,
"8": 2rem,
"9": 2.25rem,
"10": 2.5rem,
"11": 2.75rem,
"12": 3rem,
"14": 3.5rem,
"16": 4rem,
"20": 5rem,
"24": 6rem,
"28": 7rem,
"32": 8rem,
"36": 9rem,
"40": 10rem,
"44": 11rem,
"48": 12rem,
"52": 13rem,
"56": 14rem,
"60": 15rem,
"64": 16rem,
"72": 18rem,
"80": 20rem,
"96": 24rem
),
$gap
);

View File

@@ -0,0 +1,250 @@
@use "@core/scss/base/placeholders" as *;
@use "@core/scss/template/placeholders" as *;
@use "@layouts/styles/mixins" as layoutsMixins;
@use "@configured-variables" as variables;
@use "@core/scss/base/mixins" as mixins;
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
.layout-nav-type-vertical {
// 👉 Layout Vertical nav
.layout-vertical-nav {
$sl-layout-nav-type-vertical: &;
@extend %nav;
@at-root {
// Add styles for collapsed vertical nav
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}.hovered {
@include mixins.elevation(6);
}
}
background-color: variables.$vertical-nav-background-color;
// 👉 Nav header
.nav-header {
overflow: hidden;
padding: variables.$vertical-nav-header-padding;
margin-inline: variables.$vertical-nav-header-inline-spacing;
min-block-size: variables.$vertical-nav-header-height;
// TEMPLATE: Check if we need to move this to master
.app-logo {
flex-shrink: 0;
transition: transform 0.25s ease-in-out;
@at-root {
// Move logo a bit to align center with the icons in vertical nav mini variant
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}:not(.hovered) .nav-header .app-logo {
transform: translateX(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini);
@include layoutsMixins.rtl {
transform: translateX(-(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini));
}
}
}
}
.app-title {
margin-inline-start: variables.$vertical-nav-header-logo-title-spacing;
}
.header-action {
@extend %nav-header-action;
}
}
// 👉 Nav items shadow
.vertical-nav-items-shadow {
position: absolute;
z-index: 1;
background:
linear-gradient(
rgb(#{variables.$vertical-nav-background-color-rgb}) 5%,
rgba(#{variables.$vertical-nav-background-color-rgb}, 75%) 45%,
rgba(#{variables.$vertical-nav-background-color-rgb}, 20%) 80%,
transparent
);
block-size: 55px;
inline-size: 100%;
inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px);
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease-in-out;
will-change: opacity;
@include layoutsMixins.rtl {
transform: translateX(8px);
}
}
&.scrolled {
.vertical-nav-items-shadow {
opacity: 1;
}
}
.ps__rail-y {
// Setting z-index: 1 will make perfect scrollbar thumb appear on top of vertical nav items shadow
z-index: 1;
}
// 👉 Nav section title
.nav-section-title {
@extend %vertical-nav-item;
@extend %vertical-nav-section-title;
margin-block-end: variables.$vertical-nav-section-title-mb;
&:not(:first-child) {
margin-block-start: variables.$vertical-nav-section-title-mt;
}
.placeholder-icon {
margin-inline: auto;
}
}
// Nav item badge
.nav-item-badge {
@extend %vertical-nav-item-badge;
}
// 👉 Nav group & Link
.nav-link,
.nav-group {
overflow: hidden;
> :first-child {
@extend %vertical-nav-item;
@extend %vertical-nav-item-interactive;
}
.nav-item-icon {
@extend %vertical-nav-items-icon;
}
&.disabled {
opacity: var(--v-disabled-opacity);
pointer-events: none;
}
}
// 👉 Vertical nav link
.nav-link {
@extend %nav-link;
> .router-link-exact-active {
@extend %nav-link-active;
}
> a {
// Adds before psudo element to style hover state
@include mixins.before-pseudo;
// Adds vuetify states
@include vuetifyStates.states($active: false);
}
}
// 👉 Vertical nav group
.nav-group {
// Reduce the size of icon if link/group is inside group
.nav-group,
.nav-link {
.nav-item-icon {
@extend %vertical-nav-items-nested-icon;
}
}
// Hide icons after 2nd level
& .nav-group {
.nav-link,
.nav-group {
.nav-item-icon {
@extend %vertical-nav-items-icon-after-2nd-level;
}
}
}
.nav-group-arrow {
flex-shrink: 0;
transform-origin: center;
transition: transform variables.$vertical-nav-nav-group-arrow-transition-duration variables.$vertical-nav-nav-group-arrow-transition-timing-function;
will-change: transform;
}
// Rotate arrow icon if group is opened
&.open {
> .nav-group-label .nav-group-arrow {
transform: rotateZ(90deg);
}
}
// Nav group label
> :first-child {
// Adds before psudo element to style hover state
@include mixins.before-pseudo;
// Adds vuetify states
@include vuetifyStates.states($active: false);
}
// Active & open states for nav group label
&.active,
&.open {
> :first-child {
@extend %vertical-nav-group-open-active;
}
}
}
}
}
// SECTION: Transitions
.vertical-nav-section-title-enter-active,
.vertical-nav-section-title-leave-active {
transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
}
.vertical-nav-section-title-enter-from,
.vertical-nav-section-title-leave-to {
opacity: 0;
transform: translateX(15px);
@include layoutsMixins.rtl {
transform: translateX(-15px);
}
}
.transition-slide-x-enter-active,
.transition-slide-x-leave-active {
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
}
.transition-slide-x-enter-from,
.transition-slide-x-leave-to {
opacity: 0;
transform: translateX(-15px);
@include layoutsMixins.rtl {
transform: translateX(15px);
}
}
.vertical-nav-app-title-enter-active,
.vertical-nav-app-title-leave-active {
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
}
.vertical-nav-app-title-enter-from,
.vertical-nav-app-title-leave-to {
opacity: 0;
transform: translateX(-15px);
@include layoutsMixins.rtl {
transform: translateX(15px);
}
}
// !SECTION

View File

@@ -2,11 +2,6 @@ $ps-size: 0.25rem;
$ps-hover-size: 0.375rem;
$ps-track-size: 0.5rem;
.ps__thumb-x,
.ps__thumb-y {
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
}
.ps__thumb-y {
inline-size: $ps-size;
inset-inline-end: 0.0625rem;
@@ -34,10 +29,15 @@ $ps-track-size: 0.5rem;
inline-size: $ps-hover-size;
}
.ps__thumb-x,
.ps__thumb-y {
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
}
// fix bug
@media(hover: none) {
.ps > .ps__rail-x,
.ps > .ps__rail-y {
opacity: 0.6;
}
}
}

View File

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

View File

@@ -1,5 +1,5 @@
@use "@core/scss/base/utils";
@use "@configured-variables" as variables;
@use "../../../utils";
// 👉 Application
// We need accurate vh in mobile devices as well
@@ -195,6 +195,7 @@ h6,
color: rgb(var(--v-border-color));
}
// 👉 DataTable
// 👉 DataTable
.v-data-table {
/* stylelint-disable-next-line no-descending-specificity */
@@ -211,6 +212,10 @@ h6,
}
}
.v-data-table-footer {
margin-block-start: 1rem;
}
// 👉 v-field
.v-field:hover .v-field__outline {
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
@@ -249,53 +254,34 @@ h6,
.v-badge__badge {
display: flex;
align-items: center;
justify-content: center;
}
// 👉 Dialog
.v-dialog--fullscreen {
background-color: rgb(var(--v-theme-surface));
// 👉 Btn focus outline style removed
.v-btn:focus-visible::after {
opacity: 0 !important;
}
// For dialog card title
.v-card-item + .v-card-text {
padding-block-start: 0 !important;
// .v-select chip spacing for slot
.v-input:not(.v-select--chips) .v-select__selection {
.v-chip {
margin-block: 2px var(--select-chips-margin-bottom);
}
}
// 👉 v-slide-group (List of chips)
.v-slide-group {
.v-slide-group__container {
display: flex;
flex-wrap: wrap;
// 👉 VCard and VList subtitle color
.v-card-subtitle,
.v-list-item-subtitle {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
// Spacing between buttons in v-slide-group
.v-slide-group-item:not(:last-child) {
margin-inline-end: 0.5rem;
// 👉 placeholders
.v-field__input {
@at-root {
& input::placeholder,
input#{&}::placeholder,
textarea#{&}::placeholder {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
opacity: 1 !important;
}
}
}
// 👉 Expansion Panel
.v-expansion-panels {
.v-expansion-panel-title {
min-block-size: unset !important;
padding-block: 1rem !important;
}
}
// 👉 v-textarea
.v-textarea {
textarea {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
&:hover,
&:focus {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
}
}
// 👉 Cursor
.cursor-pointer {
cursor: pointer;
}

View File

@@ -0,0 +1,49 @@
// 👉 Shadow opacities
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
// 👉 Card transition properties
$card-transition-property-custom: box-shadow, opacity;
@forward "vuetify/settings" with (
// 👉 General settings
$color-pack: false !default,
// 👉 Shadow opacity
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
// 👉 Card
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
$card-elevation: 6 !default,
$card-title-line-height: 1.6 !default,
$card-actions-min-height: unset !default,
$card-text-padding: 1.25rem !default,
$card-item-padding: 1.25rem !default,
$card-actions-padding: 0 12px 12px !default,
$card-transition-property: $card-transition-property-custom !default,
$card-subtitle-opacity: 1 !default,
// 👉 Expansion Panel
$expansion-panel-active-title-min-height: 48px !default,
// 👉 List
$list-item-icon-margin-end: 16px !default,
$list-item-icon-margin-start: 16px !default,
$list-item-subtitle-opacity: 1 !default,
// 👉 Tooltip
$tooltip-background-color: rgba(59, 55, 68, 0.9) !default,
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
$tooltip-font-size: 0.75rem !default,
$button-icon-density: ("default": 2, "comfortable": 0, "compact": -1 ) !default,
// 👉 VTimeline
$timeline-dot-size: 34px !default,
// 👉 VOverlay
$overlay-opacity: 1 !default,
);

View File

@@ -1,13 +1,13 @@
@use "@configured-variables" as variables;
@use "misc";
@use "../mixins";
@use "@core/scss/base/mixins";
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
background-color: rgb(var(--v-theme-surface));
}
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
// @include mixins.elevation(variables.$vertical-nav-navbar-elevation);
@include mixins.elevation(variables.$vertical-nav-navbar-elevation);
// If navbar is contained => Squeeze navbar content on scroll
@if variables.$layout-vertical-nav-navbar-is-contained {
@@ -36,10 +36,11 @@
block-size: calc(variables.$layout-vertical-nav-navbar-height + variables.$vertical-nav-floating-navbar-top + 0.5rem);
content: "";
inset-block-start: -(variables.$vertical-nav-floating-navbar-top);
inset-inline: 0;
inset-inline-end: 0;
inset-inline-start: 0;
/* stylelint-disable property-no-vendor-prefix */
-webkit-mask: linear-gradient(black, black 18%, transparent 100%);
mask: linear-gradient(black, black 18%, transparent 100%);
/* stylelint-enable */
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
%blurry-bg {
/* stylelint-disable property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
/* stylelint-enable */
background-color: rgb(var(--v-theme-surface), 0.8);
}

View File

@@ -0,0 +1,34 @@
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@core/scss/base/mixins";
// This is common style that needs to be applied to both navs
%nav {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
.nav-item-title {
letter-spacing: 0.15px;
}
.nav-section-title {
letter-spacing: 0.4px;
}
}
/*
Active nav link styles for horizontal & vertical nav
For horizontal nav it will be only applied to top level nav items
For vertical nav it will be only applied to nav links (not nav groups)
*/
%nav-link-active {
background-color: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
@include mixins.elevation(3);
}
%nav-link {
a {
color: inherit;
}
}

View File

@@ -0,0 +1,81 @@
@use "@core/scss/base/mixins";
@use "@configured-variables" as variables;
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
%nav-header-action {
font-size: 1.25rem;
}
// Nav items styles (including section title)
%vertical-nav-item {
margin-block: 0;
margin-inline: variables.$vertical-nav-horizontal-spacing;
padding-block: 0;
padding-inline: variables.$vertical-nav-horizontal-padding;
white-space: nowrap;
}
// This is same as `%vertical-nav-item` except section title is excluded
%vertical-nav-item-interactive {
border-radius: 0.4rem;
block-size: 2.75rem;
/*
We will use `margin-block-end` instead of `margin-block` to give more space for shadow to appear.
With `margin-block`, due to small space (space gets divided between top & bottom) shadow cuts
*/
margin-block-end: 0.375rem;
}
// Common styles for nav item icon styles
// Nav group's children icon styles are not here (Adjusts height, width & margin)
%vertical-nav-items-icon {
flex-shrink: 0;
font-size: variables.$vertical-nav-items-icon-size;
margin-inline-end: variables.$vertical-nav-items-icon-margin-inline-end;
}
// Icon styling for icon nested inside another nav item (2nd level)
%vertical-nav-items-nested-icon {
/*
`margin-inline` will be (normal icon font-size - small icon font-size) / 2
(1.5rem - 0.9rem) / 2 => 0.6rem / 2 => 0.3rem
*/
$vertical-nav-items-nested-icon-margin-inline: calc((variables.$vertical-nav-items-icon-size - variables.$vertical-nav-items-nested-icon-size) / 2);
font-size: variables.$vertical-nav-items-nested-icon-size;
margin-inline-end: $vertical-nav-items-nested-icon-margin-inline + variables.$vertical-nav-items-icon-margin-inline-end;
margin-inline-start: $vertical-nav-items-nested-icon-margin-inline;
}
%vertical-nav-items-icon-after-2nd-level {
visibility: hidden;
}
// Open & Active nav group styles
%vertical-nav-group-open-active {
@include mixins.selected-states("&::before");
}
// Section title
%vertical-nav-section-title {
// Setting height will prevent jerking when text & icon is toggled
block-size: 1.5rem;
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
font-size: 0.75rem;
text-transform: uppercase;
}
// Vertical nav item badge styles
%vertical-nav-item-badge {
display: inline-block;
border-radius: 1.5rem;
font-size: 0.8em;
font-weight: 500;
line-height: 1;
padding-block: 0.25em;
padding-inline: 0.55em;
text-align: center;
vertical-align: baseline;
white-space: nowrap;
}

View File

@@ -1,5 +1,33 @@
@use "sass:map";
@use "template/index";
// 保留这个引用以向后兼容但实际功能已经移至template/index.scss
@use "variables";
// Layout
@use "vertical-nav";
@use "default-layout";
// Components
@use "components";
// Utilities
@use "utilities";
// Misc
@use "misc";
// Dark
@use "dark";
a {
color: rgb(var(--v-theme-primary));
text-decoration: none;
}
// Vuetify 3 don't provide margin bottom style like vuetify 2
p {
margin-block-end: 1rem;
}
// Iconify icon size
svg.iconify {
block-size: 1em;
inline-size: 1em;
}

View File

@@ -1,7 +1,7 @@
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$font-family-custom: inter, sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
// 👉 Card transition properties
$card-transition-property-custom: box-shadow, opacity;

View File

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

View File

@@ -1,6 +1,7 @@
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@configured-variables" as variables;
@use "mixins";
@use "@core/scss/base/mixins" as mixins_base;
// 👉 Alert
.v-alert {
@@ -175,6 +176,10 @@
th {
background: rgb(var(--v-table-header-background)) !important;
}
.v-data-table-footer {
margin-block-start: 1rem;
}
}
// 👉 Pagination
@@ -189,5 +194,5 @@
// 👉 SnackBar
.v-snackbar--variant-elevated {
@include mixins.elevation(6);
@include mixins_base.elevation(6);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
@use "../mixins";
@use "@configured-variables" as variables;
@use "@core/scss/base/mixins";
.v-application .fc {
--fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04);
@@ -17,20 +16,16 @@
padding: 0;
}
.fc-toolbar-title {
display: inline-block;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 1.25rem;
font-weight: 500;
margin-inline-start: 0.25rem;
}
.fc-col-header-cell-cushion {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 0.875rem;
font-weight: 600;
}
.fc-toolbar .fc-toolbar-title {
margin-inline-start: 0.25rem;
}
.fc-event-time {
font-size: 0.75rem;
}
@@ -97,10 +92,10 @@
.fc-header-toolbar {
flex-wrap: wrap;
margin: 1.25rem;
gap: 1rem 0.5rem;
column-gap: 0.5rem;
row-gap: 1rem;
}
// 👉 Toolbar Chunk and Button Group
.fc-toolbar-chunk {
display: flex;
align-items: center;
@@ -108,38 +103,19 @@
.fc-button-group {
.fc-button-primary {
&,
&:focus,
&:hover,
&:not(.disabled):active {
border-color: transparent;
background-color: transparent;
box-shadow: none !important;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
&:focus {
box-shadow: none !important;
}
}
}
// 👉 sidebar toggler
.fc-drawerToggler-button {
display: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(94,86,105,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
background-position: 50%;
background-repeat: no-repeat;
block-size: 1.5625rem;
font-size: 0;
inline-size: 1.5625rem;
margin-inline-end: 0.25rem;
@media (width <= 1264px) {
display: block !important;
}
.v-theme--dark & {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
}
}
// Special styling for the last toolbar chunk
&:last-child {
.fc-button-group {
border: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity));
@@ -164,6 +140,13 @@
}
}
.fc-toolbar-title {
display: inline-block;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 1.25rem;
font-weight: 500;
}
.fc-scrollgrid-section {
th {
border-inline: 0;
@@ -235,6 +218,37 @@
}
}
// 👉 sidebar toggler
.fc-toolbar-chunk {
.fc-button-group {
align-items: center;
.fc-button .fc-icon {
vertical-align: bottom;
}
// Below two `background-image` styles contains static color due to browser limitation of not parsing the css var inside CSS url()
.fc-drawerToggler-button {
display: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(94,86,105,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
background-position: 50%;
background-repeat: no-repeat;
block-size: 1.5625rem;
font-size: 0;
inline-size: 1.5625rem;
margin-inline-end: 0.25rem;
@media (max-width: 1264px) {
display: block !important;
}
.v-theme--dark & {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
}
}
}
}
// Workaround of https://github.com/fullcalendar/fullcalendar/issues/6407
.fc-col-header,
.fc-daygrid-body,

View File

@@ -2,20 +2,9 @@ $shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
/* stylelint-disable-next-line max-line-length */
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
// 👉 Card transition properties
$card-transition-property-custom: box-shadow, opacity;
@forward "vuetify/settings" with (
// 👉 General settings
$color-pack: false !default,
// 👉 Shadow opacity
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
$font-family-custom: inter, sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
@forward "../../../base/libs/vuetify/variables" with (
$body-font-family: $font-family-custom !default,
$border-radius-root: 6px !default,
@@ -121,18 +110,6 @@ $card-transition-property-custom: box-shadow, opacity;
24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom)
) !default,
// 👉 Card
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
$card-elevation: 6 !default,
$card-title-line-height: 2rem !default,
$card-actions-min-height: unset !default,
$card-text-padding: 1.25rem !default,
$card-item-padding: 1.25rem !default,
$card-actions-padding: 0 12px 12px !default,
$card-transition-property: $card-transition-property-custom !default,
$card-subtitle-opacity: 1 !default,
$card-title-letter-spacing: 0.0094rem !default,
// 👉 Typography
$typography: (
"h1": (
@@ -184,16 +161,13 @@ $card-transition-property-custom: box-shadow, opacity;
)
) !default,
// 👉 List
$list-item-icon-margin-end: 16px !default,
$list-item-icon-margin-start: 16px !default,
$list-item-subtitle-opacity: 1 !default,
$list-subheader-text-opacity: 1 !default,
// 👉 Card
$card-title-letter-spacing: 0.0094rem !default,
$card-title-line-height: 2rem !default,
$card-subtitle-opacity: 1 !default,
// 👉 Tooltip
$tooltip-background-color: #212121 !default,
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
$tooltip-font-size: 0.75rem !default,
$tooltip-background-color:#212121 !default,
$tooltip-border-radius: 4px !default,
$tooltip-padding: 4px 8px !default,
@@ -235,6 +209,9 @@ $card-transition-property-custom: box-shadow, opacity;
// 👉 Menu
$menu-content-border-radius: 5px !default,
// 👉 List
$list-subheader-text-opacity: 1 !default,
// 👉 Snackbar
$snackbar-background:#212121 !default,
$snackbar-border-radius: 4px !default,

View File

@@ -1,2 +1 @@
@use "variables";
@use "overrides";
@use "@core/scss/base/libs/vuetify";

View File

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

View File

@@ -1,45 +0,0 @@
%blurry-bg {
position: relative;
background: rgba(var(--v-theme-background), 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 4%), 0 1px 2px rgba(0, 0, 0, 2%);
@media (width > 768px) {
.v-theme--transparent & {
backdrop-filter: blur(5px);
background: rgba(var(--v-theme-background), 0.1);
}
}
@media (width <= 768px) {
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: all 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 & {
background: rgba(var(--v-theme-background), 0.3);
}
}
}
}

View File

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

View File

@@ -60,25 +60,19 @@ export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : valu
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
// 格式化为xx[TGMK]B
export function formatFileSize(bytes: number, decimals = 2, prefix = false) {
// 负数标记
let negative = false
let size = bytes
if (bytes < 0) {
negative = true
size = Math.abs(bytes)
}
export function formatFileSize(bytes: number) {
if (bytes < 0) throw new Error('字节数不能为负数。')
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
if (negative) return `-${size.toFixed(decimals)} ${units[unitIndex]}`
else
return prefix ? `+${size.toFixed(decimals)} ${units[unitIndex]}` : `${size.toFixed(decimals)} ${units[unitIndex]}`
return `${size.toFixed(2)} ${units[unitIndex]}`
}
// 将时间秒格式化为时分秒
@@ -153,12 +147,3 @@ export function formatDateDifference(dateString: string): string {
if (!dateString) return ''
return dayjs(dateString).fromNow()
}
// 格式化评份如为10及以下的数按原值显示否则格式化为xxM、xxK显示
export function formatRating(rating: number): string {
if (!rating) return ''
if (rating <= 10) return rating.toString()
if (rating < 1000) return rating.toLocaleString()
if (rating < 1000 * 1000) return `${(rating / 1000).toFixed(1)}K`
return `${(rating / 1000 / 1000).toFixed(1)}M`
}

View File

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

View File

@@ -1,10 +1,9 @@
import copy from 'copy-to-clipboard'
// 请求和获取剪贴板内容
export async function getClipboardContent() {
if (navigator.clipboard && window.isSecureContext) {
return await navigator.clipboard.readText()
} else {
}
else {
const input = document.createElement('textarea')
document.body.appendChild(input)
input.select()
@@ -15,10 +14,19 @@ export async function getClipboardContent() {
}
}
// 将内容复制到剪贴板
// 将内容复制到剪贴板,兼容非安全域场景
export async function copyToClipboard(content: string) {
const success = copy(content)
return success
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(content)
}
else {
const input = document.createElement('textarea')
input.value = content
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
}
}
// VAPID公钥转Uint8Array
@@ -34,53 +42,3 @@ export function urlBase64ToUint8Array(base64String: string) {
}
return outputArray
}
// 判断是否为PWA
export const isPWA = async (): Promise<boolean> => {
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations()
return registrations.length > 0
}
return (window.navigator as any).standalone === true
}
// 同步检测PWA显示模式
export const isPWADisplayMode = (): boolean => {
return (
window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone ||
document.referrer.includes('android-app://')
)
}
// 全面的PWA检测推荐使用
export const checkPWAStatus = async () => {
const hasServiceWorker = await isPWA()
const isStandaloneMode = isPWADisplayMode()
return {
// 是否有PWA功能Service Worker
hasPWAFeatures: hasServiceWorker,
// 是否在独立显示模式下运行
isStandaloneMode,
// 综合判断更宽松的检测在移动设备上默认启用PWA功能
isPWAEnvironment: hasServiceWorker || isStandaloneMode || isMobileDevice(),
// 完整的PWA体验既有功能又在独立模式下运行
isFullPWA: hasServiceWorker && isStandaloneMode,
}
}
// 检测是否为移动设备
const isMobileDevice = (): boolean => {
// 检查用户代理字符串
const userAgent = navigator.userAgent || ''
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i
// 检查触摸屏支持
const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0
// 检查屏幕尺寸小于768px认为是移动设备
const isMobileSize = window.innerWidth < 768
return mobileRegex.test(userAgent) || hasTouchScreen || isMobileSize
}

View File

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

View File

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

View File

@@ -12,14 +12,6 @@
*/
import { promises as fs } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
// Get current directory
const __dirname = dirname(fileURLToPath(import.meta.url))
// Create require function for importing JSON files in ESM
const require = createRequire(import.meta.url)
// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
import {

View File

@@ -1,17 +1,17 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "Node16",
"module": "CommonJS",
"declaration": false,
"declarationMap": false,
"sourceMap": false,
"composite": false,
"strict": true,
"moduleResolution": "node16",
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
},
"exclude": [
"./*.js"
]
}
}

View File

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

View File

@@ -0,0 +1,92 @@
<!-- Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/ -->
<script lang="ts">
import { Transition } from 'vue'
export default defineComponent({
name: 'TransitionExpand',
setup(_, { slots }) {
const onEnter = (element: HTMLElement) => {
const width = getComputedStyle(element).width
element.style.width = width
element.style.position = 'absolute'
element.style.visibility = 'hidden'
element.style.height = 'auto'
const height = getComputedStyle(element).height
element.style.width = ''
element.style.position = ''
element.style.visibility = ''
element.style.height = '0px'
// Force repaint to make sure the
// animation is triggered correctly.
// eslint-disable-next-line no-unused-expressions
getComputedStyle(element).height
// Trigger the animation.
// We use `requestAnimationFrame` because we need
// to make sure the browser has finished
// painting after setting the `height`
// to `0` in the line above.
requestAnimationFrame(() => {
element.style.height = height
})
}
const onAfterEnter = (element: HTMLElement) => {
element.style.height = 'auto'
}
const onLeave = (element: HTMLElement) => {
const height = getComputedStyle(element).height
element.style.height = height
// Force repaint to make sure the
// animation is triggered correctly.
// eslint-disable-next-line no-unused-expressions
getComputedStyle(element).height
requestAnimationFrame(() => {
element.style.height = '0px'
})
}
return () => h(
h(Transition),
{
name: 'expand',
onEnter,
onAfterEnter,
onLeave,
},
() => slots.default?.(),
)
},
})
</script>
<style>
.expand-enter-active,
.expand-leave-active {
overflow: hidden;
transition: block-size var(--expand-transition-duration, 0.25s) ease;
}
.expand-enter-from,
.expand-leave-to {
block-size: 0;
}
</style>
<style scoped>
* {
backface-visibility: hidden;
perspective: 62.5rem;
transform: translateZ(0);
will-change: block-size;
}
</style>

View File

@@ -53,12 +53,15 @@ function handleNavScroll(evt: Event) {
<RouterLink to="/" class="app-logo d-flex align-center app-title-wrapper">
<div class="d-flex" v-html="logo" />
<h1 class="font-weight-bold leading-normal text-xl">
MOVIEPILOT <span class="text-sm text-gray-500">v2</span>
<h1 class="font-weight-bold leading-normal text-2xl">
MOVIEPILOT
</h1>
</RouterLink>
</slot>
</div>
<slot name="before-nav-items">
<div class="vertical-nav-items-shadow" />
</slot>
<slot name="nav-items" :update-is-vertical-nav-scrolled="updateIsVerticalNavScrolled">
<PerfectScrollbar
tag="ul"

View File

@@ -38,46 +38,27 @@ export default defineComponent({
)
// 👉 Navbar
const navbar = h(
'header',
{ class: ['layout-navbar navbar-blur'] },
[
h(
'div',
{ class: 'navbar-content-container' },
[
slots.navbar?.({
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
}),
// 👉 Dynamic Header Tab in NavBar
slots['dynamic-header-tab']?.()
? h('div', { class: 'layout-dynamic-header-tab' }, slots['dynamic-header-tab']?.())
: null,
].filter(Boolean),
),
].filter(Boolean),
)
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
h(
'div',
{ class: 'navbar-content-container' },
slots.navbar?.({
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
}),
),
])
const main = h(
'main',
{ class: 'layout-page-content' },
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
h('section', { class: 'page-content-container' }, slots.default?.()),
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true },
() => h('section', { class: 'page-content-container' }, slots.default?.()),
),
)
// 👉 根据路由 meta 决定 footer 高度
const shouldShowFooter = !route.meta.hideFooter
// 👉 Footer
const footer = h('footer', { class: 'layout-footer' }, [
h(
'div',
{
class: ['footer-content-container', !shouldShowFooter && 'footer-content-container-noheight'],
},
slots.footer?.(),
),
h('div', { class: 'footer-content-container' }, slots.footer?.()),
])
// 👉 Overlay
@@ -99,7 +80,11 @@ export default defineComponent({
scrollDistance.value && 'window-scrolled',
],
},
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
[
verticalNav,
h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]),
layoutOverlay,
],
)
}
},
@@ -107,15 +92,9 @@ export default defineComponent({
</script>
<style lang="scss">
@use '@configured-variables' as variables;
@use '@layouts/styles/placeholders';
@use '@layouts/styles/mixins';
.layout-page-content {
position: relative;
z-index: 1;
margin-block-start: 0;
}
@use "@configured-variables" as variables;
@use "@layouts/styles/placeholders";
@use "@layouts/styles/mixins";
.layout-wrapper.layout-nav-type-vertical {
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
@@ -132,13 +111,13 @@ export default defineComponent({
.layout-navbar {
position: fixed;
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
inset-block-start: 0;
.navbar-content-container {
block-size: calc(
env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height + var(--navbar-tab-height)
env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height
);
}
@@ -147,6 +126,10 @@ export default defineComponent({
.layout-navbar {
@if variables.$layout-vertical-nav-navbar-is-contained {
@include mixins.boxed-content;
} @else {
.navbar-content-container {
@include mixins.boxed-content;
}
}
}
}
@@ -185,7 +168,7 @@ export default defineComponent({
}
&:not(.layout-overlay-nav) .layout-content-wrapper {
padding-inline-start: calc(variables.$layout-vertical-nav-width);
padding-inline-start: variables.$layout-vertical-nav-width;
}
// Adjust right column pl when vertical nav is collapsed
@@ -217,8 +200,7 @@ export default defineComponent({
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
.layout-navbar {
inline-size: 100%;
padding-inline: 0;
width: 100%;
}
}
</style>

View File

@@ -7,9 +7,19 @@ defineProps<{
</script>
<template>
<li class="nav-link" :class="{ disabled: item.disable }">
<Component :is="item.to ? 'RouterLink' : 'a'" :to="item.to" :href="item.href">
<VIcon :icon="item.icon as string" class="nav-item-icon" />
<li
class="nav-link"
:class="{ disabled: item.disable }"
>
<Component
:is="item.to ? 'RouterLink' : 'a'"
:to="item.to"
:href="item.href"
>
<VIcon
:icon="item.icon"
class="nav-item-icon"
/>
<!-- 👉 Title -->
<span class="nav-item-title">
{{ item.title }}

View File

@@ -10,7 +10,10 @@ defineProps<{
<li class="nav-section-title">
<div class="title-wrapper">
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<span class="title-text" v-text="item.heading" />
<span
class="title-text"
v-text="item.heading"
/>
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
</div>
</li>

View File

@@ -5,62 +5,58 @@
@use "@configured-variables" as variables;
html {
min-height: calc(100% + env(safe-area-inset-top));
background: rgb(var(--v-theme-background));
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
overflow-y: overlay;
}
body {
background: rgb(var(--v-theme-background));
overscroll-behavior-y: contain;
--webkit-overflow-scrolling: touch;
background: rgb(var(--v-theme-background));
}
body,
#app,
.v-application {
min-block-size: 100%;
min-height: 100%;
}
.layout-vertical-nav {
padding-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.navbar-content-container {
padding-block-start: env(safe-area-inset-top);
padding-inline: 0.5rem;
padding-top: env(safe-area-inset-top);
}
.layout-page-content {
@include mixins.boxed-content(true);
overflow: hidden;
flex-grow: 1;
// TODO: Use grid gutter variable here;
overflow: hidden;
// TODO: Use grid gutter variable here
padding-block: 1.5rem;
padding-inline: 0.5rem;
padding-block-start: calc(env(safe-area-inset-top) + 4.5rem);
// display: flex;display
padding-top: calc(env(safe-area-inset-top) + 4.25rem);
// display: flex;
.page-content-container {
// flex: 1;flex
// flex: 1;
display: flex;
& > div:first-child {
position: relative;
flex: auto;
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
position: relative;
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
}
}
}
@media screen and (width <= 1280px){
@media screen and (max-width: 1280px){
.page-content-container > div:first-child {
inline-size: calc(100vw - 1rem) !important;
width: calc(100vw - 1rem) !important;
}
}
@@ -69,10 +65,6 @@ body,
block-size: variables.$layout-vertical-nav-footer-height;
}
.footer-content-container-noheight {
block-size: 0 !important;
}
.layout-footer-sticky & {
position: sticky;
inset-block-end: 0;

View File

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

View File

@@ -4,7 +4,7 @@
%boxed-content {
@at-root #{&}-spacing {
// TODO: Use grid gutter variable here
// padding-inline: 0.5rem;
padding-inline: 0.5rem;
}
inline-size: 100%;

View File

@@ -19,7 +19,7 @@ $layout-horizontal-nav-layout-navbar-z-index: 11 !default;
$layout-boxed-content-width: 90rem !default;
// 👉Footer
$layout-vertical-nav-footer-height: 8rem !default;
$layout-vertical-nav-footer-height: 0rem !default;
// 👉 Layout overlay
$layout-overlay-z-index: 11 !default;

View File

@@ -114,7 +114,6 @@ export interface NavLinkProps {
export interface NavLink extends NavLinkProps, Partial<AclProperties> {
title: string
full_title?: string
icon?: unknown
badgeContent?: string
badgeClass?: string
@@ -123,9 +122,8 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
export interface NavMenu extends NavLink {
header: string
admin: boolean
description?: string
admin?: boolean
footer?: boolean
}
// 👉 Vertical nav group

View File

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

View File

@@ -1,41 +1,15 @@
<script lang="ts" setup>
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
import api from '@/api'
import { useAuthStore, useGlobalSettingsStore } from '@/stores'
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
import { SupportedLocale } from '@/types/i18n'
import { checkAndEmitUnreadMessages } from '@/utils/badge'
import { preloadImage } from './@core/utils/image'
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
const { global: globalTheme } = useTheme()
// 生效主题
const { global: globalTheme } = useTheme()
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
// 生效语言
const localeValue = getBrowserLocale()
setI18nLanguage(localeValue as SupportedLocale)
// 检查是否登录
const authStore = useAuthStore()
const isLogin = computed(() => authStore.token)
// 全局设置store
const globalSettingsStore = useGlobalSettingsStore()
// 生成背景图片key
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
// 背景图片
const backgroundImages = ref<string[]>([])
const activeImageIndex = ref(0)
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
async function setTheme() {
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
}
// ApexCharts 全局配置
declare global {
@@ -44,277 +18,37 @@ declare global {
}
}
// 配置 ApexCharts 全局选项
function configureApexCharts() {
if (typeof window !== 'undefined' && window.Apex) {
try {
// 获取当前主题
const currentTheme = globalTheme.name.value
const isDark = currentTheme === 'dark' || currentTheme === 'transparent'
// 数据标签
window.Apex.dataLabels = {
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
// 如果有小数点,保留两位小数,否则保留整数
const data = w.config.series[seriesIndex]
return data.toFixed(data % 1 === 0 ? 0 : 1)
},
}
// 图例
window.Apex.legend = {
labels: {
useSeriesColors: true,
},
}
// 标题
window.Apex.title = {
style: {
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
},
}
// 鼠标悬浮提示
window.Apex.tooltip = {
theme: isDark ? 'dark' : 'light',
}
} catch (error) {
console.warn('ApexCharts 全局配置失败:', error)
}
}
}
// 更新data-theme属性以便CSS选择器能正确匹配
function updateHtmlThemeAttribute(themeName: string) {
document.documentElement.setAttribute('data-theme', themeName)
document.body.setAttribute('data-theme', themeName)
}
// 获取背景图片
async function fetchBackgroundImages() {
try {
const controller = new AbortController()
backgroundImages.value = await api.get(`/login/wallpapers`, {
signal: controller.signal,
})
activeImageIndex.value = 0
} catch (e) {
throw e
}
}
// 背景图片轮换函数
function rotateBackgroundImage() {
if (backgroundImages.value.length > 1) {
// 计算下一个图片索引
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
// 预加载下一张图片
preloadImage(backgroundImages.value[nextIndex]).then(success => {
// 只有图片成功加载才切换
if (success) {
activeImageIndex.value = nextIndex
}
})
}
}
// 开始背景图片轮换
function startBackgroundRotation() {
// 清除现有定时器
removeBackgroundTimer('background-rotation')
if (backgroundImages.value.length > 1) {
// 使用优化的定时器管理器,后台时自动暂停
addBackgroundTimer(
'background-rotation',
rotateBackgroundImage,
10000, // 每10秒切换一次
{
runInBackground: false, // 后台时不运行
skipInitialRun: true, // 不需要立即执行
},
)
}
}
// 添加logo动画效果并延迟移除加载界面
function animateAndRemoveLoader() {
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
if (loadingBg) {
removeEl('#loading-bg')
document.documentElement.style.removeProperty('background')
}
}
// 检查PWA状态并移除加载界面
async function removeLoadingWithStateCheck() {
try {
// 设置各个组件的加载状态
globalLoadingStateManager.setLoadingState('pwa-state', true)
globalLoadingStateManager.setLoadingState('global-settings', true)
globalLoadingStateManager.setLoadingState('background-images', true)
// 静默检查PWA状态恢复
const pwaController = (window as any).pwaStateController
if (pwaController) {
await pwaController.waitForStateRestore()
}
globalLoadingStateManager.setLoadingState('pwa-state', false)
// 并行加载关键资源
await Promise.all([
globalSettingsStore.initialize().then(() => {
globalLoadingStateManager.setLoadingState('global-settings', false)
}),
new Promise(resolve => {
setTimeout(() => {
globalLoadingStateManager.setLoadingState('background-images', false)
resolve(void 0)
}, 50)
}),
])
// 等待所有加载完成
await globalLoadingStateManager.waitForAllComplete()
// 移除加载界面
animateAndRemoveLoader()
// 检查未读消息
checkAndEmitUnreadMessages()
} catch (error) {
// 即使出错也要移除加载界面
globalLoadingStateManager.reset()
animateAndRemoveLoader()
}
}
// 加载背景图片
async function loadBackgroundImages(retryCount = 0) {
const maxRetries = 3
try {
await fetchBackgroundImages()
startBackgroundRotation()
} catch (error: any) {
const isAbortError = error.name === 'AbortError' || error.code === 'ERR_CANCELED'
if (retryCount < maxRetries) {
const baseDelay = isAbortError ? 1000 : 3000
const retryDelay = Math.min(baseDelay * Math.pow(2, retryCount), 10000)
setTimeout(() => {
loadBackgroundImages(retryCount + 1)
}, retryDelay)
}
}
}
onMounted(async () => {
// 配置 ApexCharts
configureApexCharts()
// 初始化data-theme属性
updateHtmlThemeAttribute(globalTheme.name.value)
// 监听主题变化
watch(
() => globalTheme.name.value,
newTheme => {
// 更新HTML主题属性
updateHtmlThemeAttribute(newTheme)
// 重新配置ApexCharts以适应新主题
configureApexCharts()
if (window.Apex) {
// 数据标签
window.Apex.dataLabels = {
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
// 如果有小数点,保留两位小数,否则保留整数
const data = w.config.series[seriesIndex]
return data.toFixed(data % 1 === 0 ? 0 : 1)
},
)
}
// 图例
window.Apex.legend = {
labels: {
useSeriesColors: true,
},
}
// 标题
window.Apex.title = {
style: {
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
},
}
}
// 加载背景图片
loadBackgroundImages()
// 使用优化后的加载界面移除逻辑
ensureRenderComplete(() => {
nextTick(removeLoadingWithStateCheck)
})
})
onUnmounted(() => {
// 清除背景轮换定时器
removeBackgroundTimer('background-rotation')
// 页面加载时,加载当前用户数据
onBeforeMount(async () => {
setTheme()
})
</script>
<template>
<div class="app-wrapper">
<!-- 透明主题背景 -->
<div v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)" class="background-container">
<div
v-for="(imageUrl, index) in backgroundImages"
:key="`bg-${index}-${loginStateKey}`"
class="background-image"
:class="{ 'active': index === activeImageIndex }"
:style="{ 'backgroundImage': `url(${imageUrl})` }"
/>
<!-- 全局磨砂层 -->
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
</div>
<!-- 页面内容 -->
<VApp :class="{ 'transparent-app': isTransparentTheme }">
<RouterView />
<!-- PWA安装提示 -->
<PWAInstallPrompt />
</VApp>
</div>
<VApp>
<RouterView />
</VApp>
</template>
<style lang="scss">
/* 全局样式 */
.app-wrapper {
position: relative;
inline-size: 100%;
min-block-size: 100vh;
}
.background-container {
position: fixed;
z-index: 0;
overflow: hidden;
block-size: 100%;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
}
.background-image {
position: absolute;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
block-size: 100%;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
opacity: 0;
transition: opacity 1.5s ease;
&::after {
position: absolute;
background: linear-gradient(rgba(0, 0, 0, 30%) 0%, rgba(0, 0, 0, 60%) 100%);
block-size: 100%;
content: '';
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
}
&.active {
opacity: 1;
}
}
/* 全局磨砂层 */
.global-blur-layer {
position: absolute;
z-index: 1;
backdrop-filter: blur(16px);
background-color: rgba(128, 128, 128, 30%);
block-size: 100%;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
}
</style>

View File

@@ -10,7 +10,7 @@ import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
import modeIniUrl from 'ace-builds/src-noconflict/mode-ini?url'
import modePythonUrl from 'ace-builds/src-noconflict/mode-python?url'
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
@@ -40,7 +40,7 @@ import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?url'
import snippertsIniUrl from 'ace-builds/src-noconflict/snippets/ini?url'
import snippetsPythonUrl from 'ace-builds/src-noconflict/snippets/python?url'
import 'ace-builds/src-noconflict/ext-language_tools'
@@ -49,7 +49,7 @@ ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
ace.config.setModuleUrl('ace/mode/ini', modeIniUrl)
ace.config.setModuleUrl('ace/mode/python', modePythonUrl)
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
@@ -64,6 +64,6 @@ ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
ace.config.setModuleUrl('ace/snippets/ini', snippertsIniUrl)
ace.config.setModuleUrl('ace/snippets/python', snippetsPythonUrl)
ace.require('ace/ext/language_tools')

View File

@@ -1,353 +0,0 @@
import i18n from '@/plugins/i18n'
export const storageAttributes = [
{
type: 'local',
icon: 'mdi-folder-multiple-outline',
remote: false,
},
{
type: 'alipan',
icon: 'mdi-cloud-outline',
remote: true,
},
{
type: 'u115',
icon: 'mdi-cloud-outline',
remote: true,
},
{
type: 'rclone',
icon: 'mdi-server-network-outline',
remote: true,
},
{
type: 'alist',
icon: 'mdi-server-network-outline',
remote: true,
},
{
type: 'smb',
icon: 'mdi-folder-network-outline',
remote: true,
},
]
export const storageIconDict = storageAttributes.reduce((dict, item) => {
dict[item.type] = item.icon
return dict
}, {} as Record<string, string>)
export const storageRemoteDict = storageAttributes.reduce((dict, item) => {
dict[item.type] = item.remote
return dict
}, {} as Record<string, boolean>)
export const downloaderOptions = [
{
value: 'qbittorrent',
title: i18n.global.t('setting.system.qbittorrent'),
},
{
value: 'transmission',
title: i18n.global.t('setting.system.transmission'),
},
]
export const downloaderDict = downloaderOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
export const mediaServerOptions = [
{
value: 'emby',
title: i18n.global.t('setting.system.emby'),
},
{
value: 'jellyfin',
title: i18n.global.t('setting.system.jellyfin'),
},
{
value: 'plex',
title: i18n.global.t('setting.system.plex'),
},
{
value: 'trimemedia',
title: i18n.global.t('setting.system.trimeMedia'),
},
]
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
export const innerFilterRules = [
{ title: i18n.global.t('filterRules.specSub'), value: ' SPECSUB ' },
{ title: i18n.global.t('filterRules.cnSub'), value: ' CNSUB ' },
{ title: i18n.global.t('filterRules.cnVoi'), value: ' CNVOI ' },
{ title: i18n.global.t('filterRules.gz'), value: ' GZ ' },
{ title: i18n.global.t('filterRules.notCnVoi'), value: ' !CNVOI ' },
{ title: i18n.global.t('filterRules.hkVoi'), value: ' HKVOI ' },
{ title: i18n.global.t('filterRules.notHkVoi'), value: ' !HKVOI ' },
{ title: i18n.global.t('filterRules.free'), value: ' FREE ' },
{ title: i18n.global.t('filterRules.resolution4k'), value: ' 4K ' },
{ title: i18n.global.t('filterRules.resolution1080p'), value: ' 1080P ' },
{ title: i18n.global.t('filterRules.resolution720p'), value: ' 720P ' },
{ title: i18n.global.t('filterRules.not720p'), value: ' !720P ' },
{ title: i18n.global.t('filterRules.qualityBlu'), value: ' BLU ' },
{ title: i18n.global.t('filterRules.notBlu'), value: ' !BLU ' },
{ title: i18n.global.t('filterRules.qualityBluray'), value: ' BLURAY ' },
{ title: i18n.global.t('filterRules.notBluray'), value: ' !BLURAY ' },
{ title: i18n.global.t('filterRules.qualityUhd'), value: ' UHD ' },
{ title: i18n.global.t('filterRules.notUhd'), value: ' !UHD ' },
{ title: i18n.global.t('filterRules.qualityRemux'), value: ' REMUX ' },
{ title: i18n.global.t('filterRules.notRemux'), value: ' !REMUX ' },
{ title: i18n.global.t('filterRules.qualityWebdl'), value: ' WEBDL ' },
{ title: i18n.global.t('filterRules.notWebdl'), value: ' !WEBDL ' },
{ title: i18n.global.t('filterRules.quality60fps'), value: ' 60FPS ' },
{ title: i18n.global.t('filterRules.not60fps'), value: ' !60FPS ' },
{ title: i18n.global.t('filterRules.codecH265'), value: ' H265 ' },
{ title: i18n.global.t('filterRules.notH265'), value: ' !H265 ' },
{ title: i18n.global.t('filterRules.codecH264'), value: ' H264 ' },
{ title: i18n.global.t('filterRules.notH264'), value: ' !H264 ' },
{ title: i18n.global.t('filterRules.effectDolby'), value: ' DOLBY ' },
{ title: i18n.global.t('filterRules.notDolby'), value: ' !DOLBY ' },
{ title: i18n.global.t('filterRules.effectAtmos'), value: ' ATMOS ' },
{ title: i18n.global.t('filterRules.notAtmos'), value: ' !ATMOS ' },
{ title: i18n.global.t('filterRules.effectHdr'), value: ' HDR ' },
{ title: i18n.global.t('filterRules.notHdr'), value: ' !HDR ' },
{ title: i18n.global.t('filterRules.effectSdr'), value: ' SDR ' },
{ title: i18n.global.t('filterRules.notSdr'), value: ' !SDR ' },
{ title: i18n.global.t('filterRules.effect3d'), value: ' 3D ' },
{ title: i18n.global.t('filterRules.not3d'), value: ' !3D ' },
]
export const transferTypeOptions = [
{ title: i18n.global.t('transferType.copy'), value: 'copy' },
{ title: i18n.global.t('transferType.move'), value: 'move' },
{ title: i18n.global.t('transferType.link'), value: 'link' },
{ title: i18n.global.t('transferType.softlink'), value: 'softlink' },
]
export const qualityOptions = ref([
{
title: i18n.global.t('qualityOptions.all'),
value: '',
},
{
title: i18n.global.t('qualityOptions.blurayOriginal'),
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
},
{
title: i18n.global.t('qualityOptions.remux'),
value: 'Remux',
},
{
title: i18n.global.t('qualityOptions.bluray'),
value: 'Blu-?Ray',
},
{
title: i18n.global.t('qualityOptions.uhd'),
value: 'UHD|UltraHD',
},
{
title: i18n.global.t('qualityOptions.webdl'),
value: 'WEB-?DL|WEB-?RIP',
},
{
title: i18n.global.t('qualityOptions.hdtv'),
value: 'HDTV',
},
{
title: i18n.global.t('qualityOptions.h265'),
value: '[Hx].?265|HEVC',
},
{
title: i18n.global.t('qualityOptions.h264'),
value: '[Hx].?264|AVC',
},
])
// 分辨率选择框数据
export const resolutionOptions = ref([
{
title: i18n.global.t('resolutionOptions.all'),
value: '',
},
{
title: i18n.global.t('resolutionOptions.4k'),
value: '4K|2160p|x2160',
},
{
title: i18n.global.t('resolutionOptions.1080p'),
value: '1080[pi]|x1080',
},
{
title: i18n.global.t('resolutionOptions.720p'),
value: '720[pi]|x720',
},
])
// 特效选择框数据
export const effectOptions = ref([
{
title: i18n.global.t('effectOptions.all'),
value: '',
},
{
title: i18n.global.t('effectOptions.dolbyVision'),
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
},
{
title: i18n.global.t('effectOptions.dolbyAtmos'),
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
},
{
title: i18n.global.t('effectOptions.hdr'),
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
},
{
title: i18n.global.t('effectOptions.sdr'),
value: '[\\s.]+SDR[\\s.]+',
},
])
// 媒体类型选项
export const mediaTypeOptions = [
{
title: i18n.global.t('mediaType.movie'),
value: '电影',
},
{
title: i18n.global.t('mediaType.tv'),
value: '电视剧',
},
{
title: i18n.global.t('mediaType.anime'),
value: '动漫',
},
{
title: i18n.global.t('mediaType.collection'),
value: '合集',
},
{
title: i18n.global.t('mediaType.unknown'),
value: '未知',
},
]
// 媒体类型字典
export const mediaTypeDict = mediaTypeOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
// 通知开关选项
export const notificationSwitchOptions = [
{
title: i18n.global.t('notificationSwitch.resourceDownload'),
value: '资源下载',
},
{
title: i18n.global.t('notificationSwitch.organize'),
value: '整理入库',
},
{
title: i18n.global.t('notificationSwitch.subscribe'),
value: '订阅',
},
{
title: i18n.global.t('notificationSwitch.site'),
value: '站点',
},
{
title: i18n.global.t('notificationSwitch.mediaServer'),
value: '媒体服务器',
},
{
title: i18n.global.t('notificationSwitch.manual'),
value: '手动处理',
},
{
title: i18n.global.t('notificationSwitch.plugin'),
value: '插件',
},
{
title: i18n.global.t('notificationSwitch.other'),
value: '其它',
},
]
// 通知开关字典
export const notificationSwitchDict = notificationSwitchOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
// 操作步骤选项
export const actionStepOptions = [
{
title: i18n.global.t('actionStep.addDownload'),
value: '添加下载',
},
{
title: i18n.global.t('actionStep.addSubscribe'),
value: '添加订阅',
},
{
title: i18n.global.t('actionStep.fetchDownloads'),
value: '获取下载任务',
},
{
title: i18n.global.t('actionStep.fetchMedias'),
value: '获取媒体数据',
},
{
title: i18n.global.t('actionStep.fetchRss'),
value: '获取RSS资源',
},
{
title: i18n.global.t('actionStep.fetchTorrents'),
value: '搜索站点资源',
},
{
title: i18n.global.t('actionStep.filterMedias'),
value: '过滤媒体数据',
},
{
title: i18n.global.t('actionStep.filterTorrents'),
value: '过滤资源',
},
{
title: i18n.global.t('actionStep.scanFile'),
value: '扫描目录',
},
{
title: i18n.global.t('actionStep.scrapeFile'),
value: '刮削文件',
},
{
title: i18n.global.t('actionStep.sendEvent'),
value: '发送事件',
},
{
title: i18n.global.t('actionStep.sendMessage'),
value: '发送消息',
},
{
title: i18n.global.t('actionStep.transferFile'),
value: '整理文件',
},
{
title: i18n.global.t('actionStep.invokePlugin'),
value: '调用插件',
},
]
// 操作步骤字典
export const actionStepDict = actionStepOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)

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