This commit is contained in:
lzh
2025-12-31 10:50:50 +08:00
parent d45226b90b
commit 647dd1ac7e
468 changed files with 33538 additions and 14843 deletions

View File

@@ -14,8 +14,9 @@
}
html {
@apply text-foreground 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;
@@ -171,7 +172,6 @@
border-radius: 2rem !important;
box-shadow: var(--glass-shadow) !important;
backdrop-filter: blur(24px) !important;
backdrop-filter: blur(24px) !important;
transition: all 0.3s ease-out !important;
}
@@ -239,7 +239,6 @@
border: 1px solid rgb(var(--glass-border)) !important;
border-radius: 1.5rem !important;
backdrop-filter: blur(24px) !important;
backdrop-filter: blur(24px) !important;
}
/* 表头样式 */
@@ -247,7 +246,6 @@
div.vxe-table--header-wrapper {
background: rgb(255 255 255 / 30%) !important;
backdrop-filter: blur(16px) !important;
backdrop-filter: blur(16px) !important;
}
.vxe-header--column,
@@ -306,7 +304,6 @@
.glass-card {
background: rgb(var(--glass-surface));
backdrop-filter: blur(24px);
backdrop-filter: blur(24px);
}
/* 玻璃边框 */

View File

@@ -110,6 +110,7 @@
/* 基本文字大小 */
--font-size-base: 16px;
--menu-font-size: calc(var(--font-size-base) * 0.875);
/* =============component & UI============= */

View File

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

View File

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

View File

@@ -116,6 +116,7 @@ const defaultPreferences: Preferences = {
colorWarning: 'hsl(42 84% 61%)',
mode: 'dark',
radius: '0.5',
fontSize: 16,
semiDarkHeader: false,
semiDarkSidebar: false,
},

View File

@@ -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);
}

View File

@@ -239,6 +239,8 @@ interface ThemePreferences {
colorSuccess: string;
/** 警告色 */
colorWarning: string;
/** 字体大小单位px */
fontSize: number;
/** 当前主题 */
mode: ThemeModeType;
/** 圆角 */

View File

@@ -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)`,
);
}
}
/**

View File

@@ -389,7 +389,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;
@@ -434,6 +434,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;
}
@@ -445,7 +446,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;
@@ -459,7 +460,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));
@@ -753,7 +753,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;
}
@@ -761,7 +761,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;
@@ -787,7 +787,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);
}
}
@@ -814,9 +814,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%;

View File

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

View File

@@ -1 +1,43 @@
// TODO @haohao枚举可以放到这里
// ========== IOT - 设备模块 ==========
/**
* 设备状态枚举
*/
export const DeviceStateEnum = {
INACTIVE: 0, // 未激活
OFFLINE: 2, // 离线
ONLINE: 1, // 在线
} as const;
// ========== IOT - 产品模块 ==========
/**
* 产品设备类型枚举
*/
export const DeviceTypeEnum = {
DEVICE: 0, // 直连设备
GATEWAY: 2, // 网关设备
GATEWAY_SUB: 1, // 网关子设备
} as const;
/**
* 产品状态枚举
*/
export const ProductStatusEnum = {
UNPUBLISHED: 0, // 开发中
PUBLISHED: 1, // 已发布
} as const;
/**
* 产品定位类型枚举
*/
export const LocationTypeEnum = {
IP: 1, // IP 定位
MANUAL: 3, // 手动定位
MODULE: 2, // 设备定位
} as const;
/**
* 数据格式(编解码器类型)枚举
*/
export const CodecTypeEnum = {
ALINK: 'Alink', // 阿里云 Alink 协议
} as const;

View File

@@ -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]) }
: {}),

View File

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

View File

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

View File

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

View File

@@ -24,8 +24,8 @@ import {
// VxeOptgroup,
// VxeOption,
// VxePulldown,
// VxeRadio,
// VxeRadioButton,
VxeRadio,
VxeRadioButton,
VxeRadioGroup,
VxeSelect,
VxeTooltip,
@@ -88,8 +88,8 @@ export function initVxeTable() {
// VxeUI.component(VxeOption);
VxeUI.component(VxePager);
// VxeUI.component(VxePulldown);
// VxeUI.component(VxeRadio);
// VxeUI.component(VxeRadioButton);
VxeUI.component(VxeRadio);
VxeUI.component(VxeRadioButton);
VxeUI.component(VxeRadioGroup);
VxeUI.component(VxeSelect);
// VxeUI.component(VxeSwitch);

View File

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

View File

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

View File

@@ -36,7 +36,9 @@
"downloadTemplateFail": "Download template failed",
"updating": "Updating {0}...",
"updateSuccess": "Update {0} successfully",
"updateFailed": "Update {0} failed"
"updateFailed": "Update {0} failed",
"closing": "Closing {0} ...",
"closeSuccess": "{0} closed successfully"
},
"placeholder": {
"input": "Please enter",

View File

@@ -120,6 +120,8 @@
"theme": {
"title": "主题",
"radius": "圆角",
"fontSize": "字体大小",
"fontSizeTip": "调整全局字体大小,实时预览效果",
"light": "浅色",
"dark": "深色",
"darkSidebar": "深色侧边栏",

View File

@@ -36,7 +36,9 @@
"downloadTemplateFail": "下载模板失败",
"updating": "正在更新 {0}...",
"updateSuccess": "更新 {0} 成功",
"updateFailed": "更新 {0} 失败"
"updateFailed": "更新 {0} 失败",
"closing": "正在关闭 {0} ...",
"closeSuccess": "{0} 关闭成功"
},
"placeholder": {
"input": "请输入",

View File

@@ -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);
@@ -111,7 +116,7 @@ function convertServerMenuToRouteRecordStringComponent(
hideInMenu: !menu.visible,
icon: menu.icon,
link: menu.path,
orderNo: menu.sort,
order: menu.sort,
title: menu.name,
},
name: menu.name,
@@ -155,7 +160,7 @@ function convertServerMenuToRouteRecordStringComponent(
hideInMenu: !menu.visible,
icon: menu.icon,
keepAlive: menu.keepAlive,
orderNo: menu.sort,
order: menu.sort,
title: menu.name,
},
name: finalName,