@zfqh/uview-x 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +127 -3
- package/components/uv-badge/props.ts +83 -0
- package/components/uv-badge/uv-badge.vue +186 -0
- package/components/uv-button/props.ts +148 -0
- package/components/uv-button/uv-button.vue +565 -0
- package/components/uv-checkbox/props.ts +83 -0
- package/components/uv-checkbox/uv-checkbox.vue +403 -0
- package/components/uv-checkbox-group/props.ts +97 -0
- package/components/uv-checkbox-group/uv-checkbox-group.vue +163 -0
- package/components/uv-code/props.ts +48 -0
- package/components/uv-code/uv-code.vue +156 -0
- package/components/uv-dialog/props.ts +56 -0
- package/components/uv-dialog/uv-dialog.vue +492 -0
- package/components/uv-form/props.ts +59 -0
- package/components/uv-form/uv-form.vue +256 -0
- package/components/uv-form/valid.ts +1344 -0
- package/components/uv-form-item/props.ts +62 -0
- package/components/uv-form-item/uv-form-item.vue +335 -0
- package/components/uv-icon/icons.ts +161 -0
- package/components/uv-icon/props.ts +91 -0
- package/components/uv-icon/uv-icon.vue +208 -0
- package/components/uv-icon/uvicons.ttf +0 -0
- package/components/uv-input/props.ts +185 -0
- package/components/uv-input/uv-input.vue +431 -0
- package/components/uv-line/props.ts +42 -0
- package/components/uv-line/uv-line.vue +75 -0
- package/components/uv-line-progress/props.ts +43 -0
- package/components/uv-line-progress/uv-line-progress.vue +166 -0
- package/components/uv-loading-icon/props.ts +78 -0
- package/components/uv-loading-icon/uv-loading-icon.vue +226 -0
- package/components/uv-overlay/props.ts +38 -0
- package/components/uv-overlay/uv-overlay.vue +120 -0
- package/components/uv-popup/props.ts +101 -0
- package/components/uv-popup/uv-popup.vue +371 -0
- package/components/uv-safe-bottom/props.ts +18 -0
- package/components/uv-safe-bottom/uv-safe-bottom.vue +120 -0
- package/components/uv-status-bar/props.ts +22 -0
- package/components/uv-status-bar/uv-status-bar.vue +121 -0
- package/components/uv-tabs/props.ts +84 -0
- package/components/uv-tabs/uv-tabs.vue +432 -0
- package/components/uv-transition/props.ts +38 -0
- package/components/uv-transition/uv-transition.vue +170 -0
- package/hooks/index.ts +5 -0
- package/hooks/useDebounce.ts +35 -0
- package/hooks/useI18n.ts +95 -0
- package/hooks/useThrottle.ts +38 -0
- package/index.ts +23 -0
- package/libs/dayjs/index.ts +217 -0
- package/libs/index.ts +4 -0
- package/libs/luch-request/adapters/index.ts +133 -0
- package/libs/luch-request/core/InterceptorManager.ts +52 -0
- package/libs/luch-request/core/Request.ts +202 -0
- package/libs/luch-request/core/buildFullPath.ts +21 -0
- package/libs/luch-request/core/defaults.ts +34 -0
- package/libs/luch-request/core/dispatchRequest.ts +7 -0
- package/libs/luch-request/core/mergeConfig.ts +127 -0
- package/libs/luch-request/core/settle.ts +17 -0
- package/libs/luch-request/helpers/buildURL.ts +65 -0
- package/libs/luch-request/helpers/combineURLs.ts +15 -0
- package/libs/luch-request/helpers/isAbsoluteURL.ts +15 -0
- package/libs/luch-request/index.d.ts +197 -0
- package/libs/luch-request/index.ts +3 -0
- package/libs/luch-request/utils/clone.ts +265 -0
- package/libs/luch-request/utils.ts +136 -0
- package/locales/en.ts +78 -0
- package/locales/index.ts +169 -0
- package/locales/zh-Hans.ts +78 -0
- package/package.json +42 -4
- package/styles/color.scss +33 -0
- package/styles/common.scss +134 -0
- package/styles/components.scss +19 -0
- package/styles/index.scss +7 -0
- package/styles/theme.scss +44 -0
- package/styles/var.module.scss +32 -0
- package/styles/vue.scss +39 -0
- package/theme.scss +2 -0
- package/types/index.d.ts +66 -0
- package/utils/colorGradient.ts +139 -0
- package/utils/compat.ts +165 -0
- package/utils/compress.ts +34 -0
- package/utils/config.ts +95 -0
- package/utils/debounce.ts +7 -0
- package/utils/digit.ts +157 -0
- package/utils/function.ts +771 -0
- package/utils/http.ts +5 -0
- package/utils/index.ts +57 -0
- package/utils/locale.ts +296 -0
- package/utils/platform.ts +71 -0
- package/utils/route.ts +142 -0
- package/utils/test.ts +256 -0
- package/utils/throttle.ts +7 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="uv-code" :class="[p.customClass]" :style="[$uv.addStyle(p.customStyle)]">
|
|
3
|
+
<!-- 此组件功能由js完成,无需写html逻辑 -->
|
|
4
|
+
</view>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
9
|
+
import { $uv } from '../../utils'
|
|
10
|
+
import props from './props'
|
|
11
|
+
import { useI18n } from '../../hooks'
|
|
12
|
+
|
|
13
|
+
defineOptions({
|
|
14
|
+
name: 'uv-code',
|
|
15
|
+
// #ifdef UNI-APP-X
|
|
16
|
+
styleIsolation: 'app-and-page',
|
|
17
|
+
// #endif
|
|
18
|
+
virtualHost: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Code 验证码倒计时
|
|
23
|
+
* @description 考虑到用户实际发送验证码的场景,可能是一个按钮,也可能是一段文字,提示语各有不同,所以本组件不提供界面显示,只提供倒计时文本,由用户将文本嵌入到具体的场景
|
|
24
|
+
* @tutorial https://www.uvui.cn/components/code.html
|
|
25
|
+
* @property {String | Number} seconds 倒计时所需的秒数(默认 60 )
|
|
26
|
+
* @property {String} startText 开始前的提示语,默认 ''(使用内置文案「获取验证码」)
|
|
27
|
+
* @property {String} changeText 倒计时期间的提示语,必须带有字母"x",默认 ''(使用内置文案「X秒重新获取」)
|
|
28
|
+
* @property {String} endText 倒计结束的提示语,默认 ''(使用内置文案「重新获取」)
|
|
29
|
+
* @property {Boolean} keepRunning 是否在H5刷新或各端返回再进入时继续倒计时( 默认false )
|
|
30
|
+
* @property {String} uniqueKey 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
|
|
31
|
+
* @property {Object} customStyle 定义需要用到的外部样式
|
|
32
|
+
* @event {Function} change 倒计时期间,每秒触发一次
|
|
33
|
+
* @event {Function} start 开始倒计时触发
|
|
34
|
+
* @event {Function} end 结束倒计时触发
|
|
35
|
+
* @example <uv-code ref="uCode" @change="codeChange" seconds="20"></uv-code>
|
|
36
|
+
*/
|
|
37
|
+
const p = defineProps(props)
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits(['change', 'start', 'end'])
|
|
40
|
+
|
|
41
|
+
// 内置文案:props 优先,否则使用内置 i18n(locales/ 的 uv.code.*)
|
|
42
|
+
const { t } = useI18n({ useScope: 'global' })
|
|
43
|
+
const startTextValue = computed<string>(() => p.startText || t('uv.code.start'))
|
|
44
|
+
const changeTextValue = computed<string>(() => p.changeText || t('uv.code.change'))
|
|
45
|
+
const endTextValue = computed<string>(() => p.endText || t('uv.code.end'))
|
|
46
|
+
|
|
47
|
+
// 当前倒计时的秒数
|
|
48
|
+
const secNum = ref<any>(p.seconds)
|
|
49
|
+
// 定时器
|
|
50
|
+
let timer: any = null
|
|
51
|
+
// 是否可以执行验证码操作
|
|
52
|
+
const canGetCode = ref(true)
|
|
53
|
+
|
|
54
|
+
const changeEvent = (text: string) => {
|
|
55
|
+
emit('change', text)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 保存时间戳,为了防止倒计时尚未结束,H5刷新或者各端的右上角返回上一页再进来
|
|
59
|
+
const setTimeToStorage = () => {
|
|
60
|
+
if (!p.keepRunning || !timer) return
|
|
61
|
+
// 倒计时尚未结束,结果大于0;倒计时已经开始,就会小于初始值,如果等于初始值,说明没有开始倒计时,无需处理
|
|
62
|
+
if (secNum.value > 0 && secNum.value <= Number(p.seconds)) {
|
|
63
|
+
// 获取当前时间戳,除以1000变成秒,再去除小数部分
|
|
64
|
+
const nowTimestamp = Math.floor(+new Date() / 1000)
|
|
65
|
+
// 将本该结束时候的时间戳保存起来 => 当前时间戳 + 剩余的秒数
|
|
66
|
+
uni.setStorageSync(p.uniqueKey + '_$uCountDownTimestamp', nowTimestamp + Number(secNum.value))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 开始倒计时
|
|
71
|
+
const start = () => {
|
|
72
|
+
// 防止快速点击获取验证码的按钮而导致内部产生多个定时器导致混乱
|
|
73
|
+
if (timer) {
|
|
74
|
+
clearInterval(timer)
|
|
75
|
+
timer = null
|
|
76
|
+
}
|
|
77
|
+
emit('start')
|
|
78
|
+
canGetCode.value = false
|
|
79
|
+
// 这里放这句,是为了一开始时就提示,否则要等setInterval的1秒后才会有提示
|
|
80
|
+
changeEvent(changeTextValue.value.replace(/x|X/, String(secNum.value)))
|
|
81
|
+
timer = setInterval(() => {
|
|
82
|
+
secNum.value -= 1
|
|
83
|
+
if (secNum.value > 0) {
|
|
84
|
+
// 用当前倒计时的秒数替换提示字符串中的"x"字母
|
|
85
|
+
changeEvent(changeTextValue.value.replace(/x|X/, String(secNum.value)))
|
|
86
|
+
} else {
|
|
87
|
+
clearInterval(timer)
|
|
88
|
+
timer = null
|
|
89
|
+
changeEvent(endTextValue.value)
|
|
90
|
+
secNum.value = p.seconds
|
|
91
|
+
emit('end')
|
|
92
|
+
canGetCode.value = true
|
|
93
|
+
}
|
|
94
|
+
}, 1000)
|
|
95
|
+
setTimeToStorage()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 重置,可以让用户再次获取验证码
|
|
99
|
+
const reset = () => {
|
|
100
|
+
canGetCode.value = true
|
|
101
|
+
if (timer) {
|
|
102
|
+
clearInterval(timer)
|
|
103
|
+
timer = null
|
|
104
|
+
}
|
|
105
|
+
secNum.value = p.seconds
|
|
106
|
+
changeEvent(endTextValue.value)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const checkKeepRunning = () => {
|
|
110
|
+
// 获取上一次退出页面(H5还包括刷新)时的时间戳,如果没有上次的保存,此值可能为空
|
|
111
|
+
const lastTimestamp = Number(uni.getStorageSync(p.uniqueKey + '_$uCountDownTimestamp'))
|
|
112
|
+
if (!lastTimestamp) {
|
|
113
|
+
changeEvent(startTextValue.value)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
// 当前秒的时间戳
|
|
117
|
+
const nowTimestamp = Math.floor(+new Date() / 1000)
|
|
118
|
+
// 判断当前的时间戳,是否小于上一次的本该按设定结束,却提前结束的时间戳
|
|
119
|
+
if (p.keepRunning && lastTimestamp && lastTimestamp > nowTimestamp) {
|
|
120
|
+
// 剩余尚未执行完的倒计秒数
|
|
121
|
+
secNum.value = lastTimestamp - nowTimestamp
|
|
122
|
+
// 清除本地保存的变量
|
|
123
|
+
uni.removeStorageSync(p.uniqueKey + '_$uCountDownTimestamp')
|
|
124
|
+
// 开始倒计时
|
|
125
|
+
start()
|
|
126
|
+
} else {
|
|
127
|
+
// 如果不存在需要继续上一次的倒计时,执行正常的逻辑
|
|
128
|
+
changeEvent(startTextValue.value)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
watch(
|
|
133
|
+
() => p.seconds,
|
|
134
|
+
(n: any) => {
|
|
135
|
+
secNum.value = n
|
|
136
|
+
},
|
|
137
|
+
{ immediate: true }
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
checkKeepRunning()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
onUnmounted(() => {
|
|
145
|
+
setTimeToStorage()
|
|
146
|
+
clearInterval(timer)
|
|
147
|
+
timer = null
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
defineExpose({
|
|
151
|
+
start,
|
|
152
|
+
reset,
|
|
153
|
+
// 父级(merchant login/forgot/change-password)通过 ref 读取 canGetCode 控制按钮 disabled/文案
|
|
154
|
+
canGetCode
|
|
155
|
+
})
|
|
156
|
+
</script>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
|
|
3
|
+
import config from '../../utils/config';
|
|
4
|
+
|
|
5
|
+
const props = {
|
|
6
|
+
modelValue: { type: Boolean, default: false },
|
|
7
|
+
mode: { type: String, default: 'center' },
|
|
8
|
+
size: { type: String, default: 'normal' },
|
|
9
|
+
title: { type: String, default: '' },
|
|
10
|
+
content: { type: String, default: '' },
|
|
11
|
+
showHeader: { type: Boolean, default: true },
|
|
12
|
+
showTitle: { type: Boolean, default: true },
|
|
13
|
+
showContent: { type: Boolean, default: true },
|
|
14
|
+
showFooter: { type: Boolean, default: true },
|
|
15
|
+
showCancelButton: { type: Boolean, default: true },
|
|
16
|
+
showConfirmButton: { type: Boolean, default: true },
|
|
17
|
+
titleAlign: { type: String, default: 'center' },
|
|
18
|
+
titleBold: { type: Boolean, default: true },
|
|
19
|
+
align: { type: String, default: 'left' },
|
|
20
|
+
width: { type: [String, Number], default: 325 },
|
|
21
|
+
cancelText: { type: String, default: '' },
|
|
22
|
+
confirmText: { type: String, default: '' },
|
|
23
|
+
headerBorder: { type: Boolean, default: false },
|
|
24
|
+
footerBorder: { type: Boolean, default: false },
|
|
25
|
+
closeOnConfirm: { type: Boolean, default: true },
|
|
26
|
+
closeOnCancel: { type: Boolean, default: true },
|
|
27
|
+
cancelProps: { type: Object, default: () => ({}) },
|
|
28
|
+
confirmProps: { type: Object, default: () => ({}) },
|
|
29
|
+
buttonProps: { type: Object, default: () => ({}) },
|
|
30
|
+
headerStyle: { type: Object, default: () => ({}) },
|
|
31
|
+
titleStyle: { type: Object, default: () => ({}) },
|
|
32
|
+
footerStyle: { type: Object, default: () => ({}) },
|
|
33
|
+
contentStyle: { type: Object, default: () => ({}) },
|
|
34
|
+
scrollStyle: { type: Object, default: () => ({}) },
|
|
35
|
+
customStyle: { type: [Object, String], default: () => ({}) },
|
|
36
|
+
customClass: { type: String, default: '' },
|
|
37
|
+
// 透传 uv-popup
|
|
38
|
+
duration: { type: [String, Number], default: 300 },
|
|
39
|
+
overlay: { type: Boolean, default: true },
|
|
40
|
+
overlayOpacity: { type: [Number, String], default: 0.4 },
|
|
41
|
+
overlayStyle: { type: [Object, String], default: '' },
|
|
42
|
+
closeOnClickOverlay: { type: Boolean, default: true },
|
|
43
|
+
zIndex: { type: [Number, String], default: 10075 },
|
|
44
|
+
closeable: { type: Boolean, default: false },
|
|
45
|
+
closeIconPos: { type: String, default: 'top-right' },
|
|
46
|
+
closeIconColor: { type: String, default: '#909399' },
|
|
47
|
+
closeIconSize: { type: [String, Number], default: 18 },
|
|
48
|
+
bgColor: { type: String, default: '#ffffff' },
|
|
49
|
+
round: { type: [Number, String], default: 8 },
|
|
50
|
+
zoom: { type: Boolean, default: true },
|
|
51
|
+
safeAreaInsetBottom: { type: Boolean, default: true },
|
|
52
|
+
|
|
53
|
+
...config.props.dialog,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default props;
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<uv-popup
|
|
3
|
+
ref="popupRef"
|
|
4
|
+
:mode="cfg.mode"
|
|
5
|
+
:duration="Number(cfg.duration)"
|
|
6
|
+
:z-index="cfg.zIndex"
|
|
7
|
+
:bg-color="cfg.bgColor"
|
|
8
|
+
:overlay="cfg.overlay"
|
|
9
|
+
:overlay-opacity="cfg.overlayOpacity"
|
|
10
|
+
:overlay-style="cfg.overlayStyle"
|
|
11
|
+
:close-on-click-overlay="cfg.closeOnClickOverlay"
|
|
12
|
+
:closeable="cfg.closeable"
|
|
13
|
+
:close-icon-pos="cfg.closeIconPos"
|
|
14
|
+
:close-icon-color="cfg.closeIconColor"
|
|
15
|
+
:close-icon-size="cfg.closeIconSize"
|
|
16
|
+
:zoom="cfg.zoom"
|
|
17
|
+
:round="cfg.round"
|
|
18
|
+
:safe-area-inset-bottom="cfg.safeAreaInsetBottom && cfg.mode === 'bottom'"
|
|
19
|
+
@change="onPopupChange"
|
|
20
|
+
@close="onPopupClose"
|
|
21
|
+
>
|
|
22
|
+
<view class="uv-dialog" :class="[p.customClass]" :style="[dialogStyle]">
|
|
23
|
+
<slot v-if="cfg.showHeader" name="header">
|
|
24
|
+
<view class="uv-dialog__header" :class="[headerClass]" :style="[headerStyleCom]">
|
|
25
|
+
<text
|
|
26
|
+
v-if="cfg.showTitle"
|
|
27
|
+
class="uv-dialog__title"
|
|
28
|
+
:class="[cfg.titleBold ? 'uv-dialog__title--bold' : '']"
|
|
29
|
+
:style="[titleStyleCom]"
|
|
30
|
+
>{{ titleText }}</text>
|
|
31
|
+
</view>
|
|
32
|
+
</slot>
|
|
33
|
+
<slot v-if="cfg.showContent" name="content">
|
|
34
|
+
<scroll-view
|
|
35
|
+
<!-- #ifdef UNI-APP-X -->
|
|
36
|
+
direction="vertical"
|
|
37
|
+
<!-- #endif -->
|
|
38
|
+
<!-- #ifndef UNI-APP-X -->
|
|
39
|
+
scroll-y="true"
|
|
40
|
+
<!-- #endif -->
|
|
41
|
+
class="uv-dialog__scroll"
|
|
42
|
+
:style="[scrollStyleCom]"
|
|
43
|
+
>
|
|
44
|
+
<view class="uv-dialog__content" :class="[contentClass]" :style="[contentStyleCom]">
|
|
45
|
+
<slot>
|
|
46
|
+
<text class="uv-dialog__content-text" :style="[contentTextStyle]">{{ cfg.content }}</text>
|
|
47
|
+
</slot>
|
|
48
|
+
</view>
|
|
49
|
+
</scroll-view>
|
|
50
|
+
</slot>
|
|
51
|
+
<slot v-if="cfg.showFooter" name="footer">
|
|
52
|
+
<view
|
|
53
|
+
v-if="showCancelBtn || showConfirmBtn"
|
|
54
|
+
class="uv-dialog__footer"
|
|
55
|
+
:class="[cfg.footerBorder ? 'uv-dialog__footer--border' : '']"
|
|
56
|
+
:style="[footerStyleCom]"
|
|
57
|
+
>
|
|
58
|
+
<view v-if="showCancelBtn" class="uv-dialog__footer-btn">
|
|
59
|
+
<uv-button
|
|
60
|
+
:text="cancelTextValue"
|
|
61
|
+
:type="cancelButton.type"
|
|
62
|
+
:size="cancelButton.size"
|
|
63
|
+
:shape="cancelButton.shape"
|
|
64
|
+
:plain="cancelButton.plain"
|
|
65
|
+
:hairline="cancelButton.hairline"
|
|
66
|
+
:disabled="cancelButton.disabled"
|
|
67
|
+
:loading="cancelButton.loading"
|
|
68
|
+
:loading-mode="cancelButton.loadingMode"
|
|
69
|
+
:custom-style="cancelButton.customStyle"
|
|
70
|
+
@click="onCancel"
|
|
71
|
+
/>
|
|
72
|
+
</view>
|
|
73
|
+
<view v-if="showCancelBtn && showConfirmBtn" class="uv-dialog__footer-gap" :style="[gapStyle]"></view>
|
|
74
|
+
<view v-if="showConfirmBtn" class="uv-dialog__footer-btn">
|
|
75
|
+
<uv-button
|
|
76
|
+
:text="confirmTextValue"
|
|
77
|
+
:type="confirmButton.type"
|
|
78
|
+
:size="confirmButton.size"
|
|
79
|
+
:shape="confirmButton.shape"
|
|
80
|
+
:plain="confirmButton.plain"
|
|
81
|
+
:hairline="confirmButton.hairline"
|
|
82
|
+
:disabled="confirmButton.disabled"
|
|
83
|
+
:loading="confirmButton.loading"
|
|
84
|
+
:loading-mode="confirmButton.loadingMode"
|
|
85
|
+
:custom-style="confirmButton.customStyle"
|
|
86
|
+
@click="onConfirm"
|
|
87
|
+
/>
|
|
88
|
+
</view>
|
|
89
|
+
</view>
|
|
90
|
+
</slot>
|
|
91
|
+
</view>
|
|
92
|
+
</uv-popup>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
97
|
+
import { $uv } from '../../utils'
|
|
98
|
+
import props from './props'
|
|
99
|
+
import { useI18n } from '../../hooks'
|
|
100
|
+
|
|
101
|
+
defineOptions({
|
|
102
|
+
name: 'uv-dialog',
|
|
103
|
+
// #ifdef UNI-APP-X
|
|
104
|
+
styleIsolation: 'app-and-page',
|
|
105
|
+
// #endif
|
|
106
|
+
virtualHost: true,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Dialog 业务对话框
|
|
111
|
+
* @description 基于 uv-popup 的业务对话框:标题/内容/底部取消确认/关闭/确认回调,支持 ref.open() 返回 Promise;
|
|
112
|
+
* 标题与按钮内置文案支持 i18n(内置 zh-Hans / en,可通过 createI18n messages 覆盖 uv.dialog.*)
|
|
113
|
+
* @tutorial https://www.uvui.cn/components/popup.html
|
|
114
|
+
* @property {Boolean} modelValue 是否显示(v-model)
|
|
115
|
+
* @property {String} mode 弹出方式,默认 center
|
|
116
|
+
* @property {String} size 尺寸,mini/normal/large
|
|
117
|
+
* @property {String} title 标题,默认 ''(使用内置文案「提示」)
|
|
118
|
+
* @property {String} content 内容文本
|
|
119
|
+
* @property {Boolean} showHeader 是否显示头部
|
|
120
|
+
* @property {Boolean} showTitle 是否显示标题
|
|
121
|
+
* @property {Boolean} showContent 是否显示内容
|
|
122
|
+
* @property {Boolean} showFooter 是否显示底部
|
|
123
|
+
* @property {Boolean} showCancelButton 是否显示取消按钮,默认 true(隐藏按钮请用它,不要传 false 文案)
|
|
124
|
+
* @property {Boolean} showConfirmButton 是否显示确认按钮,默认 true(隐藏按钮请用它,不要传 false 文案)
|
|
125
|
+
* @property {String} titleAlign 标题对齐,left/center/right/between/around
|
|
126
|
+
* @property {Boolean} titleBold 标题加粗
|
|
127
|
+
* @property {String} align 内容对齐,left/center/right
|
|
128
|
+
* @property {String | Number} width center 模式宽度,默认 325
|
|
129
|
+
* @property {String} cancelText 取消文案,默认 ''(使用内置「取消」)
|
|
130
|
+
* @property {String} confirmText 确认文案,默认 ''(使用内置「确定」)
|
|
131
|
+
* @property {Boolean} headerBorder 头部下边框
|
|
132
|
+
* @property {Boolean} footerBorder 底部上边框
|
|
133
|
+
* @property {Boolean} closeOnConfirm 确认后是否自动关闭
|
|
134
|
+
* @property {Boolean} closeOnCancel 取消后是否自动关闭
|
|
135
|
+
* @property {Object} cancelProps 取消按钮 props 覆盖
|
|
136
|
+
* @property {Object} confirmProps 确认按钮 props 覆盖
|
|
137
|
+
* @property {Object} buttonProps 底部按钮公共 props
|
|
138
|
+
* @property {Object} headerStyle 头部样式
|
|
139
|
+
* @property {Object} titleStyle 标题样式
|
|
140
|
+
* @property {Object} footerStyle 底部样式
|
|
141
|
+
* @property {Object} contentStyle 内容区样式
|
|
142
|
+
* @property {Object} scrollStyle 滚动区样式
|
|
143
|
+
* @property {Object} customStyle 最外层样式
|
|
144
|
+
* @property {String} customClass 最外层 class
|
|
145
|
+
* @event {Function} open 对话框打开
|
|
146
|
+
* @event {Function} close 对话框关闭
|
|
147
|
+
* @event {Function} confirm 点击确认
|
|
148
|
+
* @event {Function} cancel 点击取消
|
|
149
|
+
* @event {Function} change 弹层状态变化
|
|
150
|
+
*/
|
|
151
|
+
const p = defineProps(props)
|
|
152
|
+
|
|
153
|
+
const emit = defineEmits(['update:modelValue', 'open', 'close', 'confirm', 'cancel', 'change'])
|
|
154
|
+
|
|
155
|
+
interface DialogMetrics {
|
|
156
|
+
titleSize: number
|
|
157
|
+
contentSize: number
|
|
158
|
+
lineHeight: number
|
|
159
|
+
padding: number
|
|
160
|
+
gap: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const sizeMap: Record<string, DialogMetrics> = {
|
|
164
|
+
mini: { titleSize: 14, contentSize: 12, lineHeight: 18, padding: 12, gap: 8 },
|
|
165
|
+
normal: { titleSize: 17, contentSize: 14, lineHeight: 20, padding: 16, gap: 12 },
|
|
166
|
+
large: { titleSize: 19, contentSize: 16, lineHeight: 24, padding: 20, gap: 16 }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const popupRef = ref<any>(null)
|
|
170
|
+
const visible = ref(false)
|
|
171
|
+
const overrides = ref<Record<string, any>>({})
|
|
172
|
+
let resolveFn: ((value: any) => void) | null = null
|
|
173
|
+
let pendingAction: 'confirm' | 'cancel' | 'close' = 'close'
|
|
174
|
+
|
|
175
|
+
const cfg = computed<Record<string, any>>(() => Object.assign({}, p, overrides.value))
|
|
176
|
+
const metrics = computed<DialogMetrics>(() => sizeMap[String(cfg.value.size)] || sizeMap.normal)
|
|
177
|
+
|
|
178
|
+
// 内置文案:props / open(opts) 覆盖优先,否则使用内置 i18n(locales/ 的 uv.dialog.*)
|
|
179
|
+
const { t } = useI18n({ useScope: 'global' })
|
|
180
|
+
const titleText = computed<string>(() => String(cfg.value.title || '') || t('uv.dialog.title'))
|
|
181
|
+
const cancelTextValue = computed<string>(() => String(cfg.value.cancelText || '') || t('uv.dialog.cancel'))
|
|
182
|
+
const confirmTextValue = computed<string>(() => String(cfg.value.confirmText || '') || t('uv.dialog.confirm'))
|
|
183
|
+
// 按钮显隐只由 show*Button 控制(文案只能是 String,不能传 false 隐藏按钮)
|
|
184
|
+
const showCancelBtn = computed(() => Boolean(cfg.value.showCancelButton))
|
|
185
|
+
const showConfirmBtn = computed(() => Boolean(cfg.value.showConfirmButton))
|
|
186
|
+
|
|
187
|
+
const addUnit = (value: string | number) => $uv.addUnit(value)
|
|
188
|
+
|
|
189
|
+
const titleAlignValue = computed(() => {
|
|
190
|
+
const map: Record<string, string> = {
|
|
191
|
+
left: 'flex-start',
|
|
192
|
+
center: 'center',
|
|
193
|
+
right: 'flex-end',
|
|
194
|
+
between: 'space-between',
|
|
195
|
+
around: 'space-around'
|
|
196
|
+
}
|
|
197
|
+
return map[String(cfg.value.titleAlign)] || 'center'
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const headerClass = computed(() => ['uv-dialog__header--' + (cfg.value.titleAlign || 'center')])
|
|
201
|
+
const contentClass = computed(() => ['uv-dialog__content--' + (cfg.value.align || 'left')])
|
|
202
|
+
|
|
203
|
+
const dialogStyle = computed(() => {
|
|
204
|
+
const style: any = {}
|
|
205
|
+
if (cfg.value.mode === 'center') {
|
|
206
|
+
style.width = addUnit(cfg.value.width)
|
|
207
|
+
}
|
|
208
|
+
return $uv.deepMerge(style, $uv.addStyle(cfg.value.customStyle))
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const headerStyleCom = computed(() => {
|
|
212
|
+
const m = metrics.value
|
|
213
|
+
const style: any = {
|
|
214
|
+
paddingTop: m.padding + 'px',
|
|
215
|
+
paddingLeft: m.padding + 'px',
|
|
216
|
+
paddingRight: m.padding + 'px',
|
|
217
|
+
paddingBottom: Math.round(m.padding / 2) + 'px',
|
|
218
|
+
justifyContent: titleAlignValue.value
|
|
219
|
+
}
|
|
220
|
+
if (cfg.value.headerBorder) {
|
|
221
|
+
style.borderBottomWidth = '1px'
|
|
222
|
+
style.borderBottomStyle = 'solid'
|
|
223
|
+
style.borderBottomColor = '#f2f3f5'
|
|
224
|
+
}
|
|
225
|
+
return $uv.deepMerge(style, $uv.addStyle(cfg.value.headerStyle))
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const titleStyleCom = computed(() => {
|
|
229
|
+
const m = metrics.value
|
|
230
|
+
const style: any = {
|
|
231
|
+
fontSize: m.titleSize + 'px',
|
|
232
|
+
lineHeight: m.titleSize + 8 + 'px',
|
|
233
|
+
color: '#303133'
|
|
234
|
+
}
|
|
235
|
+
return $uv.deepMerge(style, $uv.addStyle(cfg.value.titleStyle))
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const scrollStyleCom = computed(() => {
|
|
239
|
+
const style: any = {}
|
|
240
|
+
// #ifdef WEB || H5
|
|
241
|
+
style.maxHeight = '60vh'
|
|
242
|
+
// #endif
|
|
243
|
+
// #ifndef WEB || H5
|
|
244
|
+
try {
|
|
245
|
+
const info = $uv.sys()
|
|
246
|
+
style.maxHeight = Math.floor(Number(info.windowHeight || 800) * 0.6) + 'px'
|
|
247
|
+
} catch (e) {
|
|
248
|
+
style.maxHeight = '500px'
|
|
249
|
+
}
|
|
250
|
+
// #endif
|
|
251
|
+
return $uv.deepMerge(style, $uv.addStyle(cfg.value.scrollStyle))
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const contentStyleCom = computed(() => {
|
|
255
|
+
const m = metrics.value
|
|
256
|
+
const style: any = {
|
|
257
|
+
paddingLeft: m.padding + 'px',
|
|
258
|
+
paddingRight: m.padding + 'px',
|
|
259
|
+
paddingTop: Math.round(m.padding / 2) + 'px',
|
|
260
|
+
paddingBottom: m.padding + 'px'
|
|
261
|
+
}
|
|
262
|
+
const align = String(cfg.value.align || 'left')
|
|
263
|
+
const textAlign = align === 'center' ? 'center' : align === 'right' ? 'right' : 'left'
|
|
264
|
+
style.textAlign = textAlign
|
|
265
|
+
style.alignItems = textAlign === 'center' ? 'center' : textAlign === 'right' ? 'flex-end' : 'flex-start'
|
|
266
|
+
return $uv.deepMerge(style, $uv.addStyle(cfg.value.contentStyle))
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const contentTextStyle = computed(() => {
|
|
270
|
+
const m = metrics.value
|
|
271
|
+
const style: any = {
|
|
272
|
+
fontSize: m.contentSize + 'px',
|
|
273
|
+
lineHeight: m.lineHeight + 'px',
|
|
274
|
+
color: '#606266'
|
|
275
|
+
}
|
|
276
|
+
const align = String(cfg.value.align || 'left')
|
|
277
|
+
style.textAlign = align === 'center' ? 'center' : align === 'right' ? 'right' : 'left'
|
|
278
|
+
return $uv.deepMerge(style, $uv.addStyle(cfg.value.contentStyle))
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const footerStyleCom = computed(() => {
|
|
282
|
+
const m = metrics.value
|
|
283
|
+
const style: any = {
|
|
284
|
+
paddingLeft: m.padding + 'px',
|
|
285
|
+
paddingRight: m.padding + 'px',
|
|
286
|
+
paddingBottom: m.padding + 'px',
|
|
287
|
+
paddingTop: m.padding + 'px'
|
|
288
|
+
}
|
|
289
|
+
if (cfg.value.footerBorder) {
|
|
290
|
+
style.borderTopWidth = '1px'
|
|
291
|
+
style.borderTopStyle = 'solid'
|
|
292
|
+
style.borderTopColor = '#f2f3f5'
|
|
293
|
+
}
|
|
294
|
+
return $uv.deepMerge(style, $uv.addStyle(cfg.value.footerStyle))
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const gapStyle = computed(() => ({ width: metrics.value.gap + 'px' }))
|
|
298
|
+
|
|
299
|
+
const resolveButtonProps = (specific: Record<string, any>, isCancel: boolean) => {
|
|
300
|
+
const base: any = cfg.value.buttonProps || {}
|
|
301
|
+
const extra: any = specific || {}
|
|
302
|
+
return {
|
|
303
|
+
type: extra.type !== undefined ? extra.type : base.type !== undefined ? base.type : 'primary',
|
|
304
|
+
size: extra.size !== undefined ? extra.size : base.size !== undefined ? base.size : cfg.value.size,
|
|
305
|
+
shape: extra.shape !== undefined ? extra.shape : base.shape !== undefined ? base.shape : 'square',
|
|
306
|
+
plain: extra.plain !== undefined ? extra.plain : base.plain !== undefined ? base.plain : isCancel,
|
|
307
|
+
hairline: extra.hairline !== undefined ? extra.hairline : base.hairline !== undefined ? base.hairline : true,
|
|
308
|
+
disabled: extra.disabled !== undefined ? extra.disabled : base.disabled !== undefined ? base.disabled : false,
|
|
309
|
+
loading: extra.loading !== undefined ? extra.loading : base.loading !== undefined ? base.loading : false,
|
|
310
|
+
loadingMode:
|
|
311
|
+
extra.loadingMode !== undefined
|
|
312
|
+
? extra.loadingMode
|
|
313
|
+
: base.loadingMode !== undefined
|
|
314
|
+
? base.loadingMode
|
|
315
|
+
: 'spinner',
|
|
316
|
+
customStyle: Object.assign({}, base.customStyle || {}, extra.customStyle || {})
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const cancelButton = computed(() => resolveButtonProps(cfg.value.cancelProps, true))
|
|
321
|
+
const confirmButton = computed(() => resolveButtonProps(cfg.value.confirmProps, false))
|
|
322
|
+
|
|
323
|
+
const finish = (payload: Record<string, any>) => {
|
|
324
|
+
if (resolveFn) {
|
|
325
|
+
resolveFn(payload)
|
|
326
|
+
resolveFn = null
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const doOpen = (opts: Record<string, any> = {}) => {
|
|
331
|
+
overrides.value = Object.assign({}, opts)
|
|
332
|
+
if (visible.value) return
|
|
333
|
+
visible.value = true
|
|
334
|
+
emit('open')
|
|
335
|
+
emit('update:modelValue', true)
|
|
336
|
+
nextTick(() => {
|
|
337
|
+
popupRef.value && popupRef.value.open()
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const doClose = () => {
|
|
342
|
+
pendingAction = 'close'
|
|
343
|
+
if (popupRef.value) {
|
|
344
|
+
popupRef.value.close()
|
|
345
|
+
} else {
|
|
346
|
+
visible.value = false
|
|
347
|
+
emit('update:modelValue', false)
|
|
348
|
+
finish({ close: true })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const open = (opts: Record<string, any> = {}) => {
|
|
353
|
+
return new Promise<Record<string, any>>((resolve) => {
|
|
354
|
+
resolveFn = resolve
|
|
355
|
+
doOpen(opts)
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const close = () => {
|
|
360
|
+
doClose()
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const confirm = () => {
|
|
364
|
+
emit('confirm')
|
|
365
|
+
pendingAction = 'confirm'
|
|
366
|
+
if (cfg.value.closeOnConfirm) {
|
|
367
|
+
doCloseByAction()
|
|
368
|
+
} else {
|
|
369
|
+
finish({ confirm: true })
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const cancel = () => {
|
|
374
|
+
emit('cancel')
|
|
375
|
+
pendingAction = 'cancel'
|
|
376
|
+
if (cfg.value.closeOnCancel) {
|
|
377
|
+
doCloseByAction()
|
|
378
|
+
} else {
|
|
379
|
+
finish({ cancel: true })
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// confirm/cancel 触发关闭时保留 pendingAction,避免被 doClose 重置为 close
|
|
384
|
+
const doCloseByAction = () => {
|
|
385
|
+
if (popupRef.value) {
|
|
386
|
+
popupRef.value.close()
|
|
387
|
+
} else {
|
|
388
|
+
onPopupClose()
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const onConfirm = () => {
|
|
393
|
+
confirm()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const onCancel = () => {
|
|
397
|
+
cancel()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const onPopupChange = (e: any) => {
|
|
401
|
+
emit('change', e)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const onPopupClose = () => {
|
|
405
|
+
const wasVisible = visible.value
|
|
406
|
+
visible.value = false
|
|
407
|
+
emit('update:modelValue', false)
|
|
408
|
+
overrides.value = {}
|
|
409
|
+
if (wasVisible) emit('close')
|
|
410
|
+
const action = pendingAction
|
|
411
|
+
pendingAction = 'close'
|
|
412
|
+
if (action === 'confirm') finish({ confirm: true })
|
|
413
|
+
else if (action === 'cancel') finish({ cancel: true })
|
|
414
|
+
else finish({ close: true })
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
watch(
|
|
418
|
+
() => p.modelValue,
|
|
419
|
+
(value) => {
|
|
420
|
+
if (value) {
|
|
421
|
+
if (!visible.value) doOpen()
|
|
422
|
+
} else {
|
|
423
|
+
if (visible.value) doClose()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
onMounted(() => {
|
|
429
|
+
if (p.modelValue) doOpen()
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
defineExpose({
|
|
433
|
+
open,
|
|
434
|
+
close,
|
|
435
|
+
confirm,
|
|
436
|
+
cancel
|
|
437
|
+
})
|
|
438
|
+
</script>
|
|
439
|
+
|
|
440
|
+
<style lang="scss">
|
|
441
|
+
@import '../../styles/color.scss';
|
|
442
|
+
|
|
443
|
+
.uv-dialog {
|
|
444
|
+
/* #ifndef APP-NVUE */
|
|
445
|
+
box-sizing: border-box;
|
|
446
|
+
display: flex;
|
|
447
|
+
/* #endif */
|
|
448
|
+
flex-direction: column;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.uv-dialog__header {
|
|
452
|
+
/* #ifndef APP-NVUE */
|
|
453
|
+
display: flex;
|
|
454
|
+
/* #endif */
|
|
455
|
+
flex-direction: row;
|
|
456
|
+
align-items: center;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.uv-dialog__title--bold {
|
|
460
|
+
font-weight: bold;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.uv-dialog__scroll {
|
|
464
|
+
/* #ifndef APP-NVUE */
|
|
465
|
+
display: flex;
|
|
466
|
+
/* #endif */
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.uv-dialog__content {
|
|
471
|
+
/* #ifndef APP-NVUE */
|
|
472
|
+
display: flex;
|
|
473
|
+
/* #endif */
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.uv-dialog__footer {
|
|
478
|
+
/* #ifndef APP-NVUE */
|
|
479
|
+
display: flex;
|
|
480
|
+
/* #endif */
|
|
481
|
+
flex-direction: row;
|
|
482
|
+
align-items: center;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.uv-dialog__footer-btn {
|
|
486
|
+
flex: 1;
|
|
487
|
+
/* #ifndef APP-NVUE */
|
|
488
|
+
display: flex;
|
|
489
|
+
/* #endif */
|
|
490
|
+
flex-direction: column;
|
|
491
|
+
}
|
|
492
|
+
</style>
|