From 44138f578f45779856e9843cde1081b8a939c090 Mon Sep 17 00:00:00 2001 From: Netfan Date: Tue, 1 Apr 2025 15:10:18 +0800 Subject: [PATCH] feat: add preset alert, confirm, prompt components that can be simple called (#5843) * feat: add preset alert, confirm, prompt components that can be simple called * fix: type define --- docs/.vitepress/config/zh.mts | 4 + docs/src/components/common-ui/vben-alert.md | 101 +++++++++ docs/src/demos/vben-alert/alert/index.vue | 31 +++ docs/src/demos/vben-alert/confirm/index.vue | 39 ++++ docs/src/demos/vben-alert/prompt/index.vue | 41 ++++ packages/@core/base/icons/src/lucide.ts | 2 + .../src/use-simple-locale/messages.ts | 2 + .../ui-kit/popup-ui/src/alert/AlertBuilder.ts | 203 ++++++++++++++++++ .../@core/ui-kit/popup-ui/src/alert/alert.ts | 28 +++ .../@core/ui-kit/popup-ui/src/alert/alert.vue | 181 ++++++++++++++++ .../@core/ui-kit/popup-ui/src/alert/index.ts | 9 + packages/@core/ui-kit/popup-ui/src/index.ts | 1 + .../render-content/render-content.vue | 21 +- .../src/ui/alert-dialog/AlertDialog.vue | 16 ++ .../src/ui/alert-dialog/AlertDialogAction.vue | 13 ++ .../src/ui/alert-dialog/AlertDialogCancel.vue | 13 ++ .../ui/alert-dialog/AlertDialogContent.vue | 91 ++++++++ .../alert-dialog/AlertDialogDescription.vue | 28 +++ .../ui/alert-dialog/AlertDialogOverlay.vue | 8 + .../src/ui/alert-dialog/AlertDialogTitle.vue | 30 +++ .../shadcn-ui/src/ui/alert-dialog/index.ts | 6 + .../@core/ui-kit/shadcn-ui/src/ui/index.ts | 1 + playground/src/views/examples/modal/index.vue | 76 ++++++- 23 files changed, 941 insertions(+), 4 deletions(-) create mode 100644 docs/src/components/common-ui/vben-alert.md create mode 100644 docs/src/demos/vben-alert/alert/index.vue create mode 100644 docs/src/demos/vben-alert/confirm/index.vue create mode 100644 docs/src/demos/vben-alert/prompt/index.vue create mode 100644 packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts create mode 100644 packages/@core/ui-kit/popup-ui/src/alert/alert.ts create mode 100644 packages/@core/ui-kit/popup-ui/src/alert/alert.vue create mode 100644 packages/@core/ui-kit/popup-ui/src/alert/index.ts create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts diff --git a/docs/.vitepress/config/zh.mts b/docs/.vitepress/config/zh.mts index ce544393..2c3753de 100644 --- a/docs/.vitepress/config/zh.mts +++ b/docs/.vitepress/config/zh.mts @@ -168,6 +168,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] { link: 'common-ui/vben-api-component', text: 'ApiComponent Api组件包装器', }, + { + link: 'common-ui/vben-alert', + text: 'Alert 轻量提示框', + }, { link: 'common-ui/vben-modal', text: 'Modal 模态框', diff --git a/docs/src/components/common-ui/vben-alert.md b/docs/src/components/common-ui/vben-alert.md new file mode 100644 index 00000000..aac6c237 --- /dev/null +++ b/docs/src/components/common-ui/vben-alert.md @@ -0,0 +1,101 @@ +--- +outline: deep +--- + +# Vben Alert 轻量提示框 + +框架提供的一些用于轻量提示的弹窗,仅使用js代码即可快速动态创建提示而不需要在template写任何代码。 + +::: info 应用场景 + +Alert提供的功能与Modal类似,但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求,请使用VbenModal + +::: + +::: tip README + +下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。 + +::: + +## 基础用法 + +使用 `alert` 创建只有一个确认按钮的提示框。 + + + +使用 `confirm` 创建有确认和取消按钮的提示框。 + + + +使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。 + + + +## 类型说明 + +```ts +/** 预置的图标类型 */ +export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning'; + +export type AlertProps = { + /** 关闭前的回调,如果返回false,则终止关闭 */ + beforeClose?: () => boolean | Promise | undefined; + /** 边框 */ + bordered?: boolean; + /** 取消按钮的标题 */ + cancelText?: string; + /** 是否居中显示 */ + centered?: boolean; + /** 确认按钮的标题 */ + confirmText?: string; + /** 弹窗容器的额外样式 */ + containerClass?: string; + /** 弹窗提示内容 */ + content: Component | string; + /** 弹窗内容的额外样式 */ + contentClass?: string; + /** 弹窗的图标(在标题的前面) */ + icon?: Component | IconType; + /** 是否显示取消按钮 */ + showCancel?: boolean; + /** 弹窗标题 */ + title?: string; +}; + +/** + * 函数签名 + * alert和confirm的函数签名相同。 + * confirm默认会显示取消按钮,而alert默认只有一个按钮 + * */ +export function alert(options: AlertProps): Promise; +export function alert( + message: string, + options?: Partial, +): Promise; +export function alert( + message: string, + title?: string, + options?: Partial, +): Promise; + +/** + * 弹出输入框的函数签名。 + * 参数beforeClose会传入用户当前输入的值 + * component指定接受用户输入的组件,默认为Input + * componentProps 为输入组件设置的属性数据 + * defaultValue 默认的值 + * modelPropName 输入组件的值属性名称。默认为modelValue + */ +export async function prompt( + options: Omit & { + beforeClose?: ( + val: T, + ) => boolean | Promise | undefined; + component?: Component; + componentProps?: Recordable; + defaultValue?: T; + modelPropName?: string; + }, +): Promise; +``` diff --git a/docs/src/demos/vben-alert/alert/index.vue b/docs/src/demos/vben-alert/alert/index.vue new file mode 100644 index 00000000..103ce64f --- /dev/null +++ b/docs/src/demos/vben-alert/alert/index.vue @@ -0,0 +1,31 @@ + + diff --git a/docs/src/demos/vben-alert/confirm/index.vue b/docs/src/demos/vben-alert/confirm/index.vue new file mode 100644 index 00000000..07f3570b --- /dev/null +++ b/docs/src/demos/vben-alert/confirm/index.vue @@ -0,0 +1,39 @@ + + diff --git a/docs/src/demos/vben-alert/prompt/index.vue b/docs/src/demos/vben-alert/prompt/index.vue new file mode 100644 index 00000000..423124e7 --- /dev/null +++ b/docs/src/demos/vben-alert/prompt/index.vue @@ -0,0 +1,41 @@ + + diff --git a/packages/@core/base/icons/src/lucide.ts b/packages/@core/base/icons/src/lucide.ts index 44d07f94..70e6a426 100644 --- a/packages/@core/base/icons/src/lucide.ts +++ b/packages/@core/base/icons/src/lucide.ts @@ -15,8 +15,10 @@ export { ChevronsLeft, ChevronsRight, Circle, + CircleAlert, CircleCheckBig, CircleHelp, + CircleX, Copy, CornerDownLeft, Ellipsis, diff --git a/packages/@core/composables/src/use-simple-locale/messages.ts b/packages/@core/composables/src/use-simple-locale/messages.ts index 4b1eca29..efc5c3cc 100644 --- a/packages/@core/composables/src/use-simple-locale/messages.ts +++ b/packages/@core/composables/src/use-simple-locale/messages.ts @@ -6,6 +6,7 @@ export const messages: Record> = { collapse: 'Collapse', confirm: 'Confirm', expand: 'Expand', + prompt: 'Prompt', reset: 'Reset', submit: 'Submit', }, @@ -14,6 +15,7 @@ export const messages: Record> = { collapse: '收起', confirm: '确认', expand: '展开', + prompt: '提示', reset: '重置', submit: '提交', }, diff --git a/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts b/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts new file mode 100644 index 00000000..71e7d9db --- /dev/null +++ b/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts @@ -0,0 +1,203 @@ +import type { Component } from 'vue'; + +import type { Recordable } from '@vben-core/typings'; + +import type { AlertProps } from './alert'; + +import { h, ref, render } from 'vue'; + +import { useSimpleLocale } from '@vben-core/composables'; +import { Input } from '@vben-core/shadcn-ui'; +import { isFunction, isString } from '@vben-core/shared/utils'; + +import Alert from './alert.vue'; + +const alerts = ref>([]); + +const { $t } = useSimpleLocale(); + +export function vbenAlert(options: AlertProps): Promise; +export function vbenAlert( + message: string, + options?: Partial, +): Promise; +export function vbenAlert( + message: string, + title?: string, + options?: Partial, +): Promise; + +export function vbenAlert( + arg0: AlertProps | string, + arg1?: Partial | string, + arg2?: Partial, +): Promise { + return new Promise((resolve, reject) => { + const options: AlertProps = isString(arg0) + ? { + content: arg0, + } + : { ...arg0 }; + if (arg1) { + if (isString(arg1)) { + options.title = arg1; + } else if (!isString(arg1)) { + // 如果第二个参数是对象,则合并到选项中 + Object.assign(options, arg1); + } + } + + if (arg2 && !isString(arg2)) { + Object.assign(options, arg2); + } + // 创建容器元素 + const container = document.createElement('div'); + document.body.append(container); + + // 创建一个引用,用于在回调中访问实例 + const alertRef = { container, instance: null as any }; + + const props: AlertProps & Recordable = { + onClosed: (isConfirm: boolean) => { + // 移除组件实例以及创建的所有dom(恢复页面到打开前的状态) + // 从alerts数组中移除该实例 + alerts.value = alerts.value.filter((item) => item !== alertRef); + + // 从DOM中移除容器 + render(null, container); + if (container.parentNode) { + container.remove(); + } + + // 解析 Promise,传递用户操作结果 + if (isConfirm) { + resolve(); + } else { + reject(new Error('dialog cancelled')); + } + }, + ...options, + open: true, + title: options.title ?? $t.value('prompt'), + }; + + // 创建Alert组件的VNode + const vnode = h(Alert, props); + + // 渲染组件到容器 + render(vnode, container); + + // 保存组件实例引用 + alertRef.instance = vnode.component?.proxy as Component; + + // 将实例和容器添加到alerts数组中 + alerts.value.push(alertRef); + }); +} + +export function vbenConfirm(options: AlertProps): Promise; +export function vbenConfirm( + message: string, + options?: Partial, +): Promise; +export function vbenConfirm( + message: string, + title?: string, + options?: Partial, +): Promise; + +export function vbenConfirm( + arg0: AlertProps | string, + arg1?: Partial | string, + arg2?: Partial, +): Promise { + const defaultProps: Partial = { + showCancel: true, + }; + if (!arg1) { + return isString(arg0) + ? vbenAlert(arg0, defaultProps) + : vbenAlert({ ...defaultProps, ...arg0 }); + } else if (!arg2) { + return isString(arg1) + ? vbenAlert(arg0 as string, arg1, defaultProps) + : vbenAlert(arg0 as string, { ...defaultProps, ...arg1 }); + } + return vbenAlert(arg0 as string, arg1 as string, { + ...defaultProps, + ...arg2, + }); +} + +export async function vbenPrompt( + options: Omit & { + beforeClose?: ( + val: T, + ) => boolean | Promise | undefined; + component?: Component; + componentProps?: Recordable; + defaultValue?: T; + modelPropName?: string; + }, +): Promise { + const { + component: _component, + componentProps: _componentProps, + content, + defaultValue, + modelPropName: _modelPropName, + ...delegated + } = options; + const contents: Component[] = []; + const modelValue = ref(defaultValue); + if (isString(content)) { + contents.push(h('span', content)); + } else { + contents.push(content); + } + const componentProps = _componentProps || {}; + const modelPropName = _modelPropName || 'modelValue'; + componentProps[modelPropName] = modelValue.value; + componentProps[`onUpdate:${modelPropName}`] = (val: any) => { + modelValue.value = val; + }; + const componentRef = h(_component || Input, componentProps); + contents.push(componentRef); + const props: AlertProps & Recordable = { + ...delegated, + async beforeClose() { + if (delegated.beforeClose) { + return await delegated.beforeClose(modelValue.value); + } + }, + content: h( + 'div', + { class: 'flex flex-col gap-2' }, + { default: () => contents }, + ), + onOpened() { + // 组件挂载完成后,自动聚焦到输入组件 + if ( + componentRef.component?.exposed && + isFunction(componentRef.component.exposed.focus) + ) { + componentRef.component.exposed.focus(); + } else if (componentRef.el && isFunction(componentRef.el.focus)) { + componentRef.el.focus(); + } + }, + }; + await vbenConfirm(props); + return modelValue.value; +} + +export function clearAllAlerts() { + alerts.value.forEach((alert) => { + // 从DOM中移除容器 + render(null, alert.container); + if (alert.container.parentNode) { + alert.container.remove(); + } + }); + alerts.value = []; +} diff --git a/packages/@core/ui-kit/popup-ui/src/alert/alert.ts b/packages/@core/ui-kit/popup-ui/src/alert/alert.ts new file mode 100644 index 00000000..97002f70 --- /dev/null +++ b/packages/@core/ui-kit/popup-ui/src/alert/alert.ts @@ -0,0 +1,28 @@ +import type { Component } from 'vue'; + +export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning'; + +export type AlertProps = { + /** 关闭前的回调,如果返回false,则终止关闭 */ + beforeClose?: () => boolean | Promise | undefined; + /** 边框 */ + bordered?: boolean; + /** 取消按钮的标题 */ + cancelText?: string; + /** 是否居中显示 */ + centered?: boolean; + /** 确认按钮的标题 */ + confirmText?: string; + /** 弹窗容器的额外样式 */ + containerClass?: string; + /** 弹窗提示内容 */ + content: Component | string; + /** 弹窗内容的额外样式 */ + contentClass?: string; + /** 弹窗的图标(在标题的前面) */ + icon?: Component | IconType; + /** 是否显示取消按钮 */ + showCancel?: boolean; + /** 弹窗标题 */ + title?: string; +}; diff --git a/packages/@core/ui-kit/popup-ui/src/alert/alert.vue b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue new file mode 100644 index 00000000..7cc54ff5 --- /dev/null +++ b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue @@ -0,0 +1,181 @@ + + diff --git a/packages/@core/ui-kit/popup-ui/src/alert/index.ts b/packages/@core/ui-kit/popup-ui/src/alert/index.ts new file mode 100644 index 00000000..af8f424f --- /dev/null +++ b/packages/@core/ui-kit/popup-ui/src/alert/index.ts @@ -0,0 +1,9 @@ +export * from './alert'; + +export { default as Alert } from './alert.vue'; +export { + vbenAlert as alert, + clearAllAlerts, + vbenConfirm as confirm, + vbenPrompt as prompt, +} from './AlertBuilder'; diff --git a/packages/@core/ui-kit/popup-ui/src/index.ts b/packages/@core/ui-kit/popup-ui/src/index.ts index 56e7ade5..7b9c47f4 100644 --- a/packages/@core/ui-kit/popup-ui/src/index.ts +++ b/packages/@core/ui-kit/popup-ui/src/index.ts @@ -1,2 +1,3 @@ +export * from './alert'; export * from './drawer'; export * from './modal'; 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 87f9769b..4c008ea8 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 @@ -3,7 +3,7 @@ import type { Component, PropType } from 'vue'; import { defineComponent, h } from 'vue'; -import { isFunction, isObject } from '@vben-core/shared/utils'; +import { isFunction, isObject, isString } from '@vben-core/shared/utils'; export default defineComponent({ name: 'RenderContent', @@ -14,6 +14,10 @@ export default defineComponent({ | undefined, type: [Object, String, Function], }, + renderBr: { + default: false, + type: Boolean, + }, }, setup(props, { attrs, slots }) { return () => { @@ -24,7 +28,20 @@ export default defineComponent({ (isObject(props.content) || isFunction(props.content)) && props.content !== null; if (!isComponent) { - return props.content; + 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')); + } + } + return result; + } else { + return props.content; + } } return h(props.content as never, { ...attrs, diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue new file mode 100644 index 00000000..f2052887 --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue new file mode 100644 index 00000000..6b2d75c8 --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue new file mode 100644 index 00000000..9f1d2903 --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue new file mode 100644 index 00000000..b14dc0ec --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue @@ -0,0 +1,91 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue new file mode 100644 index 00000000..da231989 --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue new file mode 100644 index 00000000..5673319d --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue @@ -0,0 +1,8 @@ + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue new file mode 100644 index 00000000..01d4759f --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts new file mode 100644 index 00000000..ef14f2d5 --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts @@ -0,0 +1,6 @@ +export { default as AlertDialog } from './AlertDialog.vue'; +export { default as AlertDialogAction } from './AlertDialogAction.vue'; +export { default as AlertDialogCancel } from './AlertDialogCancel.vue'; +export { default as AlertDialogContent } from './AlertDialogContent.vue'; +export { default as AlertDialogDescription } from './AlertDialogDescription.vue'; +export { default as AlertDialogTitle } from './AlertDialogTitle.vue'; diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/index.ts b/packages/@core/ui-kit/shadcn-ui/src/ui/index.ts index 5c9e8046..1be71f86 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/ui/index.ts +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/index.ts @@ -1,4 +1,5 @@ export * from './accordion'; +export * from './alert-dialog'; export * from './avatar'; export * from './badge'; export * from './breadcrumb'; diff --git a/playground/src/views/examples/modal/index.vue b/playground/src/views/examples/modal/index.vue index 690f62e7..23cfab12 100644 --- a/playground/src/views/examples/modal/index.vue +++ b/playground/src/views/examples/modal/index.vue @@ -1,7 +1,16 @@ + +

通过快捷方法创建动态提示弹窗,适合一些轻量的提示和确认、输入等

+ +