Merge branch 'main' into main

This commit is contained in:
xueyitt
2026-04-13 16:10:29 +08:00
committed by GitHub
24 changed files with 1720 additions and 39 deletions

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
import type {
CustomPreferencesField,
CustomPreferencesRecord,
} from '@vben/preferences';
import { computed } from 'vue';
import { $t } from '@vben/locales';
import InputItem from '../input-item.vue';
import NumberFieldItem from '../number-field-item.vue';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceCustomFields',
});
const props = defineProps<{
fields: Array<CustomPreferencesField>;
values: CustomPreferencesRecord;
}>();
const emit = defineEmits<{
update: [updates: CustomPreferencesRecord];
}>();
function handleUpdate(key: string, value: boolean | number | string) {
emit('update', { [key]: value });
}
function handleBooleanUpdate(key: string, value: boolean | undefined) {
handleUpdate(key, value ?? false);
}
function resolveNumberValue(value: unknown) {
return typeof value === 'number' && Number.isFinite(value)
? value
: undefined;
}
function handleNumberUpdate(key: string, value: number | undefined) {
const resolvedValue = resolveNumberValue(value);
if (resolvedValue !== undefined) {
handleUpdate(key, resolvedValue);
}
}
function handleStringUpdate(key: string, value: string | undefined) {
handleUpdate(key, value ?? '');
}
const resolvedFields = computed(() => {
return props.fields.map((field) => {
return {
...field,
label: $t(field.label),
options:
field.component === 'select'
? field.options.map((option) => ({
...option,
label: $t(option.label),
}))
: undefined,
placeholder: field.placeholder ? $t(field.placeholder) : '',
tip: field.tip ? $t(field.tip) : '',
};
});
});
</script>
<template>
<template v-for="field in resolvedFields" :key="field.key">
<SwitchItem
v-if="field.component === 'switch'"
:disabled="field.disabled"
:model-value="Boolean(values[field.key])"
:tip="field.tip"
v-bind="field.componentProps"
@update:model-value="handleBooleanUpdate(field.key, $event)"
>
{{ field.label }}
</SwitchItem>
<NumberFieldItem
v-else-if="field.component === 'number'"
:disabled="field.disabled"
:model-value="resolveNumberValue(values[field.key])"
:placeholder="field.placeholder"
:tip="field.tip"
v-bind="field.componentProps"
@update:model-value="handleNumberUpdate(field.key, $event)"
>
{{ field.label }}
</NumberFieldItem>
<SelectItem
v-else-if="field.component === 'select'"
:disabled="field.disabled"
:items="field.options"
:model-value="String(values[field.key] ?? '')"
:placeholder="field.placeholder"
:tip="field.tip"
v-bind="field.componentProps"
@update:model-value="handleStringUpdate(field.key, $event)"
>
{{ field.label }}
</SelectItem>
<InputItem
v-else
:disabled="field.disabled"
:model-value="String(values[field.key] ?? '')"
:placeholder="field.placeholder"
:tip="field.tip"
v-bind="field.componentProps"
@update:model-value="handleStringUpdate(field.key, $event)"
>
{{ field.label }}
</InputItem>
</template>
</template>

View File

@@ -1,4 +1,5 @@
export { default as Block } from './block.vue';
export { default as Custom } from './custom/custom.vue';
export { default as Animation } from './general/animation.vue';
export { default as General } from './general/general.vue';
export { default as Breadcrumb } from './layout/breadcrumb.vue';

View File

@@ -16,10 +16,12 @@ withDefaults(
disabled?: boolean;
items?: SelectOption[];
placeholder?: string;
tip?: string;
}>(),
{
disabled: false,
placeholder: '',
tip: '',
items: () => [],
},
);
@@ -32,7 +34,7 @@ const slots = useSlots();
<template>
<div
:class="{
'hover:bg-accent': !slots.tip,
'hover:bg-accent': !(slots.tip || tip),
'pointer-events-none opacity-50': disabled,
}"
class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"
@@ -40,11 +42,17 @@ const slots = useSlots();
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<VbenTooltip v-if="slots.tip || tip" side="bottom">
<template #trigger>
<CircleHelp class="ml-1 size-3 cursor-help" />
</template>
<slot name="tip"></slot>
<slot name="tip">
<template v-if="tip">
<p v-for="(line, index) in tip.split('\n')" :key="index">
{{ line }}
</p>
</template>
</slot>
</VbenTooltip>
</span>
<div class="relative">

View File

@@ -23,10 +23,12 @@ withDefaults(
disabled?: boolean;
items?: SelectOption[];
placeholder?: string;
tip?: string;
}>(),
{
disabled: false,
placeholder: '',
tip: '',
items: () => [],
},
);
@@ -39,7 +41,7 @@ const slots = useSlots();
<template>
<div
:class="{
'hover:bg-accent': !slots.tip,
'hover:bg-accent': !(slots.tip || tip),
'pointer-events-none opacity-50': disabled,
}"
class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"
@@ -47,11 +49,17 @@ const slots = useSlots();
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<VbenTooltip v-if="slots.tip || tip" side="bottom">
<template #trigger>
<CircleHelp class="ml-1 size-3 cursor-help" />
</template>
<slot name="tip"></slot>
<slot name="tip">
<template v-if="tip">
<p v-for="(line, index) in tip.split('\n')" :key="index">
{{ line }}
</p>
</template>
</slot>
</VbenTooltip>
</span>
<Select v-model="selectValue">

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { SupportedLanguagesType } from '@vben/locales';
import type { CustomPreferencesRecord } from '@vben/preferences';
import type {
BreadcrumbStyleType,
BuiltinThemeType,
@@ -22,6 +23,7 @@ import {
clearCache,
preferences,
resetPreferences,
updateCustomPreferences,
usePreferences,
} from '@vben/preferences';
@@ -43,6 +45,7 @@ import {
ColorMode,
Content,
Copyright,
Custom,
FontSize,
Footer,
General,
@@ -177,12 +180,15 @@ const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
const widgetRefresh = defineModel<boolean>('widgetRefresh');
const {
customPreferences,
diffCustomPreference,
diffPreference,
isDark,
isFullContent,
isHeaderNav,
isHeaderSidebarNav,
isMixedNav,
preferencesExtension,
isSideMixedNav,
isSideMode,
isSideNav,
@@ -193,8 +199,42 @@ const [Drawer] = useVbenDrawer();
const activeTab = ref('appearance');
const customPreferencesTab = computed(() => {
return preferencesExtension.value;
});
const customTabLabel = computed(() => {
return customPreferencesTab.value?.tabLabel
? $t(customPreferencesTab.value.tabLabel)
: '';
});
const customTabTitle = computed(() => {
const title =
customPreferencesTab.value?.title || customPreferencesTab.value?.tabLabel;
return title ? $t(title) : '';
});
const mergedDiffPreference = computed(() => {
const result: Record<string, unknown> = {};
if (diffPreference.value) {
Object.assign(result, diffPreference.value);
}
if (diffCustomPreference.value) {
result.custom = diffCustomPreference.value;
}
return Object.keys(result).length > 0 ? result : undefined;
});
const showCustomTab = computed(() => {
return (customPreferencesTab.value?.fields.length ?? 0) > 0;
});
const tabs = computed((): SegmentedItem[] => {
return [
const items: SegmentedItem[] = [
{
label: $t('preferences.appearance'),
value: 'appearance',
@@ -212,6 +252,15 @@ const tabs = computed((): SegmentedItem[] => {
value: 'general',
},
];
if (showCustomTab.value) {
items.push({
label: customTabLabel.value,
value: 'custom',
});
}
return items;
});
const showBreadcrumbConfig = computed(() => {
@@ -224,7 +273,7 @@ const showBreadcrumbConfig = computed(() => {
});
async function handleCopy() {
await copy(JSON.stringify(diffPreference.value, null, 2));
await copy(JSON.stringify(mergedDiffPreference.value, null, 2));
message.copyPreferencesSuccess?.(
$t('preferences.copyPreferencesSuccessTitle'),
@@ -239,12 +288,16 @@ async function handleClearCache() {
}
async function handleReset() {
if (!diffPreference.value) {
if (!mergedDiffPreference.value) {
return;
}
resetPreferences();
await loadLocaleMessages(preferences.app.locale);
}
function handleCustomPreferencesUpdate(updates: CustomPreferencesRecord) {
updateCustomPreferences(updates);
}
</script>
<template>
@@ -257,13 +310,13 @@ async function handleReset() {
<template #extra>
<div class="flex items-center">
<VbenIconButton
:disabled="!diffPreference"
:disabled="!mergedDiffPreference"
:tooltip="$t('preferences.resetTip')"
class="relative"
@click="handleReset"
>
<span
v-if="diffPreference"
v-if="mergedDiffPreference"
class="absolute top-0.5 right-0.5 size-2 rounded-sm bg-primary"
></span>
<RotateCw class="size-4" />
@@ -466,13 +519,22 @@ async function handleReset() {
/>
</Block>
</template>
<template #custom>
<Block :title="customTabTitle">
<Custom
:fields="customPreferencesTab?.fields || []"
:values="customPreferences"
@update="handleCustomPreferencesUpdate"
/>
</Block>
</template>
</VbenSegmented>
</div>
<template #footer>
<VbenButton
v-if="appEnableCopyPreferences"
:disabled="!diffPreference"
:disabled="!mergedDiffPreference"
class="mx-4 w-full"
size="sm"
variant="default"
@@ -482,7 +544,7 @@ async function handleReset() {
{{ $t('preferences.copyPreferences') }}
</VbenButton>
<VbenButton
:disabled="!diffPreference"
:disabled="!mergedDiffPreference"
class="mr-4 w-full"
size="sm"
variant="ghost"