Przeglądaj źródła

feat: add `useAlertContext` for Alert component (#5947)

* 新增Alert的子组件中获取弹窗上下文的能力
Netfan 4 tygodni temu
rodzic
commit
bf7496f0d5

+ 19 - 0
docs/src/components/common-ui/vben-alert.md

@@ -12,6 +12,12 @@ Alert提供的功能与Modal类似,但只适用于简单应用场景。例如
 
 :::
 
+::: tip 注意
+
+Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下,不支持HMR(热更新),代码变更后需要关闭这些弹窗后重新打开。
+
+:::
+
 ::: tip README
 
 下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
@@ -32,6 +38,19 @@ Alert提供的功能与Modal类似,但只适用于简单应用场景。例如
 
 <DemoPreview dir="demos/vben-alert/prompt" />
 
+## useAlertContext
+
+当弹窗的content、footer、icon使用自定义组件时,在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
+
+::: tip 注意 `useAlertContext`只能用在setup或者函数式组件中。:::
+
+### Methods
+
+| 方法      | 描述               | 类型     | 版本要求 |
+| --------- | ------------------ | -------- | -------- |
+| doConfirm | 调用弹窗的确认操作 | ()=>void | >5.5.4   |
+| doCancel  | 调用弹窗的取消操作 | ()=>void | >5.5.4   |
+
 ## 类型说明
 
 ```ts

+ 24 - 10
docs/src/demos/vben-alert/prompt/index.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import { h } from 'vue';
 
-import { alert, prompt, VbenButton } from '@vben/common-ui';
+import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
 
 import { Input, RadioGroup, Select } from 'ant-design-vue';
 import { BadgeJapaneseYen } from 'lucide-vue-next';
@@ -20,16 +20,30 @@ function showPrompt() {
 
 function showSlotsPrompt() {
   prompt({
-    component: Input,
-    componentProps: {
-      placeholder: '请输入',
-      prefix: '充值金额',
-      type: 'number',
-    },
-    componentSlots: {
-      addonAfter: () => h(BadgeJapaneseYen),
+    component: () => {
+      // 获取弹窗上下文。注意:只能在setup或者函数式组件中调用
+      const { doConfirm } = useAlertContext();
+      return h(
+        Input,
+        {
+          onKeydown(e: KeyboardEvent) {
+            if (e.key === 'Enter') {
+              e.preventDefault();
+              // 调用弹窗提供的确认方法
+              doConfirm();
+            }
+          },
+          placeholder: '请输入',
+          prefix: '充值金额:',
+          type: 'number',
+        },
+        {
+          addonAfter: () => h(BadgeJapaneseYen),
+        },
+      );
     },
-    content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
+    content:
+      '此弹窗演示了如何使用自定义插槽,并且可以使用useAlertContext获取到弹窗的上下文。\n在输入框中按下回车键会触发确认操作。',
     icon: 'question',
     modelPropName: 'value',
   }).then((val) => {

+ 2 - 6
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<T = any>(
   const inputComponentRef = ref<null | VNode>(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 };

+ 27 - 0
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<T = any> = {
   /** 输入组件的值属性名 */
   modelPropName?: string;
 } & Omit<AlertProps, 'beforeClose'>;
+
+/**
+ * Alert上下文
+ */
+export type AlertContext = {
+  /** 执行取消操作 */
+  doCancel: () => void;
+  /** 执行确认操作 */
+  doConfirm: () => void;
+};
+
+export const [injectAlertContext, provideAlertContext] =
+  createContext<AlertContext>('VbenAlertContext');
+
+/**
+ * 获取Alert上下文
+ * @returns AlertContext
+ */
+export function useAlertContext() {
+  const context = injectAlertContext();
+  if (!context) {
+    throw new Error('useAlertContext must be used within an AlertProvider');
+  }
+  return context;
+}

+ 20 - 1
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<AlertProps>(), {
   bordered: true,
   buttonAlign: 'end',
@@ -87,6 +89,23 @@ const getIconRender = computed(() => {
   }
   return iconRender;
 });
+
+function doCancel() {
+  isConfirm.value = false;
+  handleOpenChange(false);
+}
+
+function doConfirm() {
+  isConfirm.value = true;
+  handleOpenChange(false);
+  emits('confirm');
+}
+
+provideAlertContext({
+  doCancel,
+  doConfirm,
+});
+
 function handleConfirm() {
   isConfirm.value = true;
   emits('confirm');
@@ -152,7 +171,7 @@ async function handleOpenChange(val: boolean) {
           </div>
         </AlertDialogTitle>
         <AlertDialogDescription>
-          <div class="m-4 mb-6 min-h-[30px]">
+          <div class="m-4 min-h-[30px]">
             <VbenRenderContent :content="content" render-br />
           </div>
           <VbenLoading v-if="loading && contentMasking" :spinning="loading" />

+ 7 - 2
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,

+ 5 - 6
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 {