This commit is contained in:
dap 2025-04-01 17:16:13 +08:00
commit 44ad2c5f8d
27 changed files with 1121 additions and 5 deletions

View File

@ -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 模态框',

View File

@ -0,0 +1,101 @@
---
outline: deep
---
# Vben Alert 轻量提示框
框架提供的一些用于轻量提示的弹窗仅使用js代码即可快速动态创建提示而不需要在template写任何代码。
::: info 应用场景
Alert提供的功能与Modal类似但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求请使用VbenModal
:::
::: tip README
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
:::
## 基础用法
使用 `alert` 创建只有一个确认按钮的提示框。
<DemoPreview dir="demos/vben-alert/alert" />
使用 `confirm` 创建有确认和取消按钮的提示框。
<DemoPreview dir="demos/vben-alert/confirm" />
使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。
<DemoPreview dir="demos/vben-alert/prompt" />
## 类型说明
```ts
/** 预置的图标类型 */
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
export type AlertProps = {
/** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: () => boolean | Promise<boolean | undefined> | 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<void>;
export function alert(
message: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function alert(
message: string,
title?: string,
options?: Partial<AlertProps>,
): Promise<void>;
/**
* 弹出输入框的函数签名。
* 参数beforeClose会传入用户当前输入的值
* component指定接受用户输入的组件默认为Input
* componentProps 为输入组件设置的属性数据
* defaultValue 默认的值
* modelPropName 输入组件的值属性名称。默认为modelValue
*/
export async function prompt<T = any>(
options: Omit<AlertProps, 'beforeClose'> & {
beforeClose?: (
val: T,
) => boolean | Promise<boolean | undefined> | undefined;
component?: Component;
componentProps?: Recordable<any>;
defaultValue?: T;
modelPropName?: string;
},
): Promise<T | undefined>;
```

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import { h } from 'vue';
import { alert, VbenButton } from '@vben/common-ui';
import { Empty } from 'ant-design-vue';
function showAlert() {
alert('This is an alert message');
}
function showIconAlert() {
alert({
content: 'This is an alert message with icon',
icon: 'success',
});
}
function showCustomAlert() {
alert({
content: h(Empty, { description: '什么都没有' }),
});
}
</script>
<template>
<div class="flex gap-4">
<VbenButton @click="showAlert">Alert</VbenButton>
<VbenButton @click="showIconAlert">Alert With Icon</VbenButton>
<VbenButton @click="showCustomAlert">Alert With Custom Content</VbenButton>
</div>
</template>

View File

@ -0,0 +1,39 @@
<script lang="ts" setup>
import { alert, confirm, VbenButton } from '@vben/common-ui';
function showConfirm() {
confirm('This is an alert message')
.then(() => {
alert('Confirmed');
})
.catch(() => {
alert('Canceled');
});
}
function showIconConfirm() {
confirm({
content: 'This is an alert message with icon',
icon: 'success',
});
}
function showAsyncConfirm() {
confirm({
beforeClose() {
return new Promise((resolve) => setTimeout(resolve, 2000));
},
content: 'This is an alert message with async confirm',
icon: 'success',
}).then(() => {
alert('Confirmed');
});
}
</script>
<template>
<div class="flex gap-4">
<VbenButton @click="showConfirm">Confirm</VbenButton>
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
</div>
</template>

View File

@ -0,0 +1,41 @@
<script lang="ts" setup>
import { alert, prompt, VbenButton } from '@vben/common-ui';
import { VbenSelect } from '@vben-core/shadcn-ui';
function showPrompt() {
prompt({
content: '请输入一些东西',
})
.then((val) => {
alert(`已收到你的输入:${val}`);
})
.catch(() => {
alert('Canceled');
});
}
function showSelectPrompt() {
prompt({
component: VbenSelect,
componentProps: {
options: [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
],
placeholder: '请选择',
},
content: 'This is an alert message with icon',
icon: 'question',
}).then((val) => {
alert(`你选择的是${val}`);
});
}
</script>
<template>
<div class="flex gap-4">
<VbenButton @click="showPrompt">Prompt</VbenButton>
<VbenButton @click="showSelectPrompt">Confirm With Select</VbenButton>
</div>
</template>

View File

@ -150,6 +150,73 @@ To run the `docs` application:
pnpm dev:docs
```
### Distinguishing Build Environments
In actual business development, multiple environments are usually distinguished during the build process, such as the test environment `test` and the production environment `build`.
At this point, you can modify three files and add corresponding script configurations to distinguish between production environments.
Take the addition of the test environment `test` to `@vben/web-antd` as an example:
- `apps\web-antd\package.json`
```json
"scripts": {
"build:prod": "pnpm vite build --mode production",
"build:test": "pnpm vite build --mode test",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
}
```
Add the command `"build:test"` and change the original `"build"` to `"build:prod"` to avoid building packages for two environments simultaneously.
- `package.json`
```json
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
"build:analyze": "turbo build:analyze",
"build:antd": "pnpm run build --filter=@vben/web-antd",
"build-test:antd": "pnpm run build --filter=@vben/web-antd build:test",
······
}
```
Add the command to build the test environment in the root directory `package.json`.
- `turbo.json`
```json
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"dist/**",
"dist.zip",
".vitepress/dist.zip",
".vitepress/dist/**"
]
},
"build-test:antd": {
"dependsOn": ["@vben/web-antd#build:test"],
"outputs": ["dist/**"]
},
"@vben/web-antd#build:test": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
······
```
Add the relevant dependent commands in `turbo.json`.
## Public Static Resources
If you need to use public static resources in the project, such as images, static HTML, etc., and you want to directly import them in the development process through `src="/xxx.png"`.

View File

@ -150,6 +150,73 @@ pnpm dev:ele
pnpm dev:docs
```
## 区分构建环境
在实际的业务开发中,通常会在构建时区分多种环境,如测试环境`test`、生产环境`build`等。
此时可以修改三个文件,在其中增加对应的脚本配置来达到区分生产环境的效果。
以`@vben/web-antd`添加测试环境`test`为例:
- `apps\web-antd\package.json`
```json
"scripts": {
"build:prod": "pnpm vite build --mode production",
"build:test": "pnpm vite build --mode test",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
```
增加命令`"build:test"`, 并将原`"build"`改为`"build:prod"`以避免同时构建两个环境的包。
- `package.json`
```json
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
"build:analyze": "turbo build:analyze",
"build:antd": "pnpm run build --filter=@vben/web-antd",
"build-test:antd": "pnpm run build --filter=@vben/web-antd build:test",
······
}
```
在根目录`package.json`中加入构建测试环境的命令
- `turbo.json`
```json
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"dist/**",
"dist.zip",
".vitepress/dist.zip",
".vitepress/dist/**"
]
},
"build-test:antd": {
"dependsOn": ["@vben/web-antd#build:test"],
"outputs": ["dist/**"]
},
"@vben/web-antd#build:test": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
······
```
在`turbo.json`中加入相关依赖的命令
## 公共静态资源
项目中需要使用到的公共静态资源图片、静态HTML等需要在开发中通过 `src="/xxx.png"` 直接引入的。

View File

@ -46,3 +46,47 @@ async function getVersionTag() {
}
}
```
## 替换为第三方库检查更新方式
如果需要通过其他方式检查更新例如使用其他版本控制方式chunkHash、version.json、使用`Web Worker`在后台轮询更新、自定义检查更新时机不使用轮询你可以通过JS库`version-polling`来实现。
```bash
pnpm add version-polling
```
以`apps/web-antd`项目为例,在项目入口文件`main.ts`或者`app.vue`添加以下代码
```ts
import { h } from 'vue';
import { Button, notification } from 'ant-design-vue';
import { createVersionPolling } from 'version-polling';
createVersionPolling({
silent: import.meta.env.MODE === 'development', // 开发环境下不检测
onUpdate: (self) => {
const key = `open${Date.now()}`;
notification.info({
message: '提示',
description: '检测到网页有更新, 是否刷新页面加载最新版本?',
btn: () =>
h(
Button,
{
type: 'primary',
size: 'small',
onClick: () => {
notification.close(key);
self.onRefresh();
},
},
{ default: () => '刷新' },
),
key,
duration: null,
placement: 'bottomRight',
});
},
});
```

View File

@ -15,8 +15,10 @@ export {
ChevronsLeft,
ChevronsRight,
Circle,
CircleAlert,
CircleCheckBig,
CircleHelp,
CircleX,
Copy,
CornerDownLeft,
Ellipsis,

View File

@ -6,6 +6,7 @@ export const messages: Record<Locale, Record<string, string>> = {
collapse: 'Collapse',
confirm: 'Confirm',
expand: 'Expand',
prompt: 'Prompt',
reset: 'Reset',
submit: 'Submit',
},
@ -14,6 +15,7 @@ export const messages: Record<Locale, Record<string, string>> = {
collapse: '收起',
confirm: '确认',
expand: '展开',
prompt: '提示',
reset: '重置',
submit: '提交',
},

View File

@ -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<Array<{ container: HTMLElement; instance: Component }>>([]);
const { $t } = useSimpleLocale();
export function vbenAlert(options: AlertProps): Promise<void>;
export function vbenAlert(
message: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenAlert(
message: string,
title?: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenAlert(
arg0: AlertProps | string,
arg1?: Partial<AlertProps> | string,
arg2?: Partial<AlertProps>,
): Promise<void> {
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<any> = {
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<void>;
export function vbenConfirm(
message: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenConfirm(
message: string,
title?: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenConfirm(
arg0: AlertProps | string,
arg1?: Partial<AlertProps> | string,
arg2?: Partial<AlertProps>,
): Promise<void> {
const defaultProps: Partial<AlertProps> = {
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<T = any>(
options: Omit<AlertProps, 'beforeClose'> & {
beforeClose?: (
val: T,
) => boolean | Promise<boolean | undefined> | undefined;
component?: Component;
componentProps?: Recordable<any>;
defaultValue?: T;
modelPropName?: string;
},
): Promise<T | undefined> {
const {
component: _component,
componentProps: _componentProps,
content,
defaultValue,
modelPropName: _modelPropName,
...delegated
} = options;
const contents: Component[] = [];
const modelValue = ref<T | undefined>(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<any> = {
...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 = [];
}

View File

@ -0,0 +1,28 @@
import type { Component } from 'vue';
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
export type AlertProps = {
/** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: () => boolean | Promise<boolean | undefined> | undefined;
/** 边框 */
bordered?: boolean;
/** 取消按钮的标题 */
cancelText?: string;
/** 是否居中显示 */
centered?: boolean;
/** 确认按钮的标题 */
confirmText?: string;
/** 弹窗容器的额外样式 */
containerClass?: string;
/** 弹窗提示内容 */
content: Component | string;
/** 弹窗内容的额外样式 */
contentClass?: string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */
title?: string;
};

View File

@ -0,0 +1,181 @@
<script lang="ts" setup>
import type { Component } from 'vue';
import type { AlertProps } from './alert';
import { computed, h, nextTick, ref, watch } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import {
CircleAlert,
CircleCheckBig,
CircleHelp,
CircleX,
Info,
X,
} from '@vben-core/icons';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
VbenButton,
VbenLoading,
VbenRenderContent,
} from '@vben-core/shadcn-ui';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
const props = withDefaults(defineProps<AlertProps>(), {
bordered: true,
centered: true,
containerClass: 'w-[520px]',
});
const emits = defineEmits(['closed', 'confirm', 'opened']);
const open = defineModel<boolean>('open', { default: false });
const { $t } = useSimpleLocale();
const components = globalShareState.getComponents();
const isConfirm = ref(false);
watch(open, async (val) => {
await nextTick();
if (val) {
isConfirm.value = false;
} else {
emits('closed', isConfirm.value);
}
});
const getIconRender = computed(() => {
let iconRender: Component | null = null;
if (props.icon) {
if (typeof props.icon === 'string') {
switch (props.icon) {
case 'error': {
iconRender = h(CircleX, {
style: { color: 'hsl(var(--destructive))' },
});
break;
}
case 'info': {
iconRender = h(Info, { style: { color: 'hsl(var(--info))' } });
break;
}
case 'question': {
iconRender = CircleHelp;
break;
}
case 'success': {
iconRender = h(CircleCheckBig, {
style: { color: 'hsl(var(--success))' },
});
break;
}
case 'warning': {
iconRender = h(CircleAlert, {
style: { color: 'hsl(var(--warning))' },
});
break;
}
default: {
iconRender = null;
break;
}
}
}
} else {
iconRender = props.icon ?? null;
}
return iconRender;
});
function handleConfirm() {
isConfirm.value = true;
emits('confirm');
}
function handleCancel() {
open.value = false;
}
const loading = ref(false);
async function handleOpenChange(val: boolean) {
if (!val && props.beforeClose) {
loading.value = true;
try {
const res = await props.beforeClose();
if (res !== false) {
open.value = false;
}
} finally {
loading.value = false;
}
} else {
open.value = val;
}
}
</script>
<template>
<AlertDialog :open="open" @update:open="handleOpenChange">
<AlertDialogContent
:open="open"
:centered="centered"
@opened="emits('opened')"
:class="
cn(
containerClass,
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
{
'border-border border': bordered,
'shadow-3xl': !bordered,
'top-1/2 !-translate-y-1/2': centered,
},
)
"
>
<div :class="cn('relative flex-1 overflow-y-auto p-3', contentClass)">
<AlertDialogTitle v-if="title">
<div class="flex items-center">
<component :is="getIconRender" class="mr-2" />
<span class="flex-auto">{{ $t(title) }}</span>
<AlertDialogCancel v-if="showCancel">
<VbenButton
variant="ghost"
size="icon"
class="rounded-full"
:disabled="loading"
>
<X class="text-muted-foreground size-4" />
</VbenButton>
</AlertDialogCancel>
</div>
</AlertDialogTitle>
<AlertDialogDescription>
<div class="m-4 mb-6 min-h-[30px]">
<VbenRenderContent :content="content" render-br />
</div>
<VbenLoading v-if="loading" :spinning="loading" />
</AlertDialogDescription>
<div class="flex justify-end gap-x-2">
<AlertDialogCancel
v-if="showCancel"
@click="handleCancel"
:disabled="loading"
>
<component
:is="components.DefaultButton || VbenButton"
variant="ghost"
>
{{ cancelText || $t('cancel') }}
</component>
</AlertDialogCancel>
<AlertDialogAction @click="handleConfirm">
<component
:is="components.PrimaryButton || VbenButton"
:loading="loading"
>
{{ confirmText || $t('confirm') }}
</component>
</AlertDialogAction>
</div>
</div>
</AlertDialogContent>
</AlertDialog>
</template>

View File

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

View File

@ -1,2 +1,3 @@
export * from './alert';
export * from './drawer';
export * from './modal';

View File

@ -1,9 +1,10 @@
<script lang="ts">
import type { Component, PropType } from 'vue';
import { isFunction, isObject } from '@vben-core/shared/utils';
import { defineComponent, h } from 'vue';
import { isFunction, isObject, isString } from '@vben-core/shared/utils';
export default defineComponent({
name: 'RenderContent',
props: {
@ -13,6 +14,10 @@ export default defineComponent({
| undefined,
type: [Object, String, Function],
},
renderBr: {
default: false,
type: Boolean,
},
},
setup(props, { attrs, slots }) {
return () => {
@ -23,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,

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { AlertDialogEmits, AlertDialogProps } from 'radix-vue';
import { AlertDialogRoot, useForwardPropsEmits } from 'radix-vue';
const props = defineProps<AlertDialogProps>();
const emits = defineEmits<AlertDialogEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot></slot>
</AlertDialogRoot>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import type { AlertDialogActionProps } from 'radix-vue';
import { AlertDialogAction } from 'radix-vue';
const props = defineProps<AlertDialogActionProps>();
</script>
<template>
<AlertDialogAction v-bind="props">
<slot></slot>
</AlertDialogAction>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import type { AlertDialogCancelProps } from 'radix-vue';
import { AlertDialogCancel } from 'radix-vue';
const props = defineProps<AlertDialogCancelProps>();
</script>
<template>
<AlertDialogCancel v-bind="props">
<slot></slot>
</AlertDialogCancel>
</template>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import type {
AlertDialogContentEmits,
AlertDialogContentProps,
} from 'radix-vue';
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import {
AlertDialogContent,
AlertDialogPortal,
useForwardPropsEmits,
} from 'radix-vue';
import AlertDialogOverlay from './AlertDialogOverlay.vue';
const props = withDefaults(
defineProps<
AlertDialogContentProps & {
centered?: boolean;
class?: ClassType;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
zIndex?: number;
}
>(),
{ modal: true },
);
const emits = defineEmits<
AlertDialogContentEmits & { close: []; closed: []; opened: [] }
>();
const delegatedProps = computed(() => {
const { class: _, modal: _modal, open: _open, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof AlertDialogContent> | null>(null);
function onAnimationEnd(event: AnimationEvent) {
// contentRef opened/closed
if (event.target === contentRef.value?.$el) {
if (props.open) {
emits('opened');
} else {
emits('closed');
}
}
}
defineExpose({
getContentRef: () => contentRef.value,
});
</script>
<template>
<AlertDialogPortal>
<Transition name="fade">
<AlertDialogOverlay
v-if="open && modal"
:style="{
...(zIndex ? { zIndex } : {}),
position: 'fixed',
backdropFilter:
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
}"
@click="() => emits('close')"
/>
</Transition>
<AlertDialogContent
ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
props.class,
)
"
>
<slot></slot>
</AlertDialogContent>
</AlertDialogPortal>
</template>

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { AlertDialogDescriptionProps } from 'radix-vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AlertDialogDescription, useForwardProps } from 'radix-vue';
const props = defineProps<AlertDialogDescriptionProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AlertDialogDescription
v-bind="forwardedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot></slot>
</AlertDialogDescription>
</template>

View File

@ -0,0 +1,8 @@
<script setup lang="ts">
import { useScrollLock } from '@vben-core/composables';
useScrollLock();
</script>
<template>
<div class="bg-overlay z-popup inset-0"></div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { AlertDialogTitleProps } from 'radix-vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AlertDialogTitle, useForwardProps } from 'radix-vue';
const props = defineProps<AlertDialogTitleProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AlertDialogTitle
v-bind="forwardedProps"
:class="
cn('text-lg font-semibold leading-none tracking-tight', props.class)
"
>
<slot></slot>
</AlertDialogTitle>
</template>

View File

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

View File

@ -1,4 +1,5 @@
export * from './accordion';
export * from './alert-dialog';
export * from './avatar';
export * from './badge';
export * from './breadcrumb';

View File

@ -33,7 +33,7 @@ async function generateRoutesByBackend(
return routes;
} catch (error) {
console.error(error);
return [];
throw error;
}
}

View File

@ -1,7 +1,16 @@
<script lang="ts" setup>
import { Page, useVbenModal } from '@vben/common-ui';
import { onBeforeUnmount } from 'vue';
import { Button, Card, Flex } from 'ant-design-vue';
import {
alert,
clearAllAlerts,
confirm,
Page,
prompt,
useVbenModal,
} from '@vben/common-ui';
import { Button, Card, Flex, message } from 'ant-design-vue';
import DocButton from '../doc-button.vue';
import AutoHeightDemo from './auto-height-demo.vue';
@ -103,6 +112,61 @@ function openFormModal() {
})
.open();
}
function openAlert() {
alert({
content: '这是一个弹窗',
icon: 'success',
}).then(() => {
message.info('用户关闭了弹窗');
});
}
onBeforeUnmount(() => {
//
clearAllAlerts();
});
function openConfirm() {
confirm({
beforeClose() {
//
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 1000);
});
},
content: '这是一个确认弹窗',
icon: 'question',
})
.then(() => {
message.success('用户确认了操作');
})
.catch(() => {
message.error('用户取消了操作');
});
}
async function openPrompt() {
prompt<string>({
async beforeClose(val) {
if (val === '芝士') {
message.error('不能吃芝士');
return false;
}
},
componentProps: { placeholder: '不能吃芝士...' },
content: '中午吃了什么?',
icon: 'question',
})
.then((res) => {
message.success(`用户输入了:${res}`);
})
.catch(() => {
message.error('用户取消了输入');
});
}
</script>
<template>
@ -195,6 +259,14 @@ function openFormModal() {
<Button type="primary" @click="openBlurModal">打开弹窗</Button>
</template>
</Card>
<Card class="w-[300px]" title="轻量提示弹窗">
<p>通过快捷方法创建动态提示弹窗适合一些轻量的提示和确认输入等</p>
<template #actions>
<Button type="primary" @click="openAlert">Alert</Button>
<Button type="primary" @click="openConfirm">Confirm</Button>
<Button type="primary" @click="openPrompt">Prompt</Button>
</template>
</Card>
</Flex>
</Page>
</template>