feat(projects): Add current time display option for watermark (#772)

* feat(projects): Add current time display option for watermark

* perf(projects): add watermark timer controls
This commit is contained in:
wenyuan 2025-06-26 14:38:30 +08:00 committed by Soybean
parent 8ba71a0857
commit f238fcbd47
10 changed files with 193 additions and 42 deletions

View File

@ -4,7 +4,6 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
import type { WatermarkProps } from 'naive-ui'; import type { WatermarkProps } from 'naive-ui';
import { useAppStore } from './store/modules/app'; import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme'; import { useThemeStore } from './store/modules/theme';
import { useAuthStore } from './store/modules/auth';
import { naiveDateLocales, naiveLocales } from './locales/naive'; import { naiveDateLocales, naiveLocales } from './locales/naive';
defineOptions({ defineOptions({
@ -13,7 +12,6 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const authStore = useAuthStore();
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined)); const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
@ -26,13 +24,8 @@ const naiveDateLocale = computed(() => {
}); });
const watermarkProps = computed<WatermarkProps>(() => { const watermarkProps = computed<WatermarkProps>(() => {
const content =
themeStore.watermark.enableUserName && authStore.userInfo.userName
? authStore.userInfo.userName
: themeStore.watermark.text;
return { return {
content, content: themeStore.watermarkContent,
cross: true, cross: true,
fullscreen: true, fullscreen: true,
fontSize: 16, fontSize: 16,

View File

@ -63,3 +63,13 @@ export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord); export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
export const DARK_CLASS = 'dark'; export const DARK_CLASS = 'dark';
export const watermarkTimeFormatOptions = [
{ label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
{ label: 'YYYY/MM/DD HH:mm', value: 'YYYY/MM/DD HH:mm' },
{ label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
{ label: 'HH:mm', value: 'HH:mm' },
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
{ label: 'MM-DD HH:mm', value: 'MM-DD HH:mm' }
];

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import GlobalSettings from './modules/global-settings.vue'; import GlobalSettings from './modules/global-settings.vue';
import WatermarkSettings from './modules/watermark-settings.vue';
defineOptions({ defineOptions({
name: 'GeneralSettings' name: 'GeneralSettings'
@ -9,6 +10,7 @@ defineOptions({
<template> <template>
<div class="flex-col-stretch gap-16px"> <div class="flex-col-stretch gap-16px">
<GlobalSettings /> <GlobalSettings />
<WatermarkSettings />
</div> </div>
</template> </template>

View File

@ -11,34 +11,14 @@ const themeStore = useThemeStore();
</script> </script>
<template> <template>
<div class="flex-col-stretch gap-16px"> <NDivider>{{ $t('theme.general.title') }}</NDivider>
<SettingItem :label="$t('theme.general.multilingual.visible')"> <SettingItem :label="$t('theme.general.multilingual.visible')">
<NSwitch v-model:value="themeStore.header.multilingual.visible" /> <NSwitch v-model:value="themeStore.header.multilingual.visible" />
</SettingItem> </SettingItem>
<SettingItem :label="$t('theme.general.globalSearch.visible')"> <SettingItem :label="$t('theme.general.globalSearch.visible')">
<NSwitch v-model:value="themeStore.header.globalSearch.visible" /> <NSwitch v-model:value="themeStore.header.globalSearch.visible" />
</SettingItem> </SettingItem>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.general.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="2" :label="$t('theme.general.watermark.enableUserName')">
<NSwitch v-model:value="themeStore.watermark.enableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="3" :label="$t('theme.general.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize
type="text"
size="small"
class="w-120px"
placeholder="SoybeanAdmin"
/>
</SettingItem>
</TransitionGroup>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { computed } from 'vue';
import { watermarkTimeFormatOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'WatermarkSettings'
});
const themeStore = useThemeStore();
const isWatermarkTextVisible = computed(
() => themeStore.watermark.visible && !themeStore.watermark.enableUserName && !themeStore.watermark.enableTime
);
</script>
<template>
<NDivider>{{ $t('theme.general.watermark.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.general.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="2" :label="$t('theme.general.watermark.enableUserName')">
<NSwitch :value="themeStore.watermark.enableUserName" @update:value="themeStore.setWatermarkEnableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="3" :label="$t('theme.general.watermark.enableTime')">
<NSwitch :value="themeStore.watermark.enableTime" @update:value="themeStore.setWatermarkEnableTime" />
</SettingItem>
<SettingItem
v-if="themeStore.watermark.visible && themeStore.watermark.enableTime"
key="4"
:label="$t('theme.general.watermark.timeFormat')"
>
<NSelect
v-model:value="themeStore.watermark.timeFormat"
:options="watermarkTimeFormatOptions"
size="small"
class="w-210px"
/>
</SettingItem>
<SettingItem v-if="isWatermarkTextVisible" key="5" :label="$t('theme.general.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize
type="text"
size="small"
class="w-120px"
placeholder="SoybeanAdmin"
/>
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -158,10 +158,14 @@ const local: App.I18n.Schema = {
} }
}, },
general: { general: {
title: 'General Settings',
watermark: { watermark: {
title: 'Watermark Settings',
visible: 'Watermark Full Screen Visible', visible: 'Watermark Full Screen Visible',
text: 'Watermark Text', text: 'Custom Watermark Text',
enableUserName: 'Enable User Name Watermark' enableUserName: 'Enable User Name Watermark',
enableTime: 'Show Current Time',
timeFormat: 'Time Format'
}, },
multilingual: { multilingual: {
title: 'Multilingual Settings', title: 'Multilingual Settings',

View File

@ -158,10 +158,14 @@ const local: App.I18n.Schema = {
} }
}, },
general: { general: {
title: '通用设置',
watermark: { watermark: {
title: '水印设置',
visible: '显示全屏水印', visible: '显示全屏水印',
text: '水印文本', text: '自定义水印文本',
enableUserName: '启用用户名水印' enableUserName: '启用用户名水印',
enableTime: '显示当前时间',
timeFormat: '时间格式'
}, },
multilingual: { multilingual: {
title: '多语言设置', title: '多语言设置',

View File

@ -1,10 +1,11 @@
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue'; import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { useEventListener, usePreferredColorScheme } from '@vueuse/core'; import { useDateFormat, useEventListener, useNow, usePreferredColorScheme } from '@vueuse/core';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getPaletteColorByNumber } from '@sa/color'; import { getPaletteColorByNumber } from '@sa/color';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
import { SetupStoreId } from '@/enum'; import { SetupStoreId } from '@/enum';
import { useAuthStore } from '../auth';
import { import {
addThemeVarsToGlobal, addThemeVarsToGlobal,
createThemeToken, createThemeToken,
@ -18,10 +19,14 @@ import {
export const useThemeStore = defineStore(SetupStoreId.Theme, () => { export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
const scope = effectScope(); const scope = effectScope();
const osTheme = usePreferredColorScheme(); const osTheme = usePreferredColorScheme();
const authStore = useAuthStore();
/** Theme settings */ /** Theme settings */
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings()); const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
/** Watermark time instance with controls */
const { now: watermarkTime, pause: pauseWatermarkTime, resume: resumeWatermarkTime } = useNow({ controls: true });
/** Dark mode */ /** Dark mode */
const darkMode = computed(() => { const darkMode = computed(() => {
if (settings.value.themeScheme === 'auto') { if (settings.value.themeScheme === 'auto') {
@ -57,6 +62,28 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
*/ */
const settingsJson = computed(() => JSON.stringify(settings.value)); const settingsJson = computed(() => JSON.stringify(settings.value));
/** Watermark time date formatter */
const formattedWatermarkTime = computed(() => {
const { watermark } = settings.value;
const date = useDateFormat(watermarkTime, watermark.timeFormat);
return date.value;
});
/** Watermark content */
const watermarkContent = computed(() => {
const { watermark } = settings.value;
if (watermark.enableUserName && authStore.userInfo.userName) {
return authStore.userInfo.userName;
}
if (watermark.enableTime) {
return formattedWatermarkTime.value;
}
return watermark.text;
});
/** Reset store */ /** Reset store */
function resetStore() { function resetStore() {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@ -153,6 +180,44 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
settings.value.layout.reverseHorizontalMix = reverse; settings.value.layout.reverseHorizontalMix = reverse;
} }
/**
* Set watermark enable user name
*
* @param enable Whether to enable user name watermark
*/
function setWatermarkEnableUserName(enable: boolean) {
settings.value.watermark.enableUserName = enable;
if (enable) {
settings.value.watermark.enableTime = false;
}
}
/**
* Set watermark enable time
*
* @param enable Whether to enable time watermark
*/
function setWatermarkEnableTime(enable: boolean) {
settings.value.watermark.enableTime = enable;
if (enable) {
settings.value.watermark.enableUserName = false;
}
}
/** Only run timer when watermark is visible and time display is enabled */
function updateWatermarkTimer() {
const { watermark } = settings.value;
const shouldRunTimer = watermark.visible && watermark.enableTime;
if (shouldRunTimer) {
resumeWatermarkTime();
} else {
pauseWatermarkTime();
}
}
/** Cache theme settings */ /** Cache theme settings */
function cacheThemeSettings() { function cacheThemeSettings() {
const isProd = import.meta.env.PROD; const isProd = import.meta.env.PROD;
@ -196,6 +261,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
}, },
{ immediate: true } { immediate: true }
); );
// watch watermark settings to control timer
watch(
() => [settings.value.watermark.visible, settings.value.watermark.enableTime],
() => {
updateWatermarkTimer();
},
{ immediate: true }
);
}); });
/** On scope dispose */ /** On scope dispose */
@ -209,6 +283,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
themeColors, themeColors,
naiveTheme, naiveTheme,
settingsJson, settingsJson,
watermarkContent,
setGrayscale, setGrayscale,
setColourWeakness, setColourWeakness,
resetStore, resetStore,
@ -216,6 +291,8 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
toggleThemeScheme, toggleThemeScheme,
updateThemeColors, updateThemeColors,
setThemeLayout, setThemeLayout,
setLayoutReverseHorizontalMix setLayoutReverseHorizontalMix,
setWatermarkEnableUserName,
setWatermarkEnableTime
}; };
}); });

View File

@ -59,7 +59,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
watermark: { watermark: {
visible: false, visible: false,
text: 'SoybeanAdmin', text: 'SoybeanAdmin',
enableUserName: false enableUserName: false,
enableTime: false,
timeFormat: 'YYYY-MM-DD HH:mm'
}, },
tokens: { tokens: {
light: { light: {

View File

@ -114,6 +114,10 @@ declare namespace App {
text: string; text: string;
/** Whether to use user name as watermark text */ /** Whether to use user name as watermark text */
enableUserName: boolean; enableUserName: boolean;
/** Whether to use current time as watermark text */
enableTime: boolean;
/** Time format for watermark text */
timeFormat: string;
}; };
/** define some theme settings tokens, will transform to css variables */ /** define some theme settings tokens, will transform to css variables */
tokens: { tokens: {
@ -420,10 +424,14 @@ declare namespace App {
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>; resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
}; };
general: { general: {
title: string;
watermark: { watermark: {
title: string;
visible: string; visible: string;
text: string; text: string;
enableUserName: string; enableUserName: string;
enableTime: string;
timeFormat: string;
}; };
multilingual: { multilingual: {
title: string; title: string;