zydx-plus 1.35.638 → 1.35.640

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zydx-plus",
3
- "version": "1.35.638",
3
+ "version": "1.35.640",
4
4
  "description": "Vue.js",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -1,3 +1,8 @@
1
+ /**
2
+ * TipBox 提示弹窗组件入口文件
3
+ * 动态渲染提示弹窗,支持多种类型:文本提示(text)、图片(img)、单选框(radio)、复选框(checkbox)、输入表单(input)、日历选择(calender)等
4
+ */
5
+
1
6
  import { createVNode, render } from 'vue'
2
7
  import XtxConfirm from './src/main.vue'
3
8
 
@@ -8,12 +13,14 @@ render(divNode, document.body)
8
13
  const container = divNode.el
9
14
 
10
15
  // 导出函数可通过调用 Confirm() 函数动态创建 XtxConfirm 组件
16
+ // 参数说明:type(弹窗类型)、title(标题)、promptContent(提示内容)、cancelShow(是否显示取消按钮)、inputArr(输入项数组)、countdown(倒计时)等
11
17
  const Confirm = ({ className, type, url ,title,radioArr,checkboxArr,submitText,cancelText,closeShow, promptContent, middle, cancelShow, inputArr, countdown, components, slotProps }) => {
12
18
  // 返回 Promise 对象
13
19
  return new Promise((resolve, reject) => {
14
20
  // 2. 点击确认按钮,触发resolve同时销毁组件
15
21
  const submit = () => {
16
22
  let val = []
23
+ // 收集输入框类型的值
17
24
  if(type === 'input') {
18
25
  for(let i=0; i< inputArr.length; i++) {
19
26
  if(inputArr[i].type === 'select') {
@@ -26,6 +33,7 @@ const Confirm = ({ className, type, url ,title,radioArr,checkboxArr,submitText,c
26
33
  }
27
34
  }
28
35
  }
36
+ // 收集单选项的值
29
37
  if(type === 'radio') {
30
38
  for(let i=0; i< radioArr.length; i++) {
31
39
  if(radioArr[i].checked) {
@@ -33,6 +41,7 @@ const Confirm = ({ className, type, url ,title,radioArr,checkboxArr,submitText,c
33
41
  }
34
42
  }
35
43
  }
44
+ // 收集多选项的值
36
45
  if(type === 'checkbox') {
37
46
  for(let i=0; i< checkboxArr.length; i++) {
38
47
  if(checkboxArr[i].checked) {
@@ -48,6 +57,7 @@ const Confirm = ({ className, type, url ,title,radioArr,checkboxArr,submitText,c
48
57
  render(null, container)
49
58
  reject('点击取消')
50
59
  }
60
+ // 关闭弹窗
51
61
  const close = () => {
52
62
  render(null, container)
53
63
  }
@@ -1,5 +1,12 @@
1
+ /**
2
+ * TipBox 提示弹窗组件主文件
3
+ * 致用大学 - 支持多种弹窗类型:文本提示、图片展示、单选框、复选框、表单输入、日历选择、年份选择、文件上传、封面图等
4
+ * 通过入口文件动态创建该组件实现函数式调用弹窗
5
+ */
6
+
1
7
  <template>
2
8
  <div class="hello">
9
+ <!-- 弹窗遮罩层 -->
3
10
  <div class="tip-popupWindowSearch">
4
11
  <div class="tip-box" :class="className">
5
12
  <img v-if="closeShow" @click="close" class="close"
@@ -10,13 +17,16 @@
10
17
  </div>
11
18
  <div class="tip-boxContent" ref="tip-boxContent" :style="overflowShow?'overflow-y: scroll':''">
12
19
  <div class="tap-p" ref="tapP">
20
+ <!-- 插槽类型:渲染自定义组件 -->
13
21
  <div v-if="type === 'slot'">
14
22
  <component :is="components" v-bind="slotProps"></component>
15
23
  </div>
24
+ <!-- 文本类型提示内容 -->
16
25
  <div class="tip-title2" ref="tipTitle" v-if="type === 'text'"
17
26
  :style="{'text-align': (middles)? 'center': 'left', 'text-indent': indent? '2em': '0' }">
18
27
  {{ promptContent }} {{ countdown === 0 ? '' : `(${timeo}s)` }}
19
28
  </div>
29
+ <!-- 图片类型提示内容 -->
20
30
  <div class="tip-img" v-if="type === 'img'">
21
31
  <img :src="url" alt=""/>
22
32
  </div>
@@ -41,9 +51,11 @@
41
51
  </div>
42
52
  </div>
43
53
  <div class="tip-img" v-if="type === 'input'">
44
- <div class="tip-input" v-for="(item,index) in inputArr" :key="index">
54
+ <div class="tip-input" v-for="(item,index) in inputArr" :key="index"
55
+ ref="inputRefs"
56
+ :class="{'is-error': errorIndex.includes(index), 'shake': errorIndex.includes(index) && shaking}">
45
57
  <span v-if="item.name"><em>{{ item.name.replace(/:/g, "").replace(/:/g, "") }}</em><b>:</b><i
46
- v-if="item.required">*</i></span>
58
+ v-if="item.required || required">*</i></span>
47
59
  <div v-if="item.type === 'file'" class="file-cont">
48
60
  <em>{{ fileName }}</em>
49
61
  <label>
@@ -56,17 +68,17 @@
56
68
  <input type="number" @keypress="isNumberKey($event,index)"
57
69
  :placeholder="item.placeholder"
58
70
  :maxlength="item.maxlength"
59
- v-model="item.value" :disabled="item.disabled"/>
71
+ v-model="item.value" :disabled="item.disabled" @input="clearError(index)"/>
60
72
  </div>
61
73
  <input v-else type="text" :placeholder="item.placeholder" :maxlength="item.maxlength"
62
- v-model="item.value" :disabled="item.disabled"/>
74
+ v-model="item.value" :disabled="item.disabled" @input="clearError(index)"/>
63
75
  <b>{{ item.tail }}</b>
64
76
  </div>
65
77
  <div class="input-name" v-if="item.type === 'query'">
66
78
  <div class="number-input">
67
79
  <input type="text"
68
80
  :placeholder="item.placeholder"
69
- v-model="item.value" @input="queryDrop($event,item)" />
81
+ v-model="item.value" @input="queryDrop($event,item); clearError(index)" />
70
82
  </div>
71
83
  <div class="drop-down" v-if="dropData.length > 0">
72
84
  <p @click="dropTap(item,item3)" v-for="item3 in dropData">{{ item3[item.column] }}</p>
@@ -86,7 +98,7 @@
86
98
  </div>
87
99
  </div>
88
100
  <div v-if="item.type === 'select'" style="display: inline-block;width: 300px;">
89
- <Select :options="item.option" :readonly="item.disabled" @select="item.select"
101
+ <Select :options="item.option" :readonly="item.disabled" @select="(...args) => { item.select && item.select(...args); clearError(index) }"
90
102
  v-model:value="item.selectValue" :placeholder="item.placeholder"></Select>
91
103
  </div>
92
104
  <div class="cal" ref="calenderRefId" v-if="item.type === 'calender'">
@@ -135,8 +147,9 @@
135
147
  </div>
136
148
  </div>
137
149
  </div>
150
+ <!-- 按钮区域:确定/取消 -->
138
151
  <div class="tip-btnBox">
139
- <div @click="submit" v-html="submitText"></div>
152
+ <div @click="submitTap" v-html="submitText"></div>
140
153
  <div v-if="cancelShow" @click="cancel" v-html="cancelText"></div>
141
154
  </div>
142
155
  </div>
@@ -145,10 +158,12 @@
145
158
  </template>
146
159
 
147
160
  <script>
161
+ // 引入子组件
148
162
  import Select from '../../select/src/select.vue'
149
163
  import Calendar from '../../calendar/src/Calendar'
150
164
  import Year from '../../year/src/year'
151
165
  import zydxTextarea from '../../textarea'
166
+ // 定时器和实例引用
152
167
  let timeStop = null
153
168
  let _that = null
154
169
  export default {
@@ -156,14 +171,16 @@ export default {
156
171
  components: {Select, Calendar, Year,zydxTextarea},
157
172
  data() {
158
173
  return {
159
- tip: false,
160
- tipYear: false,
161
- fileName: '',
162
- middles: this.middle,
163
- indent: false,
164
- overflowShow: false,
165
- timeo: 0,
166
- dropData: []
174
+ tip: false, // 日历显示状态
175
+ tipYear: false, // 年份选择显示状态
176
+ fileName: '', // 文件名
177
+ middles: this.middle, // 文字居中
178
+ indent: false, // 文字缩进
179
+ overflowShow: false, // 是否显示滚动条
180
+ timeo: 0, // 倒计时剩余秒数
181
+ dropData: [], // 下拉数据
182
+ errorIndex: [], // 必填校验未通过的项索引集合
183
+ shaking: false // 是否触发抖动动画
167
184
  }
168
185
  },
169
186
  props: {
@@ -207,6 +224,10 @@ export default {
207
224
  type: Array,
208
225
  default: []
209
226
  },
227
+ required: { // 是否开启必填校验:为 true 时所有输入项均视为必填;单项也可通过 item.required 单独控制
228
+ type: Boolean,
229
+ default: false
230
+ },
210
231
  radioArr: { // 单选数据
211
232
  type: Array,
212
233
  default: []
@@ -251,25 +272,85 @@ export default {
251
272
  mounted() {
252
273
  _that = this
253
274
  this.$nextTick(() => {
275
+ // 文本内容过长时取消居中,启用缩进
254
276
  if (this.type === 'text') {
255
277
  if (this.$refs.tapP.offsetHeight > 25) {
256
278
  this.middles = false
257
279
  this.indent = true
258
280
  }
259
281
  }
282
+ // 启动倒计时
260
283
  if (this.countdown > 0) {
261
284
  this.timeo = this.countdown
262
285
  this.countdownFun()
263
286
  }
287
+ // 内容超过600px显示滚动条
264
288
  this.overflowShow = this.$refs['tip-boxContent'].scrollHeight>600
265
289
  })
266
290
  },
267
291
  beforeUnmount() {
268
- clearInterval(timeStop)
292
+ clearInterval(timeStop) // 清理倒计时定时器
269
293
  },
270
294
  methods: {
295
+ // 点击确认:先校验必填项,全部通过才执行外部 submit
296
+ submitTap() {
297
+ if (!this.validateRequired()) return
298
+ this.submit()
299
+ },
300
+ // 判断某项是否必填(组件级 required 或单项 item.required)
301
+ isRequired(item) {
302
+ return !!(item.required || this.required)
303
+ },
304
+ // 判断某项的值是否为空(不同类型取值字段不同)
305
+ isItemEmpty(item) {
306
+ let val
307
+ switch (item.type) {
308
+ case 'select': val = item.selectValue; break
309
+ case 'cover': val = item.url; break
310
+ default: val = item.value
311
+ }
312
+ return val === undefined || val === null || val === '' ||
313
+ (Array.isArray(val) && val.length === 0)
314
+ },
315
+ // 校验必填项,返回是否通过;不通过时记录错误项并触发抖动
316
+ validateRequired() {
317
+ const errors = []
318
+ if (this.type === 'input') {
319
+ this.inputArr.forEach((item, index) => {
320
+ if (this.isRequired(item) && this.isItemEmpty(item)) {
321
+ errors.push(index)
322
+ }
323
+ })
324
+ }
325
+ this.errorIndex = errors
326
+ if (errors.length > 0) {
327
+ // 先移除抖动类,强制 reflow 后再加回,确保每次点击都能重新播放动画
328
+ this.shaking = false
329
+ this.$nextTick(() => {
330
+ // 读取布局属性触发强制重排,使浏览器提交"移除 shake"的中间态,
331
+ // 否则 nextTick 微任务内移除又加回,动画不会重新触发
332
+ const refs = this.$refs.inputRefs
333
+ if (Array.isArray(refs)) refs.forEach(el => el && el.offsetWidth)
334
+ else if (refs) refs.offsetWidth
335
+ this.shaking = true
336
+ })
337
+ return false
338
+ }
339
+ this.shaking = false
340
+ return true
341
+ },
342
+ // 清空指定索引的错误状态(用户开始输入时调用)
343
+ clearError(index) {
344
+ const i = this.errorIndex.indexOf(index)
345
+ if (i > -1) this.errorIndex.splice(i, 1)
346
+ },
347
+ // 按项引用清空错误状态
348
+ clearErrorByItem(item) {
349
+ const index = this.inputArr.indexOf(item)
350
+ if (index > -1) this.clearError(index)
351
+ },
352
+ // 封面上传:动态创建input选择图片
271
353
  uploadImg(index){
272
- let that = this
273
354
  const inputUpload = document.createElement('input')
274
355
  inputUpload.type = 'file'
275
356
  inputUpload.multiple = false
@@ -279,6 +360,7 @@ export default {
279
360
  that.inputArr[index].value = files[0]
280
361
  that.inputArr[index].url = URL.createObjectURL(files[0])
281
362
  console.log('inputArr',that.inputArr[index])
363
+ that.clearError(index)
282
364
  that.$forceUpdate()
283
365
  })
284
366
  inputUpload.click()
@@ -317,6 +399,7 @@ export default {
317
399
  let file = event.target.files[0]
318
400
  this.fileName = file.name
319
401
  this.inputArr[index].value = file
402
+ this.clearError(index)
320
403
  },
321
404
  isNumberKey(evt, index) {
322
405
  const charCode = (evt.which) ? evt.which : evt.keyCode;
@@ -344,6 +427,7 @@ export default {
344
427
  },
345
428
  radioTao2(v,n) {
346
429
  v.value = n.value
430
+ this.clearErrorByItem(v)
347
431
  },
348
432
  radioTao(index) {
349
433
  this.radioArr.forEach(item => {
@@ -357,6 +441,7 @@ export default {
357
441
  confirmYear(e, data) {
358
442
  data.value = e
359
443
  this.tipYear = false
444
+ this.clearErrorByItem(data)
360
445
  },
361
446
  cancelYear() {
362
447
  this.tipYear = false
@@ -369,6 +454,7 @@ export default {
369
454
  console.log('e',data)
370
455
  data.value = e.start
371
456
  this.tip = false
457
+ this.clearErrorByItem(data)
372
458
  },
373
459
  emptyCal(e,data) {
374
460
  data.value = ''
@@ -436,6 +522,33 @@ export default {
436
522
  max-height: 200px !important;
437
523
  overflow-y: scroll !important;
438
524
  }
525
+ /* 必填项未填写:输入框/选择框变红 */
526
+ .tip-input.is-error .input-name > input,
527
+ .tip-input.is-error .cal > input,
528
+ .tip-input.is-error .number-input > input,
529
+ .tip-input.is-error :deep(.select-css),
530
+ .tip-input.is-error .file-cont > em,
531
+ .tip-input.is-error .cover-img {
532
+ border-color: #f56c6c !important;
533
+ }
534
+ .tip-input.is-error .input-radio {
535
+ outline: 1px solid #f56c6c;
536
+ outline-offset: 2px;
537
+ border-radius: 3px;
538
+ }
539
+ /* 必填项校验失败时的轻微抖动动画 */
540
+ @keyframes tipShake {
541
+ 0%, 100% { transform: translateX(0); }
542
+ 15% { transform: translateX(-6px); }
543
+ 30% { transform: translateX(6px); }
544
+ 45% { transform: translateX(-5px); }
545
+ 60% { transform: translateX(5px); }
546
+ 75% { transform: translateX(-3px); }
547
+ 90% { transform: translateX(3px); }
548
+ }
549
+ .tip-input.shake {
550
+ animation: tipShake 0.4s ease;
551
+ }
439
552
  </style>
440
553
 
441
554
  <style scoped src="./zydxStyle.css">
@@ -28,7 +28,7 @@
28
28
  cursor: pointer;
29
29
  }
30
30
  .tip-box {
31
- width: 600px;
31
+ width: 610px;
32
32
  padding: 60px 100px 40px 100px;
33
33
  background-color: white;
34
34
  border-radius: 5px;
@@ -42,6 +42,8 @@
42
42
 
43
43
  .tip-boxContent {
44
44
  max-height: 600px;
45
+ padding-left: 10px;
46
+ box-sizing: border-box;
45
47
  }
46
48
 
47
49
  .tip-boxContent:hover {
package/src/index.js CHANGED
@@ -87,7 +87,7 @@ function install(app) {
87
87
  }
88
88
 
89
89
  export default {
90
- version: '1.35.638',
90
+ version: '1.35.640',
91
91
  install,
92
92
  Calendar,
93
93
  Message,