Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
commit
eff2f2a0b1
13
apps/backend-mock/api/upload.ts
Normal file
13
apps/backend-mock/api/upload.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { unAuthorizedResponse } from '~/utils/response';
|
||||||
|
|
||||||
|
export default eventHandler((event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
return useResponseSuccess({
|
||||||
|
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||||
|
});
|
||||||
|
// return useResponseError("test")
|
||||||
|
});
|
@ -7,6 +7,7 @@ export default defineEventHandler(() => {
|
|||||||
<li><a href="/api/menu">/api/menu/all</a></li>
|
<li><a href="/api/menu">/api/menu/all</a></li>
|
||||||
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
|
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
|
||||||
<li><a href="/api/auth/login">/api/auth/login</a></li>
|
<li><a href="/api/auth/login">/api/auth/login</a></li>
|
||||||
|
<li><a href="/api/upload">/api/upload</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
@ -59,7 +59,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
|
|||||||
::: info 注意
|
::: info 注意
|
||||||
|
|
||||||
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。另外,如果设置了`destroyOnClose`,内部Modal及其子组件会在被关闭后<b>完全销毁</b>。
|
||||||
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
@ -11,3 +11,7 @@
|
|||||||
当前只有对应的包下面存在 `tailwind.config.mjs` 文件才会启用 tailwindcss 的编译,否则不会启用 tailwindcss。如果你是纯粹的 SDK 包,不需要使用 tailwindcss,可以不用创建 `tailwind.config.mjs` 文件。
|
当前只有对应的包下面存在 `tailwind.config.mjs` 文件才会启用 tailwindcss 的编译,否则不会启用 tailwindcss。如果你是纯粹的 SDK 包,不需要使用 tailwindcss,可以不用创建 `tailwind.config.mjs` 文件。
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## 提示
|
||||||
|
|
||||||
|
现`tailwindcss`已至v4.x版本,使用方法与`tailwindcss: ^3.4.17`有差异,v4.0无法与v3.x版本兼容,在开发前请确认`package.json`中的`tailwindcss`版本。
|
||||||
|
@ -91,14 +91,13 @@ const getIconRender = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function doCancel() {
|
function doCancel() {
|
||||||
isConfirm.value = false;
|
handleCancel();
|
||||||
handleOpenChange(false);
|
handleOpenChange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function doConfirm() {
|
function doConfirm() {
|
||||||
isConfirm.value = true;
|
handleConfirm();
|
||||||
handleOpenChange(false);
|
handleOpenChange(false);
|
||||||
emits('confirm');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
provideAlertContext({
|
provideAlertContext({
|
||||||
@ -117,11 +116,13 @@ function handleCancel() {
|
|||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
async function handleOpenChange(val: boolean) {
|
async function handleOpenChange(val: boolean) {
|
||||||
|
const confirmState = isConfirm.value;
|
||||||
|
isConfirm.value = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (!val && props.beforeClose) {
|
if (!val && props.beforeClose) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await props.beforeClose({ isConfirm: isConfirm.value });
|
const res = await props.beforeClose({ isConfirm: confirmState });
|
||||||
if (res !== false) {
|
if (res !== false) {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
||||||
|
|
||||||
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
import {
|
||||||
|
defineComponent,
|
||||||
|
h,
|
||||||
|
inject,
|
||||||
|
nextTick,
|
||||||
|
provide,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import { useStore } from '@vben-core/shared/store';
|
import { useStore } from '@vben-core/shared/store';
|
||||||
|
|
||||||
@ -24,6 +32,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
const { connectedComponent } = options;
|
const { connectedComponent } = options;
|
||||||
if (connectedComponent) {
|
if (connectedComponent) {
|
||||||
const extendedApi = reactive({});
|
const extendedApi = reactive({});
|
||||||
|
const isModalReady = ref(true);
|
||||||
const Modal = defineComponent(
|
const Modal = defineComponent(
|
||||||
(props: TParentModalProps, { attrs, slots }) => {
|
(props: TParentModalProps, { attrs, slots }) => {
|
||||||
provide(USER_MODAL_INJECT_KEY, {
|
provide(USER_MODAL_INJECT_KEY, {
|
||||||
@ -33,6 +42,11 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
Object.setPrototypeOf(extendedApi, api);
|
Object.setPrototypeOf(extendedApi, api);
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
|
async reCreateModal() {
|
||||||
|
isModalReady.value = false;
|
||||||
|
await nextTick();
|
||||||
|
isModalReady.value = true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
checkProps(extendedApi as ExtendedModalApi, {
|
checkProps(extendedApi as ExtendedModalApi, {
|
||||||
...props,
|
...props,
|
||||||
@ -41,7 +55,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
});
|
});
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
connectedComponent,
|
isModalReady.value ? connectedComponent : 'div',
|
||||||
{
|
{
|
||||||
...props,
|
...props,
|
||||||
...attrs,
|
...attrs,
|
||||||
@ -70,6 +84,13 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
injectData.options?.onOpenChange?.(isOpen);
|
injectData.options?.onOpenChange?.(isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mergedOptions.onClosed = () => {
|
||||||
|
options.onClosed?.();
|
||||||
|
if (options.destroyOnClose) {
|
||||||
|
injectData.reCreateModal?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const api = new ModalApi(mergedOptions);
|
const api = new ModalApi(mergedOptions);
|
||||||
|
|
||||||
const extendedApi: ExtendedModalApi = api as never;
|
const extendedApi: ExtendedModalApi = api as never;
|
||||||
|
@ -74,7 +74,7 @@ function useMixedMenu() {
|
|||||||
*/
|
*/
|
||||||
const headerActive = computed(() => {
|
const headerActive = computed(() => {
|
||||||
if (!needSplit.value) {
|
if (!needSplit.value) {
|
||||||
return route.path;
|
return route.meta?.activePath ?? route.path;
|
||||||
}
|
}
|
||||||
return rootMenuPath.value;
|
return rootMenuPath.value;
|
||||||
});
|
});
|
||||||
|
25
playground/src/api/examples/upload.ts
Normal file
25
playground/src/api/examples/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)));
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,11 @@
|
|||||||
"dynamic": "Dynamic Form",
|
"dynamic": "Dynamic Form",
|
||||||
"custom": "Custom Component",
|
"custom": "Custom Component",
|
||||||
"api": "Api",
|
"api": "Api",
|
||||||
"merge": "Merge Form"
|
"merge": "Merge Form",
|
||||||
|
"upload-error": "Partial file upload failed",
|
||||||
|
"upload-urls": "Urls after file upload",
|
||||||
|
"file": "file",
|
||||||
|
"upload-image": "Click to upload image"
|
||||||
},
|
},
|
||||||
"vxeTable": {
|
"vxeTable": {
|
||||||
"title": "Vxe Table",
|
"title": "Vxe Table",
|
||||||
|
@ -21,7 +21,11 @@
|
|||||||
"dynamic": "动态表单",
|
"dynamic": "动态表单",
|
||||||
"custom": "自定义组件",
|
"custom": "自定义组件",
|
||||||
"api": "Api",
|
"api": "Api",
|
||||||
"merge": "合并表单"
|
"merge": "合并表单",
|
||||||
|
"upload-error": "部分文件上传失败",
|
||||||
|
"upload-urls": "文件上传后的网址",
|
||||||
|
"file": "文件",
|
||||||
|
"upload-image": "点击上传图片"
|
||||||
},
|
},
|
||||||
"vxeTable": {
|
"vxeTable": {
|
||||||
"title": "Vxe 表格",
|
"title": "Vxe 表格",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { h, ref } from 'vue';
|
import type { UploadFile } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { h, ref, toRaw } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
@ -9,6 +11,8 @@ import dayjs from 'dayjs';
|
|||||||
|
|
||||||
import { useVbenForm, z } from '#/adapter/form';
|
import { useVbenForm, z } from '#/adapter/form';
|
||||||
import { getAllMenusApi } from '#/api';
|
import { getAllMenusApi } from '#/api';
|
||||||
|
import { upload_file } from '#/api/examples/upload';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import DocButton from '../doc-button.vue';
|
import DocButton from '../doc-button.vue';
|
||||||
|
|
||||||
@ -329,12 +333,56 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
|||||||
fieldName: 'treeSelect',
|
fieldName: 'treeSelect',
|
||||||
label: '树选择',
|
label: '树选择',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'Upload',
|
||||||
|
componentProps: {
|
||||||
|
// 更多属性见:https://ant.design/components/upload-cn
|
||||||
|
accept: '.png,.jpg,.jpeg',
|
||||||
|
// 自动携带认证信息
|
||||||
|
customRequest: upload_file,
|
||||||
|
disabled: false,
|
||||||
|
maxCount: 1,
|
||||||
|
multiple: false,
|
||||||
|
showUploadList: true,
|
||||||
|
// 上传列表的内建样式,支持四种基本样式 text, picture, picture-card 和 picture-circle
|
||||||
|
listType: 'picture-card',
|
||||||
|
},
|
||||||
|
fieldName: 'files',
|
||||||
|
label: $t('examples.form.file'),
|
||||||
|
renderComponentContent: () => {
|
||||||
|
return {
|
||||||
|
default: () => $t('examples.form.upload-image'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(values: Record<string, any>) {
|
function onSubmit(values: Record<string, any>) {
|
||||||
|
const files = toRaw(values.files) as UploadFile[];
|
||||||
|
const doneFiles = files.filter((file) => file.status === 'done');
|
||||||
|
const failedFiles = files.filter((file) => file.status !== 'done');
|
||||||
|
|
||||||
|
const msg = [
|
||||||
|
...doneFiles.map((file) => file.response?.url || file.url),
|
||||||
|
...failedFiles.map((file) => file.name),
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
|
if (failedFiles.length === 0) {
|
||||||
|
message.success({
|
||||||
|
content: `${$t('examples.form.upload-urls')}: ${msg}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error({
|
||||||
|
content: `${$t('examples.form.upload-error')}: ${msg}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果需要可提交前替换为需要的urls
|
||||||
|
values.files = doneFiles.map((file) => file.response?.url || file.url);
|
||||||
message.success({
|
message.success({
|
||||||
content: `form values: ${JSON.stringify(values)}`,
|
content: `form values: ${JSON.stringify(values)}`,
|
||||||
});
|
});
|
||||||
@ -347,6 +395,14 @@ function handleSetFormValue() {
|
|||||||
baseFormApi.setValues({
|
baseFormApi.setValues({
|
||||||
checkboxGroup: ['1'],
|
checkboxGroup: ['1'],
|
||||||
datePicker: dayjs('2022-01-01'),
|
datePicker: dayjs('2022-01-01'),
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
name: 'example.png',
|
||||||
|
status: 'done',
|
||||||
|
uid: '-1',
|
||||||
|
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||||
|
},
|
||||||
|
],
|
||||||
mentions: '@afc163',
|
mentions: '@afc163',
|
||||||
number: 3,
|
number: 3,
|
||||||
options: '1',
|
options: '1',
|
||||||
|
Loading…
Reference in New Issue
Block a user