@truenewx/tnxvue3 3.0.5 → 3.0.7

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,164 @@
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
+ max-width: 100%;
146
+ }
147
+
148
+ .tnxbsv-dialog.modal-dialog-scrollable .modal-content {
149
+ max-height: calc(100vh - 1rem);
150
+ }
151
+
152
+ .tnxbsv-dialog .modal-header {
153
+ padding: 0.75rem 1rem;
154
+ }
155
+
156
+ .tnxbsv-dialog .modal-header .tnxbsv-dialog-title {
157
+ font-size: 1.1rem;
158
+ }
159
+
160
+ .tnxbsv-dialog .modal-header .btn-close {
161
+ --bs-btn-close-opacity: 0.25;
162
+ --bs-btn-close-hover-opacity: 0.5;
163
+ }
164
+ </style>
@@ -7,7 +7,6 @@
7
7
  :default-value="defaultValue"
8
8
  :empty="empty"
9
9
  :empty-value="emptyValue"
10
- :placeholder="placeholder"
11
10
  :disabled="disabled"
12
11
  :filterable="filterable"
13
12
  :theme="theme"
@@ -39,9 +38,8 @@ export default {
39
38
  },
40
39
  emptyValue: {
41
40
  type: [String, Number, Boolean, Array],
42
- default: '', // 设为null会告警,暂时先设为空字符,在非字符串类型的情况下是否有问题,有待检验
41
+ default: null,
43
42
  },
44
- placeholder: String,
45
43
  disabled: Boolean,
46
44
  tagClick: Function,
47
45
  change: Function,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <BForm :id="id" class="tnxbsv-form">
2
+ <BForm :id="id" class="tnxbsv-form" :class="{initializing: initializing, 'form-inline': inline}">
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
  },
@@ -132,17 +132,17 @@ export default {
132
132
  }
133
133
  },
134
134
  updateLabelWidth() {
135
+ if (this.inline) {
136
+ return;
137
+ }
135
138
  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
- }
139
+ let groupElements = this.$el.querySelectorAll('.b-form-group');
140
+ for (let groupElement of groupElements) {
141
+ let label = groupElement.querySelector('.form-label');
142
+ if (label) {
143
+ let width = label.offsetWidth;
144
+ if (width > maxWidth) {
145
+ maxWidth = width;
146
146
  }
147
147
  }
148
148
  }
@@ -152,7 +152,6 @@ export default {
152
152
  },
153
153
  validate() {
154
154
  this.clearValidate(); // 清除之前的反馈信息
155
- this.$el.classList.remove('was-validated'); // 移除已验证样式
156
155
 
157
156
  return new Promise((resolve, reject) => {
158
157
  let errors = {};
@@ -164,18 +163,20 @@ export default {
164
163
  }
165
164
  }
166
165
  if (Object.keys(errors).length) {
167
- this.$el.classList.add('was-validated'); // 添加已验证样式
168
166
  reject(errors);
169
167
  }
170
168
  resolve();
171
169
  });
172
170
  },
171
+ getFieldGroupElement(fieldName) {
172
+ return this.$el.querySelector(`.b-form-group[prop="${fieldName}"]`);
173
+ },
174
+ queryFieldElement(fieldGroupElement) {
175
+ return fieldGroupElement ? fieldGroupElement.querySelector('input, textarea, select') : null;
176
+ },
173
177
  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;
178
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
179
+ return this.queryFieldElement(fieldGroupElement);
179
180
  },
180
181
  validateField(fieldName, trigger) {
181
182
  const fieldValue = this.model[fieldName];
@@ -202,38 +203,40 @@ export default {
202
203
  return fieldErrorMessages;
203
204
  },
204
205
  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')) {
206
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
207
+ if (fieldGroupElement) {
208
+ let feedbackDiv = fieldGroupElement.querySelector('.invalid-feedback');
209
+ if (!feedbackDiv) {
211
210
  feedbackDiv = document.createElement('div');
212
211
  feedbackDiv.className = 'invalid-feedback';
213
- fieldElement.parentNode.insertBefore(feedbackDiv, fieldElement.nextSibling);
212
+ let contentWrapper = fieldGroupElement.querySelector('.tnxbsv-form-group__content-wrapper');
213
+ // 有分组元素的情况下,一定有contentWrapper
214
+ contentWrapper.appendChild(feedbackDiv);
214
215
  }
215
216
  // 更新 .invalid-feedback 的内容
216
217
  feedbackDiv.textContent = messages.join('; ');
217
218
  // 设置输入框为无效状态
218
- fieldElement.classList.add('is-invalid');
219
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
220
+ if (fieldElement) {
221
+ fieldElement.classList.add('is-invalid');
222
+ }
219
223
  }
220
224
  },
221
225
  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')) {
226
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
227
+ if (fieldGroupElement) {
228
+ let feedbackDiv = fieldGroupElement.querySelector('.invalid-feedback');
229
+ if (feedbackDiv) {
228
230
  feedbackDiv.remove();
229
231
  }
230
232
  // 移除无效状态
231
- fieldElement.classList.remove('is-invalid');
233
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
234
+ if (fieldElement) {
235
+ fieldElement.classList.remove('is-invalid');
236
+ }
232
237
  }
233
238
  },
234
239
  clearValidate() {
235
- this.$el.classList.remove('was-validated'); // 移除已验证样式
236
-
237
240
  const invalidElements = this.$el.querySelectorAll('.is-invalid');
238
241
  invalidElements.forEach(el => el.classList.remove('is-invalid'));
239
242
 
@@ -241,9 +244,14 @@ export default {
241
244
  invalidFeedbackElements.forEach(el => el.remove());
242
245
  },
243
246
  scrollToField(fieldName) {
244
- let fieldElement = this.getFieldElement(fieldName);
245
- if (fieldElement) {
246
- fieldElement.scrollIntoView({behavior: 'smooth', block: 'center'});
247
+ let fieldGroupElement = this.getFieldGroupElement(fieldName);
248
+ if (fieldGroupElement) {
249
+ let fieldElement = this.queryFieldElement(fieldGroupElement);
250
+ if (fieldElement) {
251
+ fieldElement.scrollIntoView({behavior: 'smooth', block: 'center'});
252
+ } else {
253
+ fieldGroupElement.scrollIntoView({behavior: 'smooth', block: 'center'});
254
+ }
247
255
  }
248
256
  },
249
257
  updateElementsDisabled() {
@@ -257,6 +265,10 @@ export default {
257
265
  </script>
258
266
 
259
267
  <style>
268
+ .tnxbsv-form.initializing .form-label {
269
+ visibility: hidden;
270
+ }
271
+
260
272
  .tnxbsv-form .b-form-group {
261
273
  display: flex;
262
274
  }
@@ -280,6 +292,7 @@ export default {
280
292
  }
281
293
 
282
294
  .tnxbsv-form .b-form-group .invalid-feedback {
295
+ display: block;
283
296
  }
284
297
 
285
298
  @media (max-width: var(--bs-breakpoint-md)) {
@@ -2,7 +2,7 @@
2
2
  <div class="tnxbsv-form-group b-form-group" :prop="prop">
3
3
  <div class="tnxbsv-form-group__label-wrapper">
4
4
  <slot name="label">
5
- <label class="form-label">{{ label }}</label>
5
+ <label class="form-label" :style="labelStyle">{{ label }}</label>
6
6
  </slot>
7
7
  </div>
8
8
  <div class="tnxbsv-form-group__content-wrapper">
@@ -16,6 +16,7 @@ export default {
16
16
  name: 'TnxbsvFormGroup',
17
17
  props: {
18
18
  label: String,
19
+ labelWidth: [String, Number],
19
20
  prop: String,
20
21
  },
21
22
  data() {
@@ -23,6 +24,26 @@ export default {
23
24
  model: {},
24
25
  };
25
26
  },
27
+ computed: {
28
+ labelStyle() {
29
+ let style = {};
30
+ if (this.labelWidth) {
31
+ let width;
32
+ if (typeof this.labelWidth === 'string') {
33
+ // 如果是纯数字内容,则附加px单位
34
+ if (/^\d+$/.test(this.labelWidth)) {
35
+ width = this.labelWidth + 'px';
36
+ } else {
37
+ width = this.labelWidth;
38
+ }
39
+ } else {
40
+ width = this.labelWidth + 'px';
41
+ }
42
+ style.width = width;
43
+ }
44
+ return style;
45
+ },
46
+ },
26
47
  methods: {}
27
48
  }
28
49
  </script>
@@ -38,5 +59,9 @@ export default {
38
59
 
39
60
  .tnxbsv-form-group__content-wrapper {
40
61
  flex-grow: 1;
62
+ min-height: 2.375rem;
63
+ display: flex;
64
+ flex-direction: column;
65
+ justify-content: center;
41
66
  }
42
67
  </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>