@truenewx/tnxvue3 3.0.3 → 3.0.4

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": "@truenewx/tnxvue3",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "互联网技术解决方案:Vue3扩展支持",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -1,18 +1,28 @@
1
1
  <template>
2
- <BButton>
3
- <i :class="icon" v-if="icon"></i>
4
- <span><slot></slot></span>
2
+ <BButton class="tnxbsv-button" :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: {
15
24
  icon: String,
25
+ loading: Boolean,
16
26
  },
17
27
  data() {
18
28
  return {};
@@ -22,5 +32,8 @@ export default {
22
32
  </script>
23
33
 
24
34
  <style>
25
-
35
+ .tnxbsv-button {
36
+ display: flex;
37
+ align-items: center;
38
+ }
26
39
  </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 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>
@@ -0,0 +1,243 @@
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
+ },
28
+ data() {
29
+ return {};
30
+ },
31
+ mounted() {
32
+ window.addEventListener('resize', this.updateLabelWidth);
33
+ this.$nextTick(() => {
34
+ setTimeout(() => {
35
+ this.initRules();
36
+ this.updateLabelWidth();
37
+ });
38
+ });
39
+ },
40
+ beforeUnmount() {
41
+ window.removeEventListener('resize', this.updateLabelWidth);
42
+ },
43
+ methods: {
44
+ getFieldGroupElements() {
45
+ 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
+ }
52
+ });
53
+ return elements;
54
+ },
55
+ getFieldRules(fieldName) {
56
+ let fieldRules = this.rules[fieldName] || [];
57
+ if (!Array.isArray(fieldRules)) {
58
+ fieldRules = [fieldRules];
59
+ }
60
+ return fieldRules;
61
+ },
62
+ initRules() {
63
+ let fieldGroupElements = this.getFieldGroupElements();
64
+ for (let fieldName in fieldGroupElements) {
65
+ let fieldGroupElement = fieldGroupElements[fieldName];
66
+ if (fieldGroupElement) {
67
+ let fieldRules = this.getFieldRules(fieldName);
68
+ if (fieldRules.some(rule => rule.required)) {
69
+ let label = fieldGroupElement.querySelector('.form-label');
70
+ if (label) {
71
+ label.classList.add('is-required');
72
+ }
73
+ }
74
+ let existsChangeValidator = fieldRules.some(fieldRule => fieldRule.trigger === 'change');
75
+ let existsBlurValidator = fieldRules.some(fieldRule => fieldRule.trigger !== 'change');
76
+ if (existsChangeValidator || existsBlurValidator) {
77
+ let fieldElement = fieldGroupElement.querySelector('input, select, textarea');
78
+ if (fieldElement) {
79
+ if (existsChangeValidator) {
80
+ fieldElement.addEventListener('change', () => {
81
+ this.validateField(fieldName, 'change');
82
+ });
83
+ }
84
+ if (existsBlurValidator) {
85
+ fieldElement.addEventListener('blur', () => {
86
+ this.validateField(fieldName, 'blur');
87
+ });
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ },
94
+ updateLabelWidth() {
95
+ 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
+ }
106
+ }
107
+ }
108
+ }
109
+ if (maxWidth) {
110
+ this.$el.style.setProperty('--label-width', `${maxWidth}px`);
111
+ }
112
+ },
113
+ validate() {
114
+ this.clearValidate(); // 清除之前的反馈信息
115
+ this.$el.classList.remove('was-validated'); // 移除已验证样式
116
+
117
+ let valid = true;
118
+ let fieldGroupElements = this.getFieldGroupElements();
119
+ for (let fieldName in fieldGroupElements) {
120
+ if (!this.validateField(fieldName)) {
121
+ valid = false;
122
+ }
123
+ }
124
+
125
+ if (!valid) {
126
+ this.$el.classList.add('was-validated'); // 添加已验证样式
127
+ }
128
+
129
+ return valid;
130
+ },
131
+ 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;
137
+ },
138
+ validateField(fieldName, trigger) {
139
+ const fieldValue = this.model[fieldName];
140
+ const fieldRules = this.getFieldRules(fieldName);
141
+ const fieldErrors = [];
142
+
143
+ fieldRules.forEach(fieldRule => {
144
+ if (typeof fieldRule.validator === 'function' && (!trigger || trigger === fieldRule.trigger)) {
145
+ fieldRule.validator(fieldRule, fieldValue, (error) => {
146
+ if (error) {
147
+ fieldErrors.push(error.message);
148
+ }
149
+ });
150
+ }
151
+ // 没有validator()方法,则不进行校验
152
+ });
153
+
154
+ if (fieldErrors.length > 0) {
155
+ this.setFieldInvalidFeedback(fieldName, fieldErrors);
156
+ return false;
157
+ } else {
158
+ // 清除错误状态
159
+ this.removeFieldInvalidFeedback(fieldName);
160
+ return true;
161
+ }
162
+ },
163
+ 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')) {
170
+ feedbackDiv = document.createElement('div');
171
+ feedbackDiv.className = 'invalid-feedback';
172
+ fieldElement.parentNode.insertBefore(feedbackDiv, fieldElement.nextSibling);
173
+ }
174
+ // 更新 .invalid-feedback 的内容
175
+ feedbackDiv.textContent = messages.join('; ');
176
+ // 设置输入框为无效状态
177
+ fieldElement.classList.add('is-invalid');
178
+ }
179
+ },
180
+ 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')) {
187
+ feedbackDiv.remove();
188
+ }
189
+ // 移除无效状态
190
+ fieldElement.classList.remove('is-invalid');
191
+ }
192
+ },
193
+ clearValidate() {
194
+ this.$el.classList.remove('was-validated'); // 移除已验证样式
195
+
196
+ const invalidElements = this.$el.querySelectorAll('.is-invalid');
197
+ invalidElements.forEach(el => el.classList.remove('is-invalid'));
198
+
199
+ const invalidFeedbackElements = this.$el.querySelectorAll('.invalid-feedback');
200
+ invalidFeedbackElements.forEach(el => el.remove());
201
+ },
202
+ }
203
+ }
204
+ </script>
205
+
206
+ <style>
207
+ .tnxbsv-form .b-form-group {
208
+ display: flex;
209
+ }
210
+
211
+ .tnxbsv-form .b-form-group:not(:last-child) {
212
+ margin-bottom: 1rem;
213
+ }
214
+
215
+ .tnxbsv-form .b-form-group .form-label {
216
+ white-space: nowrap;
217
+ min-width: var(--label-width);
218
+ text-align: right;
219
+ margin-right: 0.75rem;
220
+ margin-bottom: 0;
221
+ }
222
+
223
+ .tnxbsv-form .b-form-group .form-label.is-required::before {
224
+ content: '*';
225
+ color: var(--bs-danger);
226
+ margin-right: 0.25rem;
227
+ }
228
+
229
+ .tnxbsv-form .b-form-group .invalid-feedback {
230
+ }
231
+
232
+ @media (max-width: var(--bs-breakpoint-md)) {
233
+ .tnxbsv-form .b-form-group {
234
+ flex-direction: column;
235
+ }
236
+
237
+ .tnxbsv-form .b-form-group .form-label {
238
+ text-align: left;
239
+ margin-bottom: 0.5rem;
240
+ min-width: auto;
241
+ }
242
+ }
243
+ </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 = 'text-' + this.theme;
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: {}
@@ -29,3 +29,8 @@
29
29
  .badge.text-bg-light {
30
30
  border-color: var(--bs-border-color);
31
31
  }
32
+
33
+ .accordion-button,
34
+ .accordion-body {
35
+ padding: 1rem;
36
+ }
@@ -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,7 +7,10 @@ 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';
@@ -16,20 +20,42 @@ export const build = tnxvue.build;
16
20
 
17
21
  export default build('tnxbsv', () => {
18
22
  const components = Object.assign({}, tnxvue.components, {
19
- Button, EnumSelect, Loading, Paged, QueryTable, Select,
23
+ Button, Dialog, EnumSelect, Form, FormGroup, Loading, Paged, QueryTable, Select,
20
24
  });
21
25
 
22
- const tnxbsv = Object.assign({}, tnxvue, {
26
+ const tnxbsv = Object.assign({}, tnxjq, tnxvue, {
27
+ libs: Object.assign({}, tnxjq.libs, tnxvue.libs, {BootstrapVue}),
23
28
  components,
24
29
  componentDefaultApp: undefined, // 组件的默认app,从服务端获取数据的组件以此为远程请求的默认app
25
30
  _dialogs: [], // 对话框堆栈
26
31
  dialog(content, title, buttons, options, contentProps) {
27
-
32
+ let id = new Date().getTime();
33
+ let containerId = 'dialog-container-' + id;
34
+ let componentDefinition = Object.assign({}, Dialog,);
35
+ let dialogVm = window.tnx.createVueInstance(componentDefinition, null, {
36
+ modelValue: true,
37
+ id: id,
38
+ container: '#' + containerId,
39
+ title,
40
+ content,
41
+ contentProps,
42
+ buttons,
43
+ });
44
+ const dialogContainer = document.createElement('div');
45
+ dialogContainer.className = 'tnxbsv-dialog-container';
46
+ dialogContainer.id = containerId;
47
+ document.body.appendChild(dialogContainer);
48
+ let dialog = dialogVm.mount(dialogContainer);
49
+ dialog.onHidden = this.util.function.after(dialog.onHidden, () => {
50
+ dialogVm.unmount();
51
+ this._dialogs.remove(dialog);
52
+ document.body.removeChild(dialogContainer);
53
+ });
54
+ this._dialogs.push(dialog);
55
+ return dialogVm;
28
56
  },
29
57
  });
30
58
 
31
- tnxbsv.libs.BootstrapVue = BootstrapVue;
32
-
33
59
  tnxbsv.install = tnxbsv.util.function.around(tnxbsv.install, function (install, vm) {
34
60
  install.call(tnxbsv, vm);
35
61
  vm.use(BootstrapVue.createBootstrap());
@@ -1,6 +1,6 @@
1
1
  <template>
2
- <el-dialog :data-v-id="id"
3
- class="tnxel-dialog"
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
@@ -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]);