diff --git a/apps/backend-mock/api/upload.ts b/apps/backend-mock/api/upload.ts new file mode 100644 index 000000000..1bb9e602d --- /dev/null +++ b/apps/backend-mock/api/upload.ts @@ -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") +}); diff --git a/apps/backend-mock/routes/[...].ts b/apps/backend-mock/routes/[...].ts index 70c5f7c74..99f544b66 100644 --- a/apps/backend-mock/routes/[...].ts +++ b/apps/backend-mock/routes/[...].ts @@ -7,6 +7,7 @@ export default defineEventHandler(() => {
  • /api/menu/all
  • /api/auth/codes
  • /api/auth/login
  • +
  • /api/upload
  • `; }); diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 698f5c786..9cc430135 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -115,7 +115,9 @@ export type ComponentType = | 'DatePicker' | 'DefaultButton' | 'Divider' + | 'FileUpload' | 'IconPicker' + | 'ImageUpload' | 'Input' | 'InputNumber' | 'InputPassword' @@ -125,16 +127,14 @@ export type ComponentType = | 'RadioGroup' | 'RangePicker' | 'Rate' + | 'RichTextarea' | 'Select' | 'Space' | 'Switch' | 'Textarea' - | 'RichTextarea' | 'TimePicker' | 'TreeSelect' | 'Upload' - | 'FileUpload' - | 'ImageUpload' | BaseFormComponentType; async function initComponentAdapter() { diff --git a/docs/src/components/common-ui/vben-api-component.md b/docs/src/components/common-ui/vben-api-component.md index 33f7f045d..2c84e56b5 100644 --- a/docs/src/components/common-ui/vben-api-component.md +++ b/docs/src/components/common-ui/vben-api-component.md @@ -151,17 +151,17 @@ function fetchApi(): Promise> { | options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - | | visibleEvent | 触发重新请求数据的事件名 | `string` | - | - | | loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - | -| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| (item: OptionsItem[]) => OptionsItem \| false` | `false` | >5.5.4 | +| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 | #### autoSelect 自动设置选项 如果当前值为undefined,在选项数据成功加载之后,自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有: -- `first`:自动选择第一个选项 -- `last`:自动选择最后一个选项 -- `one`:有且仅有一个选项时,自动选择它 -- `函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项 -- false:不自动选择选项 +- `"first"`:自动选择第一个选项 +- `"last"`:自动选择最后一个选项 +- `"one"`:有且仅有一个选项时,自动选择它 +- `自定义函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项 +- `false`:不自动选择选项 ### Methods @@ -169,3 +169,5 @@ function fetchApi(): Promise> { | --- | --- | --- | --- | | getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 | | updateParam | 设置接口请求参数(将与params属性合并) | (newParams: Record)=>void | >5.5.4 | +| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 | +| getValue | 获取当前值 | ()=>any | >5.5.4 | diff --git a/docs/src/components/common-ui/vben-drawer.md b/docs/src/components/common-ui/vben-drawer.md index 16accf0e9..0eedb01b7 100644 --- a/docs/src/components/common-ui/vben-drawer.md +++ b/docs/src/components/common-ui/vben-drawer.md @@ -127,13 +127,14 @@ const [Drawer, drawerApi] = useVbenDrawer({ 除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。 -| 插槽名 | 描述 | -| -------------- | ------------------- | -| default | 默认插槽 - 弹窗内容 | -| prepend-footer | 取消按钮左侧 | -| append-footer | 取消按钮右侧 | -| close-icon | 关闭按钮图标 | -| extra | 额外内容(标题右侧) | +| 插槽名 | 描述 | +| -------------- | -------------------------------------------------- | +| default | 默认插槽 - 弹窗内容 | +| prepend-footer | 取消按钮左侧 | +| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) | +| append-footer | 确认按钮右侧 | +| close-icon | 关闭按钮图标 | +| extra | 额外内容(标题右侧) | ### drawerApi diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index 616d17cea..7abb3051e 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -310,7 +310,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单 | actionWrapperClass | 表单操作区域class | `any` | - | | handleReset | 表单重置回调 | `(values: Record,) => Promise \| void` | - | | handleSubmit | 表单提交回调 | `(values: Record,) => Promise \| void` | - | -| handleValuesChange | 表单值变化回调 | `(values: Record,) => void` | - | +| handleValuesChange | 表单值变化回调 | `(values: Record, fieldsChanged: string[]) => void` | - | | actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` | | resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - | | submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - | @@ -325,6 +325,12 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单 | submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false | | compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false | +::: tip handleValuesChange + +`handleValuesChange` 回调函数的第一个参数`values`装载了表单改变后的当前值对象,第二个参数`fieldsChanged`是一个数组,包含了所有被改变的字段名。注意:第二个参数仅在v5.5.4(不含)以上版本可用,并且传递的是已在schema中定义的字段名。如果你使用了字段映射并且需要检查是哪些字段发生了变化的话,请注意该参数并不会包含映射后的字段名。 + +::: + ::: tip fieldMappingTime 此属性用于将表单内的数组值映射成 2 个字段,它应当传入一个数组,数组的每一项是一个映射规则,规则的第一个成员是一个字符串,表示需要映射的字段名,第二个成员是一个数组,表示映射后的字段名,第三个成员是一个可选的格式掩码,用于格式化日期时间字段;也可以提供一个格式化函数(参数分别为当前值和当前字段名,返回格式化后的值)。如果明确地将格式掩码设为null,则原值映射而不进行格式化(适用于非日期时间字段)。例如:`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]`,`timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime`和`endTime`字段上。每一项的第三个参数是一个可选的格式掩码, diff --git a/docs/src/components/common-ui/vben-modal.md b/docs/src/components/common-ui/vben-modal.md index 72fdf9cfc..3c8200f90 100644 --- a/docs/src/components/common-ui/vben-modal.md +++ b/docs/src/components/common-ui/vben-modal.md @@ -59,7 +59,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda ::: info 注意 - `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及其子组件会在被关闭后完全销毁。 - 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。 ::: @@ -137,11 +137,12 @@ const [Modal, modalApi] = useVbenModal({ 除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。 -| 插槽名 | 描述 | -| -------------- | ------------------- | -| default | 默认插槽 - 弹窗内容 | -| prepend-footer | 取消按钮左侧 | -| append-footer | 取消按钮右侧 | +| 插槽名 | 描述 | +| -------------- | -------------------------------------------------- | +| default | 默认插槽 - 弹窗内容 | +| prepend-footer | 取消按钮左侧 | +| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) | +| append-footer | 确认按钮右侧 | ### modalApi diff --git a/docs/src/demos/vben-alert/prompt/index.vue b/docs/src/demos/vben-alert/prompt/index.vue index 02a4ccc5a..a19221347 100644 --- a/docs/src/demos/vben-alert/prompt/index.vue +++ b/docs/src/demos/vben-alert/prompt/index.vue @@ -1,7 +1,7 @@ diff --git a/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts b/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts index 20e4254cb..521a96472 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts +++ b/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts @@ -7,7 +7,7 @@ import type { AlertProps, BeforeCloseScope, PromptProps } from './alert'; import { h, nextTick, ref, render } from 'vue'; import { useSimpleLocale } from '@vben-core/composables'; -import { Input } from '@vben-core/shadcn-ui'; +import { Input, VbenRenderContent } from '@vben-core/shadcn-ui'; import { isFunction, isString } from '@vben-core/shared/utils'; import Alert from './alert.vue'; @@ -146,11 +146,7 @@ export async function vbenPrompt( const inputComponentRef = ref(null); const staticContents: Component[] = []; - if (isString(content)) { - staticContents.push(h('span', content)); - } else if (content) { - staticContents.push(content as Component); - } + staticContents.push(h(VbenRenderContent, { content, renderBr: true })); const modelPropName = _modelPropName || 'modelValue'; const componentProps = { ..._componentProps }; diff --git a/packages/@core/ui-kit/popup-ui/src/alert/alert.ts b/packages/@core/ui-kit/popup-ui/src/alert/alert.ts index 73d832dff..5a214fa2d 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/alert.ts +++ b/packages/@core/ui-kit/popup-ui/src/alert/alert.ts @@ -2,6 +2,8 @@ import type { Component, VNode, VNodeArrayChildren } from 'vue'; import type { Recordable } from '@vben-core/typings'; +import { createContext } from '@vben-core/shadcn-ui'; + export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning'; export type BeforeCloseScope = { @@ -70,3 +72,28 @@ export type PromptProps = { /** 输入组件的值属性名 */ modelPropName?: string; } & Omit; + +/** + * Alert上下文 + */ +export type AlertContext = { + /** 执行取消操作 */ + doCancel: () => void; + /** 执行确认操作 */ + doConfirm: () => void; +}; + +export const [injectAlertContext, provideAlertContext] = + createContext('VbenAlertContext'); + +/** + * 获取Alert上下文 + * @returns AlertContext + */ +export function useAlertContext() { + const context = injectAlertContext(); + if (!context) { + throw new Error('useAlertContext must be used within an AlertProvider'); + } + return context; +} diff --git a/packages/@core/ui-kit/popup-ui/src/alert/alert.vue b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue index 135b02780..9b133ee1d 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/alert.vue +++ b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue @@ -28,6 +28,8 @@ import { import { globalShareState } from '@vben-core/shared/global-state'; import { cn } from '@vben-core/shared/utils'; +import { provideAlertContext } from './alert'; + const props = withDefaults(defineProps(), { bordered: true, buttonAlign: 'end', @@ -87,6 +89,22 @@ const getIconRender = computed(() => { } return iconRender; }); + +function doCancel() { + handleCancel(); + handleOpenChange(false); +} + +function doConfirm() { + handleConfirm(); + handleOpenChange(false); +} + +provideAlertContext({ + doCancel, + doConfirm, +}); + function handleConfirm() { isConfirm.value = true; emits('confirm'); @@ -98,11 +116,13 @@ function handleCancel() { const loading = ref(false); async function handleOpenChange(val: boolean) { + const confirmState = isConfirm.value; + isConfirm.value = false; await nextTick(); if (!val && props.beforeClose) { loading.value = true; try { - const res = await props.beforeClose({ isConfirm: isConfirm.value }); + const res = await props.beforeClose({ isConfirm: confirmState }); if (res !== false) { open.value = false; } @@ -152,7 +172,7 @@ async function handleOpenChange(val: boolean) { -
    +
    diff --git a/packages/@core/ui-kit/popup-ui/src/alert/index.ts b/packages/@core/ui-kit/popup-ui/src/alert/index.ts index af8f424f1..8419b5b85 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/index.ts +++ b/packages/@core/ui-kit/popup-ui/src/alert/index.ts @@ -1,5 +1,10 @@ -export * from './alert'; - +export type { + AlertProps, + BeforeCloseScope, + IconType, + PromptProps, +} from './alert'; +export { useAlertContext } from './alert'; export { default as Alert } from './alert.vue'; export { vbenAlert as alert, diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts b/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts index 46dcafc30..365a2e4a0 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts +++ b/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts @@ -54,7 +54,6 @@ describe('drawerApi', () => { }); it('should close the drawer if onBeforeClose allows it', () => { - drawerApi.open(); drawerApi.close(); expect(drawerApi.store.state.isOpen).toBe(false); }); diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts b/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts index 785a9029e..a4a3ac4aa 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts +++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts @@ -86,12 +86,13 @@ export class DrawerApi { } /** - * 关闭弹窗 + * 关闭抽屉 + * @description 关闭抽屉时会调用 onBeforeClose 钩子函数,如果 onBeforeClose 返回 false,则不关闭弹窗 */ - close() { + async close() { // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗 // 如果 onBeforeClose 返回 false,则不关闭弹窗 - const allowClose = this.api.onBeforeClose?.() ?? true; + const allowClose = (await this.api.onBeforeClose?.()) ?? true; if (allowClose) { this.store.setState((prev) => ({ ...prev, diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts index b3ae0fb8b..30009a649 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts +++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts @@ -1,6 +1,6 @@ import type { Component, Ref } from 'vue'; -import type { ClassType } from '@vben-core/typings'; +import type { ClassType, MaybePromise } from '@vben-core/typings'; import type { DrawerApi } from './drawer-api'; @@ -151,7 +151,7 @@ export interface DrawerApiOptions extends DrawerState { * 关闭前的回调,返回 false 可以阻止关闭 * @returns */ - onBeforeClose?: () => void; + onBeforeClose?: () => MaybePromise; /** * 点击取消按钮的回调 */ diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue index 410c3ffe0..b5535ba47 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue +++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue @@ -274,7 +274,7 @@ const getAppendTo = computed(() => { {{ cancelText || $t('cancel') }} - + state?.value?.isOpen, @@ -186,7 +186,7 @@ const getAppendTo = computed(() => { }); const getForceMount = computed(() => { - return !unref(destroyOnClose); + return !unref(destroyOnClose) && unref(firstOpened); }); function handleClosed() { @@ -321,7 +321,7 @@ function handleClosed() { {{ cancelText || $t('cancel') }} - + ( injectData.options?.onOpenChange?.(isOpen); }; + mergedOptions.onClosed = () => { + options.onClosed?.(); + if (options.destroyOnClose) { + injectData.reCreateModal?.(); + } + }; + const api = new ModalApi(mergedOptions); const extendedApi: ExtendedModalApi = api as never; diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue b/packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue index 4c008ea86..1816051c6 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue @@ -31,12 +31,11 @@ export default defineComponent({ if (props.renderBr && isString(props.content)) { const lines = props.content.split('\n'); const result = []; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - result.push(h('span', { key: i }, line)); - if (i < lines.length - 1) { - result.push(h('br')); - } + for (const [i, line] of lines.entries()) { + result.push(h('p', { key: i }, line)); + // if (i < lines.length - 1) { + // result.push(h('br')); + // } } return result; } else { diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue b/packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue index f066b91ac..e101ba59a 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue @@ -39,6 +39,14 @@ const isAtRight = ref(false); const isAtBottom = ref(false); const isAtLeft = ref(true); +/** + * We have to check if the scroll amount is close enough to some threshold in order to + * more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded + * numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded. + * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled + */ +const ARRIVED_STATE_THRESHOLD_PIXELS = 1; + const showShadowTop = computed(() => props.shadow && props.shadowTop); const showShadowBottom = computed(() => props.shadow && props.shadowBottom); const showShadowLeft = computed(() => props.shadow && props.shadowLeft); @@ -60,14 +68,18 @@ function handleScroll(event: Event) { const target = event.target as HTMLElement; const scrollTop = target?.scrollTop ?? 0; const scrollLeft = target?.scrollLeft ?? 0; - const offsetHeight = target?.offsetHeight ?? 0; - const offsetWidth = target?.offsetWidth ?? 0; + const clientHeight = target?.clientHeight ?? 0; + const clientWidth = target?.clientWidth ?? 0; const scrollHeight = target?.scrollHeight ?? 0; const scrollWidth = target?.scrollWidth ?? 0; isAtTop.value = scrollTop <= 0; isAtLeft.value = scrollLeft <= 0; - isAtBottom.value = scrollTop + offsetHeight >= scrollHeight; - isAtRight.value = scrollLeft + offsetWidth >= scrollWidth; + isAtBottom.value = + Math.abs(scrollTop) + clientHeight >= + scrollHeight - ARRIVED_STATE_THRESHOLD_PIXELS; + isAtRight.value = + Math.abs(scrollLeft) + clientWidth >= + scrollWidth - ARRIVED_STATE_THRESHOLD_PIXELS; emit('scrollAt', { bottom: isAtBottom.value, diff --git a/packages/effects/common-ui/src/components/api-component/api-component.vue b/packages/effects/common-ui/src/components/api-component/api-component.vue index dcbacd4e2..a3e72b440 100644 --- a/packages/effects/common-ui/src/components/api-component/api-component.vue +++ b/packages/effects/common-ui/src/components/api-component/api-component.vue @@ -242,6 +242,10 @@ function emitChange() { } const componentRef = ref(); defineExpose({ + /** 获取options数据 */ + getOptions: () => unref(getOptions), + /** 获取当前值 */ + getValue: () => unref(modelValue), /** 获取被包装的组件实例 */ getComponentRef: () => componentRef.value as T, /** 更新Api参数 */ diff --git a/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts b/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts index 6129e9d80..dc727447c 100644 --- a/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts +++ b/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts @@ -74,7 +74,7 @@ function useMixedMenu() { */ const headerActive = computed(() => { if (!needSplit.value) { - return route.path; + return route.meta?.activePath ?? route.path; } return rootMenuPath.value; }); diff --git a/packages/effects/plugins/src/vxe-table/style.css b/packages/effects/plugins/src/vxe-table/style.css index cd1d67c44..5b47fa2cf 100644 --- a/packages/effects/plugins/src/vxe-table/style.css +++ b/packages/effects/plugins/src/vxe-table/style.css @@ -45,6 +45,9 @@ ); --vxe-ui-table-row-current-background-color: hsl(var(--accent)); --vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover)); + --vxe-ui-font-primary-tinge-color: hsl(var(--primary)); + --vxe-ui-font-primary-lighten-color: hsl(var(--primary) / 60%); + --vxe-ui-font-primary-darken-color: hsl(var(--primary)); height: auto !important; diff --git a/playground/src/adapter/vxe-table.ts b/playground/src/adapter/vxe-table.ts index cb24b561b..9c0cd73ba 100644 --- a/playground/src/adapter/vxe-table.ts +++ b/playground/src/adapter/vxe-table.ts @@ -212,7 +212,12 @@ setupVbenVxeTable({ Popconfirm, { getPopupContainer(el) { - return el.closest('tbody') || document.body; + return ( + el + .closest('.vxe-table--viewport-wrapper') + ?.querySelector('.vxe-table--main-wrapper') + ?.querySelector('tbody') || document.body + ); }, placement: 'topLeft', title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']), diff --git a/playground/src/api/examples/upload.ts b/playground/src/api/examples/upload.ts new file mode 100644 index 000000000..246d4f267 --- /dev/null +++ b/playground/src/api/examples/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))); + } +} diff --git a/playground/src/locales/langs/en-US/examples.json b/playground/src/locales/langs/en-US/examples.json index 1a25a983b..9335b28b7 100644 --- a/playground/src/locales/langs/en-US/examples.json +++ b/playground/src/locales/langs/en-US/examples.json @@ -18,7 +18,11 @@ "dynamic": "Dynamic Form", "custom": "Custom Component", "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": { "title": "Vxe Table", diff --git a/playground/src/locales/langs/zh-CN/examples.json b/playground/src/locales/langs/zh-CN/examples.json index 8f15d0202..ff11d7fd2 100644 --- a/playground/src/locales/langs/zh-CN/examples.json +++ b/playground/src/locales/langs/zh-CN/examples.json @@ -21,7 +21,11 @@ "dynamic": "动态表单", "custom": "自定义组件", "api": "Api", - "merge": "合并表单" + "merge": "合并表单", + "upload-error": "部分文件上传失败", + "upload-urls": "文件上传后的网址", + "file": "文件", + "upload-image": "点击上传图片" }, "vxeTable": { "title": "Vxe 表格", diff --git a/playground/src/views/examples/drawer/base-demo.vue b/playground/src/views/examples/drawer/base-demo.vue index 87ded0a2d..08bc68fc5 100644 --- a/playground/src/views/examples/drawer/base-demo.vue +++ b/playground/src/views/examples/drawer/base-demo.vue @@ -30,5 +30,6 @@ function lockDrawer() { + diff --git a/playground/src/views/examples/form/basic.vue b/playground/src/views/examples/form/basic.vue index 75e868d8f..d0e91d33a 100644 --- a/playground/src/views/examples/form/basic.vue +++ b/playground/src/views/examples/form/basic.vue @@ -1,5 +1,7 @@