mao-mobile 0.0.1

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.
@@ -0,0 +1,131 @@
1
+ # ProPopup 弹出层组件
2
+
3
+ ## 组件介绍
4
+
5
+ ProPopup 是一个灵活的弹出层组件,支持居中和底部弹出,具备遮罩、动画、圆角、主题色、安全区适配等功能,适用于多种弹窗场景。
6
+
7
+ ## 功能特点
8
+
9
+ - 支持居中和底部弹出
10
+ - 遮罩层可配置,支持点击关闭
11
+ - 圆角、宽度、背景色等可自定义
12
+ - 支持底部安全区适配
13
+ - 支持主/次按钮自定义、按钮占比自适应
14
+ - 支持插槽自定义头部、内容、底部
15
+
16
+ ## 属性(Props)
17
+
18
+ | 属性名 | 类型 | 默认值 | 说明 |
19
+ | ------------------- | ------------- | ----------------- | ------------------------------ |
20
+ | customClass | String | '' | 弹出层自定义类名 |
21
+ | maskShow | Boolean | true | 是否显示遮罩层 |
22
+ | maskClick | Boolean | true | 点击遮罩层是否关闭弹窗 |
23
+ | backgroundColor | String | '#fff' | 弹出层背景色 |
24
+ | maskBackgroundColor | String | 'rgba(0,0,0,0.4)' | 遮罩层背景色 |
25
+ | borderRadius | String/Number | - | 弹出层圆角 |
26
+ | position | String | 'center' | 弹出位置:'center' 或 'bottom' |
27
+ | width | String/Number | '80vw' | 居中弹出层宽度 |
28
+ | themeColor | String | '#0BC8C8' | 主题色 |
29
+ | isShowHeader | Boolean | false | 是否显示头部区域 |
30
+ | isShowClose | Boolean | true | 是否显示关闭图标 |
31
+ | isShowFooter | Boolean | false | 是否显示底部区域 |
32
+ | title | String | '' | 弹窗标题 |
33
+ | confirmName | String | '主按钮' | 主按钮名称 |
34
+ | cancelName | String | '取消' | 次按钮名称 |
35
+ | isShowCancel | Boolean | false | 是否显示次按钮 |
36
+ | safeArea | Boolean | true | 底部弹出层是否适配安全区 |
37
+
38
+ ## 事件(Events)
39
+
40
+ | 事件名 | 说明 | 回调参数 |
41
+ | --------- | ---------------------- | ----------------- |
42
+ | change | 弹窗显示状态变化时触发 | { show: boolean } |
43
+ | maskClick | 点击遮罩层时触发 | - |
44
+ | close | 点击关闭图标时触发 | - |
45
+ | confirm | 点击主按钮时触发 | - |
46
+ | cancel | 点击次按钮时触发 | - |
47
+
48
+ ## 方法(Expose)
49
+
50
+ | 方法名 | 说明 |
51
+ | ------ | -------- |
52
+ | open | 打开弹窗 |
53
+ | close | 关闭弹窗 |
54
+
55
+ ## 插槽(Slots)
56
+
57
+ - `header`:自定义头部内容
58
+ - 默认插槽:自定义弹窗内容
59
+ - `footer`:自定义底部内容
60
+
61
+ ## 使用示例
62
+
63
+ ```vue
64
+ <template>
65
+ <ProPopup
66
+ ref="popupRef"
67
+ position="bottom"
68
+ :is-show-header="true"
69
+ :is-show-footer="true"
70
+ :is-show-cancel="true"
71
+ title="温馨提示"
72
+ confirmName="确定"
73
+ cancelName="取消"
74
+ @confirm="onConfirm"
75
+ @cancel="onCancel"
76
+ @change="onChange"
77
+ @maskClick="onMaskClick"
78
+ >
79
+ <template #header>
80
+ <view>自定义头部</view>
81
+ </template>
82
+ <view>弹窗内容</view>
83
+ <template #footer>
84
+ <view>自定义底部</view>
85
+ </template>
86
+ </ProPopup>
87
+ </template>
88
+
89
+ <script setup>
90
+ import { ref } from "vue";
91
+ import ProPopup from "./index.vue";
92
+
93
+ const popupRef = ref(null);
94
+
95
+ const onConfirm = () => {
96
+ // 主按钮回调
97
+ };
98
+ const onCancel = () => {
99
+ // 次按钮回调
100
+ };
101
+ const onChange = ({ show }) => {
102
+ // 弹窗显示状态变化
103
+ };
104
+ const onMaskClick = () => {
105
+ // 遮罩点击
106
+ };
107
+ </script>
108
+ ```
109
+
110
+ ## 按钮布局说明
111
+
112
+ - 同时有主、次按钮时,主按钮宽度占 60%,次按钮占 40%,中间间隔 32rpx。
113
+ - 仅有主按钮时,主按钮宽度占 100%。
114
+
115
+ ## 样式定制
116
+
117
+ - `.pro-popup`:弹窗容器
118
+ - `.pro-popup__mask`:遮罩层
119
+ - `.pro-popup__wrapper`:内容容器
120
+ - `.pro-popup__header`:头部区域
121
+ - `.pro-popup__footer`:底部区域
122
+ - `.pro-popup__footer--button--confirm`:主按钮
123
+ - `.pro-popup__footer--button--cancel`:次按钮
124
+
125
+ 可通过覆盖这些类名自定义样式。
126
+
127
+ ## 注意事项
128
+
129
+ - 组件使用 `position: fixed`,请确保父元素不会影响其定位。
130
+ - 动画、圆角、宽度等均可通过 props 配置。
131
+ - 安全区适配依赖 `env(safe-area-inset-bottom)`,如需关闭可设置 `:safe-area="false"`。
@@ -0,0 +1,392 @@
1
+ <template>
2
+ <view
3
+ :class="['pro-popup', customClass, { 'pro-popup--hidden': !showPopup }]"
4
+ >
5
+ <view
6
+ v-if="maskShow"
7
+ class="pro-popup__mask"
8
+ :class="[`popup-mask-${showPopup ? 'show' : 'hide'}`]"
9
+ :style="{ backgroundColor: maskBackgroundColor }"
10
+ @tap="onTap"
11
+ >
12
+ </view>
13
+ <view
14
+ class="pro-popup__wrapper"
15
+ :class="[
16
+ `pro-popup__wrapper--${position}`,
17
+ `popup-${position}-${showPopup ? 'show' : 'hide'}`,
18
+ ]"
19
+ :style="wrapperStyle"
20
+ @tap.stop
21
+ >
22
+ <view class="pro-popup__content">
23
+ <slot name="header" v-if="isShowHeader">
24
+ <view class="pro-popup__header">
25
+ <text class="pro-popup__header--title">{{ title }}</text>
26
+ <image
27
+ v-if="isShowClose"
28
+ class="pro-popup__header--close"
29
+ src="https://static.wxb.com.cn/frontEnd/images/ideacome-mobile/popup-close.png"
30
+ mode="widthFix"
31
+ @tap="close"
32
+ ></image>
33
+ </view>
34
+ </slot>
35
+ <view class="pro-popup__content--body">
36
+ <slot></slot>
37
+ </view>
38
+ <slot name="footer" v-if="isShowFooter">
39
+ <view class="pro-popup__footer">
40
+ <view
41
+ v-if="isShowCancel"
42
+ class="pro-popup__footer--button--cancel"
43
+ @tap="handleCancel"
44
+ >{{ cancelName }}</view
45
+ >
46
+ <view
47
+ :class="[
48
+ 'pro-popup__footer--button--confirm',
49
+ {
50
+ 'pro-popup__footer--button--confirm--full': !isShowCancel,
51
+ },
52
+ ]"
53
+ @tap="handleConfirm"
54
+ >{{ confirmName }}</view
55
+ >
56
+ </view>
57
+ </slot>
58
+ </view>
59
+ </view>
60
+ </view>
61
+ </template>
62
+
63
+ <script setup>
64
+ import { ref, computed, watch } from "vue";
65
+
66
+ const props = defineProps({
67
+ // 弹出层的自定义类名
68
+ customClass: {
69
+ type: String,
70
+ default: "",
71
+ },
72
+ // 是否显示遮罩层
73
+ maskShow: {
74
+ type: Boolean,
75
+ default: true,
76
+ },
77
+ // 点击遮罩层是否关闭
78
+ maskClick: {
79
+ type: Boolean,
80
+ default: true,
81
+ },
82
+ // 弹出层背景色
83
+ backgroundColor: {
84
+ type: String,
85
+ default: "#fff",
86
+ },
87
+ // 遮罩层背景色
88
+ maskBackgroundColor: {
89
+ type: String,
90
+ default: "rgba(0, 0, 0, 0.4)",
91
+ },
92
+ // 弹出层圆角
93
+ borderRadius: {
94
+ type: [String, Number],
95
+ },
96
+ // 弹出位置
97
+ position: {
98
+ type: String,
99
+ default: "center",
100
+ validator: (value) => ["center", "bottom"].includes(value),
101
+ },
102
+ // 弹出层标题
103
+ title: {
104
+ type: String,
105
+ default: "",
106
+ },
107
+ // 是否显示头部区域
108
+ isShowHeader: {
109
+ type: Boolean,
110
+ default: false,
111
+ },
112
+ // 是否显示关闭图标
113
+ isShowClose: {
114
+ type: Boolean,
115
+ default: true,
116
+ },
117
+ // 是否显示底部区域
118
+ isShowFooter: {
119
+ type: Boolean,
120
+ default: false,
121
+ },
122
+ // 主按钮名称
123
+ confirmName: {
124
+ type: String,
125
+ default: "确定",
126
+ },
127
+ // 次按钮名称
128
+ cancelName: {
129
+ type: String,
130
+ default: "取消",
131
+ },
132
+ // 是否显示次按钮
133
+ isShowCancel: {
134
+ type: Boolean,
135
+ default: true,
136
+ },
137
+ // 居中弹出层宽度
138
+ width: {
139
+ type: [String, Number],
140
+ default: "calc(100vw - 80rpx)",
141
+ },
142
+ // 弹出层按钮主题色
143
+ themeColor: {
144
+ type: String,
145
+ default: "#0BC8C8",
146
+ },
147
+ // 底部弹出层是否适配安全区
148
+ safeArea: {
149
+ type: Boolean,
150
+ default: true,
151
+ },
152
+ });
153
+
154
+ const emit = defineEmits(["change", "maskClick", "close", "confirm", "cancel"]);
155
+
156
+ // 内部状态
157
+ const showPopup = ref(false);
158
+
159
+ // 计算样式
160
+ const wrapperStyle = computed(() => {
161
+ let defaultborderRadius =
162
+ props.position === "bottom" ? "20rpx 20rpx 0 0" : "20rpx";
163
+ let borderRadius = [undefined, null, ""].includes(props.borderRadius)
164
+ ? defaultborderRadius
165
+ : props.borderRadius;
166
+ return {
167
+ backgroundColor: props.backgroundColor,
168
+ borderRadius:
169
+ typeof props.borderRadius === "number"
170
+ ? props.position === "bottom"
171
+ ? `${borderRadius}rpx ${borderRadius}rpx 0 0`
172
+ : `${borderRadius}rpx`
173
+ : borderRadius,
174
+ };
175
+ });
176
+
177
+ const popupWidth = computed(() => {
178
+ if (props.position === "center") {
179
+ return props.width;
180
+ } else {
181
+ return "100%";
182
+ }
183
+ });
184
+ // 监听 showPopup 变化
185
+ watch(
186
+ () => showPopup.value,
187
+ (newVal) => {
188
+ emit("change", { show: newVal });
189
+ // #ifdef H5
190
+ // 处理 h5 滚动穿透的问题
191
+ setH5Visible(!newVal);
192
+ // #endif
193
+ }
194
+ );
195
+
196
+ const setH5Visible = (visible) => {
197
+ document.getElementsByTagName("body")[0].style.overflow = visible
198
+ ? "visible"
199
+ : "hidden";
200
+ };
201
+
202
+ const open = () => {
203
+ showPopup.value = true;
204
+ };
205
+
206
+ const close = () => {
207
+ showPopup.value = false;
208
+ emit("close");
209
+ };
210
+
211
+ const handleConfirm = () => {
212
+ emit("confirm");
213
+ };
214
+
215
+ const handleCancel = () => {
216
+ emit("cancel");
217
+ };
218
+
219
+ // 点击遮罩层
220
+ const onTap = () => {
221
+ emit("maskClick");
222
+ if (props.maskClick) {
223
+ showPopup.value = false;
224
+ }
225
+ };
226
+
227
+ // 暴露方法给父组件
228
+ defineExpose({
229
+ open,
230
+ close,
231
+ });
232
+ </script>
233
+
234
+ <style lang="scss" scoped>
235
+ .pro-popup {
236
+ font-family: PingFang SC, sans-serif;
237
+ position: fixed;
238
+ top: 0;
239
+ right: 0;
240
+ bottom: 0;
241
+ left: 0;
242
+ z-index: 999999;
243
+ visibility: visible;
244
+ opacity: 1;
245
+ transition: visibility 0s, opacity 0s;
246
+
247
+ &--hidden {
248
+ visibility: hidden;
249
+ opacity: 0;
250
+ transition: visibility 0s 300ms, opacity 0s 300ms;
251
+ }
252
+
253
+ &__mask {
254
+ position: absolute;
255
+ top: 0;
256
+ right: 0;
257
+ bottom: 0;
258
+ left: 0;
259
+ background-color: rgba(0, 0, 0, 0.4);
260
+ transition: opacity 300ms ease-in-out;
261
+ }
262
+
263
+ &__wrapper {
264
+ position: fixed;
265
+ will-change: transform, opacity;
266
+
267
+ &--center {
268
+ top: 50%;
269
+ left: 50%;
270
+ transform: translate(-50%, -50%);
271
+ transition: transform 300ms ease-in-out, opacity 300ms ease-in-out;
272
+ .pro-popup__content {
273
+ min-height: 440rpx;
274
+ max-height: calc(100vh - 80rpx);
275
+ }
276
+ .pro-popup__footer {
277
+ padding: 0 48rpx 48rpx;
278
+ }
279
+ }
280
+
281
+ &--bottom {
282
+ left: 0;
283
+ right: 0;
284
+ bottom: 0;
285
+ transition: transform 300ms ease-in-out, opacity 300ms ease-in-out;
286
+ .pro-popup__content {
287
+ min-height: 440rpx;
288
+ max-height: 100vh;
289
+ padding-bottom: v-bind(
290
+ 'safeArea ? "env(safe-area-inset-bottom)" : "0"'
291
+ );
292
+ }
293
+ .pro-popup__footer {
294
+ padding: 0 48rpx 76rpx 48rpx;
295
+ }
296
+ }
297
+ .pro-popup__content {
298
+ color: #333;
299
+ box-sizing: border-box;
300
+ width: v-bind(popupWidth);
301
+ position: relative;
302
+ display: flex;
303
+ flex-direction: column;
304
+
305
+ .pro-popup__header {
306
+ flex-shrink: 0;
307
+ padding: 48rpx 48rpx 0;
308
+ display: flex;
309
+ justify-content: space-between;
310
+ align-items: center;
311
+ &--title {
312
+ font-size: 32rpx;
313
+ font-weight: 600;
314
+ color: #212121;
315
+ }
316
+ &--close {
317
+ width: 28rpx;
318
+ }
319
+ }
320
+ .pro-popup__content--body {
321
+ margin: 32rpx 48rpx 48rpx 48rpx;
322
+ flex: 1;
323
+ overflow: auto;
324
+ }
325
+ .pro-popup__footer {
326
+ flex-shrink: 0;
327
+ display: flex;
328
+ gap: 32rpx;
329
+ &--button--cancel {
330
+ flex: 1;
331
+ height: 84rpx;
332
+ border: 1px solid v-bind("themeColor");
333
+ background: #fff;
334
+ border-radius: 16rpx;
335
+ font-weight: 400;
336
+ font-size: 30rpx;
337
+ color: v-bind("themeColor");
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ }
342
+ &--button--confirm {
343
+ width: calc((100% - 32rpx) * 0.618);
344
+ height: 84rpx;
345
+ background: v-bind("themeColor");
346
+ border-radius: 16rpx;
347
+ font-weight: 400;
348
+ font-size: 30rpx;
349
+ color: #ffffff;
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ &--full {
354
+ width: 100%;
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ // 遮罩层动画
363
+ .popup-mask-show {
364
+ opacity: 1;
365
+ }
366
+
367
+ .popup-mask-hide {
368
+ opacity: 0;
369
+ }
370
+
371
+ // 内容层动画 - 居中弹出
372
+ .popup-center-show {
373
+ transform: translate(-50%, -50%) scale(1);
374
+ opacity: 1;
375
+ }
376
+
377
+ .popup-center-hide {
378
+ transform: translate(-50%, -50%) scale(0.3);
379
+ opacity: 0;
380
+ }
381
+
382
+ // 内容层动画 - 底部弹出
383
+ .popup-bottom-show {
384
+ transform: translateY(0);
385
+ opacity: 1;
386
+ }
387
+
388
+ .popup-bottom-hide {
389
+ transform: translateY(100%);
390
+ opacity: 0;
391
+ }
392
+ </style>
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "mao-mobile",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "description": "基于uniapp组件库",
6
+ "author": "wangzy",
7
+ "browserslist": [
8
+ "Android >= 4",
9
+ "ios >= 8"
10
+ ],
11
+ "devDependencies": {
12
+ "scss": "^0.2.4"
13
+ }
14
+ }