diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 7fdb1434a..80d990b76 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -17,6 +17,7 @@ import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; import { + computed, defineAsyncComponent, defineComponent, h, @@ -383,12 +384,17 @@ const withPreviewUpload = () => { attrs?.fileList || attrs?.['file-list'] || [], ); + const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']); + const aspectRatio = computed( + () => attrs?.aspectRatio ?? attrs?.['aspect-ratio'], + ); + const handleBeforeUpload = async ( file: UploadFile, originFileList: Array, ) => { - if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) { - message.error($t('ui.formRules.sizeLimit', [attrs.maxSize])); + if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) { + message.error($t('ui.formRules.sizeLimit', [maxSize.value])); file.status = 'removed'; return false; } @@ -401,7 +407,7 @@ const withPreviewUpload = () => { ) { file.status = 'removed'; // antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取 - const blob = await cropImage(originFileList[0], attrs.aspectRatio); + const blob = await cropImage(originFileList[0], aspectRatio.value); return new Promise((resolve, reject) => { if (!blob) { return reject(new Error($t('ui.crop.errorTip'))); diff --git a/packages/@core/base/shared/src/utils/dom.ts b/packages/@core/base/shared/src/utils/dom.ts index 69617176b..35a7e5ffd 100644 --- a/packages/@core/base/shared/src/utils/dom.ts +++ b/packages/@core/base/shared/src/utils/dom.ts @@ -41,6 +41,18 @@ export function getElementVisibleRect( const left = Math.max(rect.left, 0); const right = Math.min(rect.right, viewWidth); + // 如果元素完全不可见,则返回一个空的矩形 + if (top >= viewHeight || bottom <= 0 || left >= viewWidth || right <= 0) { + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + }; + } + return { bottom, height: Math.max(0, bottom - top), diff --git a/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts b/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts index 0af484767..5385b2157 100644 --- a/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts +++ b/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts @@ -41,6 +41,7 @@ export function useVbenModal( // 不能用 Object.assign,会丢失 api 的原型函数 Object.setPrototypeOf(extendedApi, api); }, + consumed: false, options, async reCreateModal() { isModalReady.value = false; @@ -73,7 +74,13 @@ export function useVbenModal( return [Modal, extendedApi as ExtendedModalApi] as const; } - const injectData = inject(USER_MODAL_INJECT_KEY, {}); + let injectData = inject(USER_MODAL_INJECT_KEY, {}); + // 这个数据已经被使用了,说明这个弹窗是嵌套的弹窗,不应该merge上层的配置 + if (injectData.consumed) { + injectData = {}; + } else { + injectData.consumed = true; + } const mergedOptions = { ...DEFAULT_MODAL_PROPS, diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue index 268da9322..1544f34de 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue @@ -32,19 +32,19 @@ const props = withDefaults(defineProps(), { // const startTime = ref(0); const showSpinner = ref(false); const renderSpinner = ref(false); -const timer = ref>(); +let timer: ReturnType | undefined; watch( () => props.spinning, (show) => { if (!show) { showSpinner.value = false; - clearTimeout(timer.value); + timer && clearTimeout(timer); return; } // startTime.value = performance.now(); - timer.value = setTimeout(() => { + timer = setTimeout(() => { // const loadingTime = performance.now() - startTime.value; showSpinner.value = true; diff --git a/packages/effects/plugins/src/echarts/use-echarts.ts b/packages/effects/plugins/src/echarts/use-echarts.ts index 1a28fb125..92aea5f3b 100644 --- a/packages/effects/plugins/src/echarts/use-echarts.ts +++ b/packages/effects/plugins/src/echarts/use-echarts.ts @@ -92,7 +92,8 @@ function useEcharts(chartRef: Ref) { return; } useTimeoutFn(() => { - if (!chartInstance) { + if (!chartInstance || chartInstance?.getDom() !== el) { + chartInstance?.dispose(); const instance = initCharts(); if (!instance) return; } @@ -104,6 +105,36 @@ function useEcharts(chartRef: Ref) { }); }; + const updateDate = ( + option: EChartsOption, + notMerge = false, // false = 合并(保留动画),true = 完全替换 + lazyUpdate = false, // true 时不立即重绘,适合短时间内多次调用 + ): Promise => { + return new Promise((resolve) => { + nextTick(() => { + if (!chartInstance) { + // 还没初始化 → 当作首次渲染 + renderEcharts(option).then(resolve); + return; + } + + // 合并你原有的全局配置(比如 backgroundColor) + const finalOption = { + ...option, + ...getOptions.value, + }; + + chartInstance.setOption(finalOption, { + notMerge, + lazyUpdate, + // silent: true, // 如果追求极致性能可开启(关闭所有事件) + }); + + resolve(chartInstance); + }); + }); + }; + function resize() { const el = getChartEl(); if (isElHidden(el)) { @@ -139,6 +170,7 @@ function useEcharts(chartRef: Ref) { return { renderEcharts, resize, + updateDate, getChartInstance: () => chartInstance, }; } diff --git a/playground/src/adapter/component/index.ts b/playground/src/adapter/component/index.ts index 7fdb1434a..80d990b76 100644 --- a/playground/src/adapter/component/index.ts +++ b/playground/src/adapter/component/index.ts @@ -17,6 +17,7 @@ import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; import { + computed, defineAsyncComponent, defineComponent, h, @@ -383,12 +384,17 @@ const withPreviewUpload = () => { attrs?.fileList || attrs?.['file-list'] || [], ); + const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']); + const aspectRatio = computed( + () => attrs?.aspectRatio ?? attrs?.['aspect-ratio'], + ); + const handleBeforeUpload = async ( file: UploadFile, originFileList: Array, ) => { - if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) { - message.error($t('ui.formRules.sizeLimit', [attrs.maxSize])); + if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) { + message.error($t('ui.formRules.sizeLimit', [maxSize.value])); file.status = 'removed'; return false; } @@ -401,7 +407,7 @@ const withPreviewUpload = () => { ) { file.status = 'removed'; // antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取 - const blob = await cropImage(originFileList[0], attrs.aspectRatio); + const blob = await cropImage(originFileList[0], aspectRatio.value); return new Promise((resolve, reject) => { if (!blob) { return reject(new Error($t('ui.crop.errorTip'))); diff --git a/playground/src/store/auth.ts b/playground/src/store/auth.ts index 4adeb76e1..b50ee7729 100644 --- a/playground/src/store/auth.ts +++ b/playground/src/store/auth.ts @@ -78,15 +78,22 @@ export const useAuthStore = defineStore('auth', () => { }; } + const isLoggingOut = ref(false); // 正在 logout 标识, 防止 /logout 死循环. + async function logout(redirect: boolean = true) { + if (isLoggingOut.value) return; // 正在登出中, 说明已进入循环, 直接返回. + isLoggingOut.value = true; // 设置 标识 + try { await logoutApi(); } catch { // 不做任何处理 - } + } finally { + isLoggingOut.value = false; // 重置 标识 - resetAllStores(); - accessStore.setLoginExpired(false); + resetAllStores(); + accessStore.setLoginExpired(false); + } // 回登录页带上当前路由地址 await router.replace({