refactor: 升级框架

This commit is contained in:
caiyuchao
2025-07-09 11:28:52 +08:00
parent bbe6d7e76e
commit 258c0e2934
310 changed files with 11060 additions and 8152 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -96,6 +96,15 @@ async function generateRoutes(
);
break;
}
case 'mixed': {
const [frontend_resultRoutes, backend_resultRoutes] = await Promise.all([
generateRoutesByFrontend(routes, roles || [], forbiddenComponent),
generateRoutesByBackend(options),
]);
resultRoutes = [...frontend_resultRoutes, ...backend_resultRoutes];
break;
}
}
/**

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -3,11 +3,11 @@ import type { Component } from 'vue';
import type { AnyPromiseFunction } from '@vben/types';
import { computed, ref, unref, useAttrs, watch } from 'vue';
import { computed, nextTick, ref, unref, useAttrs, watch } from 'vue';
import { LoaderCircle } from '@vben/icons';
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
import { cloneDeep, get, isEqual, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
@@ -104,6 +104,8 @@ const refOptions = ref<OptionsItem[]>([]);
const loading = ref(false);
// 首次是否加载过了
const isFirstLoaded = ref(false);
// 标记是否有待处理的请求
const hasPendingRequest = ref(false);
const getOptions = computed(() => {
const { labelField, valueField, childrenField, numberToString } = props;
@@ -146,18 +148,26 @@ const bindProps = computed(() => {
});
async function fetchApi() {
let { api, beforeFetch, afterFetch, params, resultField } = props;
const { api, beforeFetch, afterFetch, resultField } = props;
if (!api || !isFunction(api) || loading.value) {
if (!api || !isFunction(api)) {
return;
}
// 如果正在加载,标记有待处理的请求并返回
if (loading.value) {
hasPendingRequest.value = true;
return;
}
refOptions.value = [];
try {
loading.value = true;
let finalParams = unref(mergedParams);
if (beforeFetch && isFunction(beforeFetch)) {
params = (await beforeFetch(params)) || params;
finalParams = (await beforeFetch(cloneDeep(finalParams))) || finalParams;
}
let res = await api(params);
let res = await api(finalParams);
if (afterFetch && isFunction(afterFetch)) {
res = (await afterFetch(res)) || res;
}
@@ -177,6 +187,13 @@ async function fetchApi() {
isFirstLoaded.value = false;
} finally {
loading.value = false;
// 如果有待处理的请求,立即触发新的请求
if (hasPendingRequest.value) {
hasPendingRequest.value = false;
// 使用 nextTick 确保状态更新完成后再触发新请求
await nextTick();
fetchApi();
}
}
}
@@ -190,7 +207,7 @@ async function handleFetchForVisible(visible: boolean) {
}
}
const params = computed(() => {
const mergedParams = computed(() => {
return {
...props.params,
...unref(innerParams),
@@ -198,7 +215,7 @@ const params = computed(() => {
});
watch(
params,
mergedParams,
(value, oldValue) => {
if (isEqual(value, oldValue)) {
return;

View File

@@ -1,7 +1,14 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { computed, ref, watchEffect } from 'vue';
import {
computed,
onBeforeUnmount,
onMounted,
onUpdated,
ref,
watchEffect,
} from 'vue';
import { VbenTooltip } from '@vben-core/shadcn-ui';
@@ -33,6 +40,16 @@ interface Props {
* @default true
*/
tooltip?: boolean;
/**
* 是否只在文本被截断时显示提示框
* @default false
*/
tooltipWhenEllipsis?: boolean;
/**
* 文本截断检测的像素差异阈值,越大则判断越严格
* @default 3
*/
ellipsisThreshold?: number;
/**
* 提示框背景颜色,优先级高于 overlayStyle
*/
@@ -62,12 +79,15 @@ const props = withDefaults(defineProps<Props>(), {
maxWidth: '100%',
placement: 'top',
tooltip: true,
tooltipWhenEllipsis: false,
ellipsisThreshold: 3,
tooltipBackgroundColor: '',
tooltipColor: '',
tooltipFontSize: 14,
tooltipMaxWidth: undefined,
tooltipOverlayStyle: () => ({ textAlign: 'justify' }),
});
const emit = defineEmits<{ expandChange: [boolean] }>();
const textMaxWidth = computed(() => {
@@ -79,9 +99,67 @@ const textMaxWidth = computed(() => {
const ellipsis = ref();
const isExpand = ref(false);
const defaultTooltipMaxWidth = ref();
const isEllipsis = ref(false);
const { width: eleWidth } = useElementSize(ellipsis);
// 检测文本是否被截断
const checkEllipsis = () => {
if (!ellipsis.value || !props.tooltipWhenEllipsis) return;
const element = ellipsis.value;
const originalText = element.textContent || '';
const originalTrimmed = originalText.trim();
// 对于空文本直接返回 false
if (!originalTrimmed) {
isEllipsis.value = false;
return;
}
const widthDiff = element.scrollWidth - element.clientWidth;
const heightDiff = element.scrollHeight - element.clientHeight;
// 使用足够大的差异阈值确保只有真正被截断的文本才会显示 tooltip
isEllipsis.value =
props.line === 1
? widthDiff > props.ellipsisThreshold
: heightDiff > props.ellipsisThreshold;
};
// 使用 ResizeObserver 监听尺寸变化
let resizeObserver: null | ResizeObserver = null;
onMounted(() => {
if (typeof ResizeObserver !== 'undefined' && props.tooltipWhenEllipsis) {
resizeObserver = new ResizeObserver(() => {
checkEllipsis();
});
if (ellipsis.value) {
resizeObserver.observe(ellipsis.value);
}
}
// 初始检测
checkEllipsis();
});
// 使用onUpdated钩子检测内容变化
onUpdated(() => {
if (props.tooltipWhenEllipsis) {
checkEllipsis();
}
});
onBeforeUnmount(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
watchEffect(
() => {
if (props.tooltip && eleWidth.value) {
@@ -91,9 +169,13 @@ watchEffect(
},
{ flush: 'post' },
);
function onExpand() {
isExpand.value = !isExpand.value;
emit('expandChange', isExpand.value);
if (props.tooltipWhenEllipsis) {
checkEllipsis();
}
}
function handleExpand() {
@@ -110,7 +192,9 @@ function handleExpand() {
color: tooltipColor,
backgroundColor: tooltipBackgroundColor,
}"
:disabled="!props.tooltip || isExpand"
:disabled="
!props.tooltip || isExpand || (props.tooltipWhenEllipsis && !isEllipsis)
"
:side="placement"
>
<slot name="tooltip">

View File

@@ -2,6 +2,7 @@ export * from './api-component';
export * from './captcha';
export * from './col-page';
export * from './count-to';
export * from './doc-alert';
export * from './ellipsis-text';
export * from './icon-picker';
export * from './json-viewer';

View File

@@ -63,7 +63,7 @@ onMounted(() => {
ref="docRef"
:class="
cn(
'bg-card border-border relative flex items-end rounded-md border-b p-4',
'bg-card border-border relative mx-4 flex items-start rounded-md border-b',
)
"
>

View File

@@ -1,5 +1,6 @@
export { default as AuthenticationAuthTitle } from './auth-title.vue';
export { default as AuthenticationCodeLogin } from './code-login.vue';
export { default as DocLink } from './doc-link.vue';
export { default as AuthenticationForgetPassword } from './forget-password.vue';
export { default as AuthenticationLoginExpiredModal } from './login-expired-modal.vue';
export { default as AuthenticationLogin } from './login.vue';

View File

@@ -14,6 +14,7 @@ import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton, VbenCheckbox } from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
import DocLink from './doc-link.vue';
import ThirdPartyLogin from './third-party-login.vue';
interface Props extends AuthenticationProps {
@@ -195,5 +196,8 @@ defineExpose({
</span>
</div>
</slot>
<!-- 萌新必读 -->
<DocLink />
</div>
</template>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -193,67 +193,107 @@ export function useElementPlusDesignTokens() {
'--el-border-radius-base': getCssVariableValue('--radius', false),
'--el-color-danger': getCssVariableValue('--destructive-500'),
'--el-color-danger-dark-2': getCssVariableValue('--destructive'),
'--el-color-danger-light-3': getCssVariableValue('--destructive-400'),
'--el-color-danger-light-5': getCssVariableValue('--destructive-300'),
'--el-color-danger-light-7': getCssVariableValue('--destructive-200'),
'--el-color-danger-dark-2': isDark.value
? getCssVariableValue('--destructive-400')
: getCssVariableValue('--destructive-600'),
'--el-color-danger-light-3': isDark.value
? getCssVariableValue('--destructive-600')
: getCssVariableValue('--destructive-400'),
'--el-color-danger-light-5': isDark.value
? getCssVariableValue('--destructive-700')
: getCssVariableValue('--destructive-300'),
'--el-color-danger-light-7': isDark.value
? getCssVariableValue('--destructive-800')
: getCssVariableValue('--destructive-200'),
'--el-color-danger-light-8': isDark.value
? border
? getCssVariableValue('--destructive-900')
: getCssVariableValue('--destructive-100'),
'--el-color-danger-light-9': isDark.value
? accent
? getCssVariableValue('--destructive-950')
: getCssVariableValue('--destructive-50'),
'--el-color-error': getCssVariableValue('--destructive-500'),
'--el-color-error-dark-2': getCssVariableValue('--destructive'),
'--el-color-error-light-3': getCssVariableValue('--destructive-400'),
'--el-color-error-light-5': getCssVariableValue('--destructive-300'),
'--el-color-error-light-7': getCssVariableValue('--destructive-200'),
'--el-color-error-dark-2': isDark.value
? getCssVariableValue('--destructive-400')
: getCssVariableValue('--destructive-600'),
'--el-color-error-light-3': isDark.value
? getCssVariableValue('--destructive-600')
: getCssVariableValue('--destructive-400'),
'--el-color-error-light-5': isDark.value
? getCssVariableValue('--destructive-700')
: getCssVariableValue('--destructive-300'),
'--el-color-error-light-7': isDark.value
? getCssVariableValue('--destructive-800')
: getCssVariableValue('--destructive-200'),
'--el-color-error-light-8': isDark.value
? border
? getCssVariableValue('--destructive-900')
: getCssVariableValue('--destructive-100'),
'--el-color-error-light-9': isDark.value
? accent
? getCssVariableValue('--destructive-950')
: getCssVariableValue('--destructive-50'),
'--el-color-info-light-5': border,
'--el-color-info-light-8': border,
'--el-color-info-light-9': getCssVariableValue('--info'), // getCssVariableValue('--secondary'),
'--el-color-primary': getCssVariableValue('--primary-500'),
'--el-color-primary-dark-2': getCssVariableValue('--primary'),
'--el-color-primary-light-3': getCssVariableValue('--primary-400'),
'--el-color-primary-light-5': getCssVariableValue('--primary-300'),
'--el-color-primary-dark-2': isDark.value
? getCssVariableValue('--primary-400')
: getCssVariableValue('--primary-600'),
'--el-color-primary-light-3': isDark.value
? getCssVariableValue('--primary-600')
: getCssVariableValue('--primary-400'),
'--el-color-primary-light-5': isDark.value
? getCssVariableValue('--primary-700')
: getCssVariableValue('--primary-300'),
'--el-color-primary-light-7': isDark.value
? border
? getCssVariableValue('--primary-800')
: getCssVariableValue('--primary-200'),
'--el-color-primary-light-8': isDark.value
? border
? getCssVariableValue('--primary-900')
: getCssVariableValue('--primary-100'),
'--el-color-primary-light-9': isDark.value
? accent
? getCssVariableValue('--primary-950')
: getCssVariableValue('--primary-50'),
'--el-color-success': getCssVariableValue('--success-500'),
'--el-color-success-dark-2': getCssVariableValue('--success'),
'--el-color-success-light-3': getCssVariableValue('--success-400'),
'--el-color-success-light-5': getCssVariableValue('--success-300'),
'--el-color-success-light-7': getCssVariableValue('--success-200'),
'--el-color-success-dark-2': isDark.value
? getCssVariableValue('--success-400')
: getCssVariableValue('--success-600'),
'--el-color-success-light-3': isDark.value
? getCssVariableValue('--success-600')
: getCssVariableValue('--success-400'),
'--el-color-success-light-5': isDark.value
? getCssVariableValue('--success-700')
: getCssVariableValue('--success-300'),
'--el-color-success-light-7': isDark.value
? getCssVariableValue('--success-800')
: getCssVariableValue('--success-200'),
'--el-color-success-light-8': isDark.value
? border
? getCssVariableValue('--success-900')
: getCssVariableValue('--success-100'),
'--el-color-success-light-9': isDark.value
? accent
? getCssVariableValue('--success-950')
: getCssVariableValue('--success-50'),
'--el-color-warning': getCssVariableValue('--warning-500'),
'--el-color-warning-dark-2': getCssVariableValue('--warning'),
'--el-color-warning-light-3': getCssVariableValue('--warning-400'),
'--el-color-warning-light-5': getCssVariableValue('--warning-300'),
'--el-color-warning-light-7': getCssVariableValue('--warning-200'),
'--el-color-warning-dark-2': isDark.value
? getCssVariableValue('--warning-400')
: getCssVariableValue('--warning-600'),
'--el-color-warning-light-3': isDark.value
? getCssVariableValue('--warning-600')
: getCssVariableValue('--warning-400'),
'--el-color-warning-light-5': isDark.value
? getCssVariableValue('--warning-700')
: getCssVariableValue('--warning-300'),
'--el-color-warning-light-7': isDark.value
? getCssVariableValue('--warning-800')
: getCssVariableValue('--warning-200'),
'--el-color-warning-light-8': isDark.value
? border
? getCssVariableValue('--warning-900')
: getCssVariableValue('--warning-100'),
'--el-color-warning-light-9': isDark.value
? accent
? getCssVariableValue('--warning-950')
: getCssVariableValue('--warning-50'),
'--el-fill-color': getCssVariableValue('--accent'),

View File

@@ -8,19 +8,40 @@ import { isFunction } from '@vben/utils';
import { useElementHover } from '@vueuse/core';
interface HoverDelayOptions {
/** 鼠标进入延迟时间 */
enterDelay?: (() => number) | number;
/** 鼠标离开延迟时间 */
leaveDelay?: (() => number) | number;
}
const DEFAULT_LEAVE_DELAY = 500; // 鼠标离开延迟时间,默认为 500ms
const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0立即响应
/**
* 监测鼠标是否在元素内部,如果在元素内部则返回 true否则返回 false
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
* @param delay 延迟更新状态的时间
* @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象
* @returns 返回一个数组,第一个元素是一个 ref表示鼠标是否在元素内部第二个元素是一个控制器可以通过 enable 和 disable 方法来控制监听器的启用和禁用
*/
export function useHoverToggle(
refElement: Arrayable<MaybeElementRef>,
delay: (() => number) | number = 500,
delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
) {
// 兼容旧版本API
const normalizedOptions: HoverDelayOptions =
typeof delay === 'number' || isFunction(delay)
? { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: delay }
: {
enterDelay: DEFAULT_ENTER_DELAY,
leaveDelay: DEFAULT_LEAVE_DELAY,
...delay,
};
const isHovers: Array<Ref<boolean>> = [];
const value = ref(false);
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();
const refs = Array.isArray(refElement) ? refElement : [refElement];
refs.forEach((refEle) => {
const eleRef = computed(() => {
@@ -32,15 +53,47 @@ export function useHoverToggle(
});
const isOutsideAll = computed(() => isHovers.every((v) => !v.value));
function clearTimers() {
if (enterTimer.value) {
clearTimeout(enterTimer.value);
enterTimer.value = undefined;
}
if (leaveTimer.value) {
clearTimeout(leaveTimer.value);
leaveTimer.value = undefined;
}
}
function setValueDelay(val: boolean) {
timer.value && clearTimeout(timer.value);
timer.value = setTimeout(
() => {
value.value = val;
timer.value = undefined;
},
isFunction(delay) ? delay() : delay,
);
clearTimers();
if (val) {
// 鼠标进入
const enterDelay = normalizedOptions.enterDelay ?? DEFAULT_ENTER_DELAY;
const delayTime = isFunction(enterDelay) ? enterDelay() : enterDelay;
if (delayTime <= 0) {
value.value = true;
} else {
enterTimer.value = setTimeout(() => {
value.value = true;
enterTimer.value = undefined;
}, delayTime);
}
} else {
// 鼠标离开
const leaveDelay = normalizedOptions.leaveDelay ?? DEFAULT_LEAVE_DELAY;
const delayTime = isFunction(leaveDelay) ? leaveDelay() : leaveDelay;
if (delayTime <= 0) {
value.value = false;
} else {
leaveTimer.value = setTimeout(() => {
value.value = false;
leaveTimer.value = undefined;
}, delayTime);
}
}
}
const watcher = watch(
@@ -61,7 +114,7 @@ export function useHoverToggle(
};
onUnmounted(() => {
timer.value && clearTimeout(timer.value);
clearTimers();
});
return [value, controller] as [typeof value, typeof controller];

View File

@@ -1,3 +1,4 @@
import type { ComputedRef } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
@@ -53,7 +54,24 @@ export function useTabs() {
await tabbarStore.closeTabByKey(key, router);
}
async function setTabTitle(title: string) {
/**
* 设置当前标签页的标题
*
* @description 支持设置静态标题字符串或动态计算标题
* @description 动态标题会在每次渲染时重新计算,适用于多语言或状态相关的标题
*
* @param title - 标题内容
* - 静态标题: 直接传入字符串
* - 动态标题: 传入 ComputedRef
*
* @example
* // 静态标题
* setTabTitle('标签页')
*
* // 动态标题(多语言)
* setTabTitle(computed(() => t('page.title')))
*/
async function setTabTitle(title: ComputedRef<string> | string) {
tabbarStore.setUpdateTime();
await tabbarStore.setTabTitle(route, title);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/layouts",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -38,7 +38,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<template>
<div
:class="[isDark]"
:class="[isDark ? 'dark' : '']"
class="flex min-h-full flex-1 select-none overflow-x-hidden"
>
<template v-if="toolbar">

View File

@@ -1 +1,2 @@
export { default as AuthPageLayout } from './authentication.vue';
export * from './types';

View File

@@ -9,7 +9,7 @@ import { computed } from 'vue';
import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences';
import { storeToRefs, useTabbarStore } from '@vben/stores';
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
import { IFrameRouterView } from '../../iframe';
@@ -115,13 +115,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</Transition>
<template v-else>
@@ -134,13 +134,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</template>
</RouterView>

View File

@@ -180,8 +180,16 @@ const headerSlots = computed(() => {
<VbenAdminLayout
v-model:sidebar-extra-visible="sidebarExtraVisible"
:content-compact="preferences.app.contentCompact"
:content-compact-width="preferences.app.contentCompactWidth"
:content-padding="preferences.app.contentPadding"
:content-padding-bottom="preferences.app.contentPaddingBottom"
:content-padding-left="preferences.app.contentPaddingLeft"
:content-padding-right="preferences.app.contentPaddingRight"
:content-padding-top="preferences.app.contentPaddingTop"
:footer-enable="preferences.footer.enable"
:footer-fixed="preferences.footer.fixed"
:footer-height="preferences.footer.height"
:header-height="preferences.header.height"
:header-hidden="preferences.header.hidden"
:header-mode="preferences.header.mode"
:header-theme="headerTheme"
@@ -196,11 +204,15 @@ const headerSlots = computed(() => {
:sidebar-fixed-button="preferences.sidebar.fixedButton"
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
:sidebar-extra-collapsed-width="preferences.sidebar.extraCollapsedWidth"
:sidebar-hidden="preferences.sidebar.hidden"
:sidebar-mixed-width="preferences.sidebar.mixedWidth"
:sidebar-theme="sidebarTheme"
:sidebar-width="preferences.sidebar.width"
:side-collapse-width="preferences.sidebar.collapseWidth"
:tabbar-enable="preferences.tabbar.enable"
:tabbar-height="preferences.tabbar.height"
:z-index="preferences.app.zIndex"
@side-mouse-leave="handleSideMouseLeave"
@toggle-sidebar="toggleSidebar"
@update:sidebar-collapse="
@@ -222,6 +234,7 @@ const headerSlots = computed(() => {
<template #logo>
<VbenLogo
v-if="preferences.logo.enable"
:fit="preferences.logo.fit"
:class="logoClass"
:collapsed="logoCollapsed"
:src="preferences.logo.source"
@@ -312,6 +325,7 @@ const headerSlots = computed(() => {
<template #side-extra-title>
<VbenLogo
v-if="preferences.logo.enable"
:fit="preferences.logo.fit"
:text="preferences.app.name"
:theme="theme"
>

View File

@@ -140,7 +140,10 @@ function useMixedMenu() {
watch(
() => route.path,
(path) => {
const currentPath = (route?.meta?.activePath as string) ?? path;
const currentPath = route?.meta?.activePath ?? route?.meta?.link ?? path;
if (willOpenedByWindow(currentPath)) {
return;
}
calcSideMenus(currentPath);
if (rootMenuPath.value)
defaultSubMap.set(rootMenuPath.value, currentPath);

View File

@@ -30,7 +30,7 @@ const {
} = useTabbar();
const menus = computed(() => {
const tab = tabbarStore.getTabByPath(currentActive.value);
const tab = tabbarStore.getTabByKey(currentActive.value);
const menus = createContextMenus(tab);
return menus.map((item) => {
return {

View File

@@ -22,7 +22,7 @@ import {
X,
} from '@vben/icons';
import { $t, useI18n } from '@vben/locales';
import { useAccessStore, useTabbarStore } from '@vben/stores';
import { getTabKey, useAccessStore, useTabbarStore } from '@vben/stores';
import { filterTree } from '@vben/utils';
export function useTabbar() {
@@ -44,8 +44,11 @@ export function useTabbar() {
toggleTabPin,
} = useTabs();
/**
* 当前路径对应的tab的key
*/
const currentActive = computed(() => {
return route.fullPath;
return getTabKey(route);
});
const { locale } = useI18n();
@@ -73,7 +76,8 @@ export function useTabbar() {
// 点击tab,跳转路由
const handleClick = (key: string) => {
router.push(key);
const { fullPath, path } = tabbarStore.getTabByKey(key);
router.push(fullPath || path);
};
// 关闭tab
@@ -100,7 +104,7 @@ export function useTabbar() {
);
watch(
() => route.path,
() => route.fullPath,
() => {
const meta = route.matched?.[route.matched.length - 1]?.meta;
tabbarStore.addTab({

View File

@@ -6,7 +6,7 @@ import { $t } from '@vben/locales';
import { useVbenModal } from '@vben-core/popup-ui';
interface Props {
// 轮时间,分钟
// 轮时间,分钟
checkUpdatesInterval?: number;
// 检查更新的地址
checkUpdateUrl?: string;
@@ -46,6 +46,7 @@ async function getVersionTag() {
const response = await fetch(props.checkUpdateUrl, {
cache: 'no-cache',
method: 'HEAD',
redirect: 'manual',
});
return (

View File

@@ -2,10 +2,12 @@ export { default as Breadcrumb } from './breadcrumb.vue';
export * from './check-updates';
export { default as AuthenticationColorToggle } from './color-toggle.vue';
export * from './global-search';
export * from './help';
export { default as LanguageToggle } from './language-toggle.vue';
export { default as AuthenticationLayoutToggle } from './layout-toggle.vue';
export * from './lock-screen';
export * from './notification';
export * from './preferences';
export * from './tenant-dropdown';
export * from './theme-toggle';
export * from './user-dropdown';

View File

@@ -46,7 +46,11 @@ interface Props {
/**
* 菜单数组
*/
menus?: Array<{ handler: AnyFunction; icon?: Component; text: string }>;
menus?: Array<{
handler: AnyFunction;
icon?: Component | Function | string;
text: string;
}>;
/**
* 标签文本

View File

@@ -25,6 +25,10 @@
"./motion": {
"types": "./src/motion/index.ts",
"default": "./src/motion/index.ts"
},
"./markmap": {
"types": "./src/markmap/index.ts",
"default": "./src/markmap/index.ts"
}
},
"dependencies": {
@@ -40,8 +44,16 @@
"@vueuse/core": "catalog:",
"@vueuse/motion": "catalog:",
"echarts": "catalog:",
"markdown-it": "catalog:",
"markmap-common": "catalog:",
"markmap-lib": "catalog:",
"markmap-toolbar": "catalog:",
"markmap-view": "catalog:",
"vue": "catalog:",
"vxe-pc-ui": "catalog:",
"vxe-table": "catalog:"
},
"devDependencies": {
"@types/markdown-it": "catalog:"
}
}

View File

@@ -3,13 +3,16 @@ import type {
BarSeriesOption,
GaugeSeriesOption,
LineSeriesOption,
MapSeriesOption,
} from 'echarts/charts';
import type {
DatasetComponentOption,
GeoComponentOption,
GridComponentOption,
// 组件类型的定义后缀都为 ComponentOption
TitleComponentOption,
TooltipComponentOption,
VisualMapComponentOption,
} from 'echarts/components';
import type { ComposeOption } from 'echarts/core';
@@ -17,12 +20,14 @@ import {
BarChart,
GaugeChart,
LineChart,
MapChart,
PieChart,
RadarChart,
} from 'echarts/charts';
import {
// 数据集组件
DatasetComponent,
GeoComponent,
GridComponent,
LegendComponent,
TitleComponent,
@@ -30,6 +35,7 @@ import {
TooltipComponent,
// 内置数据转换器组件 (filter, sort)
TransformComponent,
VisualMapComponent,
} from 'echarts/components';
import * as echarts from 'echarts/core';
import { LabelLayout, UniversalTransition } from 'echarts/features';
@@ -40,10 +46,13 @@ export type ECOption = ComposeOption<
| BarSeriesOption
| DatasetComponentOption
| GaugeSeriesOption
| GeoComponentOption
| GridComponentOption
| LineSeriesOption
| MapSeriesOption
| TitleComponentOption
| TooltipComponentOption
| VisualMapComponentOption
>;
// 注册必须的组件
@@ -63,6 +72,9 @@ echarts.use([
CanvasRenderer,
LegendComponent,
ToolboxComponent,
VisualMapComponent,
MapChart,
GeoComponent,
]);
export default echarts;

View File

@@ -19,6 +19,7 @@ import {
} from '@vueuse/core';
import echarts from './echarts';
import chinaMap from './map/china.json';
type EchartsUIType = typeof EchartsUI | undefined;
@@ -32,6 +33,18 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
const { height, width } = useWindowSize();
const resizeHandler: () => void = useDebounceFn(resize, 200);
echarts.registerMap('china', {
geoJSON: chinaMap as any,
specialAreas: {
china: {
left: 500,
top: 500,
width: 1000,
height: 1000,
},
},
});
const getOptions = computed((): EChartsOption => {
if (!isDark.value) {
return {};

View File

@@ -26,14 +26,14 @@ function getDefaultState(): VxeGridProps {
};
}
export class VxeGridApi {
export class VxeGridApi<T extends Record<string, any> = any> {
public formApi = {} as ExtendedFormApi;
// private prevState: null | VxeGridProps = null;
public grid = {} as VxeGridInstance;
public state: null | VxeGridProps = null;
public grid = {} as VxeGridInstance<T>;
public state: null | VxeGridProps<T> = null;
public store: Store<VxeGridProps>;
public store: Store<VxeGridProps<T>>;
private isMounted = false;
@@ -99,8 +99,8 @@ export class VxeGridApi {
setState(
stateOrFn:
| ((prev: VxeGridProps) => Partial<VxeGridProps>)
| Partial<VxeGridProps>,
| ((prev: VxeGridProps<T>) => Partial<VxeGridProps<T>>)
| Partial<VxeGridProps<T>>,
) {
if (isFunction(stateOrFn)) {
this.store.setState((prev) => {

View File

@@ -6,6 +6,7 @@ export { default as VbenVxeGrid } from './use-vxe-grid.vue';
export type {
VxeGridListeners,
VxeGridProps,
VxeGridPropTypes,
VxeTableInstance,
VxeToolbarInstance,
} from 'vxe-table';

View File

@@ -9,7 +9,7 @@ import type { Ref } from 'vue';
import type { ClassType, DeepPartial } from '@vben/types';
import type { VbenFormProps } from '@vben-core/form-ui';
import type { BaseFormComponentType, VbenFormProps } from '@vben-core/form-ui';
import type { VxeGridApi } from './api';
@@ -35,7 +35,11 @@ export interface SeparatorOptions {
show?: boolean;
backgroundColor?: string;
}
export interface VxeGridProps {
export interface VxeGridProps<
T extends Record<string, any> = any,
D extends BaseFormComponentType = BaseFormComponentType,
> {
/**
* 标题
*/
@@ -55,15 +59,15 @@ export interface VxeGridProps {
/**
* vxe-grid 配置
*/
gridOptions?: DeepPartial<VxeTableGridOptions>;
gridOptions?: DeepPartial<VxeTableGridOptions<T>>;
/**
* vxe-grid 事件
*/
gridEvents?: DeepPartial<VxeGridListeners>;
gridEvents?: DeepPartial<VxeGridListeners<T>>;
/**
* 表单配置
*/
formOptions?: VbenFormProps;
formOptions?: VbenFormProps<D>;
/**
* 显示搜索表单
*/
@@ -74,9 +78,12 @@ export interface VxeGridProps {
separator?: boolean | SeparatorOptions;
}
export type ExtendedVxeGridApi = VxeGridApi & {
useStore: <T = NoInfer<VxeGridProps>>(
selector?: (state: NoInfer<VxeGridProps>) => T,
export type ExtendedVxeGridApi<
D extends Record<string, any> = any,
F extends BaseFormComponentType = BaseFormComponentType,
> = VxeGridApi<D> & {
useStore: <T = NoInfer<VxeGridProps<D, F>>>(
selector?: (state: NoInfer<VxeGridProps<any, any>>) => T,
) => Readonly<Ref<T>>;
};

View File

@@ -1,3 +1,5 @@
import type { BaseFormComponentType } from '@vben-core/form-ui';
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
import { defineComponent, h, onBeforeUnmount } from 'vue';
@@ -7,16 +9,19 @@ import { useStore } from '@vben-core/shared/store';
import { VxeGridApi } from './api';
import VxeGrid from './use-vxe-grid.vue';
export function useVbenVxeGrid(options: VxeGridProps) {
export function useVbenVxeGrid<
T extends Record<string, any> = any,
D extends BaseFormComponentType = BaseFormComponentType,
>(options: VxeGridProps<T, D>) {
// const IS_REACTIVE = isReactive(options);
const api = new VxeGridApi(options);
const extendedApi: ExtendedVxeGridApi = api as ExtendedVxeGridApi;
const extendedApi: ExtendedVxeGridApi<T, D> = api as ExtendedVxeGridApi<T, D>;
extendedApi.useStore = (selector) => {
return useStore(api.store, selector);
};
const Grid = defineComponent(
(props: VxeGridProps, { attrs, slots }) => {
(props: VxeGridProps<T>, { attrs, slots }) => {
onBeforeUnmount(() => {
api.unmount();
});

View File

@@ -59,6 +59,7 @@ const FORM_SLOT_PREFIX = 'form-';
const TOOLBAR_ACTIONS = 'toolbar-actions';
const TOOLBAR_TOOLS = 'toolbar-tools';
const TABLE_TITLE = 'table-title';
const gridRef = useTemplateRef<VxeGridInstance>('gridRef');
@@ -129,7 +130,7 @@ const [Form, formApi] = useTableForm({
});
const showTableTitle = computed(() => {
return !!slots.tableTitle?.() || tableTitle.value;
return !!slots[TABLE_TITLE]?.() || tableTitle.value;
});
const showToolbar = computed(() => {
@@ -277,6 +278,15 @@ const delegatedFormSlots = computed(() => {
return resultSlots.map((key) => key.replace(FORM_SLOT_PREFIX, ''));
});
const showDefaultEmpty = computed(() => {
// 检查是否有原生的 VXE Table 空状态配置
const hasEmptyText = options.value.emptyText !== undefined;
const hasEmptyRender = options.value.emptyRender !== undefined;
// 如果有原生配置,就不显示默认的空状态
return !hasEmptyText && !hasEmptyRender;
});
async function init() {
await nextTick();
const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
@@ -458,7 +468,7 @@ onUnmounted(() => {
</slot>
</template>
<!-- 统一控状态 -->
<template #empty>
<template v-if="showDefaultEmpty" #empty>
<slot name="empty">
<EmptyIcon class="mx-auto" />
<div class="mt-2">{{ $t('common.noData') }}</div>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/request",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -20,6 +20,7 @@
}
},
"dependencies": {
"@microsoft/fetch-event-source": "catalog:",
"@vben/locales": "workspace:*",
"@vben/utils": "workspace:*",
"axios": "catalog:",

View File

@@ -1,2 +1,3 @@
export * from './request-client';
export * from '@microsoft/fetch-event-source';
export * from 'axios';

View File

@@ -1,6 +1,8 @@
import type { RequestClient } from '../request-client';
import type { RequestClientConfig } from '../types';
import { isUndefined } from '@vben/utils';
class FileUploader {
private client: RequestClient;
@@ -18,10 +20,10 @@ class FileUploader {
Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item, index) => {
formData.append(`${key}[${index}]`, item);
!isUndefined(item) && formData.append(`${key}[${index}]`, item);
});
} else {
formData.append(key, value);
!isUndefined(value) && formData.append(key, value);
}
});