This commit is contained in:
dap 2025-04-20 09:14:37 +08:00
commit eff2f2a0b1
11 changed files with 140 additions and 11 deletions

View 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")
});

View File

@ -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>
`; `;
}); });

View File

@ -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等。
::: :::

View File

@ -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`版本。

View File

@ -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;
} }

View File

@ -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;

View File

@ -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;
}); });

View 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)));
}
}

View File

@ -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",

View File

@ -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 表格",

View File

@ -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',
},
], ],
// 321 // 321
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',