Browse Source

Merge branch 'main' into main

pull/8019/head
abc_abc 4 months ago
committed by GitHub
parent
commit
e7f541f37e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .github/workflows/codeql.yml
  2. 2
      .github/workflows/issue-close-require.yml
  3. 2
      .github/workflows/lock.yml
  4. 2
      .github/workflows/semantic-pull-request.yml
  5. 2
      .github/workflows/stale.yml
  6. 2
      .node-version
  7. 652
      apps/web-antdv-next/src/adapter/component/index.ts
  8. 1
      apps/web-antdv-next/src/api/core/index.ts
  9. 25
      apps/web-antdv-next/src/api/core/upload.ts
  10. 2
      package.json
  11. 4
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  12. 28
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  13. 58
      packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue
  14. 22
      packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogOverlay.vue
  15. 2
      packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogScrollContent.vue
  16. 2
      playground/src/adapter/form.ts
  17. 1
      playground/src/views/examples/modal/form-modal-demo.vue
  18. 2
      playground/src/views/examples/modal/index.vue
  19. 2076
      pnpm-lock.yaml
  20. 58
      pnpm-workspace.yaml

4
.github/workflows/codeql.yml

@ -61,7 +61,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -89,6 +89,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

2
.github/workflows/issue-close-require.yml

@ -19,7 +19,7 @@ jobs:
steps: steps:
# 关闭未活动的 Issues # 关闭未活动的 Issues
- name: Close Inactive Issues - name: Close Inactive Issues
uses: actions/stale@v9 uses: actions/stale@v10
with: with:
days-before-stale: -1 # Issues and PR will never be flagged stale automatically. days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
stale-issue-label: needs-reproduction # Label that flags an issue as stale. stale-issue-label: needs-reproduction # Label that flags an issue as stale.

2
.github/workflows/lock.yml

@ -14,7 +14,7 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v6
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14' issue-inactive-days: '14'

2
.github/workflows/semantic-pull-request.yml

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@v5 uses: amannn/action-semantic-pull-request@v6
with: with:
wip: true wip: true
subjectPattern: ^(?![A-Z]).+$ subjectPattern: ^(?![A-Z]).+$

2
.github/workflows/stale.yml

@ -9,7 +9,7 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v10
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'

2
.node-version

@ -1 +1 @@
22.22.0 24.16.0

652
apps/web-antdv-next/src/adapter/component/index.ts

@ -36,8 +36,11 @@ import type { Component, Ref } from 'vue';
import type { import type {
ApiComponentSharedProps, ApiComponentSharedProps,
BaseFormComponentType, BaseFormComponentType,
CollapsibleParamsProps,
IconPickerProps, IconPickerProps,
} from '@vben/common-ui'; } from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { TipTapProps } from '@vben/plugins/tiptap';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import { import {
@ -45,6 +48,9 @@ import {
defineAsyncComponent, defineAsyncComponent,
defineComponent, defineComponent,
h, h,
nextTick,
onMounted,
onUnmounted,
ref, ref,
render, render,
unref, unref,
@ -55,19 +61,25 @@ import {
ApiComponent, ApiComponent,
globalShareState, globalShareState,
IconPicker, IconPicker,
VbenCollapsibleParams,
VCropper, VCropper,
} from '@vben/common-ui'; } from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { VbenTiptap } from '@vben/plugins/tiptap';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { message, Modal, notification } from 'antdv-next'; import { message, Modal, notification } from 'antdv-next';
import { upload_file } from '#/api';
type AdapterUploadProps = UploadProps & { type AdapterUploadProps = UploadProps & {
aspectRatio?: string; aspectRatio?: string;
crop?: boolean; crop?: boolean;
draggable?: boolean;
handleChange?: (event: UploadChangeParam) => void; handleChange?: (event: UploadChangeParam) => void;
maxSize?: number; maxSize?: number;
onDragSort?: (oldIndex: number, newIndex: number) => void;
onHandleChange?: (event: UploadChangeParam) => void; onHandleChange?: (event: UploadChangeParam) => void;
}; };
@ -80,8 +92,8 @@ const Button = defineAsyncComponent(
const Checkbox = defineAsyncComponent( const Checkbox = defineAsyncComponent(
() => import('antdv-next/dist/checkbox/index'), () => import('antdv-next/dist/checkbox/index'),
); );
const CheckboxGroup = defineAsyncComponent( const CheckboxGroup = defineAsyncComponent(() =>
() => import('antdv-next/dist/checkbox/Group'), import('antdv-next/dist/checkbox/index').then((res) => res.CheckboxGroup),
); );
const DatePicker = defineAsyncComponent( const DatePicker = defineAsyncComponent(
() => import('antdv-next/dist/date-picker/index'), () => import('antdv-next/dist/date-picker/index'),
@ -170,260 +182,263 @@ const withDefaultPlaceholder = (
}); });
}; };
const withPreviewUpload = () => { const IMAGE_EXTENSIONS = new Set([
// 检查是否为图片文件的辅助函数 'bmp',
const isImageFile = (file: UploadFile): boolean => { 'gif',
const imageExtensions = new Set([ 'jpeg',
'bmp', 'jpg',
'gif', 'png',
'jpeg', 'svg',
'jpg', 'webp',
'png', ]);
'svg',
'webp', /**
]); * 检查是否为图片文件
if (file.url) { */
try { function isImageFile(file: UploadFile): boolean {
const pathname = new URL(file.url, 'http://localhost').pathname; if (file.url) {
const ext = pathname.split('.').pop()?.toLowerCase(); try {
return ext ? imageExtensions.has(ext) : false; const pathname = new URL(file.url, 'http://localhost').pathname;
} catch { const ext = pathname.split('.').pop()?.toLowerCase();
const ext = file.url?.split('.').pop()?.toLowerCase(); return ext ? IMAGE_EXTENSIONS.has(ext) : false;
return ext ? imageExtensions.has(ext) : false; } catch {
} const ext = file.url?.split('.').pop()?.toLowerCase();
} return ext ? IMAGE_EXTENSIONS.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 (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
/**
* 创建默认的上传按钮插槽
*/
function createDefaultUploadSlots(listType: string, placeholder: string) {
if (listType === 'picture-card') {
return { default: () => placeholder };
}
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
}; };
// 创建默认的上传按钮插槽 }
const createDefaultSlotsWithUpload = (
listType: string, /**
placeholder: string, * 获取文件的 Base64
) => { */
switch (listType) { function getBase64(file: File): Promise<string> {
case 'picture-card': { return new Promise((resolve, reject) => {
return { const reader = new FileReader();
default: () => placeholder, reader.readAsDataURL(file);
}; reader.addEventListener('load', () => resolve(reader.result as string));
} reader.addEventListener('error', reject);
default: { });
return { }
default: () =>
h( /**
Button, * 预览图片
{ */
icon: h(IconifyIcon, { async function previewImage(
icon: 'ant-design:upload-outlined', file: UploadFile,
class: 'mb-1 size-4', open: Ref<boolean>,
}), fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
open: open.value,
current: currentIndex,
onOpenChange: (value: boolean) => {
open.value = value;
if (!value) {
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
}, },
() => placeholder, },
},
() =>
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
), ),
}; );
} };
} },
}; };
// 构建预览图片组
const previewImage = async (
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
// 如果当前文件不是图片,直接打开
if (!isImageFile(file)) {
if (file.url) {
window.open(file.url, '_blank');
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
// 对于图片文件,继续使用预览组 render(h(PreviewWrapper), container);
const [ImageComponent, PreviewGroupComponent] = await Promise.all([ }
Image,
PreviewGroup, /**
]); * 图片裁剪操作
*/
const getBase64 = (file: File) => { function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise((resolve, reject) => { return new Promise<Blob | string | undefined>((resolve, reject) => {
const reader = new FileReader(); const container = document.createElement('div');
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); document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false; let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const PreviewWrapper = { function closeModal() {
open.value = false;
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
const CropperWrapper = {
setup() { setup() {
return () => { return () => {
if (isUnmounted) return null; if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h( return h(
PreviewGroupComponent, Modal,
{ {
class: 'hidden', open: open.value,
preview: { title: h('div', {}, [
open: visible.value, $t('ui.crop.title'),
// 设置初始显示的图片索引 h(
current: imageFiles.findIndex((f) => f.uid === file.uid), 'span',
onOpenChange: (value: boolean) => { {
visible.value = value; class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
if (!value) { },
// 延迟清理,确保动画完成 $t('ui.crop.titleTip', [aspectRatio]),
setTimeout(() => { ),
if (!isUnmounted && container) { ]),
isUnmounted = true; centered: true,
render(null, container); width: 548,
container.remove(); keyboard: false,
} maskClosable: false,
}, 300); closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnHidden: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
if (dataUrl) {
resolve(dataUrl);
} else {
reject(new Error($t('ui.crop.errorTip')));
} }
}, } catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
}, },
}, },
() => () =>
// 渲染所有图片文件 h(VCropper, {
imageFiles.map((imgFile) => ref: (ref: any) => (cropperRef.value = ref),
h(ImageComponent, { img: objectUrl as string,
key: imgFile.uid, aspectRatio,
src: imgFile.url || imgFile.preview, }),
}),
),
); );
}; };
}, },
}; };
render(h(PreviewWrapper), container); render(h(CropperWrapper), container);
}; });
}
// 图片裁剪操作
const cropImage = (file: File, aspectRatio: string | undefined) => {
return new Promise((resolve, reject) => {
const container: HTMLElement | null = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
// 延迟清理,确保动画完成
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
Modal,
{
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnHidden: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
resolve(dataUrl);
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(CropperWrapper), container);
});
};
/**
* 带预览功能的上传组件
*/
function withPreviewUpload() {
return defineComponent({ return defineComponent({
name: 'AUpload', name: Upload.name,
emits: ['update:modelValue'], emits: ['update:modelValue'],
setup: ( setup(
props: any, props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any }, { attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => { ) {
const previewVisible = ref<boolean>(false); const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const listType = attrs?.listType || attrs?.['list-type'] || 'text'; const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>( const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [], attrs?.fileList || attrs?.['file-list'] || [],
); );
@ -433,16 +448,18 @@ const withPreviewUpload = () => {
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'], () => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
); );
const handleBeforeUpload = async ( async function handleBeforeUpload(
file: UploadFile, file: UploadFile,
originFileList: Array<File>, originFileList: Array<File>,
) => { ) {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) { if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value])); message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed'; file.status = 'removed';
return false; return false;
} }
// 多选或者非图片不唤起裁剪框
// 图片裁剪处理
if ( if (
attrs.crop && attrs.crop &&
!attrs.multiple && !attrs.multiple &&
@ -450,27 +467,21 @@ const withPreviewUpload = () => {
isImageFile(file) isImageFile(file)
) { ) {
file.status = 'removed'; file.status = 'removed';
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
const blob = await cropImage(originFileList[0], aspectRatio.value); const blob = await cropImage(originFileList[0], aspectRatio.value);
return new Promise((resolve, reject) => { if (!blob) {
if (!blob) { throw new Error($t('ui.crop.errorTip'));
return reject(new Error($t('ui.crop.errorTip'))); }
} return blob;
resolve(blob);
});
} }
return attrs.beforeUpload?.(file) ?? true; return attrs.beforeUpload?.(file) ?? true;
}; }
const handleChange = (event: UploadChangeParam) => { function handleChange(event: UploadChangeParam) {
try { try {
// 行内写法 handleChange: (event) => {}
attrs.handleChange?.(event); attrs.handleChange?.(event);
// template写法 @handle-change="(event) => {}"
attrs.onHandleChange?.(event); attrs.onHandleChange?.(event);
} catch (error) { } catch (error) {
// Avoid breaking internal v-model sync on user handler errors
console.error(error); console.error(error);
} }
fileList.value = event.fileList.filter( fileList.value = event.fileList.filter(
@ -480,28 +491,95 @@ const withPreviewUpload = () => {
'update:modelValue', 'update:modelValue',
event.fileList?.length ? fileList.value : undefined, event.fileList?.length ? fileList.value : undefined,
); );
}; }
const handlePreview = async (file: UploadFile) => { function handlePreview(file: UploadFile) {
previewVisible.value = true; previewVisible.value = true;
await previewImage(file, previewVisible, fileList); return previewImage(file, previewVisible, fileList);
}; }
const renderUploadButton = (): any => { function renderUploadButton() {
const isDisabled = attrs.disabled; if (attrs.disabled) return null;
return isEmpty(slots)
? createDefaultUploadSlots(listType, placeholder)
: slots;
}
// 如果禁用,不渲染上传按钮 // 拖拽排序
if (isDisabled) { const draggable = computed(
return null; () => (attrs.draggable ?? false) && !attrs.disabled,
);
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const sortableInstance = ref<null | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head.append(style);
} }
}
// 否则渲染默认上传按钮 function removeDragStyle() {
return isEmpty(slots) document.querySelector(`[id="${styleId}"]`)?.remove();
? createDefaultSlotsWithUpload(listType, placeholder) }
: slots;
}; async function initSortable(retryCount = 0) {
if (!draggable.value) return;
injectDragStyle();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
const container = document.querySelector(
`[data-upload-id="${uploadId}"] .ant-upload-list`,
) as HTMLElement;
if (!container) {
if (retryCount < 5) {
setTimeout(() => initSortable(retryCount + 1), 200);
}
return;
}
const { initializeSortable } = useSortable(container, {
animation: 300,
delay: 400,
delayOnTouchOnly: true,
filter:
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
const list = [...(fileList.value || [])];
const [movedItem] = list.splice(oldIndex, 1);
if (movedItem) {
list.splice(newIndex, 0, movedItem);
fileList.value = list;
}
attrs.onDragSort?.(oldIndex, newIndex);
emit('update:modelValue', fileList.value);
},
});
sortableInstance.value = await initializeSortable();
}
// 可以监听到表单API设置的值 // 监听表单值变化
watch( watch(
() => attrs.modelValue, () => attrs.modelValue,
(res) => { (res) => {
@ -509,22 +587,32 @@ const withPreviewUpload = () => {
}, },
); );
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () => return () =>
h( h(
Upload, 'div',
{ { 'data-upload-id': uploadId, class: 'w-full' },
...props, h(
...attrs, Upload,
fileList: fileList.value, {
beforeUpload: handleBeforeUpload, ...props,
onChange: handleChange, ...attrs,
onPreview: handlePreview, fileList: fileList.value,
}, beforeUpload: handleBeforeUpload,
renderUploadButton(), onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
); );
}, },
}); });
}; }
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
@ -535,6 +623,7 @@ export type ComponentType =
| 'Cascader' | 'Cascader'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'CollapsibleParams'
| 'DatePicker' | 'DatePicker'
| 'DefaultButton' | 'DefaultButton'
| 'Divider' | 'Divider'
@ -548,6 +637,7 @@ export type ComponentType =
| 'RadioGroup' | 'RadioGroup'
| 'RangePicker' | 'RangePicker'
| 'Rate' | 'Rate'
| 'RichEditor'
| 'Select' | 'Select'
| 'Space' | 'Space'
| 'Switch' | 'Switch'
@ -568,6 +658,7 @@ export interface ComponentPropsMap {
Cascader: CascaderProps; Cascader: CascaderProps;
Checkbox: CheckboxProps; Checkbox: CheckboxProps;
CheckboxGroup: CheckboxGroupProps; CheckboxGroup: CheckboxGroupProps;
CollapsibleParams: CollapsibleParamsProps;
DatePicker: DatePickerProps; DatePicker: DatePickerProps;
DefaultButton: ButtonProps; DefaultButton: ButtonProps;
Divider: DividerProps; Divider: DividerProps;
@ -581,6 +672,7 @@ export interface ComponentPropsMap {
RadioGroup: RadioGroupProps; RadioGroup: RadioGroupProps;
RangePicker: RangePickerProps; RangePicker: RangePickerProps;
Rate: RateProps; Rate: RateProps;
RichEditor: TipTapProps;
Select: SelectProps; Select: SelectProps;
Space: SpaceProps; Space: SpaceProps;
Switch: SwitchProps; Switch: SwitchProps;
@ -601,13 +693,13 @@ async function initComponentAdapter() {
fieldNames: { label: 'label', value: 'value', children: 'children' }, fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon', loadingSlot: 'suffixIcon',
modelPropName: 'value', modelPropName: 'value',
visibleEvent: 'onVisibleChange', visibleEvent: 'onOpenChange',
}), }),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', { ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: Select, component: Select,
loadingSlot: 'suffixIcon', loadingSlot: 'suffixIcon',
modelPropName: 'value', modelPropName: 'value',
visibleEvent: 'onVisibleChange', visibleEvent: 'onOpenChange',
}), }),
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', { ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: TreeSelect, component: TreeSelect,
@ -615,7 +707,7 @@ async function initComponentAdapter() {
loadingSlot: 'suffixIcon', loadingSlot: 'suffixIcon',
modelPropName: 'value', modelPropName: 'value',
optionsPropName: 'treeData', optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange', visibleEvent: 'onOpenChange',
}), }),
AutoComplete, AutoComplete,
Cascader, Cascader,
@ -646,6 +738,27 @@ async function initComponentAdapter() {
RadioGroup, RadioGroup,
RangePicker, RangePicker,
Rate, Rate,
RichEditor: withDefaultPlaceholder(VbenTiptap, 'input', {
imageUpload: {
upload: (file: any, onProgress: any) => {
return new Promise((resolve, reject) => {
upload_file({
file,
onProgress({ percent }) {
onProgress?.(percent);
},
onSuccess(response) {
// 从响应中提取图片URL
resolve(response?.data?.url ?? response?.url ?? '');
},
onError() {
reject(new Error($t('ui.tiptap.upload.uploadFailed')));
},
});
});
},
},
}),
Select: withDefaultPlaceholder(Select, 'select'), Select: withDefaultPlaceholder(Select, 'select'),
Space, Space,
Switch, Switch,
@ -653,6 +766,7 @@ async function initComponentAdapter() {
TimePicker, TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload: withPreviewUpload(), Upload: withPreviewUpload(),
CollapsibleParams: VbenCollapsibleParams,
}; };
// 将组件注册到全局共享状态中 // 将组件注册到全局共享状态中

1
apps/web-antdv-next/src/api/core/index.ts

@ -1,3 +1,4 @@
export * from './auth'; export * from './auth';
export * from './menu'; export * from './menu';
export * from './upload';
export * from './user'; export * from './user';

25
apps/web-antdv-next/src/api/core/upload.ts

@ -0,0 +1,25 @@
import { requestClient } from '#/api/request';
interface UploadFileParams {
file: File;
onError?: (error: Error) => void;
onProgress?: (progress: { percent: number }) => void;
onSuccess?: (data: any, file: File) => void;
}
export async function upload_file({
file,
onError,
onProgress,
onSuccess,
}: UploadFileParams) {
try {
onProgress?.({ percent: 0 });
const data = await requestClient.upload('/upload', { file });
onProgress?.({ percent: 100 });
onSuccess?.(data, file);
} catch (error) {
onError?.(error instanceof Error ? error : new Error(String(error)));
}
}

2
package.json

@ -106,5 +106,5 @@
"node": "^22.18.0 || ^24.0.0", "node": "^22.18.0 || ^24.0.0",
"pnpm": ">=11.0.0" "pnpm": ">=11.0.0"
}, },
"packageManager": "pnpm@11.5.0" "packageManager": "pnpm@11.5.2"
} }

4
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@ -67,7 +67,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{ const emit = defineEmits<{
sideMouseLeave: []; sideMouseLeave: [];
toggleSidebar: []; toggleSidebar: [];
'update:sidebar-width': [value: number]; 'update:sidebarWidth': [value: number];
}>(); }>();
const sidebarDraggable = defineModel<boolean>('sidebarDraggable', { const sidebarDraggable = defineModel<boolean>('sidebarDraggable', {
default: true, default: true,
@ -520,7 +520,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
:width="getSidebarWidth" :width="getSidebarWidth"
:z-index="sidebarZIndex" :z-index="sidebarZIndex"
@leave="() => emit('sideMouseLeave')" @leave="() => emit('sideMouseLeave')"
@update:width="(val) => emit('update:sidebar-width', val)" @update:width="(val) => emit('update:sidebarWidth', val)"
> >
<template v-if="isSideMode && !isMixedNav" #logo> <template v-if="isSideMode && !isMixedNav" #logo>
<slot name="logo"></slot> <slot name="logo"></slot>

28
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@ -1,7 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ExtendedModalApi, ModalProps } from './modal'; import type { ExtendedModalApi, ModalProps } from './modal';
import { computed, nextTick, onDeactivated, ref, unref, watch } from 'vue'; import {
computed,
nextTick,
onDeactivated,
provide,
ref,
unref,
useId,
watch,
} from 'vue';
import { usePriorityValues, useSimpleLocale } from '@vben-core/composables'; import { usePriorityValues, useSimpleLocale } from '@vben-core/composables';
import { Expand, Shrink } from '@vben-core/icons'; import { Expand, Shrink } from '@vben-core/icons';
@ -47,6 +56,10 @@ const footerRef = ref();
const { $t } = useSimpleLocale(); const { $t } = useSimpleLocale();
const state = props.modalApi?.useStore?.(); const state = props.modalApi?.useStore?.();
const id = useId();
// 遮罩层通过该 id 标记,仅当点击发生在当前 Modal 的遮罩上时才允许关闭
provide('DISMISSABLE_MODAL_ID', id);
const { const {
appendToMain, appendToMain,
bordered, bordered,
@ -181,8 +194,15 @@ function handleOpenAutoFocus(e: Event) {
// pointer-down-outside // pointer-down-outside
function pointerDownOutside(e: Event) { function pointerDownOutside(e: Event) {
if (!closeOnClickModal.value || submitting.value) { const target = e.target as HTMLElement;
const isDismissableModal = target?.dataset.dismissableModal;
if (
!closeOnClickModal.value ||
isDismissableModal !== id ||
submitting.value
) {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
} }
} }
@ -212,7 +232,7 @@ function handleClosed() {
</script> </script>
<template> <template>
<Dialog <Dialog
:modal="modal" :modal="false"
:open="state?.isOpen" :open="state?.isOpen"
@update:open="() => (!submitting ? modalApi?.close() : undefined)" @update:open="() => (!submitting ? modalApi?.close() : undefined)"
> >
@ -240,7 +260,7 @@ function handleClosed() {
:force-mount="getForceMount" :force-mount="getForceMount"
:modal="modal" :modal="modal"
:open="state?.isOpen" :open="state?.isOpen"
:show-close="closable" :show-close-button="closable"
:animation-type="animationType" :animation-type="animationType"
:z-index="zIndex" :z-index="zIndex"
:overlay-blur="overlayBlur" :overlay-blur="overlayBlur"

58
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

@ -1,33 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'; import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import type { ClassType } from '@vben-core/typings'; import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue'; import { computed, inject, ref } from 'vue';
import { useScrollLock } from '@vben-core/composables';
import { cn } from '@vben-core/shared/utils'; import { cn } from '@vben-core/shared/utils';
import { X } from '@lucide/vue'; import { X } from '@lucide/vue';
import { import {
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogOverlay,
DialogPortal, DialogPortal,
useForwardPropsEmits, useForwardPropsEmits,
} from 'reka-ui'; } from 'reka-ui';
defineOptions({
inheritAttrs: false,
});
const props = withDefaults( const props = withDefaults(
defineProps< defineProps<
DialogContentProps & { DialogContentProps & {
animationType?: 'scale' | 'slide'; animationType?: 'scale' | 'slide';
appendTo?: HTMLElement | string; appendTo?: HTMLElement | string;
class?: ClassType; class?: HTMLAttributes['class'];
closeClass?: ClassType; closeClass?: ClassType;
closeDisabled?: boolean; closeDisabled?: boolean;
modal?: boolean; modal?: boolean;
open?: boolean; open?: boolean;
overlayBlur?: number; overlayBlur?: number;
showClose?: boolean; showCloseButton?: boolean;
zIndex?: number; zIndex?: number;
} }
>(), >(),
@ -35,7 +41,7 @@ const props = withDefaults(
appendTo: 'body', appendTo: 'body',
animationType: 'slide', animationType: 'slide',
closeDisabled: false, closeDisabled: false,
showClose: true, showCloseButton: true,
}, },
); );
const emits = defineEmits< const emits = defineEmits<
@ -47,7 +53,7 @@ const delegatedProps = computed(() => {
class: _, class: _,
modal: _modal, modal: _modal,
open: _open, open: _open,
showClose: __, showCloseButton: __,
animationType: ___, animationType: ___,
...delegated ...delegated
} = props; } = props;
@ -67,6 +73,12 @@ const position = computed(() => {
return isAppendToBody() ? 'fixed' : 'absolute'; return isAppendToBody() ? 'fixed' : 'absolute';
}); });
// reka-ui 的 Dialog 在 modal=false 时不会渲染遮罩,这里自行渲染一个遮罩层并锁定滚动,
// 既保留遮罩/滚动锁定能力,又避免 modal=true 时 body 被设置 pointer-events:none 导致
// 弹出层(如 Select 下拉框)无法点击的问题。
useScrollLock();
const dismissableModalId = inject('DISMISSABLE_MODAL_ID', undefined);
const forwarded = useForwardPropsEmits(delegatedProps, emits); const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null); const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
@ -87,23 +99,24 @@ defineExpose({
<template> <template>
<DialogPortal :to="appendTo"> <DialogPortal :to="appendTo">
<DialogOverlay <Transition name="fade">
v-if="open && modal" <div
:style="{ v-if="open && modal"
...(zIndex ? { zIndex } : {}), :data-dismissable-modal="dismissableModalId"
position, :style="{
backdropFilter: ...(zIndex ? { zIndex } : {}),
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none', position,
}" backdropFilter:
:class=" overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
cn( }"
'z-popup bg-overlay inset-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed', :class="cn('z-popup bg-overlay inset-0 fixed')"
) @click="() => emits('close')"
" ></div>
/> </Transition>
<DialogContent <DialogContent
ref="contentRef" ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position }" :style="{ ...(zIndex ? { zIndex } : {}), position }"
data-slot="dialog-content"
@animationend="onAnimationEnd" @animationend="onAnimationEnd"
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
@ -120,8 +133,9 @@ defineExpose({
<slot></slot> <slot></slot>
<DialogClose <DialogClose
v-if="showClose" v-if="showCloseButton"
:disabled="closeDisabled" :disabled="closeDisabled"
data-slot="dialog-close"
:class=" :class="
cn( cn(
'flex-center text-foreground/80 hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-3 right-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none', 'flex-center text-foreground/80 hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-3 right-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none',
@ -130,7 +144,7 @@ defineExpose({
" "
@click="() => emits('close')" @click="() => emits('close')"
> >
<X class="h-4 w-4" /> <X class="size-4" />
</DialogClose> </DialogClose>
</DialogContent> </DialogContent>
</DialogPortal> </DialogPortal>

22
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogOverlay.vue

@ -1,13 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DialogOverlayProps } from 'reka-ui'; import type { DialogOverlayProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import { DialogOverlay } from 'reka-ui'; import { DialogOverlay } from 'reka-ui';
const props = defineProps<DialogOverlayProps>(); const props = defineProps<
DialogOverlayProps & { class?: HTMLAttributes['class'] }
>();
const delegatedProps = reactiveOmit(props, 'class');
</script> </script>
<template> <template>
<DialogOverlay data-slot="dialog-overlay" v-bind="props"> <DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
:class="
cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
props.class,
)
"
>
<slot></slot> <slot></slot>
</DialogOverlay> </DialogOverlay>
</template> </template>

2
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogScrollContent.vue

@ -60,7 +60,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<DialogClose <DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary" class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
> >
<X class="w-4 h-4" /> <X class="size-4" />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</DialogClose> </DialogClose>
</DialogContent> </DialogContent>

2
playground/src/adapter/form.ts

@ -11,7 +11,7 @@ import { $t } from '@vben/locales';
async function initSetupVbenForm() { async function initSetupVbenForm() {
setupVbenForm<ComponentType>({ setupVbenForm<ComponentType>({
config: { config: {
// ant design vue组件库默认都是 v-model:value // antdv-next 组件库默认都是 v-model:value
baseModelPropName: 'value', baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList // 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: { modelPropNameMap: {

1
playground/src/views/examples/modal/form-modal-demo.vue

@ -33,6 +33,7 @@ const [Form, formApi] = useVbenForm({
{ {
component: 'Select', component: 'Select',
componentProps: { componentProps: {
class: 'w-full',
options: [ options: [
{ label: '选项1', value: '1' }, { label: '选项1', value: '1' },
{ label: '选项2', value: '2' }, { label: '选项2', value: '2' },

2
playground/src/views/examples/modal/index.vue

@ -108,7 +108,7 @@ function openFormModal() {
formModalApi formModalApi
.setData({ .setData({
// 表单值 // 表单值
values: { field1: 'abc', field2: '123' }, values: { field1: 'abc', field2: '123', field3: '1' },
}) })
.open(); .open();
} }

2076
pnpm-lock.yaml

File diff suppressed because it is too large

58
pnpm-workspace.yaml

@ -41,14 +41,14 @@ catalog:
'@changesets/changelog-github': ^0.7.0 '@changesets/changelog-github': ^0.7.0
'@changesets/cli': ^2.31.0 '@changesets/cli': ^2.31.0
'@changesets/git': ^3.0.4 '@changesets/git': ^3.0.4
'@clack/prompts': ^1.5.0 '@clack/prompts': ^1.5.1
'@commitlint/cli': ^21.0.2 '@commitlint/cli': ^21.0.2
'@commitlint/config-conventional': ^21.0.2 '@commitlint/config-conventional': ^21.0.2
'@ctrl/tinycolor': ^4.2.0 '@ctrl/tinycolor': ^4.2.0
'@eslint-community/eslint-plugin-eslint-comments': ^4.7.2 '@eslint-community/eslint-plugin-eslint-comments': ^4.7.2
'@eslint/js': ^10.0.1 '@eslint/js': ^10.0.1
'@faker-js/faker': ^10.4.0 '@faker-js/faker': ^10.4.0
'@iconify/json': ^2.2.481 '@iconify/json': ^2.2.483
'@iconify/tailwind4': ^1.2.3 '@iconify/tailwind4': ^1.2.3
'@iconify/vue': ^5.0.1 '@iconify/vue': ^5.0.1
'@intlify/core-base': ^11.4.4 '@intlify/core-base': ^11.4.4
@ -62,27 +62,27 @@ catalog:
'@stylistic/stylelint-plugin': ^5.2.0 '@stylistic/stylelint-plugin': ^5.2.0
'@tailwindcss/typography': ^0.5.19 '@tailwindcss/typography': ^0.5.19
'@tailwindcss/vite': ^4.3.0 '@tailwindcss/vite': ^4.3.0
'@tanstack/vue-query': ^5.100.14 '@tanstack/vue-query': ^5.101.0
'@tanstack/vue-store': ^0.11.0 '@tanstack/vue-store': ^0.11.0
'@tiptap/core': ^3.24.0 '@tiptap/core': ^3.26.0
'@tiptap/extension-document': ^3.24.0 '@tiptap/extension-document': ^3.26.0
'@tiptap/extension-highlight': ^3.24.0 '@tiptap/extension-highlight': ^3.26.0
'@tiptap/extension-image': ^3.24.0 '@tiptap/extension-image': ^3.26.0
'@tiptap/extension-link': ^3.24.0 '@tiptap/extension-link': ^3.26.0
'@tiptap/extension-placeholder': ^3.24.0 '@tiptap/extension-placeholder': ^3.26.0
'@tiptap/extension-text-align': ^3.24.0 '@tiptap/extension-text-align': ^3.26.0
'@tiptap/extension-text-style': ^3.24.0 '@tiptap/extension-text-style': ^3.26.0
'@tiptap/extension-underline': ^3.24.0 '@tiptap/extension-underline': ^3.26.0
'@tiptap/pm': ^3.24.0 '@tiptap/pm': ^3.26.0
'@tiptap/starter-kit': ^3.24.0 '@tiptap/starter-kit': ^3.26.0
'@tiptap/vue-3': ^3.24.0 '@tiptap/vue-3': ^3.26.0
'@tsdown/css': ^0.22.1 '@tsdown/css': ^0.22.2
'@types/archiver': ^7.0.0 '@types/archiver': ^7.0.0
'@types/html-minifier-terser': ^7.0.2 '@types/html-minifier-terser': ^7.0.2
'@types/json-bigint': ^1.0.4 '@types/json-bigint': ^1.0.4
'@types/jsonwebtoken': ^9.0.10 '@types/jsonwebtoken': ^9.0.10
'@types/lodash.clonedeep': ^4.5.9 '@types/lodash.clonedeep': ^4.5.9
'@types/node': ^25.9.1 '@types/node': ^25.9.2
'@types/nprogress': ^0.2.3 '@types/nprogress': ^0.2.3
'@types/qrcode': ^1.5.6 '@types/qrcode': ^1.5.6
'@types/qs': ^6.15.1 '@types/qs': ^6.15.1
@ -94,14 +94,14 @@ catalog:
'@vitejs/plugin-vue': ^6.0.7 '@vitejs/plugin-vue': ^6.0.7
'@vitejs/plugin-vue-jsx': ^5.1.5 '@vitejs/plugin-vue-jsx': ^5.1.5
'@vue/shared': ^3.5.35 '@vue/shared': ^3.5.35
'@vue/test-utils': ^2.4.10 '@vue/test-utils': ^2.4.11
'@vueuse/core': ^14.3.0 '@vueuse/core': ^14.3.0
'@vueuse/integrations': ^14.3.0 '@vueuse/integrations': ^14.3.0
'@vueuse/motion': ^3.0.3 '@vueuse/motion': ^3.0.3
ant-design-vue: ^4.2.6 ant-design-vue: ^4.2.6
antdv-next: ^1.3.1 antdv-next: ^1.3.3
archiver: ^7.0.1 archiver: ^7.0.1
axios: ^1.16.1 axios: ^1.17.0
axios-mock-adapter: ^2.1.0 axios-mock-adapter: ^2.1.0
cac: ^7.0.0 cac: ^7.0.0
chalk: ^5.6.2 chalk: ^5.6.2
@ -130,14 +130,14 @@ catalog:
eslint-plugin-pnpm: ^1.6.1 eslint-plugin-pnpm: ^1.6.1
eslint-plugin-unicorn: ^64.0.0 eslint-plugin-unicorn: ^64.0.0
eslint-plugin-unused-imports: ^4.4.1 eslint-plugin-unused-imports: ^4.4.1
eslint-plugin-vue: ^10.9.1 eslint-plugin-vue: ^10.9.2
eslint-plugin-yml: ^3.4.0 eslint-plugin-yml: ^3.4.0
execa: ^9.6.1 execa: ^9.6.1
find-up: ^8.0.0 find-up: ^8.0.0
get-port: ^7.2.0 get-port: ^7.2.0
globals: ^17.6.0 globals: ^17.6.0
h3: ^1.15.11 h3: ^1.15.11
happy-dom: ^20.9.0 happy-dom: ^20.10.2
html-minifier-terser: ^7.2.0 html-minifier-terser: ^7.2.0
is-ci: ^4.1.0 is-ci: ^4.1.0
json-bigint: ^1.0.0 json-bigint: ^1.0.0
@ -163,7 +163,7 @@ catalog:
publint: ^0.3.21 publint: ^0.3.21
qrcode: ^1.5.4 qrcode: ^1.5.4
qs: ^6.15.2 qs: ^6.15.2
reka-ui: ^2.9.8 reka-ui: ^2.9.9
resolve.exports: ^2.0.3 resolve.exports: ^2.0.3
rimraf: ^6.1.3 rimraf: ^6.1.3
rollup-plugin-visualizer: ^7.0.1 rollup-plugin-visualizer: ^7.0.1
@ -178,13 +178,13 @@ catalog:
stylelint-config-recommended-vue: ^1.6.1 stylelint-config-recommended-vue: ^1.6.1
stylelint-config-standard: ^40.0.0 stylelint-config-standard: ^40.0.0
stylelint-order: ^8.1.1 stylelint-order: ^8.1.1
stylelint-scss: ^7.1.1 stylelint-scss: ^7.2.0
tailwind-merge: ^3.6.0 tailwind-merge: ^3.6.0
tailwindcss: ^4.3.0 tailwindcss: ^4.3.0
tdesign-vue-next: ^1.20.0 tdesign-vue-next: ^1.20.1
theme-colors: ^0.1.0 theme-colors: ^0.1.0
tippy.js: ^6.3.7 tippy.js: ^6.3.7
tsdown: ^0.22.1 tsdown: ^0.22.2
turbo: ^2.9.16 turbo: ^2.9.16
tw-animate-css: ^1.4.0 tw-animate-css: ^1.4.0
typescript: ^6.0.3 typescript: ^6.0.3
@ -201,14 +201,14 @@ catalog:
vitepress-plugin-group-icons: ^1.7.5 vitepress-plugin-group-icons: ^1.7.5
vitest: ^4.1.8 vitest: ^4.1.8
vue: ^3.5.35 vue: ^3.5.35
vue-eslint-parser: ^10.4.0 vue-eslint-parser: ^10.4.1
vue-i18n: ^11.4.4 vue-i18n: ^11.4.4
vue-json-pretty: ^2.6.0 vue-json-pretty: ^2.6.0
vue-router: ^5.1.0 vue-router: ^5.1.0
vue-tippy: ^6.7.1 vue-tippy: ^6.7.1
vue-tsc: ^3.3.3 vue-tsc: ^3.3.3
vxe-pc-ui: ^4.14.26 vxe-pc-ui: ^4.14.30
vxe-table: ^4.19.6 vxe-table: ^4.19.7
watermark-js-plus: ^1.6.3 watermark-js-plus: ^1.6.3
yaml-eslint-parser: ^2.0.0 yaml-eslint-parser: ^2.0.0
zod: ^3.25.76 zod: ^3.25.76

Loading…
Cancel
Save