Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -14,8 +14,9 @@
|
||||
}
|
||||
|
||||
html {
|
||||
@apply text-foreground bg-background font-sans text-[100%];
|
||||
@apply text-foreground bg-background font-sans;
|
||||
|
||||
font-size: var(--font-size-base, 16px);
|
||||
font-variation-settings: normal;
|
||||
line-height: 1.15;
|
||||
text-size-adjust: 100%;
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
|
||||
/* 基本文字大小 */
|
||||
--font-size-base: 16px;
|
||||
--menu-font-size: calc(var(--font-size-base) * 0.875);
|
||||
|
||||
/* =============component & UI============= */
|
||||
|
||||
|
||||
@@ -208,4 +208,39 @@ function treeToString(tree: any[], nodeId: number | string) {
|
||||
return str;
|
||||
}
|
||||
|
||||
export { filterTree, handleTree, mapTree, traverseTreeValues, treeToString };
|
||||
/**
|
||||
* 对树形结构数据进行递归排序
|
||||
* @param treeData - 树形数据数组
|
||||
* @param sortFunction - 排序函数,用于定义排序规则
|
||||
* @param options - 配置选项,包括子节点属性名
|
||||
* @returns 排序后的树形数据
|
||||
*/
|
||||
function sortTree<T extends Record<string, any>>(
|
||||
treeData: T[],
|
||||
sortFunction: (a: T, b: T) => number,
|
||||
options?: TreeConfigOptions,
|
||||
): T[] {
|
||||
const { childProps } = options || {
|
||||
childProps: 'children',
|
||||
};
|
||||
|
||||
return treeData.toSorted(sortFunction).map((item) => {
|
||||
const children = item[childProps];
|
||||
if (children && Array.isArray(children) && children.length > 0) {
|
||||
return {
|
||||
...item,
|
||||
[childProps]: sortTree(children, sortFunction, options),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
filterTree,
|
||||
handleTree,
|
||||
mapTree,
|
||||
sortTree,
|
||||
traverseTreeValues,
|
||||
treeToString,
|
||||
};
|
||||
|
||||
@@ -113,6 +113,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"colorPrimary": "hsl(212 100% 45%)",
|
||||
"colorSuccess": "hsl(144 57% 58%)",
|
||||
"colorWarning": "hsl(42 84% 61%)",
|
||||
"fontSize": 16,
|
||||
"mode": "dark",
|
||||
"radius": "0.5",
|
||||
"semiDarkHeader": false,
|
||||
|
||||
@@ -116,6 +116,7 @@ const defaultPreferences: Preferences = {
|
||||
colorWarning: 'hsl(42 84% 61%)',
|
||||
mode: 'dark',
|
||||
radius: '0.5',
|
||||
fontSize: 16,
|
||||
semiDarkHeader: false,
|
||||
semiDarkSidebar: false,
|
||||
},
|
||||
|
||||
@@ -141,7 +141,10 @@ class PreferenceManager {
|
||||
private handleUpdates(updates: DeepPartial<Preferences>) {
|
||||
const themeUpdates = updates.theme || {};
|
||||
const appUpdates = updates.app || {};
|
||||
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
|
||||
if (
|
||||
(themeUpdates && Object.keys(themeUpdates).length > 0) ||
|
||||
Reflect.has(themeUpdates, 'fontSize')
|
||||
) {
|
||||
updateCSSVariables(this.state);
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +239,8 @@ interface ThemePreferences {
|
||||
colorSuccess: string;
|
||||
/** 警告色 */
|
||||
colorWarning: string;
|
||||
/** 字体大小(单位:px) */
|
||||
fontSize: number;
|
||||
/** 当前主题 */
|
||||
mode: ThemeModeType;
|
||||
/** 圆角 */
|
||||
|
||||
@@ -66,6 +66,19 @@ function updateCSSVariables(preferences: Preferences) {
|
||||
if (Reflect.has(theme, 'radius')) {
|
||||
document.documentElement.style.setProperty('--radius', `${radius}rem`);
|
||||
}
|
||||
|
||||
// 更新字体大小
|
||||
if (Reflect.has(theme, 'fontSize')) {
|
||||
const fontSize = theme.fontSize;
|
||||
document.documentElement.style.setProperty(
|
||||
'--font-size-base',
|
||||
`${fontSize}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--menu-font-size',
|
||||
`calc(${fontSize}px * 0.875)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -388,7 +388,7 @@ $namespace: vben;
|
||||
padding: var(--menu-item-padding-y) var(--menu-item-padding-x);
|
||||
margin: 0 var(--menu-item-margin-x) var(--menu-item-margin-y)
|
||||
var(--menu-item-margin-x);
|
||||
font-size: var(--menu-font-size);
|
||||
font-size: var(--menu-font-size) !important;
|
||||
color: var(--menu-item-color);
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
@@ -433,6 +433,7 @@ $namespace: vben;
|
||||
max-width: var(--menu-title-width);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--menu-font-size) !important;
|
||||
white-space: nowrap;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -444,7 +445,7 @@ $namespace: vben;
|
||||
.#{$namespace}-menu__popup-container,
|
||||
.#{$namespace}-menu {
|
||||
--menu-title-width: 140px;
|
||||
--menu-item-icon-size: 16px;
|
||||
--menu-item-icon-size: var(--font-size-base, 16px);
|
||||
--menu-item-height: 38px;
|
||||
--menu-item-padding-y: 21px;
|
||||
--menu-item-padding-x: 12px;
|
||||
@@ -458,7 +459,6 @@ $namespace: vben;
|
||||
--menu-item-collapse-margin-x: 0px;
|
||||
--menu-item-radius: 0px;
|
||||
--menu-item-indent: 16px;
|
||||
--menu-font-size: 14px;
|
||||
|
||||
&.is-dark {
|
||||
--menu-background-color: hsl(var(--menu));
|
||||
@@ -752,7 +752,7 @@ $namespace: vben;
|
||||
}
|
||||
.#{$namespace}-menu__icon {
|
||||
display: block;
|
||||
font-size: 20px !important;
|
||||
font-size: calc(var(--font-size-base, 16px) * 1.25) !important;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
@@ -760,7 +760,7 @@ $namespace: vben;
|
||||
display: inline-flex;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
font-size: calc(var(--font-size-base, 16px) * 0.75);
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
transition: all 0.25s ease;
|
||||
@@ -785,7 +785,7 @@ $namespace: vben;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 var(--menu-item-padding-x);
|
||||
font-size: var(--menu-font-size);
|
||||
font-size: var(--menu-font-size) !important;
|
||||
line-height: var(--menu-item-height);
|
||||
}
|
||||
}
|
||||
@@ -812,9 +812,14 @@ $namespace: vben;
|
||||
|
||||
.#{$namespace}-sub-menu-content {
|
||||
height: var(--menu-item-height);
|
||||
font-size: var(--menu-font-size) !important;
|
||||
|
||||
@include menu-item;
|
||||
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
&__icon-arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -102,7 +102,7 @@ $namespace: vben;
|
||||
}
|
||||
|
||||
.#{$namespace}-normal-menu__icon {
|
||||
font-size: 20px;
|
||||
font-size: calc(var(--font-size-base, 16px) * 1.25);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,14 +146,14 @@ $namespace: vben;
|
||||
|
||||
&__icon {
|
||||
max-height: 20px;
|
||||
font-size: 20px;
|
||||
font-size: calc(var(--font-size-base, 16px) * 1.25);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
font-size: calc(var(--font-size-base, 16px) * 0.75);
|
||||
font-weight: 400;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ interface Props {
|
||||
childrenField?: string;
|
||||
/** value字段名 */
|
||||
valueField?: string;
|
||||
/** disabled字段名 */
|
||||
disabledField?: string;
|
||||
/** 组件接收options数据的属性名 */
|
||||
optionsPropName?: string;
|
||||
/** 是否立即调用api */
|
||||
@@ -75,6 +77,7 @@ defineOptions({ name: 'ApiComponent', inheritAttrs: false });
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
labelField: 'label',
|
||||
valueField: 'value',
|
||||
disabledField: 'disabled',
|
||||
childrenField: '',
|
||||
optionsPropName: 'options',
|
||||
resultField: '',
|
||||
@@ -108,17 +111,25 @@ const isFirstLoaded = ref(false);
|
||||
const hasPendingRequest = ref(false);
|
||||
|
||||
const getOptions = computed(() => {
|
||||
const { labelField, valueField, childrenField, numberToString } = props;
|
||||
const {
|
||||
labelField,
|
||||
valueField,
|
||||
disabledField,
|
||||
childrenField,
|
||||
numberToString,
|
||||
} = props;
|
||||
|
||||
const refOptionsData = unref(refOptions);
|
||||
|
||||
function transformData(data: OptionsItem[]): OptionsItem[] {
|
||||
return data.map((item) => {
|
||||
const value = get(item, valueField);
|
||||
const disabled = get(item, disabledField);
|
||||
return {
|
||||
...objectOmit(item, [labelField, valueField, childrenField]),
|
||||
...objectOmit(item, [labelField, valueField, disabled, childrenField]),
|
||||
label: get(item, labelField),
|
||||
value: numberToString ? `${value}` : value,
|
||||
disabled: get(item, disabledField),
|
||||
...(childrenField && item[childrenField]
|
||||
? { children: transformData(item[childrenField]) }
|
||||
: {}),
|
||||
|
||||
@@ -15,5 +15,6 @@ export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';
|
||||
export { default as SwitchItem } from './switch-item.vue';
|
||||
export { default as BuiltinTheme } from './theme/builtin.vue';
|
||||
export { default as ColorMode } from './theme/color-mode.vue';
|
||||
export { default as FontSize } from './theme/font-size.vue';
|
||||
export { default as Radius } from './theme/radius.vue';
|
||||
export { default as Theme } from './theme/theme.vue';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldContent,
|
||||
NumberFieldDecrement,
|
||||
NumberFieldIncrement,
|
||||
NumberFieldInput,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceFontSize',
|
||||
});
|
||||
|
||||
const modelValue = defineModel<number>({
|
||||
default: 16,
|
||||
});
|
||||
|
||||
const min = 15;
|
||||
const max = 22;
|
||||
const step = 1;
|
||||
|
||||
// 限制输入值在 min 和 max 之间
|
||||
watch(
|
||||
modelValue,
|
||||
(newValue) => {
|
||||
if (newValue < min) {
|
||||
modelValue.value = min;
|
||||
} else if (newValue > max) {
|
||||
modelValue.value = max;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<NumberField
|
||||
v-model="modelValue"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
class="w-full"
|
||||
>
|
||||
<NumberFieldContent>
|
||||
<NumberFieldDecrement />
|
||||
<NumberFieldInput />
|
||||
<NumberFieldIncrement />
|
||||
</NumberFieldContent>
|
||||
</NumberField>
|
||||
<span class="text-muted-foreground whitespace-nowrap text-xs">px</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ $t('preferences.theme.fontSizeTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
ColorMode,
|
||||
Content,
|
||||
Copyright,
|
||||
FontSize,
|
||||
Footer,
|
||||
General,
|
||||
GlobalShortcutKeys,
|
||||
@@ -85,6 +86,7 @@ const themeColorPrimary = defineModel<string>('themeColorPrimary');
|
||||
const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
|
||||
const themeMode = defineModel<ThemeModeType>('themeMode');
|
||||
const themeRadius = defineModel<string>('themeRadius');
|
||||
const themeFontSize = defineModel<number>('themeFontSize');
|
||||
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
|
||||
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
|
||||
|
||||
@@ -328,6 +330,9 @@ async function handleReset() {
|
||||
<Block :title="$t('preferences.theme.radius')">
|
||||
<Radius v-model="themeRadius" />
|
||||
</Block>
|
||||
<Block :title="$t('preferences.theme.fontSize')">
|
||||
<FontSize v-model="themeFontSize" />
|
||||
</Block>
|
||||
<Block :title="$t('preferences.other')">
|
||||
<ColorMode
|
||||
v-model:app-color-gray-mode="appColorGrayMode"
|
||||
|
||||
@@ -13,10 +13,28 @@ function parseSvg(svgData: string): IconifyIconStructure {
|
||||
const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml');
|
||||
const svgElement = xmlDoc.documentElement;
|
||||
|
||||
// 提取 SVG 根元素的关键样式属性
|
||||
const getAttrs = (el: Element, attrs: string[]) =>
|
||||
attrs
|
||||
.map((attr) =>
|
||||
el.hasAttribute(attr) ? `${attr}="${el.getAttribute(attr)}"` : '',
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const rootAttrs = getAttrs(svgElement, [
|
||||
'fill',
|
||||
'stroke',
|
||||
'fill-rule',
|
||||
'stroke-width',
|
||||
]);
|
||||
|
||||
const svgContent = [...svgElement.childNodes]
|
||||
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
|
||||
.map((node) => new XMLSerializer().serializeToString(node))
|
||||
.join('');
|
||||
// 若根有属性,用一个 g 标签包裹内容并继承属性
|
||||
const body = rootAttrs ? `<g ${rootAttrs}>${svgContent}</g>` : svgContent;
|
||||
|
||||
const viewBoxValue = svgElement.getAttribute('viewBox') || '';
|
||||
const [left, top, width, height] = viewBoxValue.split(' ').map((val) => {
|
||||
@@ -25,7 +43,7 @@ function parseSvg(svgData: string): IconifyIconStructure {
|
||||
});
|
||||
|
||||
return {
|
||||
body: svgContent,
|
||||
body,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"radius": "Radius",
|
||||
"fontSize": "Font Size",
|
||||
"fontSizeTip": "Adjust global font size with real-time preview",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"darkSidebar": "Semi Dark Sidebar",
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"theme": {
|
||||
"title": "主题",
|
||||
"radius": "圆角",
|
||||
"fontSize": "字体大小",
|
||||
"fontSizeTip": "调整全局字体大小,实时预览效果",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"darkSidebar": "深色侧边栏",
|
||||
|
||||
@@ -8,7 +8,12 @@ import type {
|
||||
RouteRecordStringComponent,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
import { filterTree, isHttpUrl, mapTree } from '@vben-core/shared/utils';
|
||||
import {
|
||||
filterTree,
|
||||
isHttpUrl,
|
||||
mapTree,
|
||||
sortTree,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 根据 routes 生成菜单列表
|
||||
@@ -83,7 +88,7 @@ function generateMenus(
|
||||
});
|
||||
|
||||
// 对菜单进行排序,避免order=0时被替换成999的问题
|
||||
menus = menus.toSorted((a, b) => (a?.order ?? 999) - (b?.order ?? 999));
|
||||
menus = sortTree(menus, (a, b) => (a?.order ?? 999) - (b?.order ?? 999));
|
||||
|
||||
// 过滤掉隐藏的菜单项
|
||||
return filterTree(menus, (menu) => !!menu.show);
|
||||
|
||||
Reference in New Issue
Block a user