@truenewx/tnxvue3 3.0.4 → 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.
@@ -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>
@@ -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,10 +1,18 @@
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'">
15
+ v-else-if="selector==='dropdown'">
8
16
  <BDropdownItem :active="isSelected(emptyValue)" @click="select(emptyValue)" v-if="empty">
9
17
  <span>{{ emptyText || '&nbsp;' }}</span>
10
18
  </BDropdownItem>
@@ -21,7 +29,7 @@
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"
@@ -49,20 +57,20 @@
49
57
  </template>
50
58
  </BFormSelect>
51
59
  <div class="flex-v-center" v-else>
52
- <Loading/>
60
+ <LoadingIcon/>
53
61
  </div>
54
62
  </template>
55
63
 
56
64
  <script>
57
- import {BDropdown, BDropdownItem, BFormSelect, BFormSelectOption} from 'bootstrap-vue-next';
58
- import Loading from '../loading/Loading.vue';
65
+ import {BFormRadioGroup, BDropdown, BDropdownItem, BFormSelect, BFormSelectOption} from 'bootstrap-vue-next';
66
+ import LoadingIcon from '../loading-icon/LoadingIcon.vue';
59
67
 
60
68
  export const isMultiSelector = function (selector) {
61
69
  return selector === 'checkbox' || selector === 'tags' || selector === 'multi-select' || selector === 'texts';
62
70
  }
63
71
  export default {
64
72
  name: 'TnxbsvSelect',
65
- components: {BDropdown, BDropdownItem, BFormSelect, BFormSelectOption, Loading},
73
+ components: {BFormRadioGroup, BDropdown, BDropdownItem, BFormSelect, BFormSelectOption, LoadingIcon},
66
74
  props: {
67
75
  id: [Number, String],
68
76
  modelValue: {
@@ -349,4 +357,9 @@ export default {
349
357
  .tnxbsv-select[variant="danger"]:focus {
350
358
  box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);
351
359
  }
360
+
361
+ .tnxbsv-radio-group.btn-group > .btn {
362
+ flex: none;
363
+ --bs-btn-padding-x: 1rem;
364
+ }
352
365
  </style>
@@ -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>
@@ -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);
@@ -34,3 +46,27 @@
34
46
  .accordion-body {
35
47
  padding: 1rem;
36
48
  }
49
+
50
+ .toast {
51
+ width: fit-content;
52
+ min-width: 5rem;
53
+ text-align: center;
54
+ }
55
+
56
+ .toast::before {
57
+ content: '';
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ width: 100%;
62
+ height: 100%;
63
+ background-color: rgba(255, 255, 255, 0.3);
64
+ }
65
+
66
+ .form-check {
67
+ margin-top: 0.125rem;
68
+ }
69
+
70
+ .form-check > * {
71
+ cursor: pointer;
72
+ }
@@ -7,20 +7,37 @@ 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';
16
18
  import QueryTable from './query-table/QueryTable.vue';
19
+ import RegionCascader from './region-cascader/RegionCascader.vue';
17
20
  import Select from './select/Select.vue';
21
+ import SubmitForm from './submit-form/SubmitForm.vue';
22
+ import TagsInput from './tags-input/TagsInput.vue';
18
23
 
19
24
  export const build = tnxvue.build;
20
25
 
21
26
  export default build('tnxbsv', () => {
22
27
  const components = Object.assign({}, tnxvue.components, {
23
- Button, Dialog, EnumSelect, Form, FormGroup, Loading, Paged, QueryTable, Select,
28
+ Button,
29
+ Cascader,
30
+ Dialog,
31
+ EnumSelect,
32
+ Form,
33
+ FormGroup,
34
+ LoadingIcon,
35
+ Paged,
36
+ QueryTable,
37
+ RegionCascader,
38
+ Select,
39
+ SubmitForm,
40
+ TagsInput,
24
41
  });
25
42
 
26
43
  const tnxbsv = Object.assign({}, tnxjq, tnxvue, {
@@ -54,6 +71,98 @@ export default build('tnxbsv', () => {
54
71
  this._dialogs.push(dialog);
55
72
  return dialogVm;
56
73
  },
74
+ _closeMessage() {
75
+ this.hideLoading();
76
+ this.removeToast();
77
+ },
78
+ toast(message, timeout, callback, options = {}) {
79
+ if (typeof timeout === 'function') {
80
+ options = callback || {};
81
+ callback = timeout;
82
+ timeout = undefined;
83
+ }
84
+
85
+ this._closeMessage();
86
+
87
+ const div = document.createElement('div');
88
+ document.body.appendChild(div);
89
+
90
+ const Vue = window.tnx.libs.Vue;
91
+ const ToastComponent = {
92
+ components: {
93
+ BToast: BootstrapVue.BToast
94
+ },
95
+ setup() {
96
+ const visible = Vue.ref(true);
97
+ Vue.onMounted(() => {
98
+ setTimeout(() => {
99
+ visible.value = false;
100
+ // 延迟移除组件,确保动画效果完成
101
+ setTimeout(() => {
102
+ window.tnx.toastInstance?.unmount();
103
+ try {
104
+ document.body.removeChild(div);
105
+ } catch (e) {
106
+ // 忽略异常
107
+ }
108
+ }, 500);
109
+ }, timeout || 1500);
110
+ });
111
+ return {visible};
112
+ },
113
+ render() {
114
+ return Vue.h(BootstrapVue.BToast, {
115
+ modelValue: this.visible,
116
+ variant: options.type || 'success',
117
+ static: true,
118
+ noCloseButton: true,
119
+ class: 'position-fixed',
120
+ style: {
121
+ top: '50%',
122
+ left: '50%',
123
+ transform: 'translate(-50%, -50%)',
124
+ zIndex: window.tnx.util.dom.minTopZIndex(),
125
+ }
126
+ }, () => message);
127
+ }
128
+ };
129
+
130
+ const instance = window.tnx.createVueInstance(ToastComponent);
131
+ instance.mount(div);
132
+ window.tnx.toastInstance = instance;
133
+ },
134
+ removeToast() {
135
+ if (window.tnx.toastInstance) {
136
+ window.tnx.toastInstance.unmount();
137
+ try {
138
+ document.body.removeChild(window.tnx.toastInstance._container);
139
+ } catch (e) {
140
+ // 忽略异常
141
+ }
142
+ window.tnx.toastInstance = null;
143
+ }
144
+ },
145
+ showLoading(message = '', options) {
146
+ this._closeMessage();
147
+
148
+ let div = document.createElement('div');
149
+ document.body.appendChild(div);
150
+ let instance = window.tnx.createVueInstance(LoadingOverlay, null, {message});
151
+ let component = instance.mount(div);
152
+ window.tnx.loadingInstance = instance;
153
+ window.tnx.app.eventBus.emit('tnx.showLoading', options);
154
+ return component;
155
+ },
156
+ hideLoading() {
157
+ if (window.tnx.loadingInstance) {
158
+ window.tnx.loadingInstance.unmount();
159
+ document.body.removeChild(window.tnx.loadingInstance._container);
160
+ window.tnx.loadingInstance = null;
161
+ }
162
+ },
163
+ closeLoading() {
164
+ this.hideLoading();
165
+ },
57
166
  });
58
167
 
59
168
  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);