@truenewx/tnxvue3 3.0.5 → 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 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
- }
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>
@@ -28,6 +28,7 @@ export default {
28
28
  },
29
29
  data() {
30
30
  return {
31
+ initializing: true,
31
32
  fieldEventListeners: {},
32
33
  };
33
34
  },
@@ -46,6 +47,7 @@ export default {
46
47
  this.initRules();
47
48
  this.updateLabelWidth();
48
49
  this.updateElementsDisabled();
50
+ this.initializing = false;
49
51
  });
50
52
  });
51
53
  },
@@ -57,11 +59,9 @@ export default {
57
59
  getFieldGroupElements() {
58
60
  let elements = {};
59
61
  let groups = this.$el.querySelectorAll('.b-form-group[prop]');
60
- groups.forEach(label => {
61
- let fieldName = label.getAttribute('prop');
62
- if (fieldName) {
63
- elements[fieldName] = label;
64
- }
62
+ groups.forEach(group => {
63
+ let fieldName = group.getAttribute('prop');
64
+ elements[fieldName] = group;
65
65
  });
66
66
  return elements;
67
67
  },
@@ -133,16 +133,13 @@ export default {
133
133
  },
134
134
  updateLabelWidth() {
135
135
  let maxWidth = 0;
136
- let fieldGroupElements = this.getFieldGroupElements();
137
- for (let fieldName in fieldGroupElements) {
138
- let fieldGroupElement = fieldGroupElements[fieldName];
139
- if (fieldGroupElement) {
140
- let label = fieldGroupElement.querySelector('.form-label');
141
- if (label) {
142
- let width = label.offsetWidth;
143
- if (width > maxWidth) {
144
- maxWidth = width;
145
- }
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;
146
143
  }
147
144
  }
148
145
  }
@@ -152,7 +149,6 @@ export default {
152
149
  },
153
150
  validate() {
154
151
  this.clearValidate(); // 清除之前的反馈信息
155
- this.$el.classList.remove('was-validated'); // 移除已验证样式
156
152
 
157
153
  return new Promise((resolve, reject) => {
158
154
  let errors = {};
@@ -164,18 +160,20 @@ export default {
164
160
  }
165
161
  }
166
162
  if (Object.keys(errors).length) {
167
- this.$el.classList.add('was-validated'); // 添加已验证样式
168
163
  reject(errors);
169
164
  }
170
165
  resolve();
171
166
  });
172
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;
173
+ },
173
174
  getFieldElement(fieldName) {
174
- let groupElement = this.$el.querySelector(`.b-form-group[prop="${fieldName}"]`);
175
- if (groupElement) {
176
- return groupElement.querySelector('input, textarea, select');
177
- }
178
- return undefined;
175
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
176
+ return this.queryFieldElement(fieldGroupElement);
179
177
  },
180
178
  validateField(fieldName, trigger) {
181
179
  const fieldValue = this.model[fieldName];
@@ -202,38 +200,40 @@ export default {
202
200
  return fieldErrorMessages;
203
201
  },
204
202
  setFieldInvalidFeedback(fieldName, messages) {
205
- let fieldElement = this.getFieldElement(fieldName);
206
- if (fieldElement) {
207
- // 查找已有的 .invalid-feedback 元素
208
- let feedbackDiv = fieldElement.nextElementSibling;
209
- // 如果下一个兄弟元素不是 .invalid-feedback,则创建一个新的
210
- 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) {
211
207
  feedbackDiv = document.createElement('div');
212
208
  feedbackDiv.className = 'invalid-feedback';
213
- fieldElement.parentNode.insertBefore(feedbackDiv, fieldElement.nextSibling);
209
+ let contentWrapper = fieldGroupElement.querySelector('.tnxbsv-form-group__content-wrapper');
210
+ // 有分组元素的情况下,一定有contentWrapper
211
+ contentWrapper.appendChild(feedbackDiv);
214
212
  }
215
213
  // 更新 .invalid-feedback 的内容
216
214
  feedbackDiv.textContent = messages.join('; ');
217
215
  // 设置输入框为无效状态
218
- fieldElement.classList.add('is-invalid');
216
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
217
+ if (fieldElement) {
218
+ fieldElement.classList.add('is-invalid');
219
+ }
219
220
  }
220
221
  },
221
222
  removeFieldInvalidFeedback(fieldName) {
222
- let fieldElement = this.getFieldElement(fieldName);
223
- if (fieldElement) {
224
- // 查找已有的 .invalid-feedback 元素
225
- let feedbackDiv = fieldElement.nextElementSibling;
226
- // 如果存在 .invalid-feedback 元素,则移除它
227
- 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) {
228
227
  feedbackDiv.remove();
229
228
  }
230
229
  // 移除无效状态
231
- fieldElement.classList.remove('is-invalid');
230
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
231
+ if (fieldElement) {
232
+ fieldElement.classList.remove('is-invalid');
233
+ }
232
234
  }
233
235
  },
234
236
  clearValidate() {
235
- this.$el.classList.remove('was-validated'); // 移除已验证样式
236
-
237
237
  const invalidElements = this.$el.querySelectorAll('.is-invalid');
238
238
  invalidElements.forEach(el => el.classList.remove('is-invalid'));
239
239
 
@@ -241,9 +241,14 @@ export default {
241
241
  invalidFeedbackElements.forEach(el => el.remove());
242
242
  },
243
243
  scrollToField(fieldName) {
244
- let fieldElement = this.getFieldElement(fieldName);
245
- if (fieldElement) {
246
- fieldElement.scrollIntoView({behavior: 'smooth', block: 'center'});
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
+ }
247
252
  }
248
253
  },
249
254
  updateElementsDisabled() {
@@ -257,6 +262,10 @@ export default {
257
262
  </script>
258
263
 
259
264
  <style>
265
+ .tnxbsv-form.initializing .form-label {
266
+ visibility: hidden;
267
+ }
268
+
260
269
  .tnxbsv-form .b-form-group {
261
270
  display: flex;
262
271
  }
@@ -280,6 +289,7 @@ export default {
280
289
  }
281
290
 
282
291
  .tnxbsv-form .b-form-group .invalid-feedback {
292
+ display: block;
283
293
  }
284
294
 
285
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,
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div class="loading-overlay">
3
+ <LoadingIcon theme="primary" :small="false" :text="message"/>
4
+ <span class="loading-message text-primary" v-if="message">{{ message }}</span>
5
+ </div>
6
+ </template>
7
+
8
+ <script>
9
+ import LoadingIcon from "../loading-icon/LoadingIcon.vue";
10
+
11
+ export default {
12
+ name: 'TnxbsvLoadingMask',
13
+ components: {LoadingIcon},
14
+ props: {
15
+ message: {
16
+ type: String,
17
+ default: '',
18
+ },
19
+ },
20
+ data() {
21
+ return {};
22
+ },
23
+ mounted() {
24
+ document.body.classList.add('no-scroll')
25
+ },
26
+ beforeUnmount() {
27
+ document.body.classList.remove('no-scroll');
28
+ },
29
+ methods: {}
30
+ }
31
+ </script>
32
+
33
+ <style>
34
+ .no-scroll {
35
+ overflow: hidden !important;
36
+ }
37
+
38
+ .loading-overlay {
39
+ position: fixed;
40
+ top: 0;
41
+ left: 0;
42
+ width: 100%;
43
+ height: 100%;
44
+ background-color: rgba(var(--bs-white-rgb), 0.8);
45
+ display: flex;
46
+ justify-content: center;
47
+ align-items: center;
48
+ flex-direction: column;
49
+ z-index: 2147483647; /* 浏览器支持的最大值 */
50
+ }
51
+
52
+ .loading-overlay .spinner-border {
53
+ opacity: 0.7;
54
+ }
55
+
56
+ .loading-overlay .loading-message {
57
+ margin-top: 0.25rem;
58
+ opacity: 0.7;
59
+ }
60
+ </style>