@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.
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <TnxbsvForm class="tnxbsv-query-form" :model="params" inline>
3
+ <slot></slot>
4
+ <slot name="actions">
5
+ <div class="tnxbsv-query-form-actions">
6
+ <TnxbsvButton variant="outline-primary" icon="bi bi-search" @click="query" v-if="query">
7
+ {{ queryText }}
8
+ </TnxbsvButton>
9
+ </div>
10
+ </slot>
11
+ </TnxbsvForm>
12
+ </template>
13
+
14
+ <script>
15
+ import TnxbsvForm from '../form/Form.vue';
16
+ import TnxbsvButton from '../button/Button.vue';
17
+
18
+ export default {
19
+ name: 'TnxbsvQueryForm',
20
+ components: {TnxbsvForm, TnxbsvButton},
21
+ props: {
22
+ params: {
23
+ type: Object,
24
+ default: () => ({}),
25
+ },
26
+ query: Function,
27
+ queryText: {
28
+ type: String,
29
+ default: '查询',
30
+ },
31
+ },
32
+ data() {
33
+ return {};
34
+ },
35
+ methods: {}
36
+ }
37
+ </script>
38
+
39
+ <style>
40
+
41
+ </style>
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <TnxbsvCascader v-model="model"
3
+ :options="region.subs"
4
+ :props="{
5
+ value: 'code',
6
+ label: 'caption',
7
+ children: 'subs',
8
+ }"
9
+ :placeholder="placeholder"
10
+ :disabled="disabled"
11
+ :clearable="empty"
12
+ :parent-selectable="parentSelectable"
13
+ />
14
+ </template>
15
+
16
+ <script>
17
+ import TnxbsvCascader from '../cascader/Cascader.vue';
18
+
19
+ export default {
20
+ name: 'TnxbsvRegionCascader',
21
+ components: {TnxbsvCascader},
22
+ props: {
23
+ modelValue: String,
24
+ scope: {
25
+ type: String,
26
+ default: () => 'CN',
27
+ },
28
+ maxLevel: {
29
+ type: [Number, String],
30
+ default: 3,
31
+ },
32
+ minLevel: {
33
+ type: [Number, String],
34
+ default: 3,
35
+ },
36
+ empty: {
37
+ type: Boolean,
38
+ default: false,
39
+ },
40
+ placeholder: String,
41
+ disabled: Boolean,
42
+ change: Function, // 选中值变化后的事件处理函数,由于比element的change事件传递更多参数,所以以prop的形式指定,以尽量节省性能
43
+ app: {
44
+ type: String,
45
+ default: () => window.tnx.componentDefaultApp,
46
+ },
47
+ parentSelectable: Boolean,
48
+ },
49
+ data() {
50
+ return {
51
+ model: this.modelValue,
52
+ region: {},
53
+ };
54
+ },
55
+ watch: {
56
+ model(value) {
57
+ this.$emit('update:modelValue', value);
58
+ this.triggerChange(value);
59
+ },
60
+ modelValue() {
61
+ this.model = this.getModel();
62
+ }
63
+ },
64
+ mounted() {
65
+ window.tnx.app.rpc.loadRegion(this.scope, parseInt(this.maxLevel), region => {
66
+ this.region = region;
67
+ this.model = this.getModel();
68
+ }, {
69
+ app: this.app,
70
+ });
71
+ },
72
+ methods: {
73
+ triggerChange(value) {
74
+ if (this.change) {
75
+ let item = this.getItem(this.region.subs, value);
76
+ this.change(item);
77
+ }
78
+ },
79
+ getItem(items, value) {
80
+ if (items && value !== undefined) {
81
+ for (let item of items) {
82
+ if (item.code === value) {
83
+ return item;
84
+ }
85
+ let sub = this.getItem(item.subs, value);
86
+ if (sub) {
87
+ return sub;
88
+ }
89
+ }
90
+ }
91
+ return undefined;
92
+ },
93
+ getModel() {
94
+ if (this.region) {
95
+ let items = this.region.subs;
96
+ if (items && items.length) {
97
+ let item = this.getItem(items, this.modelValue);
98
+ if (item) {
99
+ return item.code;
100
+ } else { // 如果当前值找不到匹配的选项,则需要考虑是设置为空还是默认选项
101
+ if (!this.empty) { // 如果不能为空,则默认选中第一个叶子节点选项
102
+ let firstItem = items[0];
103
+ while (firstItem.subs && firstItem.subs.length) {
104
+ firstItem = firstItem.subs[0];
105
+ }
106
+ return firstItem ? firstItem.code : null;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ }
114
+ }
115
+ </script>
116
+
117
+ <style>
118
+
119
+ </style>
@@ -1,11 +1,19 @@
1
1
  <template>
2
+ <BFormRadioGroup class="tnxbsv-select tnxbsv-radio-group"
3
+ v-model="model"
4
+ :options="items"
5
+ :value-field="valueName"
6
+ :text-field="textName"
7
+ :buttons="selector === 'radio-button'"
8
+ button-variant="outline-primary"
9
+ v-if="items && (selector==='radio' || selector === 'radio-button')"/>
2
10
  <BDropdown class="tnxbsv-select tnxbsv-dropdown"
3
11
  :key="groupKey"
4
12
  :text="currentText"
5
13
  :variant="theme"
6
14
  :size="size"
7
- v-if="selector==='dropdown'">
8
- <BDropdownItem :active="isSelected(emptyValue)" @click="select(emptyValue)" v-if="empty">
15
+ v-else-if="selector==='dropdown'">
16
+ <BDropdownItem class="empty-item" :active="isSelected(emptyValue)" @click="select(emptyValue)" v-if="empty">
9
17
  <span>{{ emptyText || '&nbsp;' }}</span>
10
18
  </BDropdownItem>
11
19
  <template v-if="items">
@@ -21,10 +29,11 @@
21
29
  </BDropdownItem>
22
30
  </template>
23
31
  <BDropdownItem v-else>
24
- <Loading/>
32
+ <LoadingIcon/>
25
33
  </BDropdownItem>
26
34
  </BDropdown>
27
35
  <BFormSelect class="tnxbsv-select"
36
+ :class="{'is-empty': model === emptyValue}"
28
37
  v-model="model"
29
38
  :key="groupKey"
30
39
  :variant="theme"
@@ -32,13 +41,9 @@
32
41
  :value-field="valueName"
33
42
  :text-field="textName"
34
43
  :size="size"
35
- :required="!empty"
36
44
  v-else-if="items">
37
- <template #default>
38
- {{ currentText || placeholder }}
39
- </template>
40
45
  <template #first v-if="empty">
41
- <BFormSelectOption :value="emptyValue">{{ emptyText || '&nbsp;' }}</BFormSelectOption>
46
+ <BFormSelectOption class="empty-item" :value="emptyValue">{{ emptyText || '&nbsp;' }}</BFormSelectOption>
42
47
  </template>
43
48
  <template #option="{value, text}">
44
49
  <slot name="option" :item="getItem(value)" v-if="$slots.option"></slot>
@@ -49,20 +54,20 @@
49
54
  </template>
50
55
  </BFormSelect>
51
56
  <div class="flex-v-center" v-else>
52
- <Loading/>
57
+ <LoadingIcon/>
53
58
  </div>
54
59
  </template>
55
60
 
56
61
  <script>
57
- import {BDropdown, BDropdownItem, BFormSelect, BFormSelectOption} from 'bootstrap-vue-next';
58
- import Loading from '../loading/Loading.vue';
62
+ import {BFormRadioGroup, BDropdown, BDropdownItem, BFormSelect, BFormSelectOption} from 'bootstrap-vue-next';
63
+ import LoadingIcon from '../loading-icon/LoadingIcon.vue';
59
64
 
60
65
  export const isMultiSelector = function (selector) {
61
66
  return selector === 'checkbox' || selector === 'tags' || selector === 'multi-select' || selector === 'texts';
62
67
  }
63
68
  export default {
64
69
  name: 'TnxbsvSelect',
65
- components: {BDropdown, BDropdownItem, BFormSelect, BFormSelectOption, Loading},
70
+ components: {BFormRadioGroup, BDropdown, BDropdownItem, BFormSelect, BFormSelectOption, LoadingIcon},
66
71
  props: {
67
72
  id: [Number, String],
68
73
  modelValue: {
@@ -97,10 +102,9 @@ export default {
97
102
  },
98
103
  emptyValue: {
99
104
  type: [String, Number, Boolean, Array],
100
- default: '', // 设为null会告警,暂时先设为空字符,在非字符串类型的情况下是否有问题,有待检验
105
+ default: null,
101
106
  },
102
107
  emptyClass: String,
103
- placeholder: String,
104
108
  disabled: Boolean,
105
109
  tagClick: Function, // 点击一个标签选项时调用,如果返回false,则选项不会被选中
106
110
  change: Function, // 选中值变化后的事件处理函数,比change事件传递更多参数数据
@@ -349,4 +353,18 @@ export default {
349
353
  .tnxbsv-select[variant="danger"]:focus {
350
354
  box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);
351
355
  }
356
+
357
+ select.tnxbsv-select.is-empty,
358
+ .tnxbsv-select .empty-item {
359
+ color: var(--bs-tertiary-color);
360
+ }
361
+
362
+ select.tnxbsv-select.is-empty option:not(.empty-item) {
363
+ color: var(--bs-body-color);
364
+ }
365
+
366
+ .tnxbsv-radio-group.btn-group > .btn {
367
+ flex: none;
368
+ --bs-btn-padding-x: 1rem;
369
+ }
352
370
  </style>
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <BFormTags class="tnxbsv-tags-input"
3
+ v-model="model"
4
+ :placeholder="placeholder"
5
+ :separator="separator"
6
+ :tag-variant="tagVariant || 'light'"
7
+ add-button-text="+"
8
+ :duplicate-tag-text="duplicateTagText"
9
+ remove-on-delete
10
+ />
11
+ </template>
12
+
13
+ <script>
14
+ import {BFormTags} from 'bootstrap-vue-next';
15
+
16
+ export default {
17
+ name: 'TnxbsvTagsInput',
18
+ components: {BFormTags},
19
+ props: {
20
+ modelValue: {
21
+ type: Array,
22
+ default: () => [],
23
+ },
24
+ placeholder: {
25
+ type: String,
26
+ default: '输入后回车以添加',
27
+ },
28
+ separator: {
29
+ type: String,
30
+ default: ','
31
+ },
32
+ tagVariant: String,
33
+ duplicateTagText: {
34
+ type: String,
35
+ default: '标签重复',
36
+ },
37
+ },
38
+ data() {
39
+ return {
40
+ model: this.modelValue,
41
+ };
42
+ },
43
+ watch: {
44
+ modelValue() {
45
+ this.model = this.modelValue;
46
+ },
47
+ model() {
48
+ this.$emit('update:modelValue', this.model);
49
+ },
50
+ },
51
+ methods: {}
52
+ }
53
+ </script>
54
+
55
+ <style>
56
+ .tnxbsv-tags-input .b-form-tag + .b-from-tags-field > div {
57
+ padding-left: 0.25rem;
58
+ }
59
+
60
+ .tnxbsv-tags-input .btn.b-form-tags-button {
61
+ --bs-btn-padding-x: 0.5rem;
62
+ margin-left: 0.5rem;
63
+ }
64
+ </style>
@@ -1,3 +1,15 @@
1
+ ::placeholder {
2
+ color: var(--bs-tertiary-color) !important;
3
+ }
4
+
5
+ ::-webkit-input-placeholder {
6
+ color: var(--bs-tertiary-color) !important;
7
+ }
8
+
9
+ ::-moz-placeholder {
10
+ color: var(--bs-tertiary-color) !important;
11
+ }
12
+
1
13
  .link {
2
14
  cursor: pointer;
3
15
  color: var(--bs-link-color);
@@ -6,22 +18,17 @@
6
18
  .form-inline {
7
19
  display: flex;
8
20
  flex-wrap: wrap;
9
- align-items: center;
10
21
  }
11
22
 
12
- .form-inline-group {
23
+ .form-inline .b-form-group {
13
24
  display: flex;
14
25
  flex-wrap: nowrap;
15
26
  height: fit-content;
27
+ margin-right: 1.5rem;
16
28
  margin-bottom: 1rem;
17
29
  width: fit-content;
18
30
  }
19
31
 
20
- .form-inline-group .col-form-label {
21
- white-space: nowrap;
22
- margin-right: 1rem;
23
- }
24
-
25
32
  .badge {
26
33
  border: 1px solid transparent;
27
34
  }
@@ -34,3 +41,36 @@
34
41
  .accordion-body {
35
42
  padding: 1rem;
36
43
  }
44
+
45
+ .toast {
46
+ width: fit-content;
47
+ min-width: 5rem;
48
+ text-align: center;
49
+ }
50
+
51
+ .toast::before {
52
+ content: '';
53
+ position: absolute;
54
+ top: 0;
55
+ left: 0;
56
+ width: 100%;
57
+ height: 100%;
58
+ background-color: rgba(255, 255, 255, 0.3);
59
+ }
60
+
61
+ .form-check {
62
+ margin-top: 0.125rem;
63
+ }
64
+
65
+ .form-check > * {
66
+ cursor: pointer;
67
+ }
68
+
69
+ .tnxbsv-dialog-confirm {
70
+ display: flex;
71
+ }
72
+
73
+ .tnxbsv-dialog-confirm i {
74
+ color: var(--bs-secondary-color);
75
+ margin-right: 0.5rem;
76
+ }
@@ -7,21 +7,39 @@ import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
7
7
  import './tnxbsv.css';
8
8
 
9
9
  import Button from './button/Button.vue';
10
+ import Cascader from './cascader/Cascader.vue';
10
11
  import Dialog from './dialog/Dialog.vue';
11
12
  import EnumSelect from './enum-select/EnumSelect.vue';
12
13
  import Form from './form/Form.vue';
13
14
  import FormGroup from './form/FormGroup.vue';
14
- import Loading from './loading/Loading.vue';
15
+ import LoadingIcon from './loading-icon/LoadingIcon.vue';
16
+ import LoadingOverlay from './loading-overlay/LoadingOverlay.vue';
15
17
  import Paged from './paged/Paged.vue';
18
+ import QueryForm from './query-form/QueryForm.vue';
16
19
  import QueryTable from './query-table/QueryTable.vue';
20
+ import RegionCascader from './region-cascader/RegionCascader.vue';
17
21
  import Select from './select/Select.vue';
18
22
  import SubmitForm from './submit-form/SubmitForm.vue';
23
+ import TagsInput from './tags-input/TagsInput.vue';
19
24
 
20
25
  export const build = tnxvue.build;
21
26
 
22
27
  export default build('tnxbsv', () => {
23
28
  const components = Object.assign({}, tnxvue.components, {
24
- Button, Dialog, EnumSelect, Form, FormGroup, Loading, Paged, QueryTable, Select, SubmitForm,
29
+ Button,
30
+ Cascader,
31
+ Dialog,
32
+ EnumSelect,
33
+ Form,
34
+ FormGroup,
35
+ LoadingIcon,
36
+ Paged,
37
+ QueryForm,
38
+ QueryTable,
39
+ RegionCascader,
40
+ Select,
41
+ SubmitForm,
42
+ TagsInput,
25
43
  });
26
44
 
27
45
  const tnxbsv = Object.assign({}, tnxjq, tnxvue, {
@@ -55,6 +73,109 @@ export default build('tnxbsv', () => {
55
73
  this._dialogs.push(dialog);
56
74
  return dialogVm;
57
75
  },
76
+ _closeMessage() {
77
+ this.hideLoading();
78
+ this.removeToast();
79
+ },
80
+ confirm(message, title, callback, options) {
81
+ if (typeof title === 'function') {
82
+ options = callback;
83
+ callback = title;
84
+ title = '确认';
85
+ }
86
+ let buttons = tnxvue.getDefaultDialogButtons('confirm', callback);
87
+ message =
88
+ `<div class="tnxbsv-dialog-confirm"><i class="bi bi-question-circle-fill"></i><div>${message}</div></div>`;
89
+ return this.dialog(message, title, buttons, options);
90
+ },
91
+ toast(message, timeout, callback, options = {}) {
92
+ if (typeof timeout === 'function') {
93
+ options = callback || {};
94
+ callback = timeout;
95
+ timeout = undefined;
96
+ }
97
+
98
+ this._closeMessage();
99
+
100
+ const div = document.createElement('div');
101
+ document.body.appendChild(div);
102
+
103
+ const Vue = window.tnx.libs.Vue;
104
+ const ToastComponent = {
105
+ components: {
106
+ BToast: BootstrapVue.BToast
107
+ },
108
+ setup() {
109
+ const visible = Vue.ref(true);
110
+ Vue.onMounted(() => {
111
+ setTimeout(() => {
112
+ visible.value = false;
113
+ // 延迟移除组件,确保动画效果完成
114
+ setTimeout(() => {
115
+ window.tnx.toastInstance?.unmount();
116
+ try {
117
+ document.body.removeChild(div);
118
+ } catch (e) {
119
+ // 忽略异常
120
+ }
121
+ }, 500);
122
+ }, timeout || 1500);
123
+ });
124
+ return {visible};
125
+ },
126
+ render() {
127
+ return Vue.h(BootstrapVue.BToast, {
128
+ modelValue: this.visible,
129
+ variant: options.type || 'success',
130
+ static: true,
131
+ noCloseButton: true,
132
+ class: 'position-fixed',
133
+ style: {
134
+ top: '50%',
135
+ left: '50%',
136
+ transform: 'translate(-50%, -50%)',
137
+ zIndex: window.tnx.util.dom.minTopZIndex(),
138
+ }
139
+ }, () => message);
140
+ }
141
+ };
142
+
143
+ const instance = window.tnx.createVueInstance(ToastComponent);
144
+ instance.mount(div);
145
+ window.tnx.toastInstance = instance;
146
+ },
147
+ removeToast() {
148
+ if (window.tnx.toastInstance) {
149
+ window.tnx.toastInstance.unmount();
150
+ try {
151
+ document.body.removeChild(window.tnx.toastInstance._container);
152
+ } catch (e) {
153
+ // 忽略异常
154
+ }
155
+ window.tnx.toastInstance = null;
156
+ }
157
+ },
158
+ showLoading(message = '', options) {
159
+ this._closeMessage();
160
+
161
+ let div = document.createElement('div');
162
+ document.body.appendChild(div);
163
+ let instance = window.tnx.createVueInstance(LoadingOverlay, null, {message});
164
+ let component = instance.mount(div);
165
+ window.tnx.loadingInstance = instance;
166
+ window.tnx.app.eventBus.emit('tnx.showLoading', options);
167
+ return component;
168
+ },
169
+ hideLoading() {
170
+ if (window.tnx.loadingInstance) {
171
+ window.tnx.loadingInstance.unmount();
172
+ document.body.removeChild(window.tnx.loadingInstance._container);
173
+ window.tnx.loadingInstance = null;
174
+ }
175
+ },
176
+ closeLoading() {
177
+ this.hideLoading();
178
+ },
58
179
  });
59
180
 
60
181
  tnxbsv.install = tnxbsv.util.function.around(tnxbsv.install, function (install, vm) {
@@ -218,9 +218,8 @@ export default build('tnxel', () => {
218
218
  this.closeLoading();
219
219
  },
220
220
  _handleZIndex(selector) {
221
- const util = this.util;
222
221
  setTimeout(function () {
223
- const topZIndex = util.dom.minTopZIndex(2);
222
+ const topZIndex = window.tnx.util.dom.minTopZIndex(2);
224
223
  if (selector.endsWith(':last')) {
225
224
  selector = selector.substring(0, selector.length - ':last'.length);
226
225
  }
@@ -355,7 +354,7 @@ export default build('tnxel', () => {
355
354
  try {
356
355
  window.tnx.loadingInstance = ElLoading.service(options);
357
356
  this._handleZIndex('.el-loading-mask');
358
- this.app.eventBus.emit('tnx.showLoading', options);
357
+ window.tnx.app.eventBus.emit('tnx.showLoading', options);
359
358
  } catch (e) {
360
359
  window.tnx.loadingInstance = null;
361
360
  console.error(e);
package/src/tnxvue.js CHANGED
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * 基于Vue 3的扩展支持
4
4
  */
5
- // import tnxcore from '@truenewx/tnxcore';
6
- import tnxcore from '../../core/src/tnxcore';
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';