@truenewx/tnxvue3 3.0.3 → 3.0.5
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 +1 -1
- package/src/bootstrap-vue/button/Button.vue +19 -5
- package/src/bootstrap-vue/dialog/Dialog.vue +158 -0
- package/src/bootstrap-vue/form/Form.vue +296 -0
- package/src/bootstrap-vue/form/FormGroup.vue +42 -0
- package/src/bootstrap-vue/loading/Loading.vue +5 -2
- package/src/bootstrap-vue/submit-form/SubmitForm.vue +176 -0
- package/src/bootstrap-vue/tnxbsv.css +5 -0
- package/src/bootstrap-vue/tnxbsv.js +32 -5
- package/src/element-plus/dialog/Dialog.vue +4 -6
- package/src/tnxvue.js +2 -3
package/package.json
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<BButton>
|
|
3
|
-
<i :class="icon" v-if="icon"></i>
|
|
4
|
-
<
|
|
2
|
+
<BButton class="tnxbsv-button" :variant="type || 'outline-secondary'" :loading="loading" :disabled="loading">
|
|
3
|
+
<i class="me-1" :class="icon" v-if="icon"></i>
|
|
4
|
+
<div>
|
|
5
|
+
<slot></slot>
|
|
6
|
+
</div>
|
|
7
|
+
<template #loading>
|
|
8
|
+
<Loading class="me-1" theme="inherit"/>
|
|
9
|
+
<div>
|
|
10
|
+
<slot></slot>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
5
13
|
</BButton>
|
|
6
14
|
</template>
|
|
7
15
|
|
|
8
16
|
<script>
|
|
9
17
|
import {BButton} from 'bootstrap-vue-next';
|
|
18
|
+
import Loading from '../loading/Loading.vue';
|
|
10
19
|
|
|
11
20
|
export default {
|
|
12
21
|
name: 'TnxbsvButton',
|
|
13
|
-
components: {BButton},
|
|
22
|
+
components: {BButton, Loading},
|
|
14
23
|
props: {
|
|
24
|
+
type: String,
|
|
15
25
|
icon: String,
|
|
26
|
+
loading: Boolean,
|
|
16
27
|
},
|
|
17
28
|
data() {
|
|
18
29
|
return {};
|
|
@@ -22,5 +33,8 @@ export default {
|
|
|
22
33
|
</script>
|
|
23
34
|
|
|
24
35
|
<style>
|
|
25
|
-
|
|
36
|
+
.tnxbsv-button {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
}
|
|
26
40
|
</style>
|
|
@@ -0,0 +1,158 @@
|
|
|
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>
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BForm :id="id" class="tnxbsv-form">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
</BForm>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import {BForm} from 'bootstrap-vue-next';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: 'TnxbsvForm',
|
|
12
|
+
components: {BForm},
|
|
13
|
+
props: {
|
|
14
|
+
id: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: () => 'tnxbsv-form-' + new Date().getTime(),
|
|
17
|
+
},
|
|
18
|
+
model: {
|
|
19
|
+
type: Object,
|
|
20
|
+
default: () => ({}),
|
|
21
|
+
},
|
|
22
|
+
inline: Boolean,
|
|
23
|
+
rules: {
|
|
24
|
+
type: Object, // key: 字段名,value: 验证规则数组
|
|
25
|
+
default: () => ({}),
|
|
26
|
+
},
|
|
27
|
+
disabled: Boolean,
|
|
28
|
+
},
|
|
29
|
+
data() {
|
|
30
|
+
return {
|
|
31
|
+
fieldEventListeners: {},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
watch: {
|
|
35
|
+
rules() {
|
|
36
|
+
this.initRules();
|
|
37
|
+
},
|
|
38
|
+
disabled() {
|
|
39
|
+
this.updateElementsDisabled();
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
mounted() {
|
|
43
|
+
window.addEventListener('resize', this.updateLabelWidth);
|
|
44
|
+
this.$nextTick(() => {
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
this.initRules();
|
|
47
|
+
this.updateLabelWidth();
|
|
48
|
+
this.updateElementsDisabled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
beforeUnmount() {
|
|
53
|
+
window.removeEventListener('resize', this.updateLabelWidth);
|
|
54
|
+
this.clearEventListeners();
|
|
55
|
+
},
|
|
56
|
+
methods: {
|
|
57
|
+
getFieldGroupElements() {
|
|
58
|
+
let elements = {};
|
|
59
|
+
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
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return elements;
|
|
67
|
+
},
|
|
68
|
+
getFieldRules(fieldName) {
|
|
69
|
+
let fieldRules = this.rules[fieldName] || [];
|
|
70
|
+
if (!Array.isArray(fieldRules)) {
|
|
71
|
+
fieldRules = [fieldRules];
|
|
72
|
+
}
|
|
73
|
+
return fieldRules;
|
|
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
|
+
},
|
|
100
|
+
initRules() {
|
|
101
|
+
this.clearEventListeners();
|
|
102
|
+
|
|
103
|
+
let fieldGroupElements = this.getFieldGroupElements();
|
|
104
|
+
for (let fieldName in fieldGroupElements) {
|
|
105
|
+
let fieldGroupElement = fieldGroupElements[fieldName];
|
|
106
|
+
if (fieldGroupElement) {
|
|
107
|
+
let fieldRules = this.getFieldRules(fieldName);
|
|
108
|
+
if (fieldRules.some(rule => rule.required)) {
|
|
109
|
+
let label = fieldGroupElement.querySelector('.form-label');
|
|
110
|
+
if (label) {
|
|
111
|
+
label.classList.add('is-required');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let existsChangeValidator = fieldRules.some(fieldRule => fieldRule.trigger === 'change');
|
|
115
|
+
let existsBlurValidator = fieldRules.some(fieldRule => fieldRule.trigger !== 'change');
|
|
116
|
+
if (existsChangeValidator || existsBlurValidator) {
|
|
117
|
+
let fieldElement = fieldGroupElement.querySelector('input, select, textarea');
|
|
118
|
+
if (fieldElement) {
|
|
119
|
+
if (existsChangeValidator) {
|
|
120
|
+
this.addFieldEventListener(fieldName, fieldElement, 'change', () => {
|
|
121
|
+
this.validateField(fieldName, 'change');
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (existsBlurValidator) {
|
|
125
|
+
this.addFieldEventListener(fieldName, fieldElement, 'blur', () => {
|
|
126
|
+
this.validateField(fieldName, 'blur');
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
updateLabelWidth() {
|
|
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
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (maxWidth) {
|
|
150
|
+
this.$el.style.setProperty('--label-width', `${maxWidth}px`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
validate() {
|
|
154
|
+
this.clearValidate(); // 清除之前的反馈信息
|
|
155
|
+
this.$el.classList.remove('was-validated'); // 移除已验证样式
|
|
156
|
+
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
let errors = {};
|
|
159
|
+
let fieldGroupElements = this.getFieldGroupElements();
|
|
160
|
+
for (let fieldName in fieldGroupElements) {
|
|
161
|
+
let fieldErrorMessages = this.validateField(fieldName);
|
|
162
|
+
if (fieldErrorMessages.length) {
|
|
163
|
+
errors[fieldName] = fieldErrorMessages;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (Object.keys(errors).length) {
|
|
167
|
+
this.$el.classList.add('was-validated'); // 添加已验证样式
|
|
168
|
+
reject(errors);
|
|
169
|
+
}
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
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;
|
|
179
|
+
},
|
|
180
|
+
validateField(fieldName, trigger) {
|
|
181
|
+
const fieldValue = this.model[fieldName];
|
|
182
|
+
const fieldRules = this.getFieldRules(fieldName);
|
|
183
|
+
const fieldErrorMessages = [];
|
|
184
|
+
|
|
185
|
+
fieldRules.forEach(fieldRule => {
|
|
186
|
+
if (typeof fieldRule.validator === 'function' && (!trigger || trigger === fieldRule.trigger)) {
|
|
187
|
+
fieldRule.validator(fieldRule, fieldValue, (error) => {
|
|
188
|
+
if (error) {
|
|
189
|
+
fieldErrorMessages.push(error.message);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// 没有validator()方法,则不进行校验
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (fieldErrorMessages.length) {
|
|
197
|
+
this.setFieldInvalidFeedback(fieldName, fieldErrorMessages);
|
|
198
|
+
} else {
|
|
199
|
+
// 清除错误状态
|
|
200
|
+
this.removeFieldInvalidFeedback(fieldName);
|
|
201
|
+
}
|
|
202
|
+
return fieldErrorMessages;
|
|
203
|
+
},
|
|
204
|
+
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')) {
|
|
211
|
+
feedbackDiv = document.createElement('div');
|
|
212
|
+
feedbackDiv.className = 'invalid-feedback';
|
|
213
|
+
fieldElement.parentNode.insertBefore(feedbackDiv, fieldElement.nextSibling);
|
|
214
|
+
}
|
|
215
|
+
// 更新 .invalid-feedback 的内容
|
|
216
|
+
feedbackDiv.textContent = messages.join('; ');
|
|
217
|
+
// 设置输入框为无效状态
|
|
218
|
+
fieldElement.classList.add('is-invalid');
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
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')) {
|
|
228
|
+
feedbackDiv.remove();
|
|
229
|
+
}
|
|
230
|
+
// 移除无效状态
|
|
231
|
+
fieldElement.classList.remove('is-invalid');
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
clearValidate() {
|
|
235
|
+
this.$el.classList.remove('was-validated'); // 移除已验证样式
|
|
236
|
+
|
|
237
|
+
const invalidElements = this.$el.querySelectorAll('.is-invalid');
|
|
238
|
+
invalidElements.forEach(el => el.classList.remove('is-invalid'));
|
|
239
|
+
|
|
240
|
+
const invalidFeedbackElements = this.$el.querySelectorAll('.invalid-feedback');
|
|
241
|
+
invalidFeedbackElements.forEach(el => el.remove());
|
|
242
|
+
},
|
|
243
|
+
scrollToField(fieldName) {
|
|
244
|
+
let fieldElement = this.getFieldElement(fieldName);
|
|
245
|
+
if (fieldElement) {
|
|
246
|
+
fieldElement.scrollIntoView({behavior: 'smooth', block: 'center'});
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
updateElementsDisabled() {
|
|
250
|
+
let elements = this.$el.querySelectorAll('input, textarea, select, button');
|
|
251
|
+
elements.forEach(element => {
|
|
252
|
+
element.disabled = this.disabled;
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
</script>
|
|
258
|
+
|
|
259
|
+
<style>
|
|
260
|
+
.tnxbsv-form .b-form-group {
|
|
261
|
+
display: flex;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.tnxbsv-form .b-form-group:not(:last-child) {
|
|
265
|
+
margin-bottom: 1rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.tnxbsv-form .b-form-group .form-label {
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
min-width: var(--label-width);
|
|
271
|
+
text-align: right;
|
|
272
|
+
margin-right: 0.75rem;
|
|
273
|
+
margin-bottom: 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.tnxbsv-form .b-form-group .form-label.is-required::before {
|
|
277
|
+
content: '*';
|
|
278
|
+
color: var(--bs-danger);
|
|
279
|
+
margin-right: 0.25rem;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.tnxbsv-form .b-form-group .invalid-feedback {
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@media (max-width: var(--bs-breakpoint-md)) {
|
|
286
|
+
.tnxbsv-form .b-form-group {
|
|
287
|
+
flex-direction: column;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.tnxbsv-form .b-form-group .form-label {
|
|
291
|
+
text-align: left;
|
|
292
|
+
margin-bottom: 0.5rem;
|
|
293
|
+
min-width: auto;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tnxbsv-form-group b-form-group" :prop="prop">
|
|
3
|
+
<div class="tnxbsv-form-group__label-wrapper">
|
|
4
|
+
<slot name="label">
|
|
5
|
+
<label class="form-label">{{ label }}</label>
|
|
6
|
+
</slot>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="tnxbsv-form-group__content-wrapper">
|
|
9
|
+
<slot></slot>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script>
|
|
15
|
+
export default {
|
|
16
|
+
name: 'TnxbsvFormGroup',
|
|
17
|
+
props: {
|
|
18
|
+
label: String,
|
|
19
|
+
prop: String,
|
|
20
|
+
},
|
|
21
|
+
data() {
|
|
22
|
+
return {
|
|
23
|
+
model: {},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
methods: {}
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.tnxbsv-form-group {
|
|
32
|
+
display: flex;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.tnxbsv-form-group__label-wrapper {
|
|
36
|
+
padding-top: 0.375rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.tnxbsv-form-group__content-wrapper {
|
|
40
|
+
flex-grow: 1;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -27,11 +27,14 @@ export default {
|
|
|
27
27
|
},
|
|
28
28
|
computed: {
|
|
29
29
|
extraClass() {
|
|
30
|
-
let extraClass = '
|
|
30
|
+
let extraClass = '';
|
|
31
|
+
if (this.theme && this.theme !== 'inherit') {
|
|
32
|
+
extraClass += ' text-' + this.theme;
|
|
33
|
+
}
|
|
31
34
|
if (this.small) {
|
|
32
35
|
extraClass += ' spinner-border-sm';
|
|
33
36
|
}
|
|
34
|
-
return extraClass;
|
|
37
|
+
return extraClass.trim();
|
|
35
38
|
},
|
|
36
39
|
},
|
|
37
40
|
methods: {}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TnxbsvForm ref="form" :model="model" :rules="validationRules" :disabled="disabled">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
<TnxbsvFormGroup v-if="submit !== undefined && submit !== null">
|
|
5
|
+
<div class="flex-v-center">
|
|
6
|
+
<TnxbsvButton :type="theme || 'primary'" :size="size" @click="toSubmit" v-if="submit !== false">
|
|
7
|
+
{{ _submitText }}
|
|
8
|
+
</TnxbsvButton>
|
|
9
|
+
<TnxbsvButton class="ms-2" :size="size" @click="toCancel" v-if="cancel !== false">
|
|
10
|
+
{{ cancelText }}
|
|
11
|
+
</TnxbsvButton>
|
|
12
|
+
</div>
|
|
13
|
+
</TnxbsvFormGroup>
|
|
14
|
+
</TnxbsvForm>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
import TnxbsvForm from '../form/Form.vue';
|
|
19
|
+
import TnxbsvFormGroup from '../form/FormGroup.vue';
|
|
20
|
+
import TnxbsvButton from '../button/Button.vue';
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
name: 'TnxbsvSubmitForm',
|
|
24
|
+
components: {TnxbsvForm, TnxbsvFormGroup, TnxbsvButton},
|
|
25
|
+
props: {
|
|
26
|
+
model: Object,
|
|
27
|
+
rules: [String, Object], // 加载字段校验规则的URL地址,或规则集对象
|
|
28
|
+
rulesApp: { // 加载字段校验规则的应用名称
|
|
29
|
+
type: String,
|
|
30
|
+
default: () => window.tnx.componentDefaultApp, // 设置为方法以延时加载,确保更改的值生效
|
|
31
|
+
},
|
|
32
|
+
rulesLoaded: Function, // 规则集加载后的附加处理函数,仅在rules为字符串类型的URL地址时有效
|
|
33
|
+
submit: {
|
|
34
|
+
type: [Function, Boolean],
|
|
35
|
+
default: null,
|
|
36
|
+
},
|
|
37
|
+
submitText: String,
|
|
38
|
+
theme: String,
|
|
39
|
+
cancel: {
|
|
40
|
+
type: [String, Function, Boolean],
|
|
41
|
+
default: true
|
|
42
|
+
},
|
|
43
|
+
cancelText: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: '取消'
|
|
46
|
+
},
|
|
47
|
+
errorFocus: {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
default: false,
|
|
50
|
+
},
|
|
51
|
+
size: String,
|
|
52
|
+
},
|
|
53
|
+
emits: ['rules-loaded', 'meta'],
|
|
54
|
+
data() {
|
|
55
|
+
return {
|
|
56
|
+
validationRules: {},
|
|
57
|
+
disabled: false,
|
|
58
|
+
fieldNames: [],
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
computed: {
|
|
62
|
+
_submitText() {
|
|
63
|
+
if (this.submitText) {
|
|
64
|
+
return this.submitText;
|
|
65
|
+
}
|
|
66
|
+
return this.cancel === false ? '保存' : '提交';
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
watch: {
|
|
70
|
+
rules() {
|
|
71
|
+
this.initRules();
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
mounted() {
|
|
75
|
+
this.initRules();
|
|
76
|
+
},
|
|
77
|
+
methods: {
|
|
78
|
+
initRules() {
|
|
79
|
+
if (this.rules) {
|
|
80
|
+
if (typeof this.rules === 'string') {
|
|
81
|
+
window.tnx.app.rpc.getMeta(this.rules, meta => {
|
|
82
|
+
if (this.rulesLoaded) {
|
|
83
|
+
this.rulesLoaded(meta.$rules);
|
|
84
|
+
} else {
|
|
85
|
+
this.$emit('rules-loaded', meta.$rules);
|
|
86
|
+
}
|
|
87
|
+
this.validationRules = meta.$rules;
|
|
88
|
+
delete meta.$rules;
|
|
89
|
+
this.$emit('meta', meta);
|
|
90
|
+
this.fieldNames = Object.keys(meta);
|
|
91
|
+
}, this.rulesApp);
|
|
92
|
+
} else {
|
|
93
|
+
this.validationRules = {};
|
|
94
|
+
Object.keys(this.rules).forEach(fieldName => {
|
|
95
|
+
let fieldRules = this.rules[fieldName] || [];
|
|
96
|
+
if (!Array.isArray(fieldRules)) {
|
|
97
|
+
fieldRules = [fieldRules];
|
|
98
|
+
}
|
|
99
|
+
if (fieldRules.length) {
|
|
100
|
+
this.validationRules[fieldName] = fieldRules.filter((rule, index) => {
|
|
101
|
+
let valid = rule !== undefined && rule !== null;
|
|
102
|
+
if (!valid) {
|
|
103
|
+
console.error(`invalid rules[${index}] for field "${fieldName}": `, rule);
|
|
104
|
+
}
|
|
105
|
+
return valid;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
this.validationRules = {};
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
validate(callback, errorFocus) {
|
|
115
|
+
this.$refs.form.validate().then(() => {
|
|
116
|
+
if (typeof callback === 'function') {
|
|
117
|
+
callback(true);
|
|
118
|
+
}
|
|
119
|
+
}).catch(errors => {
|
|
120
|
+
if (this.errorFocus && errorFocus !== false) {
|
|
121
|
+
this.$nextTick(() => {
|
|
122
|
+
this.focusError.call(this, errors);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (typeof callback === 'function') {
|
|
126
|
+
callback(false);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
focusError(errors) {
|
|
131
|
+
let fieldNames = Object.keys(errors);
|
|
132
|
+
let fieldName = fieldNames[0];
|
|
133
|
+
if (fieldName) {
|
|
134
|
+
let fieldElement = this.$refs.form.getFieldElement(fieldName);
|
|
135
|
+
if (fieldElement) {
|
|
136
|
+
fieldElement.focus();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// 没有找到错误字段输入框,则滚动到错误栏目处
|
|
140
|
+
this.$refs.form.scrollToField(fieldName);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
disable(disabled) {
|
|
144
|
+
this.disabled = disabled !== false;
|
|
145
|
+
},
|
|
146
|
+
toSubmit(callback, disabled) {
|
|
147
|
+
this.validate(success => {
|
|
148
|
+
if (success) {
|
|
149
|
+
if (typeof callback !== 'function') {
|
|
150
|
+
callback = this.submit;
|
|
151
|
+
}
|
|
152
|
+
if (typeof callback === 'function') {
|
|
153
|
+
if (disabled !== false) {
|
|
154
|
+
this.disable();
|
|
155
|
+
}
|
|
156
|
+
callback(this);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
toCancel() {
|
|
162
|
+
if (typeof this.cancel === 'function') {
|
|
163
|
+
this.cancel();
|
|
164
|
+
} else if (typeof this.cancel === 'string') {
|
|
165
|
+
this.$router.back(this.cancel);
|
|
166
|
+
} else if (this.cancel !== false) {
|
|
167
|
+
this.$router.back();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<style>
|
|
175
|
+
|
|
176
|
+
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// tnxbsv.js
|
|
2
|
+
import tnxjq from '@truenewx/tnxcore/src/tnxjq';
|
|
2
3
|
import tnxvue from '../tnxvue.js';
|
|
3
4
|
import * as BootstrapVue from 'bootstrap-vue-next';
|
|
4
5
|
import 'bootstrap/dist/css/bootstrap.css';
|
|
@@ -6,30 +7,56 @@ import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
|
|
|
6
7
|
import './tnxbsv.css';
|
|
7
8
|
|
|
8
9
|
import Button from './button/Button.vue';
|
|
10
|
+
import Dialog from './dialog/Dialog.vue';
|
|
9
11
|
import EnumSelect from './enum-select/EnumSelect.vue';
|
|
12
|
+
import Form from './form/Form.vue';
|
|
13
|
+
import FormGroup from './form/FormGroup.vue';
|
|
10
14
|
import Loading from './loading/Loading.vue';
|
|
11
15
|
import Paged from './paged/Paged.vue';
|
|
12
16
|
import QueryTable from './query-table/QueryTable.vue';
|
|
13
17
|
import Select from './select/Select.vue';
|
|
18
|
+
import SubmitForm from './submit-form/SubmitForm.vue';
|
|
14
19
|
|
|
15
20
|
export const build = tnxvue.build;
|
|
16
21
|
|
|
17
22
|
export default build('tnxbsv', () => {
|
|
18
23
|
const components = Object.assign({}, tnxvue.components, {
|
|
19
|
-
Button, EnumSelect, Loading, Paged, QueryTable, Select,
|
|
24
|
+
Button, Dialog, EnumSelect, Form, FormGroup, Loading, Paged, QueryTable, Select, SubmitForm,
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
const tnxbsv = Object.assign({}, tnxvue, {
|
|
27
|
+
const tnxbsv = Object.assign({}, tnxjq, tnxvue, {
|
|
28
|
+
libs: Object.assign({}, tnxjq.libs, tnxvue.libs, {BootstrapVue}),
|
|
23
29
|
components,
|
|
24
30
|
componentDefaultApp: undefined, // 组件的默认app,从服务端获取数据的组件以此为远程请求的默认app
|
|
25
31
|
_dialogs: [], // 对话框堆栈
|
|
26
32
|
dialog(content, title, buttons, options, contentProps) {
|
|
27
|
-
|
|
33
|
+
let id = new Date().getTime();
|
|
34
|
+
let containerId = 'dialog-container-' + id;
|
|
35
|
+
let componentDefinition = Object.assign({}, Dialog,);
|
|
36
|
+
let dialogVm = window.tnx.createVueInstance(componentDefinition, null, {
|
|
37
|
+
modelValue: true,
|
|
38
|
+
id: id,
|
|
39
|
+
container: '#' + containerId,
|
|
40
|
+
title,
|
|
41
|
+
content,
|
|
42
|
+
contentProps,
|
|
43
|
+
buttons,
|
|
44
|
+
});
|
|
45
|
+
const dialogContainer = document.createElement('div');
|
|
46
|
+
dialogContainer.className = 'tnxbsv-dialog-container';
|
|
47
|
+
dialogContainer.id = containerId;
|
|
48
|
+
document.body.appendChild(dialogContainer);
|
|
49
|
+
let dialog = dialogVm.mount(dialogContainer);
|
|
50
|
+
dialog.onHidden = this.util.function.after(dialog.onHidden, () => {
|
|
51
|
+
dialogVm.unmount();
|
|
52
|
+
this._dialogs.remove(dialog);
|
|
53
|
+
document.body.removeChild(dialogContainer);
|
|
54
|
+
});
|
|
55
|
+
this._dialogs.push(dialog);
|
|
56
|
+
return dialogVm;
|
|
28
57
|
},
|
|
29
58
|
});
|
|
30
59
|
|
|
31
|
-
tnxbsv.libs.BootstrapVue = BootstrapVue;
|
|
32
|
-
|
|
33
60
|
tnxbsv.install = tnxbsv.util.function.around(tnxbsv.install, function (install, vm) {
|
|
34
61
|
install.call(tnxbsv, vm);
|
|
35
62
|
vm.use(BootstrapVue.createBootstrap());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<el-dialog
|
|
3
|
-
|
|
2
|
+
<el-dialog class="tnxel-dialog"
|
|
3
|
+
:data-v-id="id"
|
|
4
4
|
v-model="visible"
|
|
5
5
|
destroy-on-close
|
|
6
6
|
append-to-body
|
|
@@ -34,8 +34,6 @@
|
|
|
34
34
|
import $ from 'cash-dom';
|
|
35
35
|
import DialogContent from './DialogContent.vue';
|
|
36
36
|
|
|
37
|
-
const util = window.tnx.util;
|
|
38
|
-
|
|
39
37
|
export default {
|
|
40
38
|
name: 'TnxelDialog',
|
|
41
39
|
components: {
|
|
@@ -168,7 +166,7 @@ export default {
|
|
|
168
166
|
});
|
|
169
167
|
|
|
170
168
|
if (observe) {
|
|
171
|
-
this.heightChangeObserver = util.dom.observeHeightChange($dialog[0], this.locate);
|
|
169
|
+
this.heightChangeObserver = window.tnx.util.dom.observeHeightChange($dialog[0], this.locate);
|
|
172
170
|
}
|
|
173
171
|
}
|
|
174
172
|
},
|
|
@@ -190,7 +188,7 @@ export default {
|
|
|
190
188
|
const vm = this;
|
|
191
189
|
this.beforeClose(function () {
|
|
192
190
|
if (typeof callback === 'function') {
|
|
193
|
-
vm.options.onClosed = util.function.around(vm.options.onClosed, function (onClosed) {
|
|
191
|
+
vm.options.onClosed = window.tnx.util.function.around(vm.options.onClosed, function (onClosed) {
|
|
194
192
|
if (onClosed) {
|
|
195
193
|
onClosed();
|
|
196
194
|
}
|
package/src/tnxvue.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 基于Vue 3的扩展支持
|
|
4
4
|
*/
|
|
5
|
-
import tnxcore from '@truenewx/tnxcore';
|
|
6
|
-
|
|
5
|
+
// import tnxcore from '@truenewx/tnxcore';
|
|
6
|
+
import tnxcore from '../../core/src/tnxcore';
|
|
7
7
|
import validator from './tnxvue-validator';
|
|
8
8
|
import createRouter from './tnxvue-router';
|
|
9
9
|
import Text from './text/Text.vue';
|
|
@@ -96,7 +96,6 @@ export default build('tnxvue', () => {
|
|
|
96
96
|
} else if (window.tnx.router.instance) {
|
|
97
97
|
vm.config.globalProperties.$router = window.tnx.router.instance;
|
|
98
98
|
}
|
|
99
|
-
// vm.config.unwrapInjectedRef = true;
|
|
100
99
|
window.tnx.app.eventBus = window.tnx.app.eventBus || mitt();
|
|
101
100
|
window.tnx.app.eventBus.once = function (name, handler) {
|
|
102
101
|
this.all.set(name, [handler]);
|