@truenewx/tnxvue3 3.0.4 → 3.0.6

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.
@@ -1,158 +1,163 @@
1
- <template>
2
- <BModal
3
- dialog-class="tnxbsv-dialog"
4
- header-class="tnxbsv-dialog-header"
5
- title-class="tnxbsv-dialog-title"
6
- body-class="tnxbsv-dialog-body"
7
- :footer-class="buttons?.length ? 'tnxbsv-dialog-footer' : 'd-none'"
8
- :data-v-id="id"
9
- v-model="visible"
10
- scrollable
11
- no-close-on-backdrop
12
- no-close-on-esc
13
- :teleport-to="container"
14
- @hidden="onHidden"
15
- >
16
- <template #title>
17
- <div v-html="title" v-if="title"></div>
18
- </template>
19
- <div v-html="content" v-if="typeof content === 'string'"></div>
20
- <component :is="content" v-bind="contentProps" v-else></component>
21
- <template #footer>
22
- <TnxbsvButton v-for="(button, index) in buttons" :key="index"
23
- :variant="button.type || 'outline-secondary'"
24
- :loading="buttonLoadings[index]"
25
- @click="btnClick(index)"
26
- >
27
- {{ button.caption || button.text }}
28
- </TnxbsvButton>
29
- </template>
30
- </BModal>
31
- </template>
32
-
33
- <script>
34
- import {BModal} from 'bootstrap-vue-next';
35
- import TnxbsvButton from '../button/Button.vue'
36
-
37
- export default {
38
- name: 'TnxbsvDialog',
39
- components: {BModal, TnxbsvButton,},
40
- props: {
41
- id: {
42
- type: [String, Number],
43
- default: () => window.tnx.util.string.uuid32(),
44
- },
45
- modelValue: {
46
- type: Boolean,
47
- default: false,
48
- },
49
- container: String,
50
- title: String,
51
- content: [String, Object],
52
- contentProps: {
53
- type: Object,
54
- default: () => ({}),
55
- },
56
- buttons: Array,
57
- theme: String,
58
- width: {
59
- type: [Number, String],
60
- default: 512,
61
- },
62
- },
63
- emits: ['update:modelValue', 'close'],
64
- data() {
65
- return {
66
- visible: this.modelValue,
67
- buttonLoadings: [],
68
- heightChangeObserver: null,
69
- };
70
- },
71
- watch: {
72
- modelValue(newValue, oldValue) {
73
- this.visible = this.modelValue;
74
- if (newValue && !oldValue) { // 从隐藏到显示
75
- this.$nextTick(() => {
76
- this.locate(true);
77
- });
78
- }
79
- },
80
- visible() {
81
- this.$emit('update:modelValue', this.visible);
82
- },
83
- },
84
- mounted() {
85
- window.tnx.app.eventBus.once('tnx.error', options => {
86
- this.buttonLoadings = [];
87
- });
88
- this.$nextTick(() => {
89
- if (this.visible) {
90
- this.locate(true);
91
- }
92
- });
93
- },
94
- beforeUnmount() {
95
- window.tnx.app.eventBus.off('tnx.error');
96
- },
97
- methods: {
98
- locate(observe) {
99
- const $ = window.tnx.libs.$;
100
- let $dialog = $(`${this.container} .tnxbsv-dialog`);
101
- if ($dialog.length) {
102
- let top = window.tnx.util.dom.getTopVerticallyCenteredOnPage($dialog[0]);
103
- let width = typeof this.width === 'number' ? this.width + 'px' : this.width;
104
- $dialog.css({
105
- 'margin-top': top + 'px',
106
- 'width': width,
107
- });
108
-
109
- if (observe) {
110
- this.heightChangeObserver = window.tnx.util.dom.observeHeightChange($dialog[0], this.locate);
111
- }
112
- }
113
- },
114
- btnClick(index) {
115
- const button = this.buttons[index];
116
- if (button && typeof button.click === 'function') {
117
- let result = button.click.call(this.$refs.content, this.close);
118
- if (result === 'loading') {
119
- this.buttonLoadings[index] = true;
120
- return;
121
- }
122
- if (result === false) {
123
- return;
124
- }
125
- }
126
- this.close();
127
- },
128
- open() {
129
- this.visible = true;
130
- },
131
- close() {
132
- this.visible = false;
133
- },
134
- onHidden() {
135
- this.$emit('close');
136
- },
137
- }
138
- }
139
- </script>
140
-
141
- <style>
142
- .tnxbsv-dialog {
143
- height: fit-content;
144
- }
145
-
146
- .tnxbsv-dialog .modal-header {
147
- padding: 0.75rem 1rem;
148
- }
149
-
150
- .tnxbsv-dialog .modal-header .tnxbsv-dialog-title {
151
- font-size: 1.1rem;
152
- }
153
-
154
- .tnxbsv-dialog .modal-header .btn-close {
155
- --bs-btn-close-opacity: 0.25;
156
- --bs-btn-close-hover-opacity: 0.5;
157
- }
158
- </style>
1
+ <template>
2
+ <BModal
3
+ dialog-class="tnxbsv-dialog"
4
+ header-class="tnxbsv-dialog-header"
5
+ title-class="tnxbsv-dialog-title"
6
+ body-class="tnxbsv-dialog-body"
7
+ :footer-class="buttons?.length ? 'tnxbsv-dialog-footer' : 'd-none'"
8
+ :data-v-id="id"
9
+ v-model="visible"
10
+ scrollable
11
+ no-close-on-backdrop
12
+ no-close-on-esc
13
+ :teleport-to="container"
14
+ @hidden="onHidden"
15
+ >
16
+ <template #title>
17
+ <div v-html="title" v-if="title"></div>
18
+ </template>
19
+ <div ref="content" v-html="content" v-if="typeof content === 'string'"></div>
20
+ <component ref="content" :is="content" v-bind="contentProps" v-else></component>
21
+ <template #footer>
22
+ <TnxbsvButton v-for="(button, index) in buttons" :key="index"
23
+ :type="button.type"
24
+ :loading="buttonLoadings[index]"
25
+ @click="btnClick(index)"
26
+ >
27
+ {{ button.caption || button.text }}
28
+ </TnxbsvButton>
29
+ </template>
30
+ </BModal>
31
+ </template>
32
+
33
+ <script>
34
+ import {BModal} from 'bootstrap-vue-next';
35
+ import TnxbsvButton from '../button/Button.vue'
36
+
37
+ export default {
38
+ name: 'TnxbsvDialog',
39
+ components: {BModal, TnxbsvButton,},
40
+ props: {
41
+ id: {
42
+ type: [String, Number],
43
+ default: () => window.tnx.util.string.uuid32(),
44
+ },
45
+ modelValue: {
46
+ type: Boolean,
47
+ default: false,
48
+ },
49
+ container: String,
50
+ title: String,
51
+ content: [String, Object],
52
+ contentProps: {
53
+ type: Object,
54
+ default: () => ({}),
55
+ },
56
+ buttons: Array,
57
+ theme: String,
58
+ width: {
59
+ type: [Number, String],
60
+ default: 512,
61
+ },
62
+ },
63
+ emits: ['update:modelValue', 'close'],
64
+ data() {
65
+ return {
66
+ visible: this.modelValue,
67
+ buttonLoadings: [],
68
+ heightChangeObserver: null,
69
+ };
70
+ },
71
+ watch: {
72
+ modelValue(newValue, oldValue) {
73
+ this.visible = this.modelValue;
74
+ if (newValue && !oldValue) { // 从隐藏到显示
75
+ this.$nextTick(() => {
76
+ this.locate(true);
77
+ });
78
+ }
79
+ },
80
+ visible() {
81
+ this.$emit('update:modelValue', this.visible);
82
+ },
83
+ },
84
+ mounted() {
85
+ window.tnx.app.eventBus.once('tnx.error', options => {
86
+ this.buttonLoadings = [];
87
+ });
88
+ this.$nextTick(() => {
89
+ if (this.visible) {
90
+ this.locate(true);
91
+ }
92
+ });
93
+ },
94
+ beforeUnmount() {
95
+ window.tnx.app.eventBus.off('tnx.error');
96
+ },
97
+ methods: {
98
+ locate(observe) {
99
+ const $ = window.tnx.libs.$;
100
+ let $dialog = $(`${this.container} .tnxbsv-dialog`);
101
+ if ($dialog.length) {
102
+ let top = window.tnx.util.dom.getTopVerticallyCenteredOnPage($dialog[0]);
103
+ let width = typeof this.width === 'number' ? this.width + 'px' : this.width;
104
+ $dialog.css({
105
+ 'margin-top': top + 'px',
106
+ 'width': width,
107
+ });
108
+
109
+ if (observe) {
110
+ this.heightChangeObserver = window.tnx.util.dom.observeHeightChange($dialog[0], this.locate);
111
+ }
112
+ }
113
+ },
114
+ btnClick(index) {
115
+ const button = this.buttons[index];
116
+ if (button && typeof button.click === 'function') {
117
+ let result = button.click.call(this.$refs.content, this.close);
118
+ if (result === 'loading') {
119
+ this.buttonLoadings[index] = true;
120
+ return;
121
+ }
122
+ if (result === false) {
123
+ return;
124
+ }
125
+ }
126
+ this.close();
127
+ },
128
+ open() {
129
+ this.visible = true;
130
+ },
131
+ close() {
132
+ this.visible = false;
133
+ },
134
+ onHidden() {
135
+ this.$emit('close');
136
+ },
137
+ }
138
+ }
139
+ </script>
140
+
141
+ <style>
142
+ .tnxbsv-dialog {
143
+ height: fit-content;
144
+ margin-bottom: 0;
145
+ }
146
+
147
+ .tnxbsv-dialog.modal-dialog-scrollable .modal-content {
148
+ max-height: calc(100vh - 1rem);
149
+ }
150
+
151
+ .tnxbsv-dialog .modal-header {
152
+ padding: 0.75rem 1rem;
153
+ }
154
+
155
+ .tnxbsv-dialog .modal-header .tnxbsv-dialog-title {
156
+ font-size: 1.1rem;
157
+ }
158
+
159
+ .tnxbsv-dialog .modal-header .btn-close {
160
+ --bs-btn-close-opacity: 0.25;
161
+ --bs-btn-close-hover-opacity: 0.5;
162
+ }
163
+ </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <BForm :id="id" class="tnxbsv-form">
2
+ <BForm :id="id" class="tnxbsv-form" :class="{initializing: initializing}">
3
3
  <slot></slot>
4
4
  </BForm>
5
5
  </template>
@@ -24,9 +24,21 @@ export default {
24
24
  type: Object, // key: 字段名,value: 验证规则数组
25
25
  default: () => ({}),
26
26
  },
27
+ disabled: Boolean,
27
28
  },
28
29
  data() {
29
- return {};
30
+ return {
31
+ initializing: true,
32
+ fieldEventListeners: {},
33
+ };
34
+ },
35
+ watch: {
36
+ rules() {
37
+ this.initRules();
38
+ },
39
+ disabled() {
40
+ this.updateElementsDisabled();
41
+ },
30
42
  },
31
43
  mounted() {
32
44
  window.addEventListener('resize', this.updateLabelWidth);
@@ -34,21 +46,22 @@ export default {
34
46
  setTimeout(() => {
35
47
  this.initRules();
36
48
  this.updateLabelWidth();
49
+ this.updateElementsDisabled();
50
+ this.initializing = false;
37
51
  });
38
52
  });
39
53
  },
40
54
  beforeUnmount() {
41
55
  window.removeEventListener('resize', this.updateLabelWidth);
56
+ this.clearEventListeners();
42
57
  },
43
58
  methods: {
44
59
  getFieldGroupElements() {
45
60
  let elements = {};
46
- let groups = this.$el.querySelectorAll(`#${this.id} .b-form-group[prop]`);
47
- groups.forEach(label => {
48
- let fieldName = label.getAttribute('prop');
49
- if (fieldName) {
50
- elements[fieldName] = label;
51
- }
61
+ let groups = this.$el.querySelectorAll('.b-form-group[prop]');
62
+ groups.forEach(group => {
63
+ let fieldName = group.getAttribute('prop');
64
+ elements[fieldName] = group;
52
65
  });
53
66
  return elements;
54
67
  },
@@ -59,7 +72,34 @@ export default {
59
72
  }
60
73
  return fieldRules;
61
74
  },
75
+ addFieldEventListener(fieldName, element, eventType, handler) {
76
+ if (!this.fieldEventListeners[fieldName]) {
77
+ this.fieldEventListeners[fieldName] = {};
78
+ }
79
+ if (!this.fieldEventListeners[fieldName][eventType]) {
80
+ this.fieldEventListeners[fieldName][eventType] = [];
81
+ }
82
+ element.addEventListener(eventType, handler);
83
+ this.fieldEventListeners[fieldName][eventType].push(handler);
84
+ },
85
+ clearEventListeners() {
86
+ for (let fieldName in this.fieldEventListeners) {
87
+ let listeners = this.fieldEventListeners[fieldName];
88
+ for (let eventType in listeners) {
89
+ let handlers = listeners[eventType];
90
+ let fieldElement = this.getFieldElement(fieldName);
91
+ if (fieldElement) {
92
+ handlers.forEach(handler => {
93
+ fieldElement.removeEventListener(eventType, handler);
94
+ });
95
+ }
96
+ }
97
+ }
98
+ this.fieldEventListeners = {}; // 清空映射表
99
+ },
62
100
  initRules() {
101
+ this.clearEventListeners();
102
+
63
103
  let fieldGroupElements = this.getFieldGroupElements();
64
104
  for (let fieldName in fieldGroupElements) {
65
105
  let fieldGroupElement = fieldGroupElements[fieldName];
@@ -77,12 +117,12 @@ export default {
77
117
  let fieldElement = fieldGroupElement.querySelector('input, select, textarea');
78
118
  if (fieldElement) {
79
119
  if (existsChangeValidator) {
80
- fieldElement.addEventListener('change', () => {
120
+ this.addFieldEventListener(fieldName, fieldElement, 'change', () => {
81
121
  this.validateField(fieldName, 'change');
82
122
  });
83
123
  }
84
124
  if (existsBlurValidator) {
85
- fieldElement.addEventListener('blur', () => {
125
+ this.addFieldEventListener(fieldName, fieldElement, 'blur', () => {
86
126
  this.validateField(fieldName, 'blur');
87
127
  });
88
128
  }
@@ -93,16 +133,13 @@ export default {
93
133
  },
94
134
  updateLabelWidth() {
95
135
  let maxWidth = 0;
96
- let fieldGroupElements = this.getFieldGroupElements();
97
- for (let fieldName in fieldGroupElements) {
98
- let fieldGroupElement = fieldGroupElements[fieldName];
99
- if (fieldGroupElement) {
100
- let label = fieldGroupElement.querySelector('.form-label');
101
- if (label) {
102
- let width = label.offsetWidth;
103
- if (width > maxWidth) {
104
- maxWidth = width;
105
- }
136
+ let groupElements = this.$el.querySelectorAll('.b-form-group');
137
+ for (let groupElement of groupElements) {
138
+ let label = groupElement.querySelector('.form-label');
139
+ if (label) {
140
+ let width = label.offsetWidth;
141
+ if (width > maxWidth) {
142
+ maxWidth = width;
106
143
  }
107
144
  }
108
145
  }
@@ -112,98 +149,123 @@ export default {
112
149
  },
113
150
  validate() {
114
151
  this.clearValidate(); // 清除之前的反馈信息
115
- this.$el.classList.remove('was-validated'); // 移除已验证样式
116
152
 
117
- let valid = true;
118
- let fieldGroupElements = this.getFieldGroupElements();
119
- for (let fieldName in fieldGroupElements) {
120
- if (!this.validateField(fieldName)) {
121
- valid = false;
153
+ return new Promise((resolve, reject) => {
154
+ let errors = {};
155
+ let fieldGroupElements = this.getFieldGroupElements();
156
+ for (let fieldName in fieldGroupElements) {
157
+ let fieldErrorMessages = this.validateField(fieldName);
158
+ if (fieldErrorMessages.length) {
159
+ errors[fieldName] = fieldErrorMessages;
160
+ }
122
161
  }
123
- }
124
-
125
- if (!valid) {
126
- this.$el.classList.add('was-validated'); // 添加已验证样式
127
- }
128
-
129
- return valid;
162
+ if (Object.keys(errors).length) {
163
+ reject(errors);
164
+ }
165
+ resolve();
166
+ });
167
+ },
168
+ getFieldGroupElement(fieldName) {
169
+ return this.$el.querySelector(`.b-form-group[prop="${fieldName}"]`);
170
+ },
171
+ queryFieldElement(fieldGroupElement) {
172
+ return fieldGroupElement ? fieldGroupElement.querySelector('input, textarea, select') : null;
130
173
  },
131
174
  getFieldElement(fieldName) {
132
- let groupElement = this.$el.querySelector(`#${this.id} .b-form-group[prop="${fieldName}"]`);
133
- if (groupElement) {
134
- return groupElement.querySelector('input, textarea, select');
135
- }
136
- return undefined;
175
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
176
+ return this.queryFieldElement(fieldGroupElement);
137
177
  },
138
178
  validateField(fieldName, trigger) {
139
179
  const fieldValue = this.model[fieldName];
140
180
  const fieldRules = this.getFieldRules(fieldName);
141
- const fieldErrors = [];
181
+ const fieldErrorMessages = [];
142
182
 
143
183
  fieldRules.forEach(fieldRule => {
144
184
  if (typeof fieldRule.validator === 'function' && (!trigger || trigger === fieldRule.trigger)) {
145
185
  fieldRule.validator(fieldRule, fieldValue, (error) => {
146
186
  if (error) {
147
- fieldErrors.push(error.message);
187
+ fieldErrorMessages.push(error.message);
148
188
  }
149
189
  });
150
190
  }
151
191
  // 没有validator()方法,则不进行校验
152
192
  });
153
193
 
154
- if (fieldErrors.length > 0) {
155
- this.setFieldInvalidFeedback(fieldName, fieldErrors);
156
- return false;
194
+ if (fieldErrorMessages.length) {
195
+ this.setFieldInvalidFeedback(fieldName, fieldErrorMessages);
157
196
  } else {
158
197
  // 清除错误状态
159
198
  this.removeFieldInvalidFeedback(fieldName);
160
- return true;
161
199
  }
200
+ return fieldErrorMessages;
162
201
  },
163
202
  setFieldInvalidFeedback(fieldName, messages) {
164
- let fieldElement = this.getFieldElement(fieldName);
165
- if (fieldElement) {
166
- // 查找已有的 .invalid-feedback 元素
167
- let feedbackDiv = fieldElement.nextElementSibling;
168
- // 如果下一个兄弟元素不是 .invalid-feedback,则创建一个新的
169
- if (!feedbackDiv || !feedbackDiv.classList.contains('invalid-feedback')) {
203
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
204
+ if (fieldGroupElement) {
205
+ let feedbackDiv = fieldGroupElement.querySelector('.invalid-feedback');
206
+ if (!feedbackDiv) {
170
207
  feedbackDiv = document.createElement('div');
171
208
  feedbackDiv.className = 'invalid-feedback';
172
- fieldElement.parentNode.insertBefore(feedbackDiv, fieldElement.nextSibling);
209
+ let contentWrapper = fieldGroupElement.querySelector('.tnxbsv-form-group__content-wrapper');
210
+ // 有分组元素的情况下,一定有contentWrapper
211
+ contentWrapper.appendChild(feedbackDiv);
173
212
  }
174
213
  // 更新 .invalid-feedback 的内容
175
214
  feedbackDiv.textContent = messages.join('; ');
176
215
  // 设置输入框为无效状态
177
- fieldElement.classList.add('is-invalid');
216
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
217
+ if (fieldElement) {
218
+ fieldElement.classList.add('is-invalid');
219
+ }
178
220
  }
179
221
  },
180
222
  removeFieldInvalidFeedback(fieldName) {
181
- let fieldElement = this.getFieldElement(fieldName);
182
- if (fieldElement) {
183
- // 查找已有的 .invalid-feedback 元素
184
- let feedbackDiv = fieldElement.nextElementSibling;
185
- // 如果存在 .invalid-feedback 元素,则移除它
186
- if (feedbackDiv && feedbackDiv.classList.contains('invalid-feedback')) {
223
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
224
+ if (fieldGroupElement) {
225
+ let feedbackDiv = fieldGroupElement.querySelector('.invalid-feedback');
226
+ if (feedbackDiv) {
187
227
  feedbackDiv.remove();
188
228
  }
189
229
  // 移除无效状态
190
- fieldElement.classList.remove('is-invalid');
230
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
231
+ if (fieldElement) {
232
+ fieldElement.classList.remove('is-invalid');
233
+ }
191
234
  }
192
235
  },
193
236
  clearValidate() {
194
- this.$el.classList.remove('was-validated'); // 移除已验证样式
195
-
196
237
  const invalidElements = this.$el.querySelectorAll('.is-invalid');
197
238
  invalidElements.forEach(el => el.classList.remove('is-invalid'));
198
239
 
199
240
  const invalidFeedbackElements = this.$el.querySelectorAll('.invalid-feedback');
200
241
  invalidFeedbackElements.forEach(el => el.remove());
201
242
  },
243
+ scrollToField(fieldName) {
244
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
245
+ if (fieldGroupElement) {
246
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
247
+ if (fieldElement) {
248
+ fieldElement.scrollIntoView({behavior: 'smooth', block: 'center'});
249
+ } else {
250
+ fieldGroupElement.scrollIntoView({behavior: 'smooth', block: 'center'});
251
+ }
252
+ }
253
+ },
254
+ updateElementsDisabled() {
255
+ let elements = this.$el.querySelectorAll('input, textarea, select, button');
256
+ elements.forEach(element => {
257
+ element.disabled = this.disabled;
258
+ });
259
+ },
202
260
  }
203
261
  }
204
262
  </script>
205
263
 
206
264
  <style>
265
+ .tnxbsv-form.initializing .form-label {
266
+ visibility: hidden;
267
+ }
268
+
207
269
  .tnxbsv-form .b-form-group {
208
270
  display: flex;
209
271
  }
@@ -227,6 +289,7 @@ export default {
227
289
  }
228
290
 
229
291
  .tnxbsv-form .b-form-group .invalid-feedback {
292
+ display: block;
230
293
  }
231
294
 
232
295
  @media (max-width: var(--bs-breakpoint-md)) {
@@ -38,5 +38,9 @@ export default {
38
38
 
39
39
  .tnxbsv-form-group__content-wrapper {
40
40
  flex-grow: 1;
41
+ min-height: 2.375rem;
42
+ display: flex;
43
+ flex-direction: column;
44
+ justify-content: center;
41
45
  }
42
46
  </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <span class="spinner-border" :class="extraClass" role="status">
2
+ <span class="tnxbsv-loading-icon spinner-border" :class="extraClass" role="status">
3
3
  <span class="visually-hidden">{{ text }}</span>
4
4
  </span>
5
5
  </template>
@@ -7,7 +7,7 @@
7
7
  <script>
8
8
 
9
9
  export default {
10
- name: 'TnxbsvLoading',
10
+ name: 'TnxbsvLoadingIcon',
11
11
  props: {
12
12
  small: {
13
13
  type: Boolean,