feat: implement theme customization with primary and secondary color options
This commit is contained in:
@@ -16,6 +16,16 @@
|
||||
"custom": "Custom"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"subtitle": "Customize theme primary and secondary colors. Changes apply immediately and are stored locally in your browser.",
|
||||
"customize": {
|
||||
"title": "Theme Colors",
|
||||
"primary": "Primary Color",
|
||||
"secondary": "Secondary Color",
|
||||
"reset": "Reset to Default"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "System",
|
||||
"restart": {
|
||||
@@ -119,4 +129,4 @@
|
||||
"ftpHint": "For large backup files, you can also upload directly to the data/backups directory via FTP/SFTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
"custom": "自定义"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题",
|
||||
"subtitle": "自定义主题主色与辅助色。修改后立即生效,并保存在浏览器本地。",
|
||||
"customize": {
|
||||
"title": "主题颜色",
|
||||
"primary": "主色",
|
||||
"secondary": "辅助色",
|
||||
"reset": "恢复默认"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "系统",
|
||||
"restart": {
|
||||
@@ -119,4 +129,4 @@
|
||||
"ftpHint": "对于较大的备份文件,也可以通过 FTP/SFTP 等方式直接上传到 data/backups 目录"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-1
@@ -30,6 +30,19 @@ setupI18n().then(() => {
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem('themePrimary');
|
||||
const storedSecondary = localStorage.getItem('themeSecondary');
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('❌ 新i18n系统初始化失败:', error);
|
||||
@@ -49,6 +62,19 @@ setupI18n().then(() => {
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem('themePrimary');
|
||||
const storedSecondary = localStorage.getItem('themeSecondary');
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,4 +105,4 @@ loader.config({
|
||||
paths: {
|
||||
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,8 +8,8 @@ const PurpleTheme: ThemeTypes = {
|
||||
'carousel-control-size': 10
|
||||
},
|
||||
colors: {
|
||||
primary: '#1e88e5',
|
||||
secondary: '#5e35b1',
|
||||
primary: '#3c96ca',
|
||||
secondary: '#2288b7',
|
||||
info: '#03c9d7',
|
||||
success: '#00c853',
|
||||
accent: '#FFAB91',
|
||||
|
||||
@@ -15,6 +15,41 @@
|
||||
<SidebarCustomizer></SidebarCustomizer>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-subheader>{{ tm('theme.title') }}</v-list-subheader>
|
||||
|
||||
<v-list-item :subtitle="tm('theme.subtitle')" :title="tm('theme.customize.title')">
|
||||
<v-row class="mt-2" dense>
|
||||
<v-col cols="4" sm="2">
|
||||
<v-text-field
|
||||
v-model="primaryColor"
|
||||
type="color"
|
||||
:label="tm('theme.customize.primary')"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="max-width: 220px;"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="4" sm="2 ">
|
||||
<v-text-field
|
||||
v-model="secondaryColor"
|
||||
type="color"
|
||||
:label="tm('theme.customize.secondary')"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="max-width: 220px;"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn size="small" variant="tonal" color="primary" @click="resetThemeColors">
|
||||
<v-icon class="mr-2">mdi-restore</v-icon>
|
||||
{{ tm('theme.customize.reset') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-subheader>{{ tm('system.title') }}</v-list-subheader>
|
||||
|
||||
<v-list-item :subtitle="tm('system.backup.subtitle')" :title="tm('system.backup.title')">
|
||||
@@ -42,7 +77,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ProxySelector from '@/components/shared/ProxySelector.vue';
|
||||
@@ -50,8 +85,52 @@ import MigrationDialog from '@/components/shared/MigrationDialog.vue';
|
||||
import SidebarCustomizer from '@/components/shared/SidebarCustomizer.vue';
|
||||
import BackupDialog from '@/components/shared/BackupDialog.vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { PurpleTheme } from '@/theme/LightTheme';
|
||||
|
||||
const { tm } = useModuleI18n('features/settings');
|
||||
const theme = useTheme();
|
||||
|
||||
const getStoredColor = (key, fallback) => {
|
||||
const stored = typeof window !== 'undefined' ? localStorage.getItem(key) : null;
|
||||
return stored || fallback;
|
||||
};
|
||||
|
||||
const primaryColor = ref(getStoredColor('themePrimary', PurpleTheme.colors.primary));
|
||||
const secondaryColor = ref(getStoredColor('themeSecondary', PurpleTheme.colors.secondary));
|
||||
|
||||
const resolveThemes = () => {
|
||||
if (theme?.themes?.value) return theme.themes.value;
|
||||
if (theme?.global?.themes?.value) return theme.global.themes.value;
|
||||
return null;
|
||||
};
|
||||
|
||||
const applyThemeColors = (primary, secondary) => {
|
||||
const themes = resolveThemes();
|
||||
if (!themes) return;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const themeDef = themes[name];
|
||||
if (!themeDef?.colors) return;
|
||||
if (primary) themeDef.colors.primary = primary;
|
||||
if (secondary) themeDef.colors.secondary = secondary;
|
||||
if (primary && themeDef.colors.darkprimary) themeDef.colors.darkprimary = primary;
|
||||
if (secondary && themeDef.colors.darksecondary) themeDef.colors.darksecondary = secondary;
|
||||
});
|
||||
};
|
||||
|
||||
applyThemeColors(primaryColor.value, secondaryColor.value);
|
||||
|
||||
watch(primaryColor, (value) => {
|
||||
if (!value) return;
|
||||
localStorage.setItem('themePrimary', value);
|
||||
applyThemeColors(value, secondaryColor.value);
|
||||
});
|
||||
|
||||
watch(secondaryColor, (value) => {
|
||||
if (!value) return;
|
||||
localStorage.setItem('themeSecondary', value);
|
||||
applyThemeColors(primaryColor.value, value);
|
||||
});
|
||||
|
||||
const wfr = ref(null);
|
||||
const migrationDialog = ref(null);
|
||||
@@ -81,4 +160,12 @@ const openBackupDialog = () => {
|
||||
backupDialog.value.open();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
const resetThemeColors = () => {
|
||||
primaryColor.value = PurpleTheme.colors.primary;
|
||||
secondaryColor.value = PurpleTheme.colors.secondary;
|
||||
localStorage.removeItem('themePrimary');
|
||||
localStorage.removeItem('themeSecondary');
|
||||
applyThemeColors(primaryColor.value, secondaryColor.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user