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

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 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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'] })

View File

@@ -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,

View File

@@ -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>