|
@@ -8,11 +8,13 @@ import {
|
|
|
Warning,
|
|
|
SuccessFilled,
|
|
|
InfoFilled,
|
|
|
- CircleCloseFilled
|
|
|
+ CircleCloseFilled,
|
|
|
+ DeleteFilled
|
|
|
} from '@element-plus/icons-vue'
|
|
|
-import {getCyId} from "@/components/xiao-chan/cy-message-box/cy-message-box";
|
|
|
import {useCompRef} from "@/utils/useCompRef";
|
|
|
import {useCyDraggable} from "@/utils/use-cy-draggable";
|
|
|
+import {uuid} from "@/utils/getUuid";
|
|
|
+import ElFocusTrap from "@/components/xiao-chan/focus-trap/src/focus-trap.vue";
|
|
|
|
|
|
const props = withDefaults(defineProps<{
|
|
|
message?: string;
|
|
@@ -38,8 +40,11 @@ const props = withDefaults(defineProps<{
|
|
|
selectOption?: {
|
|
|
value: string,
|
|
|
label: string
|
|
|
- }[]
|
|
|
+ }[],
|
|
|
+ closeOnPressEscape?: boolean;
|
|
|
+ closeOnClickModal?: boolean
|
|
|
}>(), {
|
|
|
+ type: 'info',
|
|
|
showCancel: true,
|
|
|
confirmButtonText: '确认',
|
|
|
cancelButtonText: '取消',
|
|
@@ -48,7 +53,11 @@ const props = withDefaults(defineProps<{
|
|
|
dangerouslyUseHTMLString: false,
|
|
|
draggable: true,
|
|
|
inputDefaultValue: '',
|
|
|
- selectOption: null
|
|
|
+ selectOption: null,
|
|
|
+ inputRows: 0,
|
|
|
+ inputMaxLength: 0,
|
|
|
+ closeOnPressEscape: true,
|
|
|
+ closeOnClickModal: true
|
|
|
})
|
|
|
|
|
|
const visible = ref(false)
|
|
@@ -56,11 +65,12 @@ const headerRef = ref<HTMLHeadElement>(null)
|
|
|
const inputValue = ref(props.inputDefaultValue)
|
|
|
const errorMsg = ref('')
|
|
|
const inputRef = useCompRef(ElInput)
|
|
|
-const confirmRef = ref<HTMLButtonElement>()
|
|
|
-const bodyRef = ref<HTMLDivElement>()
|
|
|
-const mainRef = ref<HTMLDivElement>()
|
|
|
+const confirmRef = ref<HTMLElement>()
|
|
|
+const bodyRef = ref<HTMLElement>()
|
|
|
+const mainRef = ref<HTMLElement>()
|
|
|
+const focusStartRef = ref<HTMLElement>()
|
|
|
const isSelectInput = ref<boolean>(props.selectOption !== null)
|
|
|
-const inputId = ref(getCyId())
|
|
|
+const inputId = ref('cy-' + uuid(5, 62))
|
|
|
|
|
|
let closeMode = ''
|
|
|
|
|
@@ -96,11 +106,6 @@ function onAfterLeave() {
|
|
|
*/
|
|
|
function onAfterEnter() {
|
|
|
mainStyle.value.display = 'flex';
|
|
|
- if (props.showInput && !isSelectInput.value) {
|
|
|
- inputRef.value.focus();
|
|
|
- } else {
|
|
|
- confirmRef.value.focus()
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
function handleConfirm() {
|
|
@@ -112,6 +117,11 @@ function handleConfirm() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function verificationFailed(value: string) {
|
|
|
+ errorMsg.value = value
|
|
|
+ popupJitter()
|
|
|
+}
|
|
|
+
|
|
|
function handlePrompt() {
|
|
|
function next() {
|
|
|
visible.value = false;
|
|
@@ -120,24 +130,32 @@ function handlePrompt() {
|
|
|
|
|
|
if (!props.allowToBeEmpty) {
|
|
|
if (inputValue.value === '') {
|
|
|
- errorMsg.value = '不允许为空。'
|
|
|
+ verificationFailed('不允许为空。')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.inputMaxLength !== 0) {
|
|
|
+ if (inputValue.value.length > props.inputMaxLength) {
|
|
|
+ verificationFailed(`最大长度不得超过${props.inputMaxLength}个字。`)
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
if (props.inputValidator) {
|
|
|
const inputValidator = props.inputValidator(inputValue.value)
|
|
|
if (typeof inputValidator === 'boolean') {
|
|
|
if (inputValidator) {
|
|
|
next()
|
|
|
} else {
|
|
|
- errorMsg.value = props.inputErrorMessage
|
|
|
+ verificationFailed(props.inputErrorMessage)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (typeof inputValidator === 'string') {
|
|
|
if (inputValidator) {
|
|
|
- errorMsg.value = inputValidator
|
|
|
+ verificationFailed(inputValidator)
|
|
|
} else {
|
|
|
next()
|
|
|
}
|
|
@@ -148,38 +166,56 @@ function handlePrompt() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function handleClose(val) {
|
|
|
+function handleClose(val: 'close' | 'cancel') {
|
|
|
bodyRef.value.style.transition = ''
|
|
|
visible.value = false
|
|
|
closeMode = val
|
|
|
}
|
|
|
|
|
|
+const headerData = {
|
|
|
+ success: {
|
|
|
+ icon: h(SuccessFilled),
|
|
|
+ title: '成功',
|
|
|
+ color: '#67c23a'
|
|
|
+ },
|
|
|
+ warning: {
|
|
|
+ icon: h(Warning),
|
|
|
+ title: '警告',
|
|
|
+ color: '#e6a23c'
|
|
|
+ },
|
|
|
+ info: {
|
|
|
+ icon: h(InfoFilled),
|
|
|
+ title: '提示',
|
|
|
+ color: '#909399'
|
|
|
+ },
|
|
|
+ error: {
|
|
|
+ icon: h(CircleCloseFilled),
|
|
|
+ title: '错误',
|
|
|
+ color: '#f56c6c'
|
|
|
+ },
|
|
|
+ delete: {
|
|
|
+ icon: h(DeleteFilled),
|
|
|
+ title: '删除',
|
|
|
+ color: '#f56c6c'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function headerColor() {
|
|
|
+ return headerData[props.type].color
|
|
|
+}
|
|
|
|
|
|
function headerIcon() {
|
|
|
- switch (props.type) {
|
|
|
- case "success":
|
|
|
- return h(SuccessFilled, null, null)
|
|
|
- case "warning":
|
|
|
- return h(Warning, null, null)
|
|
|
- case 'info':
|
|
|
- return h(InfoFilled, null, null)
|
|
|
- case 'error':
|
|
|
- return h(CircleCloseFilled)
|
|
|
- }
|
|
|
+ return headerData[props.type].icon
|
|
|
}
|
|
|
|
|
|
function titleRender() {
|
|
|
- const title = {
|
|
|
- 'success': '成功',
|
|
|
- 'warning': '警告',
|
|
|
- 'info': '提示',
|
|
|
- 'error': '错误',
|
|
|
- }
|
|
|
- return h('span', {style: {marginLeft: '8px'}}, props.title || title[props.type])
|
|
|
+ return h('span', {style: {marginLeft: '8px'}}, props.title || headerData[props.type].title)
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-async function maskClick() {
|
|
|
+/**
|
|
|
+ * 弹窗抖动
|
|
|
+ */
|
|
|
+async function popupJitter() {
|
|
|
const oldTransform = bodyRef.value.style.transform
|
|
|
bodyRef.value.style.transform = oldTransform + ' scale(1.1)';
|
|
|
await sleep(100)
|
|
@@ -191,18 +227,40 @@ const draggable = computed(() => props.draggable)
|
|
|
useCyDraggable(bodyRef, headerRef, draggable,
|
|
|
() => {
|
|
|
bodyRef.value.style.transition = 'none'
|
|
|
+ inputRef.value.blur();
|
|
|
}, () => {
|
|
|
mainRef.value.parentElement.style.setProperty('--cy-body-transform', bodyRef.value.style.transform)
|
|
|
bodyRef.value.style.transition = ''
|
|
|
}
|
|
|
)
|
|
|
|
|
|
+function onCloseRequested() {
|
|
|
+ if (props.closeOnPressEscape) {
|
|
|
+ handleClose('close')
|
|
|
+ } else {
|
|
|
+ popupJitter()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function maskClick() {
|
|
|
+ if (props.closeOnClickModal) {
|
|
|
+ handleClose('close')
|
|
|
+ } else {
|
|
|
+ popupJitter()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
onMounted(async () => {
|
|
|
mainStyle.value.zIndex = nextZIndex()
|
|
|
contentStyle.value.zIndex = nextZIndex()
|
|
|
document.body.className = 'el-popup-parent--hidden'
|
|
|
await nextTick()
|
|
|
+ if (props.showInput) {
|
|
|
+ focusStartRef.value = mainRef.value
|
|
|
+ } else {
|
|
|
+ focusStartRef.value = confirmRef?.value ?? mainRef.value
|
|
|
+ }
|
|
|
await sleep(100)
|
|
|
visible.value = true
|
|
|
props.setClose(close)
|
|
@@ -220,88 +278,92 @@ function close() {
|
|
|
role="dialog"
|
|
|
aria-modal="true"
|
|
|
@click.stop="maskClick">
|
|
|
- <transition name="cy-message_show" @after-enter="onAfterEnter" @after-leave="onAfterLeave">
|
|
|
- <div class="cy-message-box_body"
|
|
|
- tabindex="-1"
|
|
|
- v-show="visible"
|
|
|
- :style="contentStyle"
|
|
|
- @click.stop
|
|
|
- ref="bodyRef">
|
|
|
- <div class="cy-message-box_close" @click.stop="handleClose('close')">
|
|
|
- <ElIcon>
|
|
|
- <Close/>
|
|
|
- </ElIcon>
|
|
|
- </div>
|
|
|
- <header ref="headerRef">
|
|
|
- <el-icon :class="['cy-' + props.type]"
|
|
|
- class="cy-message_header-icon">
|
|
|
- <component :is="headerIcon"/>
|
|
|
- </el-icon>
|
|
|
- <Component :is="titleRender"/>
|
|
|
- </header>
|
|
|
- <div class="cy_message-box_content">
|
|
|
- <div class="message" v-if="props.message">
|
|
|
- <component
|
|
|
- v-if="props.dangerouslyUseHTMLString"
|
|
|
- :for="inputId"
|
|
|
- :is="props.showInput ? 'label' : 'span'"
|
|
|
- v-html="props.message">
|
|
|
- </component>
|
|
|
- <component v-else
|
|
|
- :is="props.showInput ? 'label' : 'span'"
|
|
|
- :for="inputId">
|
|
|
- {{ props.message }}
|
|
|
- </component>
|
|
|
- </div>
|
|
|
- <div v-if="props.showInput" style="margin-top: 9px">
|
|
|
- <ElSelect v-model="inputValue"
|
|
|
- v-if="isSelectInput"
|
|
|
- :id="inputId"
|
|
|
- style="width: 100%"
|
|
|
- :multiple="false"
|
|
|
- :filterable="true"
|
|
|
- :allow-create="true"
|
|
|
- :default-first-option="true"
|
|
|
- :reserve-keyword="false"
|
|
|
- ref="inputRef">
|
|
|
- <ElOption v-for="item in props.selectOption" :key="item.value" :label="item.label" :value="item.value"/>
|
|
|
- </ElSelect>
|
|
|
-
|
|
|
- <ElInput
|
|
|
- v-else
|
|
|
- ref="inputRef"
|
|
|
- :autofocus="true"
|
|
|
- :rows="props.inputRows"
|
|
|
- :type="props.inputRows === 0 ? '' : 'textarea'"
|
|
|
- :minlength="props.inputMinLength"
|
|
|
- :maxlength="props.inputMaxLength"
|
|
|
- :show-word-limit="true"
|
|
|
- :id="inputId"
|
|
|
- v-model.trim="inputValue"/>
|
|
|
+ <el-focus-trap
|
|
|
+ :trapped="visible"
|
|
|
+ :focus-trap-el="mainRef"
|
|
|
+ :focus-start-el="focusStartRef"
|
|
|
+ @release-requested="onCloseRequested">
|
|
|
+ <transition name="cy-message_show" @after-enter="onAfterEnter" @after-leave="onAfterLeave">
|
|
|
+ <div class="cy-message-box_body"
|
|
|
+ tabindex="-1"
|
|
|
+ v-show="visible"
|
|
|
+ :style="contentStyle"
|
|
|
+ @click.stop
|
|
|
+ ref="bodyRef">
|
|
|
+ <div class="cy-message-box_close" @click.stop="handleClose('close')">
|
|
|
+ <ElIcon>
|
|
|
+ <Close/>
|
|
|
+ </ElIcon>
|
|
|
</div>
|
|
|
- <div v-if="props.showInput" class="error_msg">
|
|
|
- {{ errorMsg }}
|
|
|
+ <header ref="headerRef">
|
|
|
+ <el-icon :style="{color: headerColor() }"
|
|
|
+ class="cy-message_header-icon">
|
|
|
+ <component :is="headerIcon"/>
|
|
|
+ </el-icon>
|
|
|
+ <Component :is="titleRender"/>
|
|
|
+ </header>
|
|
|
+ <div class="cy_message-box_content">
|
|
|
+ <div class="message" v-if="props.message">
|
|
|
+ <component
|
|
|
+ v-if="props.dangerouslyUseHTMLString"
|
|
|
+ :for="inputId"
|
|
|
+ :is="props.showInput ? 'label' : 'span'"
|
|
|
+ v-html="props.message">
|
|
|
+ </component>
|
|
|
+ <component v-else
|
|
|
+ :is="props.showInput ? 'label' : 'span'"
|
|
|
+ :for="inputId">
|
|
|
+ {{ props.message }}
|
|
|
+ </component>
|
|
|
+ </div>
|
|
|
+ <div v-if="props.showInput" style="margin-top: 9px">
|
|
|
+ <ElSelect v-model="inputValue"
|
|
|
+ v-if="isSelectInput"
|
|
|
+ :id="inputId"
|
|
|
+ style="width: 100%"
|
|
|
+ :multiple="false"
|
|
|
+ :filterable="true"
|
|
|
+ :allow-create="true"
|
|
|
+ :default-first-option="true"
|
|
|
+ :reserve-keyword="false"
|
|
|
+ ref="inputRef">
|
|
|
+ <ElOption v-for="item in props.selectOption" :key="item.value" :label="item.label" :value="item.value"/>
|
|
|
+ </ElSelect>
|
|
|
+ <ElInput
|
|
|
+ v-else
|
|
|
+ ref="inputRef"
|
|
|
+ :autofocus="true"
|
|
|
+ :rows="props.inputRows"
|
|
|
+ :type="props.inputRows === 0 ? '' : 'textarea'"
|
|
|
+ :minlength="props.inputMinLength"
|
|
|
+ :maxlength="props.inputMaxLength"
|
|
|
+ :show-word-limit="true"
|
|
|
+ :id="inputId"
|
|
|
+ v-model.trim="inputValue"/>
|
|
|
+ </div>
|
|
|
+ <div v-if="props.showInput" class="error_msg">
|
|
|
+ {{ errorMsg }}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ <footer>
|
|
|
+ <button class="cancel"
|
|
|
+ style="margin-right: 12px"
|
|
|
+ v-if="props.showCancel"
|
|
|
+ @click="handleClose('cancel')"
|
|
|
+ :style="{'--cy--bg-color': '245,108,108','--cy-color': '#F56C6C'}">
|
|
|
+ {{ props.cancelButtonText }}
|
|
|
+ </button>
|
|
|
+ <button class="confirm"
|
|
|
+ ref="confirmRef"
|
|
|
+ :style="{'--cy--bg-color': '26, 92, 255','--cy-color': '#2C69FF'}"
|
|
|
+ @click="handleConfirm">
|
|
|
+ {{ props.confirmButtonText }}
|
|
|
+ </button>
|
|
|
+ </footer>
|
|
|
</div>
|
|
|
- <footer>
|
|
|
- <button class="cancel"
|
|
|
- style="margin-right: 12px"
|
|
|
- v-if="props.showCancel"
|
|
|
- @click="handleClose('cancel')"
|
|
|
- :style="{'--cy--bg-color': '245,108,108','--cy-color': '#F56C6C'}">
|
|
|
- {{ props.cancelButtonText }}
|
|
|
- </button>
|
|
|
- <button class="confirm"
|
|
|
- ref="confirmRef"
|
|
|
- :style="{'--cy--bg-color': '26, 92, 255','--cy-color': '#2C69FF'}"
|
|
|
- @click="handleConfirm">
|
|
|
- {{ props.confirmButtonText }}
|
|
|
- </button>
|
|
|
- </footer>
|
|
|
- </div>
|
|
|
- </transition>
|
|
|
+ </transition>
|
|
|
+ </el-focus-trap>
|
|
|
</div>
|
|
|
-
|
|
|
</template>
|
|
|
|
|
|
<style lang="scss">
|
|
@@ -322,18 +384,20 @@ function close() {
|
|
|
}
|
|
|
|
|
|
.cy-message-box_main {
|
|
|
- background: rgba(0, 0, 0, .5);
|
|
|
+ background: rgba(0, 0, 0, .2);
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
|
|
|
.cy-message-box_body {
|
|
|
display: inline-block;
|
|
|
height: max-content;
|
|
|
width: 418px;
|
|
|
- margin: auto;
|
|
|
background-color: white;
|
|
|
border-radius: 20px;
|
|
|
padding: 5px;
|
|
@@ -386,21 +450,6 @@ function close() {
|
|
|
font-size: 24px;
|
|
|
}
|
|
|
|
|
|
- .cy-warning {
|
|
|
- color: #e6a23c;
|
|
|
- }
|
|
|
-
|
|
|
- .cy-success {
|
|
|
- color: #67c23a;
|
|
|
- }
|
|
|
-
|
|
|
- .cy-info {
|
|
|
- color: #909399;
|
|
|
- }
|
|
|
-
|
|
|
- .cy-error {
|
|
|
- color: #f56c6c;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
.cy_message-box_content {
|
|
@@ -446,7 +495,6 @@ function close() {
|
|
|
padding: 0 16px 10px 16px;
|
|
|
|
|
|
button {
|
|
|
- border: 0;
|
|
|
width: 60px;
|
|
|
line-height: 28px;
|
|
|
text-align: center;
|
|
@@ -458,6 +506,17 @@ function close() {
|
|
|
list-style: none;
|
|
|
user-select: none;
|
|
|
background: transparent;
|
|
|
+ box-sizing: border-box;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ transition: border .3s ease-out;
|
|
|
+
|
|
|
+ &:focus-visible {
|
|
|
+ border: 1px solid var(--cy-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border: 1px solid transparent !important;
|
|
|
+ }
|
|
|
|
|
|
&:hover:before {
|
|
|
transform: scale(1.3);
|