mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 07:28:37 +08:00
feat: add Agent Assistant component and integrate with theme customizer
- Implemented Agent Assistant widget with chat functionality, including message handling and streaming responses. - Added new localization strings for Agent Assistant in English, Simplified Chinese, and Traditional Chinese. - Updated DefaultLayout to include the Agent Assistant and Theme Customizer components. - Enhanced UserProfile to manage the opening of the Theme Customizer through global events. - Adjusted CSS styles for the Agent Assistant and its interactions with other components. - Introduced new events for opening the Theme Customizer and managing its state.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+216
-353
@@ -12,20 +12,9 @@ import {
|
|||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import { VDialog, VNavigationDrawer } from 'vuetify/components'
|
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
modelValue?: boolean
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
modelValue: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: boolean]
|
'close': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const customColorInput = ref<HTMLInputElement | null>(null)
|
const customColorInput = ref<HTMLInputElement | null>(null)
|
||||||
@@ -45,15 +34,9 @@ const {
|
|||||||
const { appMode } = usePWA()
|
const { appMode } = usePWA()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { global: globalTheme } = useTheme()
|
const { global: globalTheme } = useTheme()
|
||||||
const display = useDisplay()
|
|
||||||
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
||||||
const customizerViewportHeight = ref('100dvh')
|
const customizerViewportHeight = ref('100dvh')
|
||||||
|
|
||||||
const drawer = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: value => emit('update:modelValue', value),
|
|
||||||
})
|
|
||||||
|
|
||||||
function getVisibleViewportHeight() {
|
function getVisibleViewportHeight() {
|
||||||
if (typeof window === 'undefined') return '100dvh'
|
if (typeof window === 'undefined') return '100dvh'
|
||||||
|
|
||||||
@@ -87,13 +70,14 @@ function clearThemeCustomizerOpenState() {
|
|||||||
syncThemeCustomizerOpenState(false)
|
syncThemeCustomizerOpenState(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(drawer, syncThemeCustomizerOpenState, { immediate: true })
|
function handleGlobalKeydown(event: KeyboardEvent) {
|
||||||
watch(drawer, isOpen => {
|
// 固定侧栏不再依赖 Vuetify overlay,手动补上常见的 Esc 关闭行为。
|
||||||
if (isOpen) nextTick(syncCustomizerViewportHeight)
|
if (event.key === 'Escape') emit('close')
|
||||||
})
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
syncCustomizerViewportHeight()
|
syncCustomizerViewportHeight()
|
||||||
|
window.addEventListener('keydown', handleGlobalKeydown)
|
||||||
window.addEventListener('resize', syncCustomizerViewportHeight)
|
window.addEventListener('resize', syncCustomizerViewportHeight)
|
||||||
window.addEventListener('orientationchange', syncCustomizerViewportHeight)
|
window.addEventListener('orientationchange', syncCustomizerViewportHeight)
|
||||||
window.visualViewport?.addEventListener('resize', syncCustomizerViewportHeight)
|
window.visualViewport?.addEventListener('resize', syncCustomizerViewportHeight)
|
||||||
@@ -104,41 +88,16 @@ onScopeDispose(clearThemeCustomizerOpenState)
|
|||||||
onScopeDispose(() => {
|
onScopeDispose(() => {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
window.removeEventListener('keydown', handleGlobalKeydown)
|
||||||
window.removeEventListener('resize', syncCustomizerViewportHeight)
|
window.removeEventListener('resize', syncCustomizerViewportHeight)
|
||||||
window.removeEventListener('orientationchange', syncCustomizerViewportHeight)
|
window.removeEventListener('orientationchange', syncCustomizerViewportHeight)
|
||||||
window.visualViewport?.removeEventListener('resize', syncCustomizerViewportHeight)
|
window.visualViewport?.removeEventListener('resize', syncCustomizerViewportHeight)
|
||||||
window.visualViewport?.removeEventListener('scroll', syncCustomizerViewportHeight)
|
window.visualViewport?.removeEventListener('scroll', syncCustomizerViewportHeight)
|
||||||
})
|
})
|
||||||
|
|
||||||
const customizerContainer = computed(() => (appMode.value ? VDialog : VNavigationDrawer))
|
const customizerContainerStyle = computed<CSSProperties>(() => ({
|
||||||
|
'--theme-customizer-viewport-height': customizerViewportHeight.value,
|
||||||
const customizerContainerStyle = computed<CSSProperties>(() => {
|
}))
|
||||||
if (!appMode.value) return {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'--theme-customizer-viewport-height': customizerViewportHeight.value,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const customizerContainerProps = computed(() => {
|
|
||||||
if (appMode.value && display.mdAndDown.value) {
|
|
||||||
return {
|
|
||||||
class: 'theme-customizer-dialog-overlay',
|
|
||||||
scrim: true,
|
|
||||||
fullscreen: !display.mdAndUp.value,
|
|
||||||
scrollable: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
class: 'theme-customizer-drawer',
|
|
||||||
location: 'right' as const,
|
|
||||||
scrim: false,
|
|
||||||
temporary: true,
|
|
||||||
width: 420,
|
|
||||||
persistent: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const themeOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerTheme }>>(() => [
|
const themeOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerTheme }>>(() => [
|
||||||
{ title: t('theme.light'), value: 'light', icon: 'mdi-white-balance-sunny' },
|
{ title: t('theme.light'), value: 'light', icon: 'mdi-white-balance-sunny' },
|
||||||
@@ -273,255 +232,224 @@ async function handleResetSettings() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<aside
|
||||||
<Transition name="theme-customizer-glass">
|
class="theme-customizer-panel-host"
|
||||||
<div
|
:style="customizerContainerStyle"
|
||||||
v-if="drawer"
|
role="dialog"
|
||||||
class="theme-customizer-glass-backdrop"
|
:aria-label="t('theme.customizer.title')"
|
||||||
:class="{ 'theme-customizer-glass-backdrop--dialog': appMode }"
|
>
|
||||||
/>
|
<div class="theme-customizer-panel" :class="{ 'theme-customizer-panel--dialog': appMode, 'app-surface': appMode }">
|
||||||
</Transition>
|
<div class="theme-customizer-header py-5 px-4">
|
||||||
|
<div>
|
||||||
<component
|
<h2 class="theme-customizer-title">{{ t('theme.customizer.title') }}</h2>
|
||||||
:is="customizerContainer"
|
|
||||||
v-model="drawer"
|
|
||||||
v-bind="customizerContainerProps"
|
|
||||||
:style="customizerContainerStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="theme-customizer-panel"
|
|
||||||
:class="{ 'theme-customizer-panel--dialog': appMode, 'app-surface': appMode }"
|
|
||||||
>
|
|
||||||
<div class="theme-customizer-header py-5 px-4">
|
|
||||||
<div>
|
|
||||||
<h2 class="theme-customizer-title">{{ t('theme.customizer.title') }}</h2>
|
|
||||||
</div>
|
|
||||||
<div class="theme-customizer-header-actions">
|
|
||||||
<VBadge color="error" dot :model-value="showResetBadge" location="top end" offset-x="2" offset-y="2">
|
|
||||||
<IconBtn :aria-label="t('theme.customizer.reset')" @click="handleResetSettings">
|
|
||||||
<VIcon class="text-high-emphasis" icon="mdi-refresh" />
|
|
||||||
</IconBtn>
|
|
||||||
</VBadge>
|
|
||||||
<IconBtn :aria-label="t('common.close')" @click="drawer = false">
|
|
||||||
<VIcon class="text-high-emphasis" icon="mdi-close" />
|
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="theme-customizer-header-actions">
|
||||||
|
<VBadge color="error" dot :model-value="showResetBadge" location="top end" offset-x="2" offset-y="2">
|
||||||
|
<IconBtn :aria-label="t('theme.customizer.reset')" @click="handleResetSettings">
|
||||||
|
<VIcon class="text-high-emphasis" icon="mdi-refresh" />
|
||||||
|
</IconBtn>
|
||||||
|
</VBadge>
|
||||||
|
<IconBtn :aria-label="t('common.close')" @click="emit('close')">
|
||||||
|
<VIcon class="text-high-emphasis" icon="mdi-close" />
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<VDivider />
|
<VDivider />
|
||||||
|
|
||||||
<PerfectScrollbar class="theme-customizer-body" :options="{ wheelPropagation: false }">
|
<PerfectScrollbar class="theme-customizer-body" :options="{ wheelPropagation: false }">
|
||||||
<section class="theme-customizer-section">
|
<section class="theme-customizer-section">
|
||||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.primaryColor') }}</h3>
|
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.primaryColor') }}</h3>
|
||||||
<div class="theme-customizer-color-grid">
|
<div class="theme-customizer-color-grid">
|
||||||
<div
|
<div
|
||||||
v-for="color in themeCustomizerPrimaryColors"
|
v-for="color in themeCustomizerPrimaryColors"
|
||||||
:key="color.value"
|
:key="color.value"
|
||||||
class="theme-customizer-color-option"
|
class="theme-customizer-color-option"
|
||||||
:class="{ 'is-active': settings.primaryColor === color.value }"
|
:class="{ 'is-active': settings.primaryColor === color.value }"
|
||||||
:aria-label="t('theme.customizer.usePrimaryColor', { color: color.name })"
|
:aria-label="t('theme.customizer.usePrimaryColor', { color: color.name })"
|
||||||
@click="setPrimaryColor(color.value)"
|
@click="setPrimaryColor(color.value)"
|
||||||
>
|
>
|
||||||
<span class="theme-customizer-color-swatch" :style="{ backgroundColor: color.value }" />
|
<span class="theme-customizer-color-swatch" :style="{ backgroundColor: color.value }" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!appMode"
|
|
||||||
class="theme-customizer-color-option theme-customizer-color-option--picker"
|
|
||||||
:class="{
|
|
||||||
'is-active': !themeCustomizerPrimaryColors.some(color => color.value === settings.primaryColor),
|
|
||||||
}"
|
|
||||||
:aria-label="t('theme.customizer.chooseCustomColor')"
|
|
||||||
@click="openColorPicker"
|
|
||||||
>
|
|
||||||
<VIcon class="theme-customizer-native-icon" icon="mdi-palette-outline" size="30" />
|
|
||||||
<input
|
|
||||||
ref="customColorInput"
|
|
||||||
class="theme-customizer-native-color"
|
|
||||||
type="color"
|
|
||||||
:value="settings.primaryColor"
|
|
||||||
@input="handleCustomColorInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="theme-customizer-section-title">{{ t('common.theme') }}</h3>
|
<div
|
||||||
<div class="theme-customizer-option-grid theme-customizer-option-grid--theme">
|
v-if="!appMode"
|
||||||
<div
|
class="theme-customizer-color-option theme-customizer-color-option--picker"
|
||||||
v-for="theme in themeOptions"
|
:class="{
|
||||||
:key="theme.value"
|
'is-active': !themeCustomizerPrimaryColors.some(color => color.value === settings.primaryColor),
|
||||||
class="theme-customizer-card-option"
|
}"
|
||||||
:class="{ 'is-active': settings.theme === theme.value }"
|
:aria-label="t('theme.customizer.chooseCustomColor')"
|
||||||
@click="setTheme(theme.value)"
|
@click="openColorPicker"
|
||||||
>
|
>
|
||||||
<VIcon class="theme-customizer-theme-icon" :icon="theme.icon" size="36" />
|
<VIcon class="theme-customizer-native-icon" icon="mdi-palette-outline" size="30" />
|
||||||
<span>{{ theme.title }}</span>
|
<input
|
||||||
</div>
|
ref="customColorInput"
|
||||||
</div>
|
class="theme-customizer-native-color"
|
||||||
|
type="color"
|
||||||
<VDivider class="mt-7" />
|
:value="settings.primaryColor"
|
||||||
|
@input="handleCustomColorInput"
|
||||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.skins') }}</h3>
|
|
||||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--skins">
|
|
||||||
<div
|
|
||||||
v-for="skin in skinOptions"
|
|
||||||
:key="skin.value"
|
|
||||||
class="theme-customizer-preview-option"
|
|
||||||
:class="{ 'is-active': settings.skin === skin.value }"
|
|
||||||
@click="setSkin(skin.value)"
|
|
||||||
>
|
|
||||||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${skin.value}`">
|
|
||||||
<span class="mini-sidebar">
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
</span>
|
|
||||||
<span class="mini-content">
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>{{ skin.title }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VDivider class="mt-7" />
|
|
||||||
|
|
||||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.radius') }}</h3>
|
|
||||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--radius">
|
|
||||||
<div
|
|
||||||
v-for="radius in radiusOptions"
|
|
||||||
:key="radius.value"
|
|
||||||
class="theme-customizer-preview-option"
|
|
||||||
:class="{ 'is-active': settings.radius === radius.value }"
|
|
||||||
@click="setRadius(radius.value)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="theme-customizer-radius-scene"
|
|
||||||
:style="{ '--theme-customizer-radius-preview': radius.previewRadius }"
|
|
||||||
>
|
|
||||||
<span class="theme-customizer-radius-scene__card">
|
|
||||||
<span class="theme-customizer-radius-scene__badge" />
|
|
||||||
<span class="theme-customizer-radius-scene__line" />
|
|
||||||
<span class="theme-customizer-radius-scene__line theme-customizer-radius-scene__line--short" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>{{ radius.title }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VDivider class="mt-7" />
|
|
||||||
|
|
||||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.shadow') }}</h3>
|
|
||||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--shadow">
|
|
||||||
<div
|
|
||||||
v-for="shadow in shadowOptions"
|
|
||||||
:key="shadow.value"
|
|
||||||
class="theme-customizer-preview-option"
|
|
||||||
:class="{ 'is-active': settings.shadow === shadow.value }"
|
|
||||||
@click="setShadow(shadow.value)"
|
|
||||||
>
|
|
||||||
<span class="theme-customizer-shadow-scene" :class="`theme-customizer-shadow-scene--${shadow.value}`">
|
|
||||||
<span class="theme-customizer-shadow-scene__panel">
|
|
||||||
<span class="theme-customizer-shadow-scene__panel-line" />
|
|
||||||
<span
|
|
||||||
class="theme-customizer-shadow-scene__panel-line theme-customizer-shadow-scene__panel-line--short"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="theme-customizer-shadow-scene__card">
|
|
||||||
<span class="theme-customizer-shadow-scene__badge" />
|
|
||||||
<span class="theme-customizer-shadow-scene__line theme-customizer-shadow-scene__line--short" />
|
|
||||||
<span class="theme-customizer-shadow-scene__line" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>{{ shadow.title }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showSemiDarkMenuOption" class="theme-customizer-semi-dark">
|
|
||||||
<span>{{ t('theme.customizer.semiDarkMenu') }}</span>
|
|
||||||
<VSwitch
|
|
||||||
:model-value="settings.semiDarkMenu"
|
|
||||||
color="primary"
|
|
||||||
inset
|
|
||||||
hide-details
|
|
||||||
@update:model-value="setSemiDarkMenu(Boolean($event))"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<VDivider v-if="showLayoutSection" />
|
<h3 class="theme-customizer-section-title">{{ t('common.theme') }}</h3>
|
||||||
|
<div class="theme-customizer-option-grid theme-customizer-option-grid--theme">
|
||||||
<section v-if="showLayoutSection" class="theme-customizer-section">
|
<div
|
||||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.layout') }}</h3>
|
v-for="theme in themeOptions"
|
||||||
<div class="theme-customizer-preview-grid">
|
:key="theme.value"
|
||||||
<div
|
class="theme-customizer-card-option"
|
||||||
v-for="layout in layoutOptions"
|
:class="{ 'is-active': settings.theme === theme.value }"
|
||||||
:key="layout.value"
|
@click="setTheme(theme.value)"
|
||||||
class="theme-customizer-preview-option"
|
>
|
||||||
:class="{ 'is-active': settings.layout === layout.value, 'is-disabled': appMode }"
|
<VIcon class="theme-customizer-theme-icon" :icon="theme.icon" size="36" />
|
||||||
@click="handleLayoutChange(layout.value)"
|
<span>{{ theme.title }}</span>
|
||||||
>
|
|
||||||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${layout.value}`">
|
|
||||||
<span class="mini-sidebar">
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
</span>
|
|
||||||
<span class="mini-content">
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>{{ layout.title }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</PerfectScrollbar>
|
|
||||||
</div>
|
<VDivider class="mt-7" />
|
||||||
</component>
|
|
||||||
</Teleport>
|
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.skins') }}</h3>
|
||||||
|
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--skins">
|
||||||
|
<div
|
||||||
|
v-for="skin in skinOptions"
|
||||||
|
:key="skin.value"
|
||||||
|
class="theme-customizer-preview-option"
|
||||||
|
:class="{ 'is-active': settings.skin === skin.value }"
|
||||||
|
@click="setSkin(skin.value)"
|
||||||
|
>
|
||||||
|
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${skin.value}`">
|
||||||
|
<span class="mini-sidebar">
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
</span>
|
||||||
|
<span class="mini-content">
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>{{ skin.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VDivider class="mt-7" />
|
||||||
|
|
||||||
|
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.radius') }}</h3>
|
||||||
|
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--radius">
|
||||||
|
<div
|
||||||
|
v-for="radius in radiusOptions"
|
||||||
|
:key="radius.value"
|
||||||
|
class="theme-customizer-preview-option"
|
||||||
|
:class="{ 'is-active': settings.radius === radius.value }"
|
||||||
|
@click="setRadius(radius.value)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="theme-customizer-radius-scene"
|
||||||
|
:style="{ '--theme-customizer-radius-preview': radius.previewRadius }"
|
||||||
|
>
|
||||||
|
<span class="theme-customizer-radius-scene__card">
|
||||||
|
<span class="theme-customizer-radius-scene__badge" />
|
||||||
|
<span class="theme-customizer-radius-scene__line" />
|
||||||
|
<span class="theme-customizer-radius-scene__line theme-customizer-radius-scene__line--short" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>{{ radius.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VDivider class="mt-7" />
|
||||||
|
|
||||||
|
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.shadow') }}</h3>
|
||||||
|
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--shadow">
|
||||||
|
<div
|
||||||
|
v-for="shadow in shadowOptions"
|
||||||
|
:key="shadow.value"
|
||||||
|
class="theme-customizer-preview-option"
|
||||||
|
:class="{ 'is-active': settings.shadow === shadow.value }"
|
||||||
|
@click="setShadow(shadow.value)"
|
||||||
|
>
|
||||||
|
<span class="theme-customizer-shadow-scene" :class="`theme-customizer-shadow-scene--${shadow.value}`">
|
||||||
|
<span class="theme-customizer-shadow-scene__panel">
|
||||||
|
<span class="theme-customizer-shadow-scene__panel-line" />
|
||||||
|
<span
|
||||||
|
class="theme-customizer-shadow-scene__panel-line theme-customizer-shadow-scene__panel-line--short"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="theme-customizer-shadow-scene__card">
|
||||||
|
<span class="theme-customizer-shadow-scene__badge" />
|
||||||
|
<span class="theme-customizer-shadow-scene__line theme-customizer-shadow-scene__line--short" />
|
||||||
|
<span class="theme-customizer-shadow-scene__line" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>{{ shadow.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showSemiDarkMenuOption" class="theme-customizer-semi-dark">
|
||||||
|
<span>{{ t('theme.customizer.semiDarkMenu') }}</span>
|
||||||
|
<VSwitch
|
||||||
|
:model-value="settings.semiDarkMenu"
|
||||||
|
color="primary"
|
||||||
|
inset
|
||||||
|
hide-details
|
||||||
|
@update:model-value="setSemiDarkMenu(Boolean($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<VDivider v-if="showLayoutSection" />
|
||||||
|
|
||||||
|
<section v-if="showLayoutSection" class="theme-customizer-section">
|
||||||
|
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.layout') }}</h3>
|
||||||
|
<div class="theme-customizer-preview-grid">
|
||||||
|
<div
|
||||||
|
v-for="layout in layoutOptions"
|
||||||
|
:key="layout.value"
|
||||||
|
class="theme-customizer-preview-option"
|
||||||
|
:class="{ 'is-active': settings.layout === layout.value, 'is-disabled': appMode }"
|
||||||
|
@click="handleLayoutChange(layout.value)"
|
||||||
|
>
|
||||||
|
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${layout.value}`">
|
||||||
|
<span class="mini-sidebar">
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
</span>
|
||||||
|
<span class="mini-content">
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>{{ layout.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PerfectScrollbar>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
/* stylelint-disable no-descending-specificity */
|
/* stylelint-disable no-descending-specificity */
|
||||||
|
|
||||||
.theme-customizer-drawer {
|
.theme-customizer-panel-host {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
z-index: 12000 !important;
|
z-index: 2102 !important;
|
||||||
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
block-size: 100dvh !important;
|
border-radius: 0;
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
block-size: var(--theme-customizer-viewport-height, 100dvh) !important;
|
||||||
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
|
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
|
||||||
box-shadow: var(--app-surface-shadow) !important;
|
box-shadow: var(--app-surface-shadow) !important;
|
||||||
inset-block: 0 !important;
|
inline-size: 420px !important;
|
||||||
|
inset-block-start: 0 !important;
|
||||||
inset-inline-end: 0 !important;
|
inset-inline-end: 0 !important;
|
||||||
max-block-size: 100dvh !important;
|
max-block-size: var(--theme-customizer-viewport-height, 100dvh) !important;
|
||||||
|
|
||||||
.v-navigation-drawer__content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
block-size: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-customizer-dialog-overlay {
|
|
||||||
--theme-customizer-viewport-height: 100dvh;
|
|
||||||
|
|
||||||
z-index: 12000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-customizer-dialog-overlay > .v-overlay__content {
|
|
||||||
overflow: hidden;
|
|
||||||
block-size: var(--theme-customizer-viewport-height);
|
|
||||||
margin-block: 0 !important;
|
|
||||||
max-block-size: var(--theme-customizer-viewport-height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-customizer-panel {
|
.theme-customizer-panel {
|
||||||
@@ -529,16 +457,16 @@ async function handleResetSettings() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-customizer-panel--dialog {
|
.theme-customizer-panel--dialog {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgb(var(--v-theme-surface));
|
block-size: 100%;
|
||||||
block-size: var(--theme-customizer-viewport-height, 100dvh);
|
max-block-size: 100%;
|
||||||
max-block-size: var(--theme-customizer-viewport-height, 100dvh);
|
|
||||||
|
|
||||||
/* fullscreen dialog 会贴到 viewport-fit=cover 顶部,iOS 需要在面板内部避开系统状态栏。 */
|
/* 独立 App 模式会贴近 viewport-fit=cover 顶部,面板内部需要避开 iOS 状态栏。 */
|
||||||
padding-block-start: env(safe-area-inset-top);
|
padding-block-start: env(safe-area-inset-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,73 +475,12 @@ async function handleResetSettings() {
|
|||||||
padding-block-end: env(safe-area-inset-bottom);
|
padding-block-end: env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-customizer-drawer.v-theme--transparent,
|
|
||||||
.v-theme--transparent .theme-customizer-drawer,
|
|
||||||
html[data-theme='transparent'] .theme-customizer-drawer,
|
|
||||||
.v-theme--transparent .theme-customizer-panel--dialog,
|
|
||||||
html[data-theme='transparent'] .theme-customizer-panel--dialog {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-customizer-glass-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 11999;
|
|
||||||
block-size: 100dvh;
|
|
||||||
inline-size: 420px;
|
|
||||||
inset-block: 0;
|
|
||||||
inset-inline-end: 0;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-glass-backdrop {
|
|
||||||
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
|
|
||||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy, 0.5));
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-customizer-glass-enter-active,
|
|
||||||
.theme-customizer-glass-leave-active {
|
|
||||||
transition:
|
|
||||||
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
|
||||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-customizer-glass-enter-from,
|
|
||||||
.theme-customizer-glass-leave-to {
|
|
||||||
opacity: 0 !important;
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-drawer .v-navigation-drawer__content {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 600px) {
|
@media (width <= 600px) {
|
||||||
.theme-customizer-glass-backdrop {
|
.theme-customizer-panel-host {
|
||||||
inline-size: 100vw;
|
inline-size: 100vw !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 透明主题的全局 overlay 毛玻璃会影响临时抽屉绘制,主题定制器改由 drawer 自身承担背景。
|
|
||||||
html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer),
|
|
||||||
.v-theme--transparent .v-overlay__content:has(.theme-customizer-drawer) {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
backdrop-filter: none !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-card-option .theme-customizer-theme-icon,
|
|
||||||
:is(html[data-theme='transparent'], .v-theme--transparent)
|
|
||||||
.theme-customizer-color-option
|
|
||||||
.theme-customizer-native-icon {
|
|
||||||
backdrop-filter: none !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-customizer-header {
|
.theme-customizer-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1030,10 +897,6 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 600px) {
|
@media (width <= 600px) {
|
||||||
.theme-customizer-drawer {
|
|
||||||
inline-size: min(100vw, 420px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-customizer-header,
|
.theme-customizer-header,
|
||||||
.theme-customizer-section {
|
.theme-customizer-section {
|
||||||
padding-inline: 22px;
|
padding-inline: 22px;
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ function submitCustomCSS() {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.custom-css-dialog {
|
.custom-css-dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-block-size: calc(100dvh - 2rem);
|
max-block-size: calc(100dvh - 2rem);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-css-header {
|
.custom-css-header {
|
||||||
@@ -111,7 +111,7 @@ function submitCustomCSS() {
|
|||||||
|
|
||||||
.custom-css-editor-body {
|
.custom-css-editor-body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-block-size: 0;
|
min-block-size: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-css-editor {
|
.custom-css-editor {
|
||||||
@@ -141,8 +141,8 @@ function submitCustomCSS() {
|
|||||||
|
|
||||||
.custom-css-editor {
|
.custom-css-editor {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-block-size: 0;
|
|
||||||
block-size: auto;
|
block-size: auto;
|
||||||
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-css-actions {
|
.custom-css-actions {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { themeManager } from '@/utils/themeManager'
|
|||||||
|
|
||||||
export const THEME_CUSTOMIZER_STORAGE_KEY = 'moviepilot-theme-customizer'
|
export const THEME_CUSTOMIZER_STORAGE_KEY = 'moviepilot-theme-customizer'
|
||||||
export const THEME_CUSTOMIZER_CHANGE_EVENT = 'moviepilot-theme-customizer-change'
|
export const THEME_CUSTOMIZER_CHANGE_EVENT = 'moviepilot-theme-customizer-change'
|
||||||
|
export const THEME_CUSTOMIZER_OPEN_EVENT = 'moviepilot-theme-customizer-open'
|
||||||
|
|
||||||
export const themeCustomizerPrimaryColors = [
|
export const themeCustomizerPrimaryColors = [
|
||||||
{ name: 'Purple', value: '#9155FD' },
|
{ name: 'Purple', value: '#9155FD' },
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
|||||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||||
import QuickAccess from '@/layouts/components/QuickAccess.vue'
|
import QuickAccess from '@/layouts/components/QuickAccess.vue'
|
||||||
import HeaderTab from '@/layouts/components/HeaderTab.vue'
|
import HeaderTab from '@/layouts/components/HeaderTab.vue'
|
||||||
import { usePluginSidebarNavStore, useUserStore } from '@/stores'
|
import AgentAssistantWidget from '@/components/AgentAssistantWidget.vue'
|
||||||
|
import ThemeCustomizer from '@/components/ThemeCustomizer.vue'
|
||||||
|
import { useGlobalSettingsStore, usePluginSidebarNavStore, useUserStore } from '@/stores'
|
||||||
import { getNavMenus } from '@/router/i18n-menu'
|
import { getNavMenus } from '@/router/i18n-menu'
|
||||||
import { filterPluginSidebarNavEntries } from '@/utils/pluginSidebarNav'
|
import { filterPluginSidebarNavEntries } from '@/utils/pluginSidebarNav'
|
||||||
import { NavMenu } from '@/@layouts/types'
|
import { NavMenu } from '@/@layouts/types'
|
||||||
@@ -31,6 +33,7 @@ import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
|||||||
import {
|
import {
|
||||||
readThemeCustomizerSettings,
|
readThemeCustomizerSettings,
|
||||||
THEME_CUSTOMIZER_CHANGE_EVENT,
|
THEME_CUSTOMIZER_CHANGE_EVENT,
|
||||||
|
THEME_CUSTOMIZER_OPEN_EVENT,
|
||||||
type ThemeCustomizerSettings,
|
type ThemeCustomizerSettings,
|
||||||
} from '@/composables/useThemeCustomizer'
|
} from '@/composables/useThemeCustomizer'
|
||||||
import logo from '@images/logo.svg?raw'
|
import logo from '@images/logo.svg?raw'
|
||||||
@@ -42,14 +45,17 @@ const { t } = useI18n()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const themeLayout = ref(readThemeCustomizerSettings().layout)
|
const themeLayout = ref(readThemeCustomizerSettings().layout)
|
||||||
|
const showThemeCustomizer = ref(false)
|
||||||
|
|
||||||
// 用户 Store
|
// 用户 Store
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const pluginSidebarNavStore = usePluginSidebarNavStore()
|
const pluginSidebarNavStore = usePluginSidebarNavStore()
|
||||||
|
const globalSettingsStore = useGlobalSettingsStore()
|
||||||
|
|
||||||
// 获取用户权限信息
|
// 获取用户权限信息
|
||||||
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
||||||
const canAdmin = computed(() => hasPermission(userPermissions.value, 'admin'))
|
const canAdmin = computed(() => hasPermission(userPermissions.value, 'admin'))
|
||||||
|
const showAgentAssistant = computed(() => canAdmin.value && globalSettingsStore.get('AI_AGENT_ENABLE') === true)
|
||||||
|
|
||||||
// 开始菜单项
|
// 开始菜单项
|
||||||
const startMenus = ref<NavMenu[]>([])
|
const startMenus = ref<NavMenu[]>([])
|
||||||
@@ -279,6 +285,10 @@ function handleThemeCustomizerChange(event: Event) {
|
|||||||
themeLayout.value = (event as CustomEvent<ThemeCustomizerSettings>).detail.layout
|
themeLayout.value = (event as CustomEvent<ThemeCustomizerSettings>).detail.layout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleThemeCustomizerOpen() {
|
||||||
|
showThemeCustomizer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function isHorizontalNavActive(item: NavMenu) {
|
function isHorizontalNavActive(item: NavMenu) {
|
||||||
const targetPath = normalizeMenuPath(item.to)
|
const targetPath = normalizeMenuPath(item.to)
|
||||||
if (!targetPath) return false
|
if (!targetPath) return false
|
||||||
@@ -416,6 +426,10 @@ function appendPluginSidebarMenus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// 主题定制器由布局统一承载,监听需要尽早注册,避免异步加载菜单期间丢失打开事件。
|
||||||
|
window.addEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
||||||
|
window.addEventListener(THEME_CUSTOMIZER_OPEN_EVENT, handleThemeCustomizerOpen)
|
||||||
|
|
||||||
// 获取菜单列表
|
// 获取菜单列表
|
||||||
startMenus.value = getMenuList(t('menu.start'))
|
startMenus.value = getMenuList(t('menu.start'))
|
||||||
discoveryMenus.value = getMenuList(t('menu.discovery'))
|
discoveryMenus.value = getMenuList(t('menu.discovery'))
|
||||||
@@ -431,11 +445,10 @@ onMounted(async () => {
|
|||||||
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage)
|
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
|
||||||
|
|
||||||
// 组件卸载时清理监听
|
// 组件卸载时清理监听
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
window.removeEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
||||||
|
window.removeEventListener(THEME_CUSTOMIZER_OPEN_EVENT, handleThemeCustomizerOpen)
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage)
|
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage)
|
||||||
}
|
}
|
||||||
@@ -692,6 +705,12 @@ onMounted(async () => {
|
|||||||
@close="handleClosePluginQuickAccess"
|
@close="handleClosePluginQuickAccess"
|
||||||
@plugin-click="handlePluginClick"
|
@plugin-click="handlePluginClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 👉 Theme Customizer -->
|
||||||
|
<ThemeCustomizer v-if="showThemeCustomizer" @close="showThemeCustomizer = false" />
|
||||||
|
|
||||||
|
<!-- 👉 Agent Assistant -->
|
||||||
|
<AgentAssistantWidget v-if="showAgentAssistant" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
persistPartialThemeCustomizerSettings,
|
persistPartialThemeCustomizerSettings,
|
||||||
readThemeCustomizerSettings,
|
readThemeCustomizerSettings,
|
||||||
THEME_CUSTOMIZER_CHANGE_EVENT,
|
THEME_CUSTOMIZER_CHANGE_EVENT,
|
||||||
|
THEME_CUSTOMIZER_OPEN_EVENT,
|
||||||
type ThemeCustomizerSettings,
|
type ThemeCustomizerSettings,
|
||||||
} from '@/composables/useThemeCustomizer'
|
} from '@/composables/useThemeCustomizer'
|
||||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||||
@@ -30,7 +31,6 @@ const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/Pr
|
|||||||
const TransparencySettingsDialog = defineAsyncComponent(
|
const TransparencySettingsDialog = defineAsyncComponent(
|
||||||
() => import('@/components/dialog/TransparencySettingsDialog.vue'),
|
() => import('@/components/dialog/TransparencySettingsDialog.vue'),
|
||||||
)
|
)
|
||||||
const ThemeCustomizer = defineAsyncComponent(() => import('@/components/ThemeCustomizer.vue'))
|
|
||||||
const UserAuthDialog = defineAsyncComponent(() => import('@/components/dialog/UserAuthDialog.vue'))
|
const UserAuthDialog = defineAsyncComponent(() => import('@/components/dialog/UserAuthDialog.vue'))
|
||||||
|
|
||||||
// 认证 Store
|
// 认证 Store
|
||||||
@@ -50,12 +50,12 @@ const $toast = useToast()
|
|||||||
// UI模式菜单是否显示
|
// UI模式菜单是否显示
|
||||||
const showUIModeMenu = ref(false)
|
const showUIModeMenu = ref(false)
|
||||||
|
|
||||||
|
// 用户头像主菜单是否显示;打开布局级面板前需要主动关闭,避免菜单 overlay 残留。
|
||||||
|
const showUserMenu = ref(false)
|
||||||
|
|
||||||
// 主题菜单是否显示
|
// 主题菜单是否显示
|
||||||
const showThemeMenu = ref(false)
|
const showThemeMenu = ref(false)
|
||||||
|
|
||||||
// 主题定制器面板是否显示
|
|
||||||
const showThemeCustomizer = ref(false)
|
|
||||||
|
|
||||||
// 语言菜单是否显示
|
// 语言菜单是否显示
|
||||||
const showLanguageMenu = ref(false)
|
const showLanguageMenu = ref(false)
|
||||||
|
|
||||||
@@ -442,8 +442,11 @@ function showTransparencySettingsDialog() {
|
|||||||
|
|
||||||
/** 从用户菜单打开主题定制器,App 模式会在面板内部隐藏布局设置。 */
|
/** 从用户菜单打开主题定制器,App 模式会在面板内部隐藏布局设置。 */
|
||||||
function showThemeCustomizerDrawer() {
|
function showThemeCustomizerDrawer() {
|
||||||
|
showUserMenu.value = false
|
||||||
showThemeMenu.value = false
|
showThemeMenu.value = false
|
||||||
showThemeCustomizer.value = true
|
|
||||||
|
// 主题定制器由 DefaultLayout 统一挂载
|
||||||
|
window.dispatchEvent(new CustomEvent(THEME_CUSTOMIZER_OPEN_EVENT))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存自定义 CSS。 */
|
/** 保存自定义 CSS。 */
|
||||||
@@ -558,6 +561,7 @@ onUnmounted(() => {
|
|||||||
<VImg :src="avatar" />
|
<VImg :src="avatar" />
|
||||||
|
|
||||||
<VMenu
|
<VMenu
|
||||||
|
v-model="showUserMenu"
|
||||||
activator="parent"
|
activator="parent"
|
||||||
width="15rem"
|
width="15rem"
|
||||||
location="bottom end"
|
location="bottom end"
|
||||||
@@ -777,7 +781,6 @@ onUnmounted(() => {
|
|||||||
</VMenu>
|
</VMenu>
|
||||||
<!-- !SECTION -->
|
<!-- !SECTION -->
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
<ThemeCustomizer v-model="showThemeCustomizer" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -697,6 +697,19 @@ export default {
|
|||||||
subtitle: 'Scheduled Services',
|
subtitle: 'Scheduled Services',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
agentAssistant: {
|
||||||
|
title: 'AI Assistant',
|
||||||
|
assistant: 'Assistant',
|
||||||
|
ready: 'Ready',
|
||||||
|
thinking: 'Thinking',
|
||||||
|
newChat: 'New Chat',
|
||||||
|
emptyTitle: 'Ask about sites, subscriptions, downloads, or organization tasks',
|
||||||
|
placeholder: 'Ask MoviePilot...',
|
||||||
|
stop: 'Stop generating',
|
||||||
|
download: 'Download',
|
||||||
|
error: 'Assistant response failed',
|
||||||
|
noStream: 'This browser cannot read streaming responses',
|
||||||
|
},
|
||||||
workflow: {
|
workflow: {
|
||||||
components: 'Action Components',
|
components: 'Action Components',
|
||||||
clickToAdd: 'Click to Add',
|
clickToAdd: 'Click to Add',
|
||||||
|
|||||||
@@ -693,6 +693,19 @@ export default {
|
|||||||
subtitle: '定时服务',
|
subtitle: '定时服务',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
agentAssistant: {
|
||||||
|
title: '智能助手',
|
||||||
|
assistant: '助手',
|
||||||
|
ready: '随时待命',
|
||||||
|
thinking: '思考中',
|
||||||
|
newChat: '新会话',
|
||||||
|
emptyTitle: '可以直接询问站点、订阅、下载和整理任务',
|
||||||
|
placeholder: '询问 MoviePilot...',
|
||||||
|
stop: '停止生成',
|
||||||
|
download: '下载',
|
||||||
|
error: '智能助手响应失败',
|
||||||
|
noStream: '当前浏览器无法读取流式响应',
|
||||||
|
},
|
||||||
workflow: {
|
workflow: {
|
||||||
components: '动作组件',
|
components: '动作组件',
|
||||||
clickToAdd: '点击添加',
|
clickToAdd: '点击添加',
|
||||||
|
|||||||
@@ -693,6 +693,19 @@ export default {
|
|||||||
subtitle: '定時服務',
|
subtitle: '定時服務',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
agentAssistant: {
|
||||||
|
title: '智能助手',
|
||||||
|
assistant: '助手',
|
||||||
|
ready: '隨時待命',
|
||||||
|
thinking: '思考中',
|
||||||
|
newChat: '新會話',
|
||||||
|
emptyTitle: '可以直接詢問站點、訂閱、下載和整理任務',
|
||||||
|
placeholder: '詢問 MoviePilot...',
|
||||||
|
stop: '停止生成',
|
||||||
|
download: '下載',
|
||||||
|
error: '智能助手響應失敗',
|
||||||
|
noStream: '目前瀏覽器無法讀取串流響應',
|
||||||
|
},
|
||||||
workflow: {
|
workflow: {
|
||||||
components: '動作組件',
|
components: '動作組件',
|
||||||
clickToAdd: '點擊添加',
|
clickToAdd: '點擊添加',
|
||||||
|
|||||||
@@ -922,6 +922,7 @@ html[data-theme="transparent"] .app-card-colorful,
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--agent-assistant-fab-offset: 30rem;
|
||||||
--theme-customizer-fab-offset: 420px;
|
--theme-customizer-fab-offset: 420px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,6 +1041,14 @@ html[data-theme="transparent"] .app-card-colorful,
|
|||||||
html[data-theme-customizer-open='true'] .global-action-buttons {
|
html[data-theme-customizer-open='true'] .global-action-buttons {
|
||||||
inset-inline-end: calc(var(--theme-customizer-fab-offset) + 2rem);
|
inset-inline-end: calc(var(--theme-customizer-fab-offset) + 2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-agent-assistant-open='true'] .compact-fab-stack {
|
||||||
|
inset-inline-end: calc(var(--agent-assistant-fab-offset) + max(1rem, calc(env(safe-area-inset-right) + 1rem)));
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-agent-assistant-open='true'] .global-action-buttons {
|
||||||
|
inset-inline-end: calc(var(--agent-assistant-fab-offset) + 2rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width >= 601px) and (width <= 768px) {
|
@media (width >= 601px) and (width <= 768px) {
|
||||||
@@ -1048,6 +1057,12 @@ html[data-theme="transparent"] .app-card-colorful,
|
|||||||
var(--theme-customizer-fab-offset) + max(0.875rem, calc(env(safe-area-inset-right) + 0.875rem))
|
var(--theme-customizer-fab-offset) + max(0.875rem, calc(env(safe-area-inset-right) + 0.875rem))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-agent-assistant-open='true'] .compact-fab-stack {
|
||||||
|
inset-inline-end: calc(
|
||||||
|
var(--agent-assistant-fab-offset) + max(0.875rem, calc(env(safe-area-inset-right) + 0.875rem))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.apexcharts-title-text {
|
.apexcharts-title-text {
|
||||||
|
|||||||
Reference in New Issue
Block a user