mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-21 23:53:57 +08:00
feat: update Ace editor themes to follow system color scheme and standardize dialog UI layouts
This commit is contained in:
@@ -14,6 +14,10 @@ import modeIniUrl from 'ace-builds/src-noconflict/mode-ini?url'
|
||||
|
||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||
|
||||
import themeGithubDarkUrl from 'ace-builds/src-noconflict/theme-github_dark?url'
|
||||
|
||||
import themeGithubLightDefaultUrl from 'ace-builds/src-noconflict/theme-github_light_default?url'
|
||||
|
||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||
|
||||
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
|
||||
@@ -533,6 +537,8 @@ 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/theme/github', themeGithubUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github_dark', themeGithubDarkUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github_light_default', themeGithubLightDefaultUrl)
|
||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
|
||||
|
||||
@@ -40,6 +40,14 @@ const visible = computed({
|
||||
|
||||
// 正在编辑的 CSS 内容
|
||||
const editableCSS = ref(props.css)
|
||||
const editorOptions = {
|
||||
displayIndentGuides: true,
|
||||
fontSize: 14,
|
||||
highlightActiveLine: true,
|
||||
scrollPastEnd: 0.2,
|
||||
showPrintMargin: false,
|
||||
tabSize: 2,
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.css,
|
||||
@@ -56,25 +64,66 @@ function submitCustomCSS() {
|
||||
|
||||
<template>
|
||||
<VDialog v-if="visible" v-model="visible" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCard class="custom-css-dialog">
|
||||
<VCardItem class="custom-css-header py-3">
|
||||
<template #prepend>
|
||||
<VAvatar color="primary" variant="tonal" rounded size="40" class="me-2">
|
||||
<VIcon icon="mdi-palette" size="22" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-palette" class="me-2" />
|
||||
{{ t('theme.custom') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn v-model="visible" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VAceEditor v-model:value="editableCSS" lang="css" :theme="props.editorTheme" class="w-full min-h-[30rem]" />
|
||||
<VDivider />
|
||||
<VCardText class="text-center">
|
||||
<VBtn @click="submitCustomCSS" class="w-1/2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-content-save" />
|
||||
</template>
|
||||
<div>
|
||||
<VAceEditor
|
||||
v-model:value="editableCSS"
|
||||
lang="css"
|
||||
:theme="props.editorTheme"
|
||||
:options="editorOptions"
|
||||
wrap
|
||||
class="custom-css-editor"
|
||||
/>
|
||||
</div>
|
||||
<VCardActions class="custom-css-actions">
|
||||
<VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="submitCustomCSS">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-css-dialog {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-css-header {
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.custom-css-editor {
|
||||
overflow: hidden;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
block-size: min(62vh, 34rem);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.custom-css-actions {
|
||||
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
padding-block: 0.875rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.custom-css-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.custom-css-editor {
|
||||
block-size: calc(100dvh - 10rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,6 +38,14 @@ const visible = computed({
|
||||
})
|
||||
|
||||
const editableContent = ref(props.content)
|
||||
const editorOptions = {
|
||||
displayIndentGuides: true,
|
||||
fontSize: 14,
|
||||
highlightActiveLine: true,
|
||||
scrollPastEnd: 0.2,
|
||||
showPrintMargin: false,
|
||||
tabSize: 2,
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
@@ -58,10 +66,12 @@ function submitTemplate() {
|
||||
|
||||
<template>
|
||||
<VDialog v-if="visible" v-model="visible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<VCard class="notification-template-editor-dialog">
|
||||
<VCardItem class="template-editor-header py-3">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-code-json" class="me-2" />
|
||||
<VAvatar color="primary" variant="tonal" rounded size="40" class="me-2">
|
||||
<VIcon icon="mdi-code-json" size="22" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('setting.notification.templateConfigTitle') }}
|
||||
@@ -71,16 +81,18 @@ function submitTemplate() {
|
||||
</VCardSubtitle>
|
||||
<VDialogCloseBtn v-model="visible" />
|
||||
</VCardItem>
|
||||
<VCardText class="py-0">
|
||||
<div>
|
||||
<VAceEditor
|
||||
:key="`${props.templateType}-jinja2-json`"
|
||||
v-model:value="editableContent"
|
||||
lang="jinja2_json"
|
||||
:theme="props.editorTheme"
|
||||
class="w-full h-full min-h-[30rem] rounded"
|
||||
:options="editorOptions"
|
||||
wrap
|
||||
class="template-ace-editor"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
</div>
|
||||
<VCardActions class="template-editor-actions">
|
||||
<VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="submitTemplate">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
@@ -88,3 +100,36 @@ function submitTemplate() {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notification-template-editor-dialog {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-editor-header {
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.template-ace-editor {
|
||||
overflow: hidden;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
block-size: min(62vh, 34rem);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.template-editor-actions {
|
||||
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
padding-block: 0.875rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.template-editor-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.template-ace-editor {
|
||||
block-size: calc(100dvh - 11rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -293,8 +293,8 @@ const themes: ThemeSwitcherTheme[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// 编辑器主题
|
||||
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
|
||||
// Ace 跟随 Vuetify 当前生效主题,避免 auto 模式或弹窗打开后切主题时颜色不同步。
|
||||
const editorTheme = computed(() => (globalTheme.current.value.dark ? 'github_dark' : 'github_light_default'))
|
||||
|
||||
// 更新主题
|
||||
async function updateTheme() {
|
||||
@@ -369,6 +369,11 @@ function showCustomCssDialog() {
|
||||
)
|
||||
}
|
||||
|
||||
// 共享弹窗打开后也要同步主题变化,否则 Ace 会停留在打开时的配色。
|
||||
watch(editorTheme, theme => {
|
||||
customCssDialogController?.updateProps({ editorTheme: theme })
|
||||
})
|
||||
|
||||
/** 打开透明主题设置共享弹窗。 */
|
||||
function showTransparencySettingsDialog() {
|
||||
openSharedDialog(TransparencySettingsDialog, {}, {}, { closeOn: ['close', 'update:modelValue'] })
|
||||
|
||||
@@ -64,7 +64,8 @@ const SystemSettings = ref<any>({
|
||||
})
|
||||
|
||||
// 编辑器主题
|
||||
const editorTheme = computed(() => (globalTheme.name.value === 'light' ? 'github' : 'monokai'))
|
||||
// Ace 跟随 Vuetify 当前生效主题,auto 模式下也按实际明暗色渲染。
|
||||
const editorTheme = computed(() => (globalTheme.current.value.dark ? 'github_dark' : 'github_light_default'))
|
||||
|
||||
const renameEditorOptions = {
|
||||
fontSize: 14,
|
||||
|
||||
@@ -25,39 +25,51 @@ const NotificationTemplateEditorDialog = defineAsyncComponent(
|
||||
() => import('@/components/dialog/NotificationTemplateEditorDialog.vue'),
|
||||
)
|
||||
|
||||
// 初始化模板配置字典
|
||||
const templateConfigs = ref<Record<string, string>>({
|
||||
organizeSuccess: '{}',
|
||||
downloadAdded: '{}',
|
||||
subscribeAdded: '{}',
|
||||
subscribeComplete: '{}',
|
||||
})
|
||||
|
||||
// 模板类型配置
|
||||
const templateTypes = ref([
|
||||
// 通知模板入口的图标和强调色统一维护,避免模板中散落长判断。
|
||||
const templateTypeDefaults = [
|
||||
{
|
||||
type: 'organizeSuccess',
|
||||
label: t('setting.notification.organizeSuccess'),
|
||||
icon: 'mdi-folder-check',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
type: 'downloadAdded',
|
||||
label: t('setting.notification.downloadAdded'),
|
||||
icon: 'mdi-download-box',
|
||||
color: 'info',
|
||||
},
|
||||
{
|
||||
type: 'subscribeAdded',
|
||||
label: t('setting.notification.subscribeAdded'),
|
||||
icon: 'mdi-rss-box',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
type: 'subscribeComplete',
|
||||
label: t('setting.notification.subscribeComplete'),
|
||||
icon: 'mdi-check-circle',
|
||||
color: 'success',
|
||||
},
|
||||
])
|
||||
] as const
|
||||
|
||||
// 编辑器主题
|
||||
const { name: themeName, global: globalTheme } = useTheme()
|
||||
const savedTheme = ref(localStorage.getItem('theme') ?? 'auto')
|
||||
const currentThemeName = ref(savedTheme.value)
|
||||
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
|
||||
type NotificationTemplateType = (typeof templateTypeDefaults)[number]['type']
|
||||
|
||||
// 初始化模板配置字典
|
||||
const templateConfigs = ref<Record<string, string>>(
|
||||
templateTypeDefaults.reduce<Record<string, string>>((configs, item) => {
|
||||
configs[item.type] = '{}'
|
||||
return configs
|
||||
}, {}),
|
||||
)
|
||||
|
||||
// 模板类型配置
|
||||
const templateTypes = computed(() =>
|
||||
templateTypeDefaults.map(item => ({
|
||||
...item,
|
||||
label: t(`setting.notification.${item.type}`),
|
||||
})),
|
||||
)
|
||||
|
||||
// Ace 直接跟随 Vuetify 当前生效主题,auto 模式下也能按实际明暗色切换。
|
||||
const { global: globalTheme } = useTheme()
|
||||
const editorTheme = computed(() => (globalTheme.current.value.dark ? 'github_dark' : 'github_light_default'))
|
||||
|
||||
// 所有消息渠道
|
||||
const notifications = ref<NotificationConf[]>([])
|
||||
@@ -66,7 +78,7 @@ const notifications = ref<NotificationConf[]>([])
|
||||
const $toast = useToast()
|
||||
|
||||
const editorDialogOpen = ref(false)
|
||||
const currentTemplate = ref('')
|
||||
const currentTemplate = ref<NotificationTemplateType | ''>('')
|
||||
const editorContent = ref('')
|
||||
|
||||
// 消息类型开关
|
||||
@@ -127,7 +139,7 @@ function closeTemplateEditorDialog() {
|
||||
}
|
||||
|
||||
// 打开通知模板共享弹窗,保持内容通过事件回写到设置页。
|
||||
function openTemplateEditorDialog(type: string) {
|
||||
function openTemplateEditorDialog(type: NotificationTemplateType) {
|
||||
closeTemplateEditorDialog()
|
||||
editorDialogOpen.value = true
|
||||
editorDialogController = openSharedDialog(
|
||||
@@ -158,6 +170,13 @@ function openTemplateEditorDialog(type: string) {
|
||||
)
|
||||
}
|
||||
|
||||
// 共享弹窗的 props 是打开时写入的,主题切换时主动推送给已打开的编辑器。
|
||||
watch(editorTheme, theme => {
|
||||
if (!editorDialogOpen.value) return
|
||||
|
||||
editorDialogController?.updateProps({ editorTheme: theme })
|
||||
})
|
||||
|
||||
// 添加通知渠道
|
||||
function addNotification(notification: string) {
|
||||
let name = `${t('setting.notification.channel')}${notifications.value.length + 1}`
|
||||
@@ -229,7 +248,7 @@ async function loadNotificationSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openEditor(type: string) {
|
||||
async function openEditor(type: NotificationTemplateType) {
|
||||
try {
|
||||
currentTemplate.value = type
|
||||
const result: { [key: string]: any } = await api.get('system/setting/NotificationTemplates')
|
||||
@@ -460,34 +479,25 @@ useSilentSettingRefresh(loadPageData, {
|
||||
<VCardSubtitle>{{ t('setting.notification.templateConfigDesc') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol v-for="item in templateTypes" :key="item.type" cols="12" sm="6" md="3">
|
||||
<VCard variant="tonal" class="template-card" :class="{ 'on-hover': true }" @click="openEditor(item.type)">
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VAvatar color="primary" variant="tonal" rounded size="42" class="me-3">
|
||||
<VIcon
|
||||
size="24"
|
||||
:icon="
|
||||
item.type === 'organizeSuccess'
|
||||
? 'mdi-folder-check'
|
||||
: item.type === 'downloadAdded'
|
||||
? 'mdi-download'
|
||||
: item.type === 'subscribeAdded'
|
||||
? 'mdi-rss'
|
||||
: 'mdi-check-circle'
|
||||
"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardTitle>{{ item.label }}</VCardTitle>
|
||||
<template #append>
|
||||
<VIcon icon="mdi-chevron-right" />
|
||||
</template>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<div class="notification-template-grid">
|
||||
<button
|
||||
v-for="item in templateTypes"
|
||||
:key="item.type"
|
||||
type="button"
|
||||
class="notification-template-card"
|
||||
:class="`template-accent-${item.color}`"
|
||||
@click="openEditor(item.type)"
|
||||
>
|
||||
<span class="template-card-icon">
|
||||
<VIcon :icon="item.icon" size="24" />
|
||||
</span>
|
||||
<span class="template-card-copy">
|
||||
<span class="template-card-title">{{ item.label }}</span>
|
||||
<span class="template-card-subtitle">Jinja2 JSON</span>
|
||||
</span>
|
||||
<VIcon class="template-card-arrow" icon="mdi-chevron-right" size="22" />
|
||||
</button>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
@@ -575,20 +585,126 @@ useSilentSettingRefresh(loadPageData, {
|
||||
</VRow>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* Monaco编辑器容器样式 */
|
||||
.monaco-editor-container {
|
||||
/* 模板入口保持设置页的紧凑密度,同时用轻量强调色区分不同通知场景。 */
|
||||
.notification-template-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
}
|
||||
|
||||
.notification-template-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
align-items: center;
|
||||
border: 1px solid rgba(var(--template-accent), 0.18);
|
||||
border-radius: 8px;
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(var(--template-accent), 0.12), rgba(var(--v-theme-surface), 0) 58%),
|
||||
rgba(var(--v-theme-surface), 0.72);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
gap: 0.875rem;
|
||||
inline-size: 100%;
|
||||
min-block-size: 5.25rem;
|
||||
padding: 1rem;
|
||||
text-align: start;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.template-card.on-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
.notification-template-card::before {
|
||||
position: absolute;
|
||||
background: rgb(var(--template-accent));
|
||||
block-size: 100%;
|
||||
content: "";
|
||||
inline-size: 0.25rem;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.notification-template-card:hover {
|
||||
border-color: rgba(var(--template-accent), 0.36);
|
||||
box-shadow: 0 0.75rem 1.75rem rgba(var(--template-accent), 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.notification-template-card:focus-visible {
|
||||
outline: 2px solid rgba(var(--template-accent), 0.7);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.template-card-icon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--template-accent), 0.16);
|
||||
block-size: 2.75rem;
|
||||
color: rgb(var(--template-accent));
|
||||
inline-size: 2.75rem;
|
||||
}
|
||||
|
||||
.template-card-copy {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.template-card-title {
|
||||
overflow: hidden;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.template-card-subtitle {
|
||||
margin-block-start: 0.25rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.template-card-arrow {
|
||||
flex: 0 0 auto;
|
||||
color: rgba(var(--v-theme-on-surface), 0.42);
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-template-card:hover .template-card-arrow {
|
||||
color: rgb(var(--template-accent));
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.template-accent-primary {
|
||||
--template-accent: var(--v-theme-primary);
|
||||
}
|
||||
|
||||
.template-accent-info {
|
||||
--template-accent: var(--v-theme-info);
|
||||
}
|
||||
|
||||
.template-accent-warning {
|
||||
--template-accent: var(--v-theme-warning);
|
||||
}
|
||||
|
||||
.template-accent-success {
|
||||
--template-accent: var(--v-theme-success);
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.notification-template-grid {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.notification-template-card {
|
||||
min-block-size: 4.75rem;
|
||||
padding: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user