This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'
import { CANCEL_EVENT, CLOSED_EVENT, OPENED_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
import type { Interceptor } from '../_utils'
import { commonProps, isBoolean, makeStringProp, truthProp } from '../_utils'
import { popupProps } from '../popup/popup'
import type { FooterDirection, TextAlign } from './type'
export const dialogProps = {
...popupProps,
...commonProps,
/**
* @description 点击蒙层是否关闭对话框
*/
closeOnClickOverlay: truthProp,
/**
* @description 标题
*/
title: makeStringProp(''),
/**
* @description 内容,支持 HTML
*/
content: makeStringProp(''),
/**
* @description 是否隐藏底部按钮栏
*/
noFooter: Boolean,
/**
* @description 是否隐藏确定按钮
*/
noOkBtn: Boolean,
/**
* @description 是否隐藏取消按钮
*/
noCancelBtn: Boolean,
/**
* @description 取消按钮文案
*/
cancelText: makeStringProp(''),
/**
* @description 确定按钮文案
*/
okText: makeStringProp(''),
/**
* @description 确认按钮是否默认关闭弹窗
*/
okAutoClose: truthProp,
/**
* @description 取消按钮是否默认关闭弹窗
*/
cancelAutoClose: truthProp,
/**
* @description 文字对齐方向,可选值同 css 的 text-align
*/
textAlign: makeStringProp<TextAlign>('center'),
/**
* @description 是否在页面回退时自动关闭
*/
closeOnPopstate: Boolean,
/**
* @description 使用横纵方向,可选值`horizontal`、`vertical`
*/
footerDirection: makeStringProp<FooterDirection>('horizontal'),
/**
* @description 自定义类名
*/
customClass: makeStringProp(''),
/**
* @description 自定义 popup 弹框样式
*/
popStyle: {
type: Object as PropType<CSSProperties>,
},
/**
* @description 是否在页面回退时自动关闭
*/
beforeClose: Function as PropType<Interceptor>,
}
export type DialogProps = ExtractPropTypes<typeof dialogProps>
export const dialogEmits = {
update: (val: boolean) => isBoolean(val),
[UPDATE_VISIBLE_EVENT]: (val: boolean) => isBoolean(val),
ok: () => true,
[CANCEL_EVENT]: () => true,
[OPENED_EVENT]: () => true,
[CLOSED_EVENT]: () => true,
}
export type DialogEmits = typeof dialogEmits

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import { defineComponent } from 'vue'
import { PREFIX } from '../_constants'
import { useTranslate } from '../../locale'
import NutButton from '../button/button.vue'
import NutPopup from '../popup/popup.vue'
import { dialogEmits, dialogProps } from './dialog'
import { useDialog } from './use-dialog'
const props = defineProps(dialogProps)
const emit = defineEmits(dialogEmits)
const {
contentStyle,
showPopup,
onClickOverlay,
onCancel,
onOk,
classes,
closed,
dialogStatus,
showDialog,
} = useDialog(props, emit)
defineExpose({ showDialog, onOk, onCancel })
</script>
<script lang="ts">
const componentName = `${PREFIX}-dialog`
export default defineComponent({
name: componentName,
inheritAttrs: false,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
const { translate } = useTranslate(componentName)
</script>
<template>
<NutPopup
v-model:visible="showPopup"
:close-on-click-overlay="false"
:lock-scroll="lockScroll"
:pop-class="popClass"
:overlay-class="overlayClass"
:overlay-style="overlayStyle"
:custom-style="popStyle"
:z-index="zIndex"
round
:transition="transition"
@click-overlay="onClickOverlay"
@click-close-icon="closed"
>
<view :class="classes" :style="customStyle">
<view v-if="$slots.header || dialogStatus.title" class="nut-dialog__header">
<slot v-if="$slots.header" name="header" />
<template v-else>
{{ dialogStatus.title || props.title }}
</template>
</view>
<view class="nut-dialog__content" :style="contentStyle">
<slot v-if="$slots.default" name="default" />
<rich-text v-else-if="typeof content === 'string'" :nodes="dialogStatus.content || props.content" />
<!-- <component :is="content" v-else /> -->
</view>
<view v-if="!dialogStatus.noFooter" class="nut-dialog__footer" :class="{ [footerDirection]: dialogStatus.footerDirection }">
<slot v-if="$slots.footer" name="footer" />
<template v-else>
<NutButton
v-if="!dialogStatus.noCancelBtn"
size="small"
plain
type="primary"
custom-class="nut-dialog__footer-cancel"
@click="onCancel"
>
{{ dialogStatus.cancelText || props.cancelText || translate('cancel') }}
</NutButton>
<NutButton
v-if="!dialogStatus.noOkBtn"
size="small"
type="primary"
custom-class="nut-dialog__footer-ok"
@click="onOk"
>
{{ dialogStatus.okText || props.okText || translate('confirm') }}
</NutButton>
</template>
</view>
</view>
</NutPopup>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,79 @@
@import '../button/index';
@import '../popup/index';
.nut-theme-dark {
.nut-dialog {
&__header {
color: $dark-color3;
}
}
}
.nut-dialog {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
width: $dialog-width;
min-height: 156px;
padding: 28px 24px 16px;
&__header {
display: block;
height: 20px;
font-size: 16px;
font-weight: $dialog-header-font-weight;
color: $dialog-header-color;
text-align: center;
@include oneline-ellipsis;
}
&__content {
flex: 1;
width: 100%;
max-height: 268px;
margin: 20px 0;
overflow: auto;
font-size: 12px;
line-height: 16px;
color: $text-color;
word-break: break-all;
word-wrap: break-word;
white-space: pre-wrap;
}
&__footer {
display: flex;
align-items: center;
justify-content: $dialog-footer-justify-content;
width: 100%;
&.vertical {
flex-direction: column;
.nut-button {
min-width: 100%;
margin: 0;
&.nut-dialog__footer-cancel {
border: 0;
}
&.nut-dialog__footer-ok {
margin-top: 10px;
}
}
}
.nut-button {
min-width: 100px;
overflow: hidden;
}
&-ok {
max-width: 128px;
}
}
}

View File

@@ -0,0 +1,3 @@
export type * from './dialog'
export type * from './type'
export * from './use-dialog'

View File

@@ -0,0 +1,71 @@
import type { NutAnimationName } from '../transition'
export const textAlign = ['left', 'center', 'right', 'top'] as const
export type TextAlign = (typeof textAlign)[number]
export const footerDirection = ['horizontal', 'vertical'] as const
export type FooterDirection = (typeof footerDirection)[number]
export interface DialogOptions {
/**
* @description 标题
*/
title?: string
/**
* @description 内容,支持 HTML
*/
content?: string
/**
* @description 是否隐藏底部按钮栏
*/
noFooter?: boolean
/**
* @description 是否隐藏确定按钮
*/
noOkBtn?: boolean
/**
* @description 是否隐藏取消按钮
*/
noCancelBtn?: boolean
/**
* @description 取消按钮文案
*/
cancelText?: string
/**
* @description 确定按钮文案
*/
okText?: string
/**
* @description 文字对齐方向,可选值同 css 的 text-align
*/
textAlign?: TextAlign
/**
* @description 使用横纵方向 可选值 horizontal、vertical
*/
footerDirection?: FooterDirection
/**
* @description 弹出动画类型
*/
transition?: NutAnimationName
/**
* @description 点击蒙层是否关闭对话框
*/
closeOnClickOverlay?: boolean
/**
* @description 确认按钮是否默认关闭弹窗
*/
okAutoClose?: boolean
}
export interface DialogInst {
/**
* @description 弹出对话框
*/
showDialog: (options: DialogOptions) => void
/**
* @description 点击确定
*/
onOk: () => void
/**
* @description 点击取消
*/
onCancel: () => void
}

View File

@@ -0,0 +1,123 @@
import type { CSSProperties, SetupContext } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { CANCEL_EVENT, CLOSED_EVENT, OPENED_EVENT, PREFIX, UPDATE_VISIBLE_EVENT } from '../_constants'
import { funInterceptor, getMainClass } from '../_utils'
import type { DialogEmits, DialogProps } from './dialog'
import type { DialogOptions } from './type'
const componentName = `${PREFIX}-dialog`
export function useDialog(props: DialogProps, emit: SetupContext<DialogEmits>['emit']) {
const showPopup = ref(props.visible)
const dialogStatus = ref<DialogOptions>({
title: props.title,
content: props.content,
cancelText: props.cancelText,
okText: props.okText,
textAlign: props.textAlign,
footerDirection: props.footerDirection,
noFooter: props.noFooter,
noOkBtn: props.noOkBtn,
noCancelBtn: props.noCancelBtn,
transition: props.transition,
closeOnClickOverlay: props.closeOnClickOverlay,
okAutoClose: props.okAutoClose,
})
watch(() => props.title, title => dialogStatus.value.title = title)
const showDialog = (options: DialogOptions) => {
dialogStatus.value = {
title: options.title || props.title,
content: options.content || props.content,
cancelText: options.cancelText || props.cancelText,
okText: options.okText || props.okText,
okAutoClose: options.okAutoClose || props.okAutoClose,
textAlign: options.textAlign || props.textAlign,
footerDirection: options.footerDirection || props.footerDirection,
noFooter: options.noFooter || props.noFooter,
noOkBtn: options.noOkBtn || props.noOkBtn,
transition: options.transition || props.transition,
noCancelBtn: options.noCancelBtn || props.noCancelBtn,
closeOnClickOverlay: options.closeOnClickOverlay || props.closeOnClickOverlay,
}
showPopup.value = true
}
onMounted(() => {
if (props.closeOnPopstate) {
// #ifdef H5
window.addEventListener('popstate', () => {
closed('page')
})
// #endif
}
})
watch(
() => props.visible,
(value) => {
showPopup.value = value
if (value)
emit(OPENED_EVENT)
},
)
const classes = computed(() => {
return getMainClass(props, componentName)
})
function update(val: boolean) {
emit('update', val)
emit(UPDATE_VISIBLE_EVENT, val)
}
function closed(action?: string) {
funInterceptor(props.beforeClose, {
args: [action],
done: () => {
showPopup.value = false
update(false)
emit(CLOSED_EVENT)
},
})
}
function onCancel() {
emit(CANCEL_EVENT)
if (props.cancelAutoClose) {
showPopup.value = false
closed(CANCEL_EVENT)
}
}
function onOk() {
emit('ok')
if (props.okAutoClose)
closed('ok')
}
function onClickOverlay() {
if (props.closeOnClickOverlay)
closed('')
}
const contentStyle = computed(() => {
return {
textAlign: dialogStatus.value.textAlign,
} as CSSProperties
})
return {
contentStyle,
showPopup,
onClickOverlay,
onCancel,
onOk,
closed,
classes,
showDialog,
dialogStatus,
}
}