diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 68e7e88b3..a62b88065 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -3,15 +3,31 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { Component } from 'vue'; +import type { + UploadChangeParam, + UploadFile, + UploadProps, +} from 'ant-design-vue'; + +import type { Component, Ref } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; +import { + defineAsyncComponent, + defineComponent, + h, + ref, + render, + unref, + watch, +} from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; +import { IconifyIcon } from '@vben/icons'; import { $t } from '@vben/locales'; +import { isEmpty } from '@vben/utils'; import { notification } from 'ant-design-vue'; @@ -22,9 +38,6 @@ const AutoComplete = defineAsyncComponent( () => import('ant-design-vue/es/auto-complete'), ); const Button = defineAsyncComponent(() => import('ant-design-vue/es/button')); -const Cascader = defineAsyncComponent( - () => import('ant-design-vue/es/cascader'), -); const Checkbox = defineAsyncComponent( () => import('ant-design-vue/es/checkbox'), ); @@ -68,7 +81,14 @@ const TimeRangePicker = defineAsyncComponent(() => const TreeSelect = defineAsyncComponent( () => import('ant-design-vue/es/tree-select'), ); +const Cascader = defineAsyncComponent( + () => import('ant-design-vue/es/cascader'), +); const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload')); +const Image = defineAsyncComponent(() => import('ant-design-vue/es/image')); +const PreviewGroup = defineAsyncComponent(() => + import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup), +); const withDefaultPlaceholder = ( component: T, @@ -104,12 +124,223 @@ const withDefaultPlaceholder = ( }); }; +const withPreviewUpload = () => { + return defineComponent({ + name: Upload.name, + emits: ['change', 'update:modelValue'], + setup: ( + props: any, + { attrs, slots, emit }: { attrs: any; emit: any; slots: any }, + ) => { + const previewVisible = ref(false); + + const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`); + + const listType = attrs?.listType || attrs?.['list-type'] || 'text'; + + const fileList = ref( + attrs?.fileList || attrs?.['file-list'] || [], + ); + + const handleChange = async (event: UploadChangeParam) => { + fileList.value = event.fileList; + emit('change', event); + emit( + 'update:modelValue', + event.fileList?.length ? fileList.value : undefined, + ); + }; + + const handlePreview = async (file: UploadFile) => { + previewVisible.value = true; + await previewImage(file, previewVisible, fileList); + }; + + const renderUploadButton = (): any => { + const isDisabled = attrs.disabled; + + // 如果禁用,不渲染上传按钮 + if (isDisabled) { + return null; + } + + // 否则渲染默认上传按钮 + return isEmpty(slots) + ? createDefaultSlotsWithUpload(listType, placeholder) + : slots; + }; + + // 可以监听到表单API设置的值 + watch( + () => attrs.modelValue, + (res) => { + fileList.value = res; + }, + ); + + return () => + h( + Upload, + { + ...props, + ...attrs, + fileList: fileList.value, + onChange: handleChange, + onPreview: handlePreview, + }, + renderUploadButton(), + ); + }, + }); +}; + +const createDefaultSlotsWithUpload = ( + listType: string, + placeholder: string, +) => { + switch (listType) { + case 'picture-card': { + return { + default: () => placeholder, + }; + } + default: { + return { + default: () => + h( + Button, + { + icon: h(IconifyIcon, { + icon: 'ant-design:upload-outlined', + class: 'mb-1 size-4', + }), + }, + () => placeholder, + ), + }; + } + } +}; + +const previewImage = async ( + file: UploadFile, + visible: Ref, + fileList: Ref, +) => { + // 检查是否为图片文件的辅助函数 + const isImageFile = (file: UploadFile): boolean => { + const imageExtensions = new Set([ + 'bmp', + 'gif', + 'jpeg', + 'jpg', + 'png', + 'webp', + ]); + if (file.url) { + const ext = file.url?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + if (!file.type) { + const ext = file.name?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + return file.type.startsWith('image/'); + }; + + // 如果当前文件不是图片,直接打开 + if (!isImageFile(file)) { + if (file.url) { + window.open(file.url, '_blank'); + } else if (file.preview) { + window.open(file.preview, '_blank'); + } else { + console.warn('无法打开文件,没有可用的URL或预览地址'); + } + return; + } + + // 对于图片文件,继续使用预览组 + const [ImageComponent, PreviewGroupComponent] = await Promise.all([ + Image, + PreviewGroup, + ]); + + const getBase64 = (file: File) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.addEventListener('load', () => resolve(reader.result)); + reader.addEventListener('error', (error) => reject(error)); + }); + }; + // 从fileList中过滤出所有图片文件 + const imageFiles = (unref(fileList) || []).filter((element) => + isImageFile(element), + ); + + // 为所有没有预览地址的图片生成预览 + for (const imgFile of imageFiles) { + if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) { + imgFile.preview = (await getBase64(imgFile.originFileObj)) as string; + } + } + const container: HTMLElement | null = document.createElement('div'); + document.body.append(container); + + // 用于追踪组件是否已卸载 + let isUnmounted = false; + + const PreviewWrapper = { + setup() { + return () => { + if (isUnmounted) return null; + return h( + PreviewGroupComponent, + { + class: 'hidden', + preview: { + visible: visible.value, + // 设置初始显示的图片索引 + current: imageFiles.findIndex((f) => f.uid === file.uid), + onVisibleChange: (value: boolean) => { + visible.value = value; + if (!value) { + // 延迟清理,确保动画完成 + setTimeout(() => { + if (!isUnmounted && container) { + isUnmounted = true; + render(null, container); + container.remove(); + } + }, 300); + } + }, + }, + }, + () => + // 渲染所有图片文件 + imageFiles.map((imgFile) => + h(ImageComponent, { + key: imgFile.uid, + src: imgFile.url || imgFile.preview, + }), + ), + ); + }; + }, + }; + + render(h(PreviewWrapper), container); +}; + // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiCascader' | 'ApiSelect' | 'ApiTreeSelect' | 'AutoComplete' + | 'Cascader' | 'Checkbox' | 'CheckboxGroup' | 'DatePicker' @@ -143,21 +374,13 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), - ApiCascader: withDefaultPlaceholder( - { - ...ApiComponent, - name: 'ApiCascader', - }, - 'select', - { - component: Cascader, - fieldNames: { label: 'label', value: 'value', children: 'children' }, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - optionsPropName: 'treeData', - visibleEvent: 'onVisibleChange', - }, - ), + ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', { + component: Cascader, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + visibleEvent: 'onVisibleChange', + }), ApiSelect: withDefaultPlaceholder( { ...ApiComponent, @@ -187,6 +410,7 @@ async function initComponentAdapter() { }, ), AutoComplete, + Cascader, Checkbox, CheckboxGroup, DatePicker, @@ -221,6 +445,7 @@ async function initComponentAdapter() { TimeRangePicker, TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), Upload, + PreviewUpload: withPreviewUpload(), FileUpload, ImageUpload, }; diff --git a/apps/web-antd/src/adapter/vxe-table.ts b/apps/web-antd/src/adapter/vxe-table.ts index d41f39113..15ab9212a 100644 --- a/apps/web-antd/src/adapter/vxe-table.ts +++ b/apps/web-antd/src/adapter/vxe-table.ts @@ -84,9 +84,10 @@ setupVbenVxeTable({ // 表格配置项可以用 cellRender: { name: 'CellImage' }, vxeUI.renderer.add('CellImage', { - renderTableDefault(_renderOpts, params) { + renderTableDefault(renderOpts, params) { + const { props } = renderOpts; const { column, row } = params; - return h(Image, { src: row[column.field] }); + return h(Image, { src: row[column.field], ...props }); }, }); diff --git a/apps/web-ele/src/adapter/vxe-table.ts b/apps/web-ele/src/adapter/vxe-table.ts index c93703bbb..9f46883c2 100644 --- a/apps/web-ele/src/adapter/vxe-table.ts +++ b/apps/web-ele/src/adapter/vxe-table.ts @@ -91,6 +91,7 @@ setupVbenVxeTable({ }, previewTeleported: true, }); + // return h(ElImage, { src, previewSrcList: [src], ...props }); }, }); diff --git a/apps/web-naive/src/adapter/vxe-table.ts b/apps/web-naive/src/adapter/vxe-table.ts index db032db3b..95f69e679 100644 --- a/apps/web-naive/src/adapter/vxe-table.ts +++ b/apps/web-naive/src/adapter/vxe-table.ts @@ -66,9 +66,10 @@ setupVbenVxeTable({ // 表格配置项可以用 cellRender: { name: 'CellImage' }, vxeUI.renderer.add('CellImage', { - renderTableDefault(_renderOpts, params) { + renderTableDefault(renderOpts, params) { + const { props } = renderOpts; const { column, row } = params; - return h(NImage, { src: row[column.field] }); + return h(NImage, { src: row[column.field], ...props }); }, }); diff --git a/apps/web-tdesign/src/adapter/vxe-table.ts b/apps/web-tdesign/src/adapter/vxe-table.ts index d774885cf..3d5ace212 100644 --- a/apps/web-tdesign/src/adapter/vxe-table.ts +++ b/apps/web-tdesign/src/adapter/vxe-table.ts @@ -72,9 +72,10 @@ setupVbenVxeTable({ // 表格配置项可以用 cellRender: { name: 'CellImage' }, vxeUI.renderer.add('CellImage', { - renderTableDefault(_renderOpts, params) { + renderTableDefault(renderOpts, params) { + const { props } = renderOpts; const { column, row } = params; - return h(Image, { src: row[column.field] }); + return h(Image, { src: row[column.field], ...props }); }, }); diff --git a/apps/web-tdesign/src/bootstrap.ts b/apps/web-tdesign/src/bootstrap.ts index 732030e1a..5d07717c6 100644 --- a/apps/web-tdesign/src/bootstrap.ts +++ b/apps/web-tdesign/src/bootstrap.ts @@ -5,6 +5,8 @@ import { registerLoadingDirective } from '@vben/common-ui/es/loading'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; import '@vben/styles'; +// import '@vben/styles/antd'; +// 引入组件库的少量全局样式变量 import { useTitle } from '@vueuse/core'; @@ -15,8 +17,6 @@ import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; -// import '@vben/styles/antd'; -// 引入组件库的少量全局样式变量 import 'tdesign-vue-next/es/style/index.css'; async function bootstrap(namespace: string) { diff --git a/docs/src/_env/adapter/vxe-table.ts b/docs/src/_env/adapter/vxe-table.ts index bab7f3d38..7bc27056e 100644 --- a/docs/src/_env/adapter/vxe-table.ts +++ b/docs/src/_env/adapter/vxe-table.ts @@ -40,9 +40,10 @@ if (!import.meta.env.SSR) { // 表格配置项可以用 cellRender: { name: 'CellImage' }, vxeUI.renderer.add('CellImage', { - renderTableDefault(_renderOpts, params) { + renderTableDefault(renderOpts, params) { + const { props } = renderOpts; const { column, row } = params; - return h(Image, { src: row[column.field] }); + return h(Image, { src: row[column.field], ...props }); }, }); diff --git a/docs/src/guide/introduction/thin.md b/docs/src/guide/introduction/thin.md index 157d51ffc..8a37c2578 100644 --- a/docs/src/guide/introduction/thin.md +++ b/docs/src/guide/introduction/thin.md @@ -24,7 +24,7 @@ apps/web-naive ## 演示代码精简 -如果你不需要演示代码,你可以直接删除的`playground`文件夹。 +如果你不需要演示代码,你可以直接删除 `playground` 文件夹。 ## 文档精简 @@ -88,7 +88,7 @@ pnpm install - 在应用的 `src/router/routes` 文件中,你可以删除不需要的路由。其中 `core` 文件夹内,如果只需要登录和忘记密码,你可以删除其他路由,如忘记密码、注册等。路由删除后,你可以删除对应的页面文件,在 `src/views/_core` 文件夹中。 -- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以删除对应的页面文件,在 `src/views` 文件夹中。 +- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以在 `src/views` 文件夹中删除对应的页面文件。 ### 删除不需要的组件 diff --git a/packages/@core/base/design/src/css/global.css b/packages/@core/base/design/src/css/global.css index d19990985..dc154fc72 100644 --- a/packages/@core/base/design/src/css/global.css +++ b/packages/@core/base/design/src/css/global.css @@ -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%; diff --git a/packages/@core/base/design/src/design-tokens/default.css b/packages/@core/base/design/src/design-tokens/default.css index 64679f854..4ce7ff954 100644 --- a/packages/@core/base/design/src/design-tokens/default.css +++ b/packages/@core/base/design/src/design-tokens/default.css @@ -93,6 +93,7 @@ /* 基本文字大小 */ --font-size-base: 16px; + --menu-font-size: calc(var(--font-size-base) * 0.875); /* =============component & UI============= */ diff --git a/packages/@core/base/shared/src/utils/tree.ts b/packages/@core/base/shared/src/utils/tree.ts index 468d204f7..31026f18a 100644 --- a/packages/@core/base/shared/src/utils/tree.ts +++ b/packages/@core/base/shared/src/utils/tree.ts @@ -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>( + 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, +}; diff --git a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap index a6452b399..a3c0c3175 100644 --- a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap @@ -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, diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts index 5b8d72363..d9977b1ef 100644 --- a/packages/@core/preferences/src/config.ts +++ b/packages/@core/preferences/src/config.ts @@ -116,6 +116,7 @@ const defaultPreferences: Preferences = { colorWarning: 'hsl(42 84% 61%)', mode: 'dark', radius: '0.5', + fontSize: 16, semiDarkHeader: false, semiDarkSidebar: false, }, diff --git a/packages/@core/preferences/src/preferences.ts b/packages/@core/preferences/src/preferences.ts index 9ac8fb054..7fb245e31 100644 --- a/packages/@core/preferences/src/preferences.ts +++ b/packages/@core/preferences/src/preferences.ts @@ -141,7 +141,10 @@ class PreferenceManager { private handleUpdates(updates: DeepPartial) { 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); } diff --git a/packages/@core/preferences/src/types.ts b/packages/@core/preferences/src/types.ts index 9a4d94bdf..17224b048 100644 --- a/packages/@core/preferences/src/types.ts +++ b/packages/@core/preferences/src/types.ts @@ -239,6 +239,8 @@ interface ThemePreferences { colorSuccess: string; /** 警告色 */ colorWarning: string; + /** 字体大小(单位:px) */ + fontSize: number; /** 当前主题 */ mode: ThemeModeType; /** 圆角 */ diff --git a/packages/@core/preferences/src/update-css-variables.ts b/packages/@core/preferences/src/update-css-variables.ts index 0d3466a01..6ee5f748c 100644 --- a/packages/@core/preferences/src/update-css-variables.ts +++ b/packages/@core/preferences/src/update-css-variables.ts @@ -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)`, + ); + } } /** diff --git a/packages/@core/ui-kit/menu-ui/src/components/menu.vue b/packages/@core/ui-kit/menu-ui/src/components/menu.vue index 887604567..237aff00a 100644 --- a/packages/@core/ui-kit/menu-ui/src/components/menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/components/menu.vue @@ -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%; diff --git a/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue b/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue index e0947b00e..86e86c914 100644 --- a/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue @@ -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; } diff --git a/packages/effects/common-ui/src/components/api-component/api-component.vue b/packages/effects/common-ui/src/components/api-component/api-component.vue index 70e86e0ea..38419a7d7 100644 --- a/packages/effects/common-ui/src/components/api-component/api-component.vue +++ b/packages/effects/common-ui/src/components/api-component/api-component.vue @@ -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(), { 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]) } : {}), diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/index.ts b/packages/effects/layouts/src/widgets/preferences/blocks/index.ts index 59595dc48..cdd6bb1db 100644 --- a/packages/effects/layouts/src/widgets/preferences/blocks/index.ts +++ b/packages/effects/layouts/src/widgets/preferences/blocks/index.ts @@ -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'; diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/theme/font-size.vue b/packages/effects/layouts/src/widgets/preferences/blocks/theme/font-size.vue new file mode 100644 index 000000000..b1aaa2d81 --- /dev/null +++ b/packages/effects/layouts/src/widgets/preferences/blocks/theme/font-size.vue @@ -0,0 +1,62 @@ + + + diff --git a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue index 301004d6e..c88a9bc45 100644 --- a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue +++ b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue @@ -43,6 +43,7 @@ import { ColorMode, Content, Copyright, + FontSize, Footer, General, GlobalShortcutKeys, @@ -85,6 +86,7 @@ const themeColorPrimary = defineModel('themeColorPrimary'); const themeBuiltinType = defineModel('themeBuiltinType'); const themeMode = defineModel('themeMode'); const themeRadius = defineModel('themeRadius'); +const themeFontSize = defineModel('themeFontSize'); const themeSemiDarkSidebar = defineModel('themeSemiDarkSidebar'); const themeSemiDarkHeader = defineModel('themeSemiDarkHeader'); @@ -328,6 +330,9 @@ async function handleReset() { + + + + 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 ? `${svgContent}` : 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, diff --git a/packages/locales/src/langs/en-US/preferences.json b/packages/locales/src/langs/en-US/preferences.json index b0bbe3741..977632acf 100644 --- a/packages/locales/src/langs/en-US/preferences.json +++ b/packages/locales/src/langs/en-US/preferences.json @@ -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", diff --git a/packages/locales/src/langs/zh-CN/preferences.json b/packages/locales/src/langs/zh-CN/preferences.json index de8413df9..f8e390f2b 100644 --- a/packages/locales/src/langs/zh-CN/preferences.json +++ b/packages/locales/src/langs/zh-CN/preferences.json @@ -120,6 +120,8 @@ "theme": { "title": "主题", "radius": "圆角", + "fontSize": "字体大小", + "fontSizeTip": "调整全局字体大小,实时预览效果", "light": "浅色", "dark": "深色", "darkSidebar": "深色侧边栏", diff --git a/packages/utils/src/helpers/generate-menus.ts b/packages/utils/src/helpers/generate-menus.ts index 6a1308ebb..5f41f0063 100644 --- a/packages/utils/src/helpers/generate-menus.ts +++ b/packages/utils/src/helpers/generate-menus.ts @@ -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);