Merge branch 'main' into fix

This commit is contained in:
xingyu
2026-01-19 10:54:43 +08:00
committed by GitHub
22 changed files with 1587 additions and 119 deletions

View File

@@ -3,6 +3,8 @@
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
/* eslint-disable vue/one-component-per-file */
import type {
UploadChangeParam,
UploadFile,
@@ -24,12 +26,17 @@ import {
watch,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import {
ApiComponent,
globalShareState,
IconPicker,
VCropper,
} from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils';
import { message, notification } from 'ant-design-vue';
import { message, Modal, notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
@@ -119,6 +126,33 @@ const withDefaultPlaceholder = <T extends Component>(
};
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;
}
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
}
return file.type.startsWith('image/');
};
// 创建默认的上传按钮插槽
const createDefaultSlotsWithUpload = (
listType: string,
@@ -153,27 +187,6 @@ const withPreviewUpload = () => {
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
// 检查是否为图片文件的辅助函数
const isImageFile = (file: UploadFile): boolean => {
const imageExtensions = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'webp',
]);
if (file.url) {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.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 (!isImageFile(file)) {
if (file.url) {
@@ -259,6 +272,107 @@ const withPreviewUpload = () => {
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: $t('ui.crop.title'),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: 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);
});
};
const base64ToBlob = (base64: Base64URLString) => {
try {
const [typeStr, encodeStr] = base64.split(',');
if (!typeStr || !encodeStr) return;
const mime = typeStr.match(/:(.*?);/)?.[1];
const raw = window.atob(encodeStr);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.codePointAt(i) as number;
}
return new Blob([uInt8Array], { type: mime });
} catch {
return undefined;
}
};
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
@@ -276,16 +390,50 @@ const withPreviewUpload = () => {
attrs?.fileList || attrs?.['file-list'] || [],
);
const handleBeforeUpload = (file: UploadFile) => {
const handleBeforeUpload = async (
file: UploadFile,
originFileList: Array<File>,
) => {
if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
file.status = 'removed';
return false;
}
// 多选或者非图片不唤起裁剪框
if (
attrs.crop &&
!attrs.multiple &&
originFileList[0] &&
isImageFile(file)
) {
file.status = 'removed';
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
const base64 = await cropImage(originFileList[0], attrs.aspectRatio);
return new Promise((resolve, reject) => {
if (!base64) {
return reject(new Error($t('ui.crop.cancel')));
}
const blob = base64ToBlob(base64 as string);
if (!blob) {
return reject(new Error($t('ui.crop.errorTip')));
}
resolve(blob);
});
}
return attrs.beforeUpload?.(file) ?? true;
};
const handleChange = async (event: UploadChangeParam) => {
const 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(
(file) => file.status !== 'removed',
);
@@ -375,6 +523,7 @@ async function initComponentAdapter() {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
component: Cascader,
fieldNames: { label: 'label', value: 'value', children: 'children' },
@@ -382,34 +531,20 @@ async function initComponentAdapter() {
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
}),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: Select,
loadingSlot: 'suffixIcon',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
}),
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
}),
AutoComplete,
Cascader,
Checkbox,

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { GlobalConfigProvider } from 'tdesign-vue-next';
import { onMounted } from 'vue';
import { watch } from 'vue';
import { usePreferences } from '@vben/preferences';
@@ -12,12 +12,13 @@ import zhConfig from 'tdesign-vue-next/es/locale/zh_CN';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
onMounted(() => {
document.documentElement.setAttribute(
'theme-mode',
isDark.value ? 'dark' : '',
);
});
watch(
() => isDark.value,
(dark) => {
document.documentElement.setAttribute('theme-mode', dark ? 'dark' : '');
},
{ immediate: true },
);
const customConfig: GlobalConfigProvider = {
// 可以在此处定义更多自定义配置,具体可配置内容参看 API 文档

View File

@@ -38,7 +38,7 @@ function notify(type: NotificationType) {
description="支持多语言,主题功能集成切换等"
title="TDesign Vue组件使用演示"
>
<Card class="mb-5" title="按钮">
<Card class="!mb-5" title="按钮">
<Space>
<Button>Default</Button>
<Button theme="primary"> Primary </Button>
@@ -46,7 +46,7 @@ function notify(type: NotificationType) {
<Button theme="danger"> Error </Button>
</Space>
</Card>
<Card class="mb-5" title="Message">
<Card class="!mb-5" title="Message">
<Space>
<Button @click="info"> 信息 </Button>
<Button theme="danger" @click="error"> 错误 </Button>
@@ -55,7 +55,7 @@ function notify(type: NotificationType) {
</Space>
</Card>
<Card class="mb-5" title="Notification">
<Card class="!mb-5" title="Notification">
<Space>
<Button @click="notify('info')"> 信息 </Button>
<Button theme="danger" @click="notify('error')"> 错误 </Button>