{
- if (isNodeDisabled(item)) {
- event.preventDefault();
- event.stopPropagation();
- return;
+ "
+ />
+
{
+ if (isNodeDisabled(item)) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+ handleSelect();
}
- event.stopPropagation();
- event.preventDefault();
- handleSelect();
- }
- "
- >
-
-
- {{ get(item.value, labelField) }}
-
+ "
+ >
+
+
+ {{ get(item.value, labelField) }}
+
+
+
+
diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts
index 97b09139..bf2d6ece 100644
--- a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts
+++ b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts
@@ -40,3 +40,23 @@ export interface TreeProps {
/** 值字段 */
valueField?: string;
}
+
+export function treePropsDefaults() {
+ return {
+ allowClear: false,
+ autoCheckParent: true,
+ bordered: false,
+ checkStrictly: false,
+ defaultExpandedKeys: () => [],
+ defaultExpandedLevel: 0,
+ disabled: false,
+ disabledField: 'disabled',
+ iconField: 'icon',
+ labelField: 'label',
+ multiple: false,
+ showIcon: true,
+ transition: true,
+ valueField: 'value',
+ childrenField: 'children',
+ };
+}
diff --git a/packages/@core/ui-kit/tabs-ui/package.json b/packages/@core/ui-kit/tabs-ui/package.json
index 93c16e1e..0bb4dc4e 100644
--- a/packages/@core/ui-kit/tabs-ui/package.json
+++ b/packages/@core/ui-kit/tabs-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben-core/tabs-ui",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/constants/package.json b/packages/constants/package.json
index 2b098a2a..249418fe 100644
--- a/packages/constants/package.json
+++ b/packages/constants/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/constants",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/effects/access/package.json b/packages/effects/access/package.json
index f620d6dc..c23018aa 100644
--- a/packages/effects/access/package.json
+++ b/packages/effects/access/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/access",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/effects/common-ui/package.json b/packages/effects/common-ui/package.json
index 2c32421b..5a34a264 100644
--- a/packages/effects/common-ui/package.json
+++ b/packages/effects/common-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/effects/common-ui/src/components/index.ts b/packages/effects/common-ui/src/components/index.ts
index 543fcf3c..5914d44c 100644
--- a/packages/effects/common-ui/src/components/index.ts
+++ b/packages/effects/common-ui/src/components/index.ts
@@ -9,6 +9,7 @@ export * from './loading';
export * from './page';
export * from './resize';
export * from './tippy';
+export * from './tree';
export * from '@vben-core/form-ui';
export * from '@vben-core/popup-ui';
@@ -27,7 +28,6 @@ export {
VbenPinInput,
VbenSelect,
VbenSpinner,
- VbenTree,
} from '@vben-core/shadcn-ui';
export type { FlattenedItem } from '@vben-core/shadcn-ui';
diff --git a/packages/effects/common-ui/src/components/page/page.vue b/packages/effects/common-ui/src/components/page/page.vue
index c95a3b45..4a0ea54a 100644
--- a/packages/effects/common-ui/src/components/page/page.vue
+++ b/packages/effects/common-ui/src/components/page/page.vue
@@ -25,7 +25,7 @@ const footerRef = useTemplateRef
('footerRef');
const contentStyle = computed(() => {
if (autoContentHeight) {
return {
- height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,
+ height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${footerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,
overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
};
}
@@ -50,7 +50,7 @@ onMounted(() => {
-
+
-
diff --git a/packages/effects/common-ui/src/components/tree/index.ts b/packages/effects/common-ui/src/components/tree/index.ts
new file mode 100644
index 00000000..ce3bc5c6
--- /dev/null
+++ b/packages/effects/common-ui/src/components/tree/index.ts
@@ -0,0 +1 @@
+export { default as Tree } from './tree.vue';
diff --git a/packages/effects/common-ui/src/components/tree/tree.vue b/packages/effects/common-ui/src/components/tree/tree.vue
new file mode 100644
index 00000000..1f2fcc17
--- /dev/null
+++ b/packages/effects/common-ui/src/components/tree/tree.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
{{ $t('common.noData') }}
+
+
diff --git a/packages/effects/common-ui/src/ui/authentication/code-login.vue b/packages/effects/common-ui/src/ui/authentication/code-login.vue
index 5dce7402..d7563d8f 100644
--- a/packages/effects/common-ui/src/ui/authentication/code-login.vue
+++ b/packages/effects/common-ui/src/ui/authentication/code-login.vue
@@ -35,6 +35,10 @@ interface Props {
* @zh_CN 按钮文本
*/
submitButtonText?: string;
+ /**
+ * @zh_CN 是否显示返回按钮
+ */
+ showBack?: boolean;
}
defineOptions({
@@ -43,6 +47,7 @@ defineOptions({
const props = withDefaults(defineProps
(), {
loading: false,
+ showBack: true,
loginPath: '/auth/login',
submitButtonText: '',
subTitle: '',
@@ -110,7 +115,12 @@ defineExpose({
{{ submitButtonText || $t('common.login') }}
-
+
{{ $t('common.back') }}
diff --git a/packages/effects/common-ui/src/ui/authentication/dingding-login.vue b/packages/effects/common-ui/src/ui/authentication/dingding-login.vue
index 4c63301e..80b123c1 100644
--- a/packages/effects/common-ui/src/ui/authentication/dingding-login.vue
+++ b/packages/effects/common-ui/src/ui/authentication/dingding-login.vue
@@ -1,7 +1,7 @@
@@ -16,7 +20,8 @@ defineOptions({
diff --git a/packages/effects/layouts/src/basic/layout.vue b/packages/effects/layouts/src/basic/layout.vue
index 2dd7d2f8..35f28c1d 100644
--- a/packages/effects/layouts/src/basic/layout.vue
+++ b/packages/effects/layouts/src/basic/layout.vue
@@ -158,7 +158,9 @@ function clickLogo() {
function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {
// 只在双列模式下生效
if (
- preferences.app.layout === 'sidebar-mixed-nav' &&
+ ['header-mixed-nav', 'sidebar-mixed-nav'].includes(
+ preferences.app.layout,
+ ) &&
route.meta &&
route.meta.hideInMenu
) {
diff --git a/packages/effects/layouts/src/basic/menu/use-navigation.ts b/packages/effects/layouts/src/basic/menu/use-navigation.ts
index 6ba484a4..daa6dc71 100644
--- a/packages/effects/layouts/src/basic/menu/use-navigation.ts
+++ b/packages/effects/layouts/src/basic/menu/use-navigation.ts
@@ -29,7 +29,8 @@ function useNavigation() {
return true;
}
const route = routeMetaMap.get(path);
- return route?.meta?.openInNewWindow ?? false;
+ // 如果有外链或者设置了在新窗口打开,返回 true
+ return !!(route?.meta?.link || route?.meta?.openInNewWindow);
};
const resolveHref = (path: string): string => {
@@ -39,7 +40,13 @@ function useNavigation() {
const navigation = async (path: string) => {
try {
const route = routeMetaMap.get(path);
- const { openInNewWindow = false, query = {} } = route?.meta ?? {};
+ const { openInNewWindow = false, query = {}, link } = route?.meta ?? {};
+
+ // 检查是否有外链
+ if (link && typeof link === 'string') {
+ openWindow(link, { target: '_blank' });
+ return;
+ }
if (isHttpUrl(path)) {
openWindow(path, { target: '_blank' });
diff --git a/packages/effects/layouts/src/widgets/language-toggle.vue b/packages/effects/layouts/src/widgets/language-toggle.vue
index 728fca04..f92432f9 100644
--- a/packages/effects/layouts/src/widgets/language-toggle.vue
+++ b/packages/effects/layouts/src/widgets/language-toggle.vue
@@ -31,7 +31,7 @@ async function handleUpdate(value: string | undefined) {
:model-value="preferences.app.locale"
@update:model-value="handleUpdate"
>
-
+
diff --git a/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue b/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue
index 5121d688..908e6706 100644
--- a/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue
+++ b/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue
@@ -27,29 +27,30 @@ const emit = defineEmits<{
submit: [Recordable];
}>();
-const [Form, { resetForm, validate, getValues }] = useVbenForm(
- reactive({
- commonConfig: {
- hideLabel: true,
- hideRequiredMark: true,
- },
- schema: computed(() => [
- {
- component: 'VbenInputPassword' as const,
- componentProps: {
- placeholder: $t('ui.widgets.lockScreen.placeholder'),
- },
- fieldName: 'lockScreenPassword',
- formFieldProps: { validateOnBlur: false },
- label: $t('authentication.password'),
- rules: z
- .string()
- .min(1, { message: $t('ui.widgets.lockScreen.placeholder') }),
+const [Form, { resetForm, validate, getValues, getFieldComponentRef }] =
+ useVbenForm(
+ reactive({
+ commonConfig: {
+ hideLabel: true,
+ hideRequiredMark: true,
},
- ]),
- showDefaultActions: false,
- }),
-);
+ schema: computed(() => [
+ {
+ component: 'VbenInputPassword' as const,
+ componentProps: {
+ placeholder: $t('ui.widgets.lockScreen.placeholder'),
+ },
+ fieldName: 'lockScreenPassword',
+ formFieldProps: { validateOnBlur: false },
+ label: $t('authentication.password'),
+ rules: z
+ .string()
+ .min(1, { message: $t('ui.widgets.lockScreen.placeholder') }),
+ },
+ ]),
+ showDefaultActions: false,
+ }),
+ );
const [Modal] = useVbenModal({
onConfirm() {
@@ -60,6 +61,13 @@ const [Modal] = useVbenModal({
resetForm();
}
},
+ onOpened() {
+ requestAnimationFrame(() => {
+ getFieldComponentRef('lockScreenPassword')
+ ?.$el?.querySelector('[name="lockScreenPassword"]')
+ ?.focus();
+ });
+ },
});
async function handleSubmit() {
diff --git a/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue b/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue
index 6fb7a54a..3f700334 100644
--- a/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue
+++ b/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue
@@ -37,7 +37,7 @@ const date = useDateFormat(now, 'YYYY-MM-DD dddd', { locales: locale.value });
const showUnlockForm = ref(false);
const { lockScreenPassword } = storeToRefs(accessStore);
-const [Form, { form, validate }] = useVbenForm(
+const [Form, { form, validate, getFieldComponentRef }] = useVbenForm(
reactive({
commonConfig: {
hideLabel: true,
@@ -75,6 +75,13 @@ async function handleSubmit() {
function toggleUnlockForm() {
showUnlockForm.value = !showUnlockForm.value;
+ if (showUnlockForm.value) {
+ requestAnimationFrame(() => {
+ getFieldComponentRef('password')
+ ?.$el?.querySelector('[name="password"]')
+ ?.focus();
+ });
+ }
}
useScrollLock();
diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue b/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue
index 630882f4..c69cd3aa 100644
--- a/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue
+++ b/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue
@@ -2,6 +2,7 @@
import { SUPPORT_LANGUAGES } from '@vben/constants';
import { $t } from '@vben/locales';
+import InputItem from '../input-item.vue';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';
@@ -12,6 +13,7 @@ defineOptions({
const appLocale = defineModel('appLocale');
const appDynamicTitle = defineModel('appDynamicTitle');
const appWatermark = defineModel('appWatermark');
+const appWatermarkContent = defineModel('appWatermarkContent');
const appEnableCheckUpdates = defineModel('appEnableCheckUpdates');
@@ -22,9 +24,23 @@ const appEnableCheckUpdates = defineModel('appEnableCheckUpdates');
{{ $t('preferences.dynamicTitle') }}
-
+ {
+ if (!val) appWatermarkContent = '';
+ }
+ "
+ >
{{ $t('preferences.watermark') }}
+
+ {{ $t('preferences.watermarkContent') }}
+
{{ $t('preferences.checkUpdates') }}
diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue b/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue
index 2bf8660c..8e9f09e7 100644
--- a/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue
+++ b/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue
@@ -3,7 +3,7 @@ import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
-import { CircleHelp } from '@vben/icons';
+import { CircleHelp, CircleX } from '@vben/icons';
import { Input, VbenTooltip } from '@vben-core/shadcn-ui';
@@ -47,6 +47,17 @@ const slots = useSlots();
-
+
+
+ (inputValue = '')"
+ />
+
diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue b/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue
index 27dfd28a..d0dba8ec 100644
--- a/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue
+++ b/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue
@@ -104,7 +104,7 @@ function selectColor() {
watch(
() => [modelValue.value, props.isDark] as [BuiltinThemeType, boolean],
- ([themeType, isDark]) => {
+ ([themeType, isDark], [_, isDarkPrev]) => {
const theme = builtinThemePresets.value.find(
(item) => item.type === themeType,
);
@@ -113,7 +113,9 @@ watch(
? theme.darkPrimaryColor || theme.primaryColor
: theme.primaryColor;
- themeColorPrimary.value = primaryColor || theme.color;
+ if (!(theme.type === 'custom' && isDark !== isDarkPrev)) {
+ themeColorPrimary.value = primaryColor || theme.color;
+ }
}
},
);
@@ -132,14 +134,14 @@ watch(
-
+
-
+
diff --git a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
index 7d62d4e0..301004d6 100644
--- a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
+++ b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
@@ -16,7 +16,7 @@ import type { SegmentedItem } from '@vben-core/shadcn-ui';
import { computed, ref } from 'vue';
-import { Copy, RotateCw } from '@vben/icons';
+import { Copy, Pin, PinOff, RotateCw } from '@vben/icons';
import { $t, loadLocaleMessages } from '@vben/locales';
import {
clearPreferencesCache,
@@ -67,7 +67,11 @@ const appColorGrayMode = defineModel('appColorGrayMode');
const appColorWeakMode = defineModel('appColorWeakMode');
const appContentCompact = defineModel('appContentCompact');
const appWatermark = defineModel('appWatermark');
+const appWatermarkContent = defineModel('appWatermarkContent');
const appEnableCheckUpdates = defineModel('appEnableCheckUpdates');
+const appEnableStickyPreferencesNavigationBar = defineModel(
+ 'appEnableStickyPreferencesNavigationBar',
+);
const appPreferencesButtonPosition = defineModel(
'appPreferencesButtonPosition',
);
@@ -240,7 +244,7 @@ async function handleReset() {
@@ -248,18 +252,44 @@ async function handleReset() {
:disabled="!diffPreference"
:tooltip="$t('preferences.resetTip')"
class="relative"
+ @click="handleReset"
>
-
+
+
+
+ (appEnableStickyPreferencesNavigationBar =
+ !appEnableStickyPreferencesNavigationBar)
+ "
+ >
+
+
-
-
+
+
@@ -447,3 +478,11 @@ async function handleReset() {
+
+
diff --git a/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue b/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue
index 9ee4bceb..7d9ff660 100644
--- a/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue
+++ b/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue
@@ -64,7 +64,7 @@ function toggleTheme(event: MouseEvent) {
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
];
- document.documentElement.animate(
+ const animate = document.documentElement.animate(
{
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
},
@@ -76,6 +76,9 @@ function toggleTheme(event: MouseEvent) {
: '::view-transition-new(root)',
},
);
+ animate.onfinish = () => {
+ transition.skipTransition();
+ };
});
}
@@ -85,7 +88,7 @@ function toggleTheme(event: MouseEvent) {
:aria-label="theme"
:class="[`is-${theme}`]"
aria-live="polite"
- class="theme-toggle cursor-pointer border-none bg-none"
+ class="theme-toggle cursor-pointer border-none bg-none hover:animate-[shrink_0.3s_ease-in-out]"
v-bind="bindProps"
@click.stop="toggleTheme"
>
diff --git a/packages/effects/plugins/package.json b/packages/effects/plugins/package.json
index edbb7284..add6e78e 100644
--- a/packages/effects/plugins/package.json
+++ b/packages/effects/plugins/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/plugins",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/effects/plugins/src/echarts/use-echarts.ts b/packages/effects/plugins/src/echarts/use-echarts.ts
index d2e96ebb..22a791a2 100644
--- a/packages/effects/plugins/src/echarts/use-echarts.ts
+++ b/packages/effects/plugins/src/echarts/use-echarts.ts
@@ -1,24 +1,23 @@
-import type { EChartsOption } from 'echarts';
+import type { EChartsOption } from "echarts";
-import type { Ref } from 'vue';
+import type { Ref } from "vue";
+import { computed, nextTick, watch } from "vue";
-import type { Nullable } from '@vben/types';
+import type { Nullable } from "@vben/types";
-import type EchartsUI from './echarts-ui.vue';
+import type EchartsUI from "./echarts-ui.vue";
-import { computed, nextTick, watch } from 'vue';
-
-import { usePreferences } from '@vben/preferences';
+import { usePreferences } from "@vben/preferences";
import {
tryOnUnmounted,
useDebounceFn,
useResizeObserver,
useTimeoutFn,
- useWindowSize,
-} from '@vueuse/core';
+ useWindowSize
+} from "@vueuse/core";
-import echarts from './echarts';
+import echarts from "./echarts";
type EchartsUIType = typeof EchartsUI | undefined;
@@ -32,6 +31,21 @@ function useEcharts(chartRef: Ref) {
const { height, width } = useWindowSize();
const resizeHandler: () => void = useDebounceFn(resize, 200);
+ const getChartEl = (): HTMLElement | null => {
+ const refValue = chartRef?.value as unknown;
+ if (!refValue) return null;
+ if (refValue instanceof HTMLElement) {
+ return refValue;
+ }
+ const maybeComponent = refValue as { $el?: HTMLElement };
+ return maybeComponent.$el ?? null;
+ };
+
+ const isElHidden = (el: HTMLElement | null): boolean => {
+ if (!el) return true;
+ return el.offsetHeight === 0 || el.offsetWidth === 0;
+ };
+
const getOptions = computed((): EChartsOption => {
if (!isDark.value) {
return {};
@@ -54,7 +68,7 @@ function useEcharts(chartRef: Ref) {
const renderEcharts = (
options: EChartsOption,
- clear = true,
+ clear = true
): Promise> => {
cacheOptions = options;
const currentOptions = {
@@ -69,6 +83,13 @@ function useEcharts(chartRef: Ref) {
return;
}
nextTick(() => {
+ const el = getChartEl();
+ if (isElHidden(el)) {
+ useTimeoutFn(async () => {
+ resolve(await renderEcharts(currentOptions));
+ }, 30);
+ return;
+ }
useTimeoutFn(() => {
if (!chartInstance) {
const instance = initCharts();
@@ -83,6 +104,10 @@ function useEcharts(chartRef: Ref) {
};
function resize() {
+ const el = getChartEl();
+ if (isElHidden(el)) {
+ return;
+ }
chartInstance?.resize({
animation: {
duration: 300,
diff --git a/packages/effects/request/package.json b/packages/effects/request/package.json
index 5e6bb141..0b557d3f 100644
--- a/packages/effects/request/package.json
+++ b/packages/effects/request/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/request",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/effects/request/src/request-client/modules/sse.test.ts b/packages/effects/request/src/request-client/modules/sse.test.ts
new file mode 100644
index 00000000..4e8c6a9d
--- /dev/null
+++ b/packages/effects/request/src/request-client/modules/sse.test.ts
@@ -0,0 +1,142 @@
+import type { RequestClient } from '../request-client';
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { SSE } from './sse';
+
+// 模拟 TextDecoder
+const OriginalTextDecoder = globalThis.TextDecoder;
+
+beforeEach(() => {
+ vi.stubGlobal(
+ 'TextDecoder',
+ class {
+ private decoder = new OriginalTextDecoder();
+ decode(value: Uint8Array, opts?: any) {
+ return this.decoder.decode(value, opts);
+ }
+ },
+ );
+});
+
+// 创建 fetch mock
+const createFetchMock = (chunks: string[], ok = true) => {
+ const encoder = new TextEncoder();
+ let index = 0;
+ return vi.fn().mockResolvedValue({
+ ok,
+ status: ok ? 200 : 500,
+ body: {
+ getReader: () => ({
+ read: async () => {
+ if (index < chunks.length) {
+ return { done: false, value: encoder.encode(chunks[index++]) };
+ }
+ return { done: true, value: undefined };
+ },
+ }),
+ },
+ });
+};
+
+describe('sSE', () => {
+ let client: RequestClient;
+ let sse: SSE;
+
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ client = {
+ getBaseUrl: () => 'http://localhost',
+ instance: {
+ interceptors: {
+ request: {
+ handlers: [],
+ },
+ },
+ },
+ } as unknown as RequestClient;
+ sse = new SSE(client);
+ });
+
+ it('should call requestSSE when postSSE is used', async () => {
+ const spy = vi.spyOn(sse, 'requestSSE').mockResolvedValue(undefined);
+ await sse.postSSE('/test', { foo: 'bar' }, { headers: { a: '1' } });
+ expect(spy).toHaveBeenCalledWith(
+ '/test',
+ { foo: 'bar' },
+ {
+ headers: { a: '1' },
+ method: 'POST',
+ },
+ );
+ });
+
+ it('should throw error if fetch response not ok', async () => {
+ vi.stubGlobal('fetch', createFetchMock([], false));
+ await expect(sse.requestSSE('/bad')).rejects.toThrow(
+ 'HTTP error! status: 500',
+ );
+ });
+
+ it('should trigger onMessage and onEnd callbacks', async () => {
+ const messages: string[] = [];
+ const onMessage = vi.fn((msg: string) => messages.push(msg));
+ const onEnd = vi.fn();
+
+ vi.stubGlobal('fetch', createFetchMock(['hello', ' world']));
+
+ await sse.requestSSE('/sse', undefined, { onMessage, onEnd });
+
+ expect(onMessage).toHaveBeenCalledTimes(2);
+ expect(messages.join('')).toBe('hello world');
+ // onEnd 不再带参数
+ expect(onEnd).toHaveBeenCalled();
+ });
+
+ it('should apply request interceptors', async () => {
+ const interceptor = vi.fn(async (config) => {
+ config.headers['x-test'] = 'intercepted';
+ return config;
+ });
+ (client.instance.interceptors.request as any).handlers.push({
+ fulfilled: interceptor,
+ });
+
+ // 创建 fetch mock,并挂到全局
+ const fetchMock = createFetchMock(['data']);
+ vi.stubGlobal('fetch', fetchMock);
+
+ await sse.requestSSE('/sse', undefined, {});
+
+ expect(interceptor).toHaveBeenCalled();
+ expect(fetchMock).toHaveBeenCalledWith(
+ 'http://localhost/sse',
+ expect.objectContaining({
+ headers: expect.any(Headers),
+ }),
+ );
+
+ const calls = fetchMock.mock?.calls;
+ expect(calls).toBeDefined();
+ expect(calls?.length).toBeGreaterThan(0);
+
+ const init = calls?.[0]?.[1] as RequestInit;
+ expect(init).toBeDefined();
+
+ const headers = init?.headers as Headers;
+ expect(headers?.get('x-test')).toBe('intercepted');
+ expect(headers?.get('accept')).toBe('text/event-stream');
+ });
+
+ it('should throw error when no reader', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ body: null,
+ }),
+ );
+ await expect(sse.requestSSE('/sse')).rejects.toThrow('No reader');
+ });
+});
diff --git a/packages/effects/request/src/request-client/modules/sse.ts b/packages/effects/request/src/request-client/modules/sse.ts
new file mode 100644
index 00000000..09d13017
--- /dev/null
+++ b/packages/effects/request/src/request-client/modules/sse.ts
@@ -0,0 +1,136 @@
+import type { AxiosRequestHeaders, InternalAxiosRequestConfig } from 'axios';
+
+import type { RequestClient } from '../request-client';
+import type { SseRequestOptions } from '../types';
+
+/**
+ * SSE模块
+ */
+class SSE {
+ private client: RequestClient;
+
+ constructor(client: RequestClient) {
+ this.client = client;
+ }
+
+ public async postSSE(
+ url: string,
+ data?: any,
+ requestOptions?: SseRequestOptions,
+ ) {
+ return this.requestSSE(url, data, {
+ ...requestOptions,
+ method: 'POST',
+ });
+ }
+
+ /**
+ * SSE请求方法
+ * @param url - 请求URL
+ * @param data - 请求数据
+ * @param requestOptions - SSE请求选项
+ */
+ public async requestSSE(
+ url: string,
+ data?: any,
+ requestOptions?: SseRequestOptions,
+ ) {
+ const baseUrl = this.client.getBaseUrl() || '';
+
+ let axiosConfig: InternalAxiosRequestConfig = {
+ url,
+ method: (requestOptions?.method as any) ?? 'GET',
+ headers: {} as AxiosRequestHeaders,
+ };
+ const requestInterceptors = this.client.instance.interceptors
+ .request as any;
+ if (
+ requestInterceptors.handlers &&
+ requestInterceptors.handlers.length > 0
+ ) {
+ for (const handler of requestInterceptors.handlers) {
+ if (typeof handler?.fulfilled === 'function') {
+ const next = await handler.fulfilled(axiosConfig as any);
+ if (next) axiosConfig = next as InternalAxiosRequestConfig;
+ }
+ }
+ }
+
+ const merged = new Headers();
+ Object.entries(
+ (axiosConfig.headers ?? {}) as Record,
+ ).forEach(([k, v]) => merged.set(k, String(v)));
+ if (requestOptions?.headers) {
+ new Headers(requestOptions.headers).forEach((v, k) => merged.set(k, v));
+ }
+ if (!merged.has('accept')) {
+ merged.set('accept', 'text/event-stream');
+ }
+
+ let bodyInit = requestOptions?.body ?? data;
+ const ct = (merged.get('content-type') || '').toLowerCase();
+ if (
+ bodyInit &&
+ typeof bodyInit === 'object' &&
+ !ArrayBuffer.isView(bodyInit as any) &&
+ !(bodyInit instanceof ArrayBuffer) &&
+ !(bodyInit instanceof Blob) &&
+ !(bodyInit instanceof FormData) &&
+ ct.includes('application/json')
+ ) {
+ bodyInit = JSON.stringify(bodyInit);
+ }
+ const requestInit: RequestInit = {
+ ...requestOptions,
+ method: axiosConfig.method,
+ headers: merged,
+ body: bodyInit,
+ };
+
+ const response = await fetch(safeJoinUrl(baseUrl, url), requestInit);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const reader = response.body?.getReader();
+ const decoder = new TextDecoder();
+
+ if (!reader) {
+ throw new Error('No reader');
+ }
+ let isEnd = false;
+ while (!isEnd) {
+ const { done, value } = await reader.read();
+ if (done) {
+ isEnd = true;
+ decoder.decode(new Uint8Array(0), { stream: false });
+ requestOptions?.onEnd?.();
+ reader.releaseLock?.();
+ break;
+ }
+ const content = decoder.decode(value, { stream: true });
+ requestOptions?.onMessage?.(content);
+ }
+ }
+}
+
+function safeJoinUrl(baseUrl: string | undefined, url: string): string {
+ if (!baseUrl) {
+ return url; // 没有 baseUrl,直接返回 url
+ }
+
+ // 如果 url 本身就是绝对地址,直接返回
+ if (/^https?:\/\//i.test(url)) {
+ return url;
+ }
+
+ // 如果 baseUrl 是完整 URL,就用 new URL
+ if (/^https?:\/\//i.test(baseUrl)) {
+ return new URL(url, baseUrl).toString();
+ }
+
+ // 否则,当作路径拼接
+ return `${baseUrl.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`;
+}
+
+export { SSE };
diff --git a/packages/effects/request/src/request-client/request-client.ts b/packages/effects/request/src/request-client/request-client.ts
index e5811673..453913b2 100644
--- a/packages/effects/request/src/request-client/request-client.ts
+++ b/packages/effects/request/src/request-client/request-client.ts
@@ -9,6 +9,7 @@ import qs from 'qs';
import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor';
+import { SSE } from './modules/sse';
import { FileUploader } from './modules/uploader';
function getParamsSerializer(
@@ -41,12 +42,14 @@ class RequestClient {
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
public download: FileDownloader['download'];
+ public readonly instance: AxiosInstance;
// 是否正在刷新token
public isRefreshing = false;
+ public postSSE: SSE['postSSE'];
// 刷新token队列
public refreshTokenQueue: ((token: string) => void)[] = [];
+ public requestSSE: SSE['requestSSE'];
public upload: FileUploader['upload'];
- private readonly instance: AxiosInstance;
/**
* 构造函数,用于创建Axios实例
@@ -84,6 +87,10 @@ class RequestClient {
// 实例化文件下载器
const fileDownloader = new FileDownloader(this);
this.download = fileDownloader.download.bind(fileDownloader);
+ // 实例化SSE模块
+ const sse = new SSE(this);
+ this.postSSE = sse.postSSE.bind(sse);
+ this.requestSSE = sse.requestSSE.bind(sse);
}
/**
@@ -103,6 +110,13 @@ class RequestClient {
return this.request(url, { ...config, method: 'GET' });
}
+ /**
+ * 获取基础URL
+ */
+ public getBaseUrl() {
+ return this.instance.defaults.baseURL;
+ }
+
/**
* POST请求方法
*/
diff --git a/packages/effects/request/src/request-client/types.ts b/packages/effects/request/src/request-client/types.ts
index 494741dc..d40ee8a5 100644
--- a/packages/effects/request/src/request-client/types.ts
+++ b/packages/effects/request/src/request-client/types.ts
@@ -41,6 +41,14 @@ type RequestContentType =
type RequestClientOptions = CreateAxiosDefaults & ExtendOptions;
+/**
+ * SSE 请求选项
+ */
+interface SseRequestOptions extends RequestInit {
+ onMessage?: (message: string) => void;
+ onEnd?: () => void;
+}
+
interface RequestInterceptorConfig {
fulfilled?: (
config: ExtendOptions & InternalAxiosRequestConfig,
@@ -78,4 +86,5 @@ export type {
RequestInterceptorConfig,
RequestResponse,
ResponseInterceptorConfig,
+ SseRequestOptions,
};
diff --git a/packages/icons/package.json b/packages/icons/package.json
index 8ac80940..77fa0c4f 100644
--- a/packages/icons/package.json
+++ b/packages/icons/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/icons",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/icons/src/iconify/index.ts b/packages/icons/src/iconify/index.ts
index bec4d5a5..4e648be0 100644
--- a/packages/icons/src/iconify/index.ts
+++ b/packages/icons/src/iconify/index.ts
@@ -3,13 +3,3 @@ import { createIconifyIcon } from '@vben-core/icons';
export * from '@vben-core/icons';
export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');
-
-export const MdiWechat = createIconifyIcon('mdi:wechat');
-
-export const MdiGithub = createIconifyIcon('mdi:github');
-
-export const MdiGoogle = createIconifyIcon('mdi:google');
-
-export const MdiQqchat = createIconifyIcon('mdi:qqchat');
-
-export const RiDingding = createIconifyIcon('ri:dingding-fill');
diff --git a/packages/icons/src/svg/icons/dingding.svg b/packages/icons/src/svg/icons/dingding.svg
new file mode 100644
index 00000000..c2fc786b
--- /dev/null
+++ b/packages/icons/src/svg/icons/dingding.svg
@@ -0,0 +1 @@
+
diff --git a/packages/icons/src/svg/icons/github.svg b/packages/icons/src/svg/icons/github.svg
new file mode 100644
index 00000000..39d092d9
--- /dev/null
+++ b/packages/icons/src/svg/icons/github.svg
@@ -0,0 +1 @@
+
diff --git a/packages/icons/src/svg/icons/google.svg b/packages/icons/src/svg/icons/google.svg
new file mode 100644
index 00000000..f2ad0a40
--- /dev/null
+++ b/packages/icons/src/svg/icons/google.svg
@@ -0,0 +1 @@
+
diff --git a/packages/icons/src/svg/icons/qqchat.svg b/packages/icons/src/svg/icons/qqchat.svg
new file mode 100644
index 00000000..ac9a7f03
--- /dev/null
+++ b/packages/icons/src/svg/icons/qqchat.svg
@@ -0,0 +1 @@
+
diff --git a/packages/icons/src/svg/icons/wechat.svg b/packages/icons/src/svg/icons/wechat.svg
new file mode 100644
index 00000000..346b0379
--- /dev/null
+++ b/packages/icons/src/svg/icons/wechat.svg
@@ -0,0 +1 @@
+
diff --git a/packages/icons/src/svg/index.ts b/packages/icons/src/svg/index.ts
index 3b5cb995..accf9582 100644
--- a/packages/icons/src/svg/index.ts
+++ b/packages/icons/src/svg/index.ts
@@ -11,6 +11,11 @@ const SvgCardIcon = createIconifyIcon('svg:card');
const SvgBellIcon = createIconifyIcon('svg:bell');
const SvgCakeIcon = createIconifyIcon('svg:cake');
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
+const SvgGithubIcon = createIconifyIcon('svg:github');
+const SvgGoogleIcon = createIconifyIcon('svg:google');
+const SvgQQChatIcon = createIconifyIcon('svg:qqchat');
+const SvgWeChatIcon = createIconifyIcon('svg:wechat');
+const SvgDingDingIcon = createIconifyIcon('svg:dingding');
export {
SvgAntdvLogoIcon,
@@ -21,5 +26,10 @@ export {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
+ SvgDingDingIcon,
SvgDownloadIcon,
+ SvgGithubIcon,
+ SvgGoogleIcon,
+ SvgQQChatIcon,
+ SvgWeChatIcon,
};
diff --git a/packages/locales/package.json b/packages/locales/package.json
index 281df25a..7d479159 100644
--- a/packages/locales/package.json
+++ b/packages/locales/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/locales",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/locales/src/langs/en-US/authentication.json b/packages/locales/src/langs/en-US/authentication.json
index ec9b8ca7..7dd34622 100644
--- a/packages/locales/src/langs/en-US/authentication.json
+++ b/packages/locales/src/langs/en-US/authentication.json
@@ -51,6 +51,10 @@
"sendCode": "Get Security code",
"sendText": "Resend in {0}s",
"thirdPartyLogin": "Or continue with",
+ "weChat": "WeChat",
+ "qq": "QQ",
+ "gitHub": "GitHub",
+ "google": "Google",
"loginAgainTitle": "Please Log In Again",
"loginAgainSubTitle": "Your login session has expired. Please log in again to continue.",
"layout": {
diff --git a/packages/locales/src/langs/en-US/preferences.json b/packages/locales/src/langs/en-US/preferences.json
index cda24a4c..b0bbe374 100644
--- a/packages/locales/src/langs/en-US/preferences.json
+++ b/packages/locales/src/langs/en-US/preferences.json
@@ -1,6 +1,8 @@
{
"title": "Preferences",
"subtitle": "Customize Preferences & Preview in Real Time",
+ "enableStickyPreferencesNavigationBar": "Enable sticky preferences navigation bar",
+ "disableStickyPreferencesNavigationBar": "Disable sticky preferences navigation bar",
"resetTip": "Data has changed, click to reset",
"resetTitle": "Reset Preferences",
"resetSuccess": "Preferences reset successfully",
@@ -37,6 +39,7 @@
"language": "Language",
"dynamicTitle": "Dynamic Title",
"watermark": "Watermark",
+ "watermarkContent": "Please input Watermark content",
"checkUpdates": "Periodic update check",
"position": {
"title": "Preferences Postion",
diff --git a/packages/locales/src/langs/zh-CN/authentication.json b/packages/locales/src/langs/zh-CN/authentication.json
index ee4a2ec8..ade52ac2 100644
--- a/packages/locales/src/langs/zh-CN/authentication.json
+++ b/packages/locales/src/langs/zh-CN/authentication.json
@@ -51,6 +51,10 @@
"sendCode": "获取验证码",
"sendText": "{0}秒后重新获取",
"thirdPartyLogin": "其他登录方式",
+ "weChat": "微信",
+ "qq": "QQ",
+ "gitHub": "GitHub",
+ "google": "Google",
"loginAgainTitle": "重新登录",
"loginAgainSubTitle": "您的登录状态已过期,请重新登录以继续。",
"layout": {
diff --git a/packages/locales/src/langs/zh-CN/preferences.json b/packages/locales/src/langs/zh-CN/preferences.json
index cd9e15be..de8413df 100644
--- a/packages/locales/src/langs/zh-CN/preferences.json
+++ b/packages/locales/src/langs/zh-CN/preferences.json
@@ -1,6 +1,8 @@
{
"title": "偏好设置",
"subtitle": "自定义偏好设置 & 实时预览",
+ "enableStickyPreferencesNavigationBar": "开启首选项导航栏吸顶效果",
+ "disableStickyPreferencesNavigationBar": "关闭首选项导航栏吸顶效果",
"resetTitle": "重置偏好设置",
"resetTip": "数据有变化,点击可进行重置",
"resetSuccess": "重置偏好设置成功",
@@ -37,6 +39,7 @@
"language": "语言",
"dynamicTitle": "动态标题",
"watermark": "水印",
+ "watermarkContent": "请输入水印文案",
"checkUpdates": "定时检查更新",
"position": {
"title": "偏好设置位置",
diff --git a/packages/preferences/package.json b/packages/preferences/package.json
index e3334f33..e6e52e23 100644
--- a/packages/preferences/package.json
+++ b/packages/preferences/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/preferences",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/stores/package.json b/packages/stores/package.json
index bb5796f4..72bcf497 100644
--- a/packages/stores/package.json
+++ b/packages/stores/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/stores",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/styles/package.json b/packages/styles/package.json
index 960842da..0716a5e5 100644
--- a/packages/styles/package.json
+++ b/packages/styles/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/styles",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/types/package.json b/packages/types/package.json
index 74a4b547..07dbca0e 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/types",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 447f3b6c..faa7b5d5 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/utils",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/playground/package.json b/playground/package.json
index c918f2de..c8651563 100644
--- a/playground/package.json
+++ b/playground/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/playground",
- "version": "5.5.8",
+ "version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
diff --git a/playground/src/layouts/basic.vue b/playground/src/layouts/basic.vue
index 227ce955..05787651 100644
--- a/playground/src/layouts/basic.vue
+++ b/playground/src/layouts/basic.vue
@@ -6,7 +6,7 @@ import { computed, onBeforeMount, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import {
BasicLayout,
LockScreen,
@@ -89,7 +89,7 @@ const menus = computed(() => [
target: '_blank',
});
},
- icon: MdiGithub,
+ icon: SvgGithubIcon,
text: 'GitHub',
},
{
@@ -122,11 +122,16 @@ function handleMakeAll() {
function handleClickLogo() {}
watch(
- () => preferences.app.watermark,
- async (enable) => {
+ () => ({
+ enable: preferences.app.watermark,
+ content: preferences.app.watermarkContent,
+ }),
+ async ({ enable, content }) => {
if (enable) {
await updateWatermark({
- content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+ content:
+ content ||
+ `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();
diff --git a/playground/src/locales/langs/zh-CN/system.json b/playground/src/locales/langs/zh-CN/system.json
index b0f5e7fa..be5a7ae3 100644
--- a/playground/src/locales/langs/zh-CN/system.json
+++ b/playground/src/locales/langs/zh-CN/system.json
@@ -1,4 +1,5 @@
{
+ "title": "系统管理",
"dept": {
"list": "部门列表",
"createTime": "创建时间",
@@ -62,6 +63,5 @@
"operation": "操作",
"permissions": "权限",
"setPermissions": "授权"
- },
- "title": "系统管理"
+ }
}
diff --git a/playground/src/views/demos/features/icons/index.vue b/playground/src/views/demos/features/icons/index.vue
index 590ff305..994c9955 100644
--- a/playground/src/views/demos/features/icons/index.vue
+++ b/playground/src/views/demos/features/icons/index.vue
@@ -3,11 +3,7 @@ import { h, ref } from 'vue';
import { IconPicker, Page } from '@vben/common-ui';
import {
- MdiGithub,
- MdiGoogle,
MdiKeyboardEsc,
- MdiQqchat,
- MdiWechat,
SvgAvatar1Icon,
SvgAvatar2Icon,
SvgAvatar3Icon,
@@ -16,6 +12,10 @@ import {
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
+ SvgGithubIcon,
+ SvgGoogleIcon,
+ SvgQQChatIcon,
+ SvgWeChatIcon,
} from '@vben/icons';
import { Card, Input } from 'ant-design-vue';
@@ -46,10 +46,10 @@ const inputComponent = h(Input);
-
-
-
-
+
+
+
+
diff --git a/playground/src/views/demos/features/watermark/index.vue b/playground/src/views/demos/features/watermark/index.vue
index 77b3f179..27875fcc 100644
--- a/playground/src/views/demos/features/watermark/index.vue
+++ b/playground/src/views/demos/features/watermark/index.vue
@@ -8,7 +8,7 @@ const { destroyWatermark, updateWatermark, watermark } = useWatermark();
async function recreateWaterMark() {
destroyWatermark();
- await updateWatermark({});
+ await createWaterMark();
}
async function createWaterMark() {
diff --git a/playground/src/views/examples/form/query.vue b/playground/src/views/examples/form/query.vue
index c9595fb9..5efae67b 100644
--- a/playground/src/views/examples/form/query.vue
+++ b/playground/src/views/examples/form/query.vue
@@ -86,6 +86,62 @@ const [QueryForm] = useVbenForm({
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
+const [InlineForm] = useVbenForm({
+ layout: 'inline',
+ schema: [
+ {
+ // 组件需要在 #/adapter.ts内注册,并加上类型
+ component: 'Input',
+ // 对应组件的参数
+ componentProps: {
+ placeholder: '请输入用户名',
+ },
+ // 字段名
+ fieldName: 'username',
+ // 界面显示的label
+ label: '字符串',
+ },
+ {
+ component: 'InputPassword',
+ componentProps: {
+ placeholder: '请输入密码',
+ },
+ fieldName: 'password',
+ label: '密码',
+ },
+ {
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入',
+ },
+ fieldName: 'number',
+ label: '数字(带后缀)',
+ suffix: () => '¥',
+ },
+ {
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ filterOption: true,
+ options: [
+ {
+ label: '选项1',
+ value: '1',
+ },
+ {
+ label: '选项2',
+ value: '2',
+ },
+ ],
+ placeholder: '请选择',
+ showSearch: true,
+ },
+ fieldName: 'options',
+ label: '下拉选',
+ },
+ ],
+});
+
const [QueryForm1] = useVbenForm({
// 默认展开
collapsed: true,
@@ -205,6 +261,10 @@ function onSubmit(values: Record) {
+
+
+
+
diff --git a/playground/src/views/examples/layout/col-page.vue b/playground/src/views/examples/layout/col-page.vue
index 5b18330e..5070ac3d 100644
--- a/playground/src/views/examples/layout/col-page.vue
+++ b/playground/src/views/examples/layout/col-page.vue
@@ -22,8 +22,8 @@ const props = reactive({
leftWidth: 30,
resizable: true,
rightWidth: 70,
- splitHandle: false,
- splitLine: false,
+ splitHandle: true,
+ splitLine: true,
});
const leftMinWidth = ref(props.leftMinWidth || 1);
const leftMaxWidth = ref(props.leftMaxWidth || 100);
@@ -42,7 +42,11 @@ const leftMaxWidth = ref(props.leftMaxWidth || 100);
-