feat: update Ace editor themes to follow system color scheme and standardize dialog UI layouts

This commit is contained in:
jxxghp
2026-05-18 10:07:22 +08:00
parent 1759e666ba
commit 91dbf065db
6 changed files with 305 additions and 83 deletions
+6
View File
@@ -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 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 themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?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/css', modeCssUrl)
ace.config.setModuleUrl('ace/mode/ini', modeIniUrl) ace.config.setModuleUrl('ace/mode/ini', modeIniUrl)
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl) 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/chrome', themeChromeUrl)
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl) ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl) ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
+61 -12
View File
@@ -40,6 +40,14 @@ const visible = computed({
// 正在编辑的 CSS 内容 // 正在编辑的 CSS 内容
const editableCSS = ref(props.css) const editableCSS = ref(props.css)
const editorOptions = {
displayIndentGuides: true,
fontSize: 14,
highlightActiveLine: true,
scrollPastEnd: 0.2,
showPrintMargin: false,
tabSize: 2,
}
watch( watch(
() => props.css, () => props.css,
@@ -56,25 +64,66 @@ function submitCustomCSS() {
<template> <template>
<VDialog v-if="visible" v-model="visible" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value"> <VDialog v-if="visible" v-model="visible" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard> <VCard class="custom-css-dialog">
<VCardItem> <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> <VCardTitle>
<VIcon icon="mdi-palette" class="me-2" />
{{ t('theme.custom') }} {{ t('theme.custom') }}
</VCardTitle> </VCardTitle>
<VDialogCloseBtn v-model="visible" /> <VDialogCloseBtn v-model="visible" />
</VCardItem> </VCardItem>
<VDivider /> <div>
<VAceEditor v-model:value="editableCSS" lang="css" :theme="props.editorTheme" class="w-full min-h-[30rem]" /> <VAceEditor
<VDivider /> v-model:value="editableCSS"
<VCardText class="text-center"> lang="css"
<VBtn @click="submitCustomCSS" class="w-1/2"> :theme="props.editorTheme"
<template #prepend> :options="editorOptions"
<VIcon icon="mdi-content-save" /> wrap
</template> 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') }} {{ t('common.save') }}
</VBtn> </VBtn>
</VCardText> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
</template> </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 editableContent = ref(props.content)
const editorOptions = {
displayIndentGuides: true,
fontSize: 14,
highlightActiveLine: true,
scrollPastEnd: 0.2,
showPrintMargin: false,
tabSize: 2,
}
watch( watch(
() => props.content, () => props.content,
@@ -58,10 +66,12 @@ function submitTemplate() {
<template> <template>
<VDialog v-if="visible" v-model="visible" max-width="50rem" :fullscreen="!display.mdAndUp.value"> <VDialog v-if="visible" v-model="visible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard> <VCard class="notification-template-editor-dialog">
<VCardItem class="py-2"> <VCardItem class="template-editor-header py-3">
<template #prepend> <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> </template>
<VCardTitle> <VCardTitle>
{{ t('setting.notification.templateConfigTitle') }} {{ t('setting.notification.templateConfigTitle') }}
@@ -71,16 +81,18 @@ function submitTemplate() {
</VCardSubtitle> </VCardSubtitle>
<VDialogCloseBtn v-model="visible" /> <VDialogCloseBtn v-model="visible" />
</VCardItem> </VCardItem>
<VCardText class="py-0"> <div>
<VAceEditor <VAceEditor
:key="`${props.templateType}-jinja2-json`" :key="`${props.templateType}-jinja2-json`"
v-model:value="editableContent" v-model:value="editableContent"
lang="jinja2_json" lang="jinja2_json"
:theme="props.editorTheme" :theme="props.editorTheme"
class="w-full h-full min-h-[30rem] rounded" :options="editorOptions"
wrap
class="template-ace-editor"
/> />
</VCardText> </div>
<VCardActions class="pt-3"> <VCardActions class="template-editor-actions">
<VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="submitTemplate"> <VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="submitTemplate">
{{ t('common.save') }} {{ t('common.save') }}
</VBtn> </VBtn>
@@ -88,3 +100,36 @@ function submitTemplate() {
</VCard> </VCard>
</VDialog> </VDialog>
</template> </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>
+7 -2
View File
@@ -293,8 +293,8 @@ const themes: ThemeSwitcherTheme[] = [
}, },
] ]
// 编辑器主题 // Ace 跟随 Vuetify 当前生效主题,避免 auto 模式或弹窗打开后切主题时颜色不同步。
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai')) const editorTheme = computed(() => (globalTheme.current.value.dark ? 'github_dark' : 'github_light_default'))
// 更新主题 // 更新主题
async function updateTheme() { async function updateTheme() {
@@ -369,6 +369,11 @@ function showCustomCssDialog() {
) )
} }
// 共享弹窗打开后也要同步主题变化,否则 Ace 会停留在打开时的配色。
watch(editorTheme, theme => {
customCssDialogController?.updateProps({ editorTheme: theme })
})
/** 打开透明主题设置共享弹窗。 */ /** 打开透明主题设置共享弹窗。 */
function showTransparencySettingsDialog() { function showTransparencySettingsDialog() {
openSharedDialog(TransparencySettingsDialog, {}, {}, { closeOn: ['close', 'update:modelValue'] }) 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 = { const renameEditorOptions = {
fontSize: 14, fontSize: 14,
+177 -61
View File
@@ -25,39 +25,51 @@ const NotificationTemplateEditorDialog = defineAsyncComponent(
() => import('@/components/dialog/NotificationTemplateEditorDialog.vue'), () => import('@/components/dialog/NotificationTemplateEditorDialog.vue'),
) )
// 初始化模板配置字典 // 通知模板入口的图标和强调色统一维护,避免模板中散落长判断。
const templateConfigs = ref<Record<string, string>>({ const templateTypeDefaults = [
organizeSuccess: '{}',
downloadAdded: '{}',
subscribeAdded: '{}',
subscribeComplete: '{}',
})
// 模板类型配置
const templateTypes = ref([
{ {
type: 'organizeSuccess', type: 'organizeSuccess',
label: t('setting.notification.organizeSuccess'), icon: 'mdi-folder-check',
color: 'primary',
}, },
{ {
type: 'downloadAdded', type: 'downloadAdded',
label: t('setting.notification.downloadAdded'), icon: 'mdi-download-box',
color: 'info',
}, },
{ {
type: 'subscribeAdded', type: 'subscribeAdded',
label: t('setting.notification.subscribeAdded'), icon: 'mdi-rss-box',
color: 'warning',
}, },
{ {
type: 'subscribeComplete', type: 'subscribeComplete',
label: t('setting.notification.subscribeComplete'), icon: 'mdi-check-circle',
color: 'success',
}, },
]) ] as const
// 编辑器主题 type NotificationTemplateType = (typeof templateTypeDefaults)[number]['type']
const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? 'auto') // 初始化模板配置字典
const currentThemeName = ref(savedTheme.value) const templateConfigs = ref<Record<string, string>>(
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai')) 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[]>([]) const notifications = ref<NotificationConf[]>([])
@@ -66,7 +78,7 @@ const notifications = ref<NotificationConf[]>([])
const $toast = useToast() const $toast = useToast()
const editorDialogOpen = ref(false) const editorDialogOpen = ref(false)
const currentTemplate = ref('') const currentTemplate = ref<NotificationTemplateType | ''>('')
const editorContent = ref('') const editorContent = ref('')
// 消息类型开关 // 消息类型开关
@@ -127,7 +139,7 @@ function closeTemplateEditorDialog() {
} }
// 打开通知模板共享弹窗,保持内容通过事件回写到设置页。 // 打开通知模板共享弹窗,保持内容通过事件回写到设置页。
function openTemplateEditorDialog(type: string) { function openTemplateEditorDialog(type: NotificationTemplateType) {
closeTemplateEditorDialog() closeTemplateEditorDialog()
editorDialogOpen.value = true editorDialogOpen.value = true
editorDialogController = openSharedDialog( 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) { function addNotification(notification: string) {
let name = `${t('setting.notification.channel')}${notifications.value.length + 1}` 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 { try {
currentTemplate.value = type currentTemplate.value = type
const result: { [key: string]: any } = await api.get('system/setting/NotificationTemplates') const result: { [key: string]: any } = await api.get('system/setting/NotificationTemplates')
@@ -460,34 +479,25 @@ useSilentSettingRefresh(loadPageData, {
<VCardSubtitle>{{ t('setting.notification.templateConfigDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.notification.templateConfigDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<VRow> <div class="notification-template-grid">
<VCol v-for="item in templateTypes" :key="item.type" cols="12" sm="6" md="3"> <button
<VCard variant="tonal" class="template-card" :class="{ 'on-hover': true }" @click="openEditor(item.type)"> v-for="item in templateTypes"
<VCardItem> :key="item.type"
<template #prepend> type="button"
<VAvatar color="primary" variant="tonal" rounded size="42" class="me-3"> class="notification-template-card"
<VIcon :class="`template-accent-${item.color}`"
size="24" @click="openEditor(item.type)"
:icon=" >
item.type === 'organizeSuccess' <span class="template-card-icon">
? 'mdi-folder-check' <VIcon :icon="item.icon" size="24" />
: item.type === 'downloadAdded' </span>
? 'mdi-download' <span class="template-card-copy">
: item.type === 'subscribeAdded' <span class="template-card-title">{{ item.label }}</span>
? 'mdi-rss' <span class="template-card-subtitle">Jinja2 JSON</span>
: 'mdi-check-circle' </span>
" <VIcon class="template-card-arrow" icon="mdi-chevron-right" size="22" />
/> </button>
</VAvatar> </div>
</template>
<VCardTitle>{{ item.label }}</VCardTitle>
<template #append>
<VIcon icon="mdi-chevron-right" />
</template>
</VCardItem>
</VCard>
</VCol>
</VRow>
</VCardText> </VCardText>
</VCard> </VCard>
</VCol> </VCol>
@@ -575,20 +585,126 @@ useSilentSettingRefresh(loadPageData, {
</VRow> </VRow>
</template> </template>
<style scoped> <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; 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; border-radius: 8px;
margin-block-start: 1rem; background:
} linear-gradient(135deg, rgba(var(--template-accent), 0.12), rgba(var(--v-theme-surface), 0) 58%),
rgba(var(--v-theme-surface), 0.72);
.template-card { color: rgb(var(--v-theme-on-surface));
cursor: pointer; 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 { .notification-template-card::before {
transform: translateY(-4px); 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> </style>