Merge branch 'main' into main
This commit is contained in:
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -89,6 +89,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
2
.github/workflows/issue-close-require.yml
vendored
2
.github/workflows/issue-close-require.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
# 关闭未活动的 Issues
|
||||
- name: Close Inactive Issues
|
||||
uses: actions/stale@v9
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
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.
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-inactive-days: '14'
|
||||
|
||||
2
.github/workflows/semantic-pull-request.yml
vendored
2
.github/workflows/semantic-pull-request.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate PR title
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
uses: amannn/action-semantic-pull-request@v6
|
||||
with:
|
||||
wip: true
|
||||
subjectPattern: ^(?![A-Z]).+$
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
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'
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.22.0
|
||||
24.16.0
|
||||
|
||||
@@ -36,8 +36,11 @@ import type { Component, Ref } from 'vue';
|
||||
import type {
|
||||
ApiComponentSharedProps,
|
||||
BaseFormComponentType,
|
||||
CollapsibleParamsProps,
|
||||
IconPickerProps,
|
||||
} from '@vben/common-ui';
|
||||
import type { Sortable } from '@vben/hooks';
|
||||
import type { TipTapProps } from '@vben/plugins/tiptap';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
@@ -45,6 +48,9 @@ import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
h,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
render,
|
||||
unref,
|
||||
@@ -55,19 +61,25 @@ import {
|
||||
ApiComponent,
|
||||
globalShareState,
|
||||
IconPicker,
|
||||
VbenCollapsibleParams,
|
||||
VCropper,
|
||||
} from '@vben/common-ui';
|
||||
import { useSortable } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { VbenTiptap } from '@vben/plugins/tiptap';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message, Modal, notification } from 'antdv-next';
|
||||
|
||||
import { upload_file } from '#/api';
|
||||
type AdapterUploadProps = UploadProps & {
|
||||
aspectRatio?: string;
|
||||
crop?: boolean;
|
||||
draggable?: boolean;
|
||||
handleChange?: (event: UploadChangeParam) => void;
|
||||
maxSize?: number;
|
||||
onDragSort?: (oldIndex: number, newIndex: number) => void;
|
||||
onHandleChange?: (event: UploadChangeParam) => void;
|
||||
};
|
||||
|
||||
@@ -80,8 +92,8 @@ const Button = defineAsyncComponent(
|
||||
const Checkbox = defineAsyncComponent(
|
||||
() => import('antdv-next/dist/checkbox/index'),
|
||||
);
|
||||
const CheckboxGroup = defineAsyncComponent(
|
||||
() => import('antdv-next/dist/checkbox/Group'),
|
||||
const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('antdv-next/dist/checkbox/index').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
const DatePicker = defineAsyncComponent(
|
||||
() => import('antdv-next/dist/date-picker/index'),
|
||||
@@ -170,260 +182,263 @@ const withDefaultPlaceholder = (
|
||||
});
|
||||
};
|
||||
|
||||
const withPreviewUpload = () => {
|
||||
// 检查是否为图片文件的辅助函数
|
||||
const isImageFile = (file: UploadFile): boolean => {
|
||||
const imageExtensions = new Set([
|
||||
'bmp',
|
||||
'gif',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'svg',
|
||||
'webp',
|
||||
]);
|
||||
if (file.url) {
|
||||
try {
|
||||
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
} catch {
|
||||
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
}
|
||||
const IMAGE_EXTENSIONS = new Set([
|
||||
'bmp',
|
||||
'gif',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'svg',
|
||||
'webp',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 检查是否为图片文件
|
||||
*/
|
||||
function isImageFile(file: UploadFile): boolean {
|
||||
if (file.url) {
|
||||
try {
|
||||
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||
return ext ? IMAGE_EXTENSIONS.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,
|
||||
) => {
|
||||
switch (listType) {
|
||||
case 'picture-card': {
|
||||
return {
|
||||
default: () => placeholder,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
icon: h(IconifyIcon, {
|
||||
icon: 'ant-design:upload-outlined',
|
||||
class: 'mb-1 size-4',
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 Base64
|
||||
*/
|
||||
function getBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result as string));
|
||||
reader.addEventListener('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览图片
|
||||
*/
|
||||
async function previewImage(
|
||||
file: UploadFile,
|
||||
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;
|
||||
}
|
||||
|
||||
// 对于图片文件,继续使用预览组
|
||||
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||
Image,
|
||||
PreviewGroup,
|
||||
]);
|
||||
render(h(PreviewWrapper), container);
|
||||
}
|
||||
|
||||
const getBase64 = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result));
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
};
|
||||
// 从fileList中过滤出所有图片文件
|
||||
const imageFiles = (unref(fileList) || []).filter((element) =>
|
||||
isImageFile(element),
|
||||
);
|
||||
|
||||
// 为所有没有预览地址的图片生成预览
|
||||
for (const imgFile of imageFiles) {
|
||||
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
||||
}
|
||||
}
|
||||
const container: HTMLElement | null = document.createElement('div');
|
||||
/**
|
||||
* 图片裁剪操作
|
||||
*/
|
||||
function cropImage(file: File, aspectRatio: string | undefined) {
|
||||
return new Promise<Blob | string | undefined>((resolve, reject) => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
// 用于追踪组件是否已卸载
|
||||
let isUnmounted = false;
|
||||
let objectUrl: null | string = null;
|
||||
|
||||
const PreviewWrapper = {
|
||||
const open = ref<boolean>(true);
|
||||
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
|
||||
|
||||
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() {
|
||||
return () => {
|
||||
if (isUnmounted) return null;
|
||||
if (!objectUrl) {
|
||||
objectUrl = URL.createObjectURL(file);
|
||||
}
|
||||
return h(
|
||||
PreviewGroupComponent,
|
||||
Modal,
|
||||
{
|
||||
class: 'hidden',
|
||||
preview: {
|
||||
open: visible.value,
|
||||
// 设置初始显示的图片索引
|
||||
current: imageFiles.findIndex((f) => f.uid === file.uid),
|
||||
onOpenChange: (value: boolean) => {
|
||||
visible.value = value;
|
||||
if (!value) {
|
||||
// 延迟清理,确保动画完成
|
||||
setTimeout(() => {
|
||||
if (!isUnmounted && container) {
|
||||
isUnmounted = true;
|
||||
render(null, container);
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
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();
|
||||
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();
|
||||
},
|
||||
},
|
||||
() =>
|
||||
// 渲染所有图片文件
|
||||
imageFiles.map((imgFile) =>
|
||||
h(ImageComponent, {
|
||||
key: imgFile.uid,
|
||||
src: imgFile.url || imgFile.preview,
|
||||
}),
|
||||
),
|
||||
h(VCropper, {
|
||||
ref: (ref: any) => (cropperRef.value = ref),
|
||||
img: objectUrl as string,
|
||||
aspectRatio,
|
||||
}),
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
render(h(PreviewWrapper), 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);
|
||||
});
|
||||
};
|
||||
render(h(CropperWrapper), container);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 带预览功能的上传组件
|
||||
*/
|
||||
function withPreviewUpload() {
|
||||
return defineComponent({
|
||||
name: 'AUpload',
|
||||
name: Upload.name,
|
||||
emits: ['update:modelValue'],
|
||||
setup: (
|
||||
setup(
|
||||
props: any,
|
||||
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
||||
) => {
|
||||
) {
|
||||
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 fileList = ref<UploadProps['fileList']>(
|
||||
attrs?.fileList || attrs?.['file-list'] || [],
|
||||
);
|
||||
@@ -433,16 +448,18 @@ const withPreviewUpload = () => {
|
||||
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
|
||||
);
|
||||
|
||||
const handleBeforeUpload = async (
|
||||
async function handleBeforeUpload(
|
||||
file: UploadFile,
|
||||
originFileList: Array<File>,
|
||||
) => {
|
||||
) {
|
||||
// 文件大小限制
|
||||
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
|
||||
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
|
||||
file.status = 'removed';
|
||||
return false;
|
||||
}
|
||||
// 多选或者非图片不唤起裁剪框
|
||||
|
||||
// 图片裁剪处理
|
||||
if (
|
||||
attrs.crop &&
|
||||
!attrs.multiple &&
|
||||
@@ -450,27 +467,21 @@ const withPreviewUpload = () => {
|
||||
isImageFile(file)
|
||||
) {
|
||||
file.status = 'removed';
|
||||
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
|
||||
const blob = await cropImage(originFileList[0], aspectRatio.value);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!blob) {
|
||||
return reject(new Error($t('ui.crop.errorTip')));
|
||||
}
|
||||
resolve(blob);
|
||||
});
|
||||
if (!blob) {
|
||||
throw new Error($t('ui.crop.errorTip'));
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
return attrs.beforeUpload?.(file) ?? true;
|
||||
};
|
||||
}
|
||||
|
||||
const handleChange = (event: UploadChangeParam) => {
|
||||
function handleChange(event: UploadChangeParam) {
|
||||
try {
|
||||
// 行内写法 handleChange: (event) => {}
|
||||
attrs.handleChange?.(event);
|
||||
// template写法 @handle-change="(event) => {}"
|
||||
attrs.onHandleChange?.(event);
|
||||
} catch (error) {
|
||||
// Avoid breaking internal v-model sync on user handler errors
|
||||
console.error(error);
|
||||
}
|
||||
fileList.value = event.fileList.filter(
|
||||
@@ -480,28 +491,95 @@ const withPreviewUpload = () => {
|
||||
'update:modelValue',
|
||||
event.fileList?.length ? fileList.value : undefined,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
function handlePreview(file: UploadFile) {
|
||||
previewVisible.value = true;
|
||||
await previewImage(file, previewVisible, fileList);
|
||||
};
|
||||
return previewImage(file, previewVisible, fileList);
|
||||
}
|
||||
|
||||
const renderUploadButton = (): any => {
|
||||
const isDisabled = attrs.disabled;
|
||||
function renderUploadButton() {
|
||||
if (attrs.disabled) return null;
|
||||
return isEmpty(slots)
|
||||
? createDefaultUploadSlots(listType, placeholder)
|
||||
: slots;
|
||||
}
|
||||
|
||||
// 如果禁用,不渲染上传按钮
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
// 拖拽排序
|
||||
const draggable = computed(
|
||||
() => (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() {
|
||||
document.querySelector(`[id="${styleId}"]`)?.remove();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 否则渲染默认上传按钮
|
||||
return isEmpty(slots)
|
||||
? createDefaultSlotsWithUpload(listType, placeholder)
|
||||
: slots;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
// 可以监听到表单API设置的值
|
||||
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();
|
||||
}
|
||||
|
||||
// 监听表单值变化
|
||||
watch(
|
||||
() => attrs.modelValue,
|
||||
(res) => {
|
||||
@@ -509,22 +587,32 @@ const withPreviewUpload = () => {
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(initSortable);
|
||||
onUnmounted(() => {
|
||||
sortableInstance.value?.destroy();
|
||||
removeDragStyle();
|
||||
});
|
||||
|
||||
return () =>
|
||||
h(
|
||||
Upload,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
fileList: fileList.value,
|
||||
beforeUpload: handleBeforeUpload,
|
||||
onChange: handleChange,
|
||||
onPreview: handlePreview,
|
||||
},
|
||||
renderUploadButton(),
|
||||
'div',
|
||||
{ 'data-upload-id': uploadId, class: 'w-full' },
|
||||
h(
|
||||
Upload,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
fileList: fileList.value,
|
||||
beforeUpload: handleBeforeUpload,
|
||||
onChange: handleChange,
|
||||
onPreview: handlePreview,
|
||||
},
|
||||
renderUploadButton() as any,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
@@ -535,6 +623,7 @@ export type ComponentType =
|
||||
| 'Cascader'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'CollapsibleParams'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
@@ -548,6 +637,7 @@ export type ComponentType =
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'RichEditor'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
@@ -568,6 +658,7 @@ export interface ComponentPropsMap {
|
||||
Cascader: CascaderProps;
|
||||
Checkbox: CheckboxProps;
|
||||
CheckboxGroup: CheckboxGroupProps;
|
||||
CollapsibleParams: CollapsibleParamsProps;
|
||||
DatePicker: DatePickerProps;
|
||||
DefaultButton: ButtonProps;
|
||||
Divider: DividerProps;
|
||||
@@ -581,6 +672,7 @@ export interface ComponentPropsMap {
|
||||
RadioGroup: RadioGroupProps;
|
||||
RangePicker: RangePickerProps;
|
||||
Rate: RateProps;
|
||||
RichEditor: TipTapProps;
|
||||
Select: SelectProps;
|
||||
Space: SpaceProps;
|
||||
Switch: SwitchProps;
|
||||
@@ -601,13 +693,13 @@ async function initComponentAdapter() {
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
visibleEvent: 'onOpenChange',
|
||||
}),
|
||||
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
visibleEvent: 'onOpenChange',
|
||||
}),
|
||||
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: TreeSelect,
|
||||
@@ -615,7 +707,7 @@ async function initComponentAdapter() {
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
visibleEvent: 'onOpenChange',
|
||||
}),
|
||||
AutoComplete,
|
||||
Cascader,
|
||||
@@ -646,6 +738,27 @@ async function initComponentAdapter() {
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
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'),
|
||||
Space,
|
||||
Switch,
|
||||
@@ -653,6 +766,7 @@ async function initComponentAdapter() {
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload: withPreviewUpload(),
|
||||
CollapsibleParams: VbenCollapsibleParams,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './upload';
|
||||
export * from './user';
|
||||
|
||||
25
apps/web-antdv-next/src/api/core/upload.ts
Normal file
25
apps/web-antdv-next/src/api/core/upload.ts
Normal file
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -106,5 +106,5 @@
|
||||
"node": "^22.18.0 || ^24.0.0",
|
||||
"pnpm": ">=11.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.5.0"
|
||||
"packageManager": "pnpm@11.5.2"
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const emit = defineEmits<{
|
||||
sideMouseLeave: [];
|
||||
toggleSidebar: [];
|
||||
'update:sidebar-width': [value: number];
|
||||
'update:sidebarWidth': [value: number];
|
||||
}>();
|
||||
const sidebarDraggable = defineModel<boolean>('sidebarDraggable', {
|
||||
default: true,
|
||||
@@ -520,7 +520,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
||||
:width="getSidebarWidth"
|
||||
:z-index="sidebarZIndex"
|
||||
@leave="() => emit('sideMouseLeave')"
|
||||
@update:width="(val) => emit('update:sidebar-width', val)"
|
||||
@update:width="(val) => emit('update:sidebarWidth', val)"
|
||||
>
|
||||
<template v-if="isSideMode && !isMixedNav" #logo>
|
||||
<slot name="logo"></slot>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
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 { Expand, Shrink } from '@vben-core/icons';
|
||||
@@ -47,6 +56,10 @@ const footerRef = ref();
|
||||
const { $t } = useSimpleLocale();
|
||||
const state = props.modalApi?.useStore?.();
|
||||
|
||||
const id = useId();
|
||||
// 遮罩层通过该 id 标记,仅当点击发生在当前 Modal 的遮罩上时才允许关闭
|
||||
provide('DISMISSABLE_MODAL_ID', id);
|
||||
|
||||
const {
|
||||
appendToMain,
|
||||
bordered,
|
||||
@@ -181,8 +194,15 @@ function handleOpenAutoFocus(e: Event) {
|
||||
|
||||
// pointer-down-outside
|
||||
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.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +232,7 @@ function handleClosed() {
|
||||
</script>
|
||||
<template>
|
||||
<Dialog
|
||||
:modal="modal"
|
||||
:modal="false"
|
||||
:open="state?.isOpen"
|
||||
@update:open="() => (!submitting ? modalApi?.close() : undefined)"
|
||||
>
|
||||
@@ -240,7 +260,7 @@ function handleClosed() {
|
||||
:force-mount="getForceMount"
|
||||
:modal="modal"
|
||||
:open="state?.isOpen"
|
||||
:show-close="closable"
|
||||
:show-close-button="closable"
|
||||
:animation-type="animationType"
|
||||
:z-index="zIndex"
|
||||
:overlay-blur="overlayBlur"
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
|
||||
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
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 { X } from '@lucide/vue';
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
DialogContentProps & {
|
||||
animationType?: 'scale' | 'slide';
|
||||
appendTo?: HTMLElement | string;
|
||||
class?: ClassType;
|
||||
class?: HTMLAttributes['class'];
|
||||
closeClass?: ClassType;
|
||||
closeDisabled?: boolean;
|
||||
modal?: boolean;
|
||||
open?: boolean;
|
||||
overlayBlur?: number;
|
||||
showClose?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
zIndex?: number;
|
||||
}
|
||||
>(),
|
||||
@@ -35,7 +41,7 @@ const props = withDefaults(
|
||||
appendTo: 'body',
|
||||
animationType: 'slide',
|
||||
closeDisabled: false,
|
||||
showClose: true,
|
||||
showCloseButton: true,
|
||||
},
|
||||
);
|
||||
const emits = defineEmits<
|
||||
@@ -47,7 +53,7 @@ const delegatedProps = computed(() => {
|
||||
class: _,
|
||||
modal: _modal,
|
||||
open: _open,
|
||||
showClose: __,
|
||||
showCloseButton: __,
|
||||
animationType: ___,
|
||||
...delegated
|
||||
} = props;
|
||||
@@ -67,6 +73,12 @@ const position = computed(() => {
|
||||
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 contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
|
||||
@@ -87,23 +99,24 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<DialogPortal :to="appendTo">
|
||||
<DialogOverlay
|
||||
v-if="open && modal"
|
||||
:style="{
|
||||
...(zIndex ? { zIndex } : {}),
|
||||
position,
|
||||
backdropFilter:
|
||||
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
|
||||
}"
|
||||
:class="
|
||||
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',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="open && modal"
|
||||
:data-dismissable-modal="dismissableModalId"
|
||||
:style="{
|
||||
...(zIndex ? { zIndex } : {}),
|
||||
position,
|
||||
backdropFilter:
|
||||
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
|
||||
}"
|
||||
:class="cn('z-popup bg-overlay inset-0 fixed')"
|
||||
@click="() => emits('close')"
|
||||
></div>
|
||||
</Transition>
|
||||
<DialogContent
|
||||
ref="contentRef"
|
||||
:style="{ ...(zIndex ? { zIndex } : {}), position }"
|
||||
data-slot="dialog-content"
|
||||
@animationend="onAnimationEnd"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
@@ -120,8 +133,9 @@ defineExpose({
|
||||
<slot></slot>
|
||||
|
||||
<DialogClose
|
||||
v-if="showClose"
|
||||
v-if="showCloseButton"
|
||||
:disabled="closeDisabled"
|
||||
data-slot="dialog-close"
|
||||
:class="
|
||||
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',
|
||||
@@ -130,7 +144,7 @@ defineExpose({
|
||||
"
|
||||
@click="() => emits('close')"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<X class="size-4" />
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
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';
|
||||
|
||||
const props = defineProps<DialogOverlayProps>();
|
||||
const props = defineProps<
|
||||
DialogOverlayProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<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>
|
||||
</DialogOverlay>
|
||||
</template>
|
||||
|
||||
@@ -60,7 +60,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
<DialogClose
|
||||
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>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { $t } from '@vben/locales';
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
// antdv-next 组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
|
||||
@@ -33,6 +33,7 @@ const [Form, formApi] = useVbenForm({
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
|
||||
@@ -108,7 +108,7 @@ function openFormModal() {
|
||||
formModalApi
|
||||
.setData({
|
||||
// 表单值
|
||||
values: { field1: 'abc', field2: '123' },
|
||||
values: { field1: 'abc', field2: '123', field3: '1' },
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
2072
pnpm-lock.yaml
generated
2072
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -41,14 +41,14 @@ catalog:
|
||||
'@changesets/changelog-github': ^0.7.0
|
||||
'@changesets/cli': ^2.31.0
|
||||
'@changesets/git': ^3.0.4
|
||||
'@clack/prompts': ^1.5.0
|
||||
'@clack/prompts': ^1.5.1
|
||||
'@commitlint/cli': ^21.0.2
|
||||
'@commitlint/config-conventional': ^21.0.2
|
||||
'@ctrl/tinycolor': ^4.2.0
|
||||
'@eslint-community/eslint-plugin-eslint-comments': ^4.7.2
|
||||
'@eslint/js': ^10.0.1
|
||||
'@faker-js/faker': ^10.4.0
|
||||
'@iconify/json': ^2.2.481
|
||||
'@iconify/json': ^2.2.483
|
||||
'@iconify/tailwind4': ^1.2.3
|
||||
'@iconify/vue': ^5.0.1
|
||||
'@intlify/core-base': ^11.4.4
|
||||
@@ -62,27 +62,27 @@ catalog:
|
||||
'@stylistic/stylelint-plugin': ^5.2.0
|
||||
'@tailwindcss/typography': ^0.5.19
|
||||
'@tailwindcss/vite': ^4.3.0
|
||||
'@tanstack/vue-query': ^5.100.14
|
||||
'@tanstack/vue-query': ^5.101.0
|
||||
'@tanstack/vue-store': ^0.11.0
|
||||
'@tiptap/core': ^3.24.0
|
||||
'@tiptap/extension-document': ^3.24.0
|
||||
'@tiptap/extension-highlight': ^3.24.0
|
||||
'@tiptap/extension-image': ^3.24.0
|
||||
'@tiptap/extension-link': ^3.24.0
|
||||
'@tiptap/extension-placeholder': ^3.24.0
|
||||
'@tiptap/extension-text-align': ^3.24.0
|
||||
'@tiptap/extension-text-style': ^3.24.0
|
||||
'@tiptap/extension-underline': ^3.24.0
|
||||
'@tiptap/pm': ^3.24.0
|
||||
'@tiptap/starter-kit': ^3.24.0
|
||||
'@tiptap/vue-3': ^3.24.0
|
||||
'@tsdown/css': ^0.22.1
|
||||
'@tiptap/core': ^3.26.0
|
||||
'@tiptap/extension-document': ^3.26.0
|
||||
'@tiptap/extension-highlight': ^3.26.0
|
||||
'@tiptap/extension-image': ^3.26.0
|
||||
'@tiptap/extension-link': ^3.26.0
|
||||
'@tiptap/extension-placeholder': ^3.26.0
|
||||
'@tiptap/extension-text-align': ^3.26.0
|
||||
'@tiptap/extension-text-style': ^3.26.0
|
||||
'@tiptap/extension-underline': ^3.26.0
|
||||
'@tiptap/pm': ^3.26.0
|
||||
'@tiptap/starter-kit': ^3.26.0
|
||||
'@tiptap/vue-3': ^3.26.0
|
||||
'@tsdown/css': ^0.22.2
|
||||
'@types/archiver': ^7.0.0
|
||||
'@types/html-minifier-terser': ^7.0.2
|
||||
'@types/json-bigint': ^1.0.4
|
||||
'@types/jsonwebtoken': ^9.0.10
|
||||
'@types/lodash.clonedeep': ^4.5.9
|
||||
'@types/node': ^25.9.1
|
||||
'@types/node': ^25.9.2
|
||||
'@types/nprogress': ^0.2.3
|
||||
'@types/qrcode': ^1.5.6
|
||||
'@types/qs': ^6.15.1
|
||||
@@ -94,14 +94,14 @@ catalog:
|
||||
'@vitejs/plugin-vue': ^6.0.7
|
||||
'@vitejs/plugin-vue-jsx': ^5.1.5
|
||||
'@vue/shared': ^3.5.35
|
||||
'@vue/test-utils': ^2.4.10
|
||||
'@vue/test-utils': ^2.4.11
|
||||
'@vueuse/core': ^14.3.0
|
||||
'@vueuse/integrations': ^14.3.0
|
||||
'@vueuse/motion': ^3.0.3
|
||||
ant-design-vue: ^4.2.6
|
||||
antdv-next: ^1.3.1
|
||||
antdv-next: ^1.3.3
|
||||
archiver: ^7.0.1
|
||||
axios: ^1.16.1
|
||||
axios: ^1.17.0
|
||||
axios-mock-adapter: ^2.1.0
|
||||
cac: ^7.0.0
|
||||
chalk: ^5.6.2
|
||||
@@ -130,14 +130,14 @@ catalog:
|
||||
eslint-plugin-pnpm: ^1.6.1
|
||||
eslint-plugin-unicorn: ^64.0.0
|
||||
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
|
||||
execa: ^9.6.1
|
||||
find-up: ^8.0.0
|
||||
get-port: ^7.2.0
|
||||
globals: ^17.6.0
|
||||
h3: ^1.15.11
|
||||
happy-dom: ^20.9.0
|
||||
happy-dom: ^20.10.2
|
||||
html-minifier-terser: ^7.2.0
|
||||
is-ci: ^4.1.0
|
||||
json-bigint: ^1.0.0
|
||||
@@ -163,7 +163,7 @@ catalog:
|
||||
publint: ^0.3.21
|
||||
qrcode: ^1.5.4
|
||||
qs: ^6.15.2
|
||||
reka-ui: ^2.9.8
|
||||
reka-ui: ^2.9.9
|
||||
resolve.exports: ^2.0.3
|
||||
rimraf: ^6.1.3
|
||||
rollup-plugin-visualizer: ^7.0.1
|
||||
@@ -178,13 +178,13 @@ catalog:
|
||||
stylelint-config-recommended-vue: ^1.6.1
|
||||
stylelint-config-standard: ^40.0.0
|
||||
stylelint-order: ^8.1.1
|
||||
stylelint-scss: ^7.1.1
|
||||
stylelint-scss: ^7.2.0
|
||||
tailwind-merge: ^3.6.0
|
||||
tailwindcss: ^4.3.0
|
||||
tdesign-vue-next: ^1.20.0
|
||||
tdesign-vue-next: ^1.20.1
|
||||
theme-colors: ^0.1.0
|
||||
tippy.js: ^6.3.7
|
||||
tsdown: ^0.22.1
|
||||
tsdown: ^0.22.2
|
||||
turbo: ^2.9.16
|
||||
tw-animate-css: ^1.4.0
|
||||
typescript: ^6.0.3
|
||||
@@ -201,14 +201,14 @@ catalog:
|
||||
vitepress-plugin-group-icons: ^1.7.5
|
||||
vitest: ^4.1.8
|
||||
vue: ^3.5.35
|
||||
vue-eslint-parser: ^10.4.0
|
||||
vue-eslint-parser: ^10.4.1
|
||||
vue-i18n: ^11.4.4
|
||||
vue-json-pretty: ^2.6.0
|
||||
vue-router: ^5.1.0
|
||||
vue-tippy: ^6.7.1
|
||||
vue-tsc: ^3.3.3
|
||||
vxe-pc-ui: ^4.14.26
|
||||
vxe-table: ^4.19.6
|
||||
vxe-pc-ui: ^4.14.30
|
||||
vxe-table: ^4.19.7
|
||||
watermark-js-plus: ^1.6.3
|
||||
yaml-eslint-parser: ^2.0.0
|
||||
zod: ^3.25.76
|
||||
|
||||
Reference in New Issue
Block a user