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/docs/src/en/guide/essentials/development.md b/docs/src/en/guide/essentials/development.md index da7cfd8c..83f8647f 100644 --- a/docs/src/en/guide/essentials/development.md +++ b/docs/src/en/guide/essentials/development.md @@ -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"`. diff --git a/docs/src/guide/essentials/development.md b/docs/src/guide/essentials/development.md index 556e4c9d..81cdc590 100644 --- a/docs/src/guide/essentials/development.md +++ b/docs/src/guide/essentials/development.md @@ -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"` 直接引入的。 diff --git a/docs/src/guide/in-depth/check-updates.md b/docs/src/guide/in-depth/check-updates.md index 4af9e333..d4a74da6 100644 --- a/docs/src/guide/in-depth/check-updates.md +++ b/docs/src/guide/in-depth/check-updates.md @@ -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', + }); + }, +}); +``` 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 31a9d1cc..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 @@ -1,9 +1,10 @@ + + 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/packages/utils/src/helpers/generate-routes-backend.ts b/packages/utils/src/helpers/generate-routes-backend.ts index e5d87809..a31237e1 100644 --- a/packages/utils/src/helpers/generate-routes-backend.ts +++ b/packages/utils/src/helpers/generate-routes-backend.ts @@ -33,7 +33,7 @@ async function generateRoutesByBackend( return routes; } catch (error) { console.error(error); - return []; + throw error; } } 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 @@ + +

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

+ +