@neatui/vue 1.0.0

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.
Files changed (100) hide show
  1. package/README.md +139 -0
  2. package/package.json +26 -0
  3. package/src/components/basic/IDraggable.vue +84 -0
  4. package/src/components/basic/IDraggable@b.vue +80 -0
  5. package/src/components/basic/IFollowView.vue +172 -0
  6. package/src/components/basic/IFollowView@v2.vue +188 -0
  7. package/src/components/basic/IPickerView.vue +351 -0
  8. package/src/components/basic/IRouterView@v2.vue.backup +119 -0
  9. package/src/components/basic/IRouterView@v3.vue.backup +252 -0
  10. package/src/components/basic/IRouterView@v4.vue +362 -0
  11. package/src/components/basic/IScrollView.vue +127 -0
  12. package/src/components/basic/Icon.vue +54 -0
  13. package/src/components/basic/LayerView/Layer.vue +333 -0
  14. package/src/components/basic/LayerView/index.ts +5 -0
  15. package/src/components/basic/follow.ts +119 -0
  16. package/src/components/basic/index.ts +7 -0
  17. package/src/components/display/Avatar.vue +1 -0
  18. package/src/components/display/Badge.vue +1 -0
  19. package/src/components/display/Calendar.vue +245 -0
  20. package/src/components/display/CalendarReg.vue +245 -0
  21. package/src/components/display/Card.vue +1 -0
  22. package/src/components/display/Carousel.vue +1 -0
  23. package/src/components/display/Collapse.vue +1 -0
  24. package/src/components/display/Desriptions.vue +1 -0
  25. package/src/components/display/Empty.vue +1 -0
  26. package/src/components/display/Image.vue +112 -0
  27. package/src/components/display/List.vue +1 -0
  28. package/src/components/display/PhotoEditor.vue +181 -0
  29. package/src/components/display/PhotoViewer.vue +50 -0
  30. package/src/components/display/Popover.vue +1 -0
  31. package/src/components/display/QRCode.vue +1 -0
  32. package/src/components/display/Segmented.vue +1 -0
  33. package/src/components/display/Statistic.vue +1 -0
  34. package/src/components/display/Table.vue +1 -0
  35. package/src/components/display/Tabs.vue +1 -0
  36. package/src/components/display/Tag.vue +1 -0
  37. package/src/components/display/Timeline.vue +1 -0
  38. package/src/components/display/Tooltip.vue +1 -0
  39. package/src/components/display/Tour.vue +1 -0
  40. package/src/components/display/Tree.vue +431 -0
  41. package/src/components/display/TreeView.vue +222 -0
  42. package/src/components/display/index.ts +7 -0
  43. package/src/components/form/Cascader.vue +435 -0
  44. package/src/components/form/DatePicker.vue +124 -0
  45. package/src/components/form/DateRangePicker@v2.vue.backup +224 -0
  46. package/src/components/form/DateRangePicker@v3.vue +116 -0
  47. package/src/components/form/DateRangeView@v3.vue +386 -0
  48. package/src/components/form/DateView.vue +231 -0
  49. package/src/components/form/DateView@v2.vue +386 -0
  50. package/src/components/form/DateView@v3.vue +471 -0
  51. package/src/components/form/EditUpload.vue +4 -0
  52. package/src/components/form/ImgUpload.vue +174 -0
  53. package/src/components/form/Input.vue.backup +230 -0
  54. package/src/components/form/Input@v3.vue +267 -0
  55. package/src/components/form/InputNumber.vue +200 -0
  56. package/src/components/form/InputRange.vue +235 -0
  57. package/src/components/form/MoreSelect.vue.backup +144 -0
  58. package/src/components/form/MoreSelect@v3.vue +190 -0
  59. package/src/components/form/MoreSelectList.vue +120 -0
  60. package/src/components/form/MoreSelectPanel@v3.vue +190 -0
  61. package/src/components/form/MoreSelectPicker.vue +119 -0
  62. package/src/components/form/MoreSelectTags.vue +119 -0
  63. package/src/components/form/PageMoreSelect.vue +187 -0
  64. package/src/components/form/PageSelect.vue +189 -0
  65. package/src/components/form/SearchMoreSelect.vue +168 -0
  66. package/src/components/form/SearchSelect.vue.backup +194 -0
  67. package/src/components/form/SearchSelect@v3.vue +202 -0
  68. package/src/components/form/Select@v3.vue +201 -0
  69. package/src/components/form/SelectList.vue +58 -0
  70. package/src/components/form/SelectPicker.vue +97 -0
  71. package/src/components/form/SelectTags.vue +52 -0
  72. package/src/components/form/SelectTree/SelectTree@v1.vue +227 -0
  73. package/src/components/form/Switch.vue +135 -0
  74. package/src/components/form/TextArea.vue +171 -0
  75. package/src/components/form/TimePicker.vue +11 -0
  76. package/src/components/form/TimeView.vue +244 -0
  77. package/src/components/form/Upload.vue +349 -0
  78. package/src/components/form/index.ts +82 -0
  79. package/src/components/loader/FormLoader/FormLoader@v2.vue +422 -0
  80. package/src/components/loader/FormLoader/FormLoader@v3.vue.backup +318 -0
  81. package/src/components/loader/FormLoader/index.ts +2 -0
  82. package/src/components/loader/LimitLoader/LimitLoader.vue.backup +131 -0
  83. package/src/components/loader/LimitLoader/LimitLoader@v2.vue.backup +174 -0
  84. package/src/components/loader/LimitLoader/LimitLoader@v3.vue +183 -0
  85. package/src/components/loader/LimitLoader/index.ts +2 -0
  86. package/src/components/loader/TableLoader/TableColView.vue +115 -0
  87. package/src/components/loader/TableLoader/TableLoader.vue +356 -0
  88. package/src/components/loader/TableLoader/index.ts +2 -0
  89. package/src/components/loader/ViewLoader/ViewLoader@v1.vue +256 -0
  90. package/src/components/loader/ViewLoader/index.ts +2 -0
  91. package/src/components/loader/index.ts +5 -0
  92. package/src/components/tools/FormDraftsView.vue +308 -0
  93. package/src/components/tools/FormVerifyView.vue +206 -0
  94. package/src/components/tools/MoreTools.vue +74 -0
  95. package/src/components/tools/MoreTools@v2.vue +74 -0
  96. package/src/components/tools/Pagination@a.vue +222 -0
  97. package/src/components/tools/Pagination@b.vue +221 -0
  98. package/src/components/tools/index.ts +5 -0
  99. package/src/index.ts +5 -0
  100. package/tsconfig.json +24 -0
@@ -0,0 +1,181 @@
1
+ <template>
2
+ <div class="photo-editor bg-fore n-mm">
3
+ <div class="mt-mm" ui-note="@b risk">抠图和压缩处理需要较长时间,期间请耐心等待不要关闭本窗口或进行操作,本功能需要付费,请尽量珍惜每一次处理避免不必要的浪费~</div>
4
+ <div v-if="state.img" class="photo-editor-viewer bg-weak my-mm" ui-flex="row">
5
+ <div class="mr-mm" style="flex: 1">
6
+ <vue-cropper ref="cropperRef" :img="state.img" :autoCrop="state.autoCrop" @realTime="realTime"></vue-cropper>
7
+ </div>
8
+ <div style="flex: 1" class="photo-editor-transparent">
9
+ <img :src="state.previews || ''" />
10
+ </div>
11
+ </div>
12
+ <div v-else class="photo-editor-viewer bg-weak my-mm" ui-flex="row cm">
13
+ <input type="file" class="input pa" @change="change" />
14
+ <div class="ac input-icon">
15
+ <i class="icon icon-upload fs-ml"></i>
16
+ <div class="fs-sl">请先上传图片</div>
17
+ </div>
18
+ </div>
19
+ <div ui-flex="row cm" class="mr-ls-sub">
20
+ <button ui-btn="@a s none :square" class="nx-mm" @click="reset">还原</button>
21
+ <button ui-btn="@a s none :square" class="nx-mm" @click="crop">
22
+ <i v-if="state.cropLoad" className="icon icon-loading am-r360"></i>
23
+ 抠图
24
+ </button>
25
+ <button ui-btn="@a s none :square" class="nx-mm" @click="use">使用</button>
26
+ </div>
27
+ <div class="fekit-layer-close" @click="LayerById(layerId).hide()"></div>
28
+ </div>
29
+ </template>
30
+ <script setup lang="ts">
31
+ import { ref, reactive } from 'vue';
32
+ import { VueCropper } from 'vue-cropper/next';
33
+ import Compressor from 'compressorjs';
34
+ import { LayerById } from '../basic';
35
+ import axios from 'axios';
36
+ import 'vue-cropper/next/dist/index.css';
37
+
38
+ const props: any = defineProps({
39
+ layerId: {
40
+ type: String,
41
+ default: 'PhotoEditor'
42
+ }
43
+ });
44
+
45
+ const state = reactive({
46
+ img: null as any, //blob格式
47
+ // outputType: 'jepg', // 裁剪生成图片格式
48
+ autoCrop: true,
49
+ previews: null, //base格式
50
+ cropLoad: 0
51
+ });
52
+
53
+ const emit = defineEmits(['fileChange']);
54
+
55
+ const cropperRef = ref<any>(null);
56
+
57
+ const change = async ({ target: { files = [] } = {} }: any = {}) => {
58
+ const file = files[0];
59
+ const blob: any = await new Promise((resolve, reject) => {
60
+ new Compressor(file, {
61
+ quality: 1,
62
+ checkOrientation: false,
63
+ success: resolve,
64
+ maxWidth: 1000,
65
+ maxHeight: 700,
66
+ error: reject
67
+ });
68
+ });
69
+ const _file = blobToFile(blob);
70
+ const _blob: any = URL.createObjectURL(_file);
71
+ state.img = _blob;
72
+ };
73
+
74
+ // 实时响应预览图片
75
+ const realTime = (data: any) => {
76
+ cropperRef.value.getCropBlob((data: any) => {
77
+ blobToDataURI(data, (res: any) => {
78
+ state.previews = res;
79
+ });
80
+ });
81
+ };
82
+
83
+ //还原
84
+ const reset = () => {
85
+ cropperRef.value.clearCrop(); // 清除截图信息
86
+ cropperRef.value.goAutoCrop(); // 自动生成截图框函数
87
+ };
88
+
89
+ //抠图
90
+ const crop = async () => {
91
+ if (!state.cropLoad) {
92
+ state.cropLoad = 1;
93
+
94
+ const formData = {
95
+ image_file_b64: state.previews,
96
+ image_url: '',
97
+ size: 'preview',
98
+ type: 'auto',
99
+ type_level: '1',
100
+ format: 'auto',
101
+ roi: '0% 0% 100% 100%',
102
+ crop: false,
103
+ crop_margin: '0',
104
+ scale: 'original',
105
+ position: 'original',
106
+ channels: 'rgba',
107
+ add_shadow: false,
108
+ semitransparency: true,
109
+ bg_color: '',
110
+ bg_image_url: ''
111
+ };
112
+ const res: any = await axios.post(
113
+ 'https://api.remove.bg/v1.0/removebg',
114
+ { ...formData },
115
+ { responseType: 'arraybuffer', headers: { 'X-Api-Key': 'kg4jrcKZd6Gjdx5CmBYmksuN' } }
116
+ );
117
+ if (res.status === 200) {
118
+ const blob = new Blob([res.data], { type: 'image/png' });
119
+ blobToDataURI(blob, (data: any) => {
120
+ state.previews = data;
121
+ state.cropLoad = 0;
122
+ });
123
+ }
124
+ }
125
+ };
126
+
127
+ //使用
128
+ const use = () => {
129
+ const file = base64ToFile(state.previews);
130
+ LayerById(props.layerId).hide();
131
+ emit('fileChange', file);
132
+ };
133
+
134
+ //blob 转 base64
135
+ const blobToDataURI = (blob: any, callback: any) => {
136
+ const reader = new FileReader();
137
+ reader.readAsDataURL(blob);
138
+ reader.onload = function (e: any) {
139
+ callback(e.target.result);
140
+ };
141
+ };
142
+ //base64 转 file
143
+ const base64ToFile = (data: any) => {
144
+ const arr = data.split(','),
145
+ mime = arr[0].match(/:(.*?);/)[1],
146
+ bstr = atob(arr[1]);
147
+ let n = bstr.length;
148
+ const u8arr = new Uint8Array(n);
149
+ while (n--) {
150
+ u8arr[n] = bstr.charCodeAt(n);
151
+ }
152
+ const blob = new Blob([u8arr], { type: mime });
153
+ const type = mime.replace(/image\//, '');
154
+ const file = new File([blob], new Date().getTime() + `.${type}`);
155
+ return file;
156
+ };
157
+ //blob转file
158
+ const blobToFile = (blob: any) => {
159
+ const { name = '' } = blob || {};
160
+ const file = new File([blob], name);
161
+ return file;
162
+ };
163
+ </script>
164
+ <style lang="scss">
165
+ .photo-editor {
166
+ &-viewer {
167
+ height: 27rem;
168
+ }
169
+ .input {
170
+ opacity: 0;
171
+ height: 100%;
172
+ width: 100%;
173
+ }
174
+ .input-icon {
175
+ line-height: 1.5;
176
+ }
177
+ &-transparent {
178
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAIElEQVQoU2NkQAOXLl36jyzESAcF6Hbq6emhWMtIewUAeQ8d3cWD32MAAAAASUVORK5CYII=);
179
+ }
180
+ }
181
+ </style>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <div class="full nx-mm">
3
+ <Swiper
4
+ :slidesPerView="1"
5
+ :initialSlide="initialSlide"
6
+ :spaceBetween="30"
7
+ :keyboard="{
8
+ enabled: true
9
+ }"
10
+ :pagination="{
11
+ clickable: true
12
+ }"
13
+ :navigation="true"
14
+ :modules="[Keyboard, Pagination, Navigation]"
15
+ class="mySwiper"
16
+ >
17
+ <SwiperSlide v-for="item in files" :key="item.id" class="ac" ui-flex="col cm">
18
+ <img :src="item[srcName]" class="vc" />
19
+ </SwiperSlide>
20
+ </Swiper>
21
+ <div class="fekit-layer-close close-img m-mm" @click="LayerById(id).hide()"></div>
22
+ </div>
23
+ </template>
24
+ <script setup lang="ts">
25
+ import { Swiper, SwiperSlide } from 'swiper/vue';
26
+ import { Keyboard, Pagination, Navigation } from 'swiper/modules';
27
+ import { LayerById } from '../basic';
28
+ import 'swiper/css';
29
+ import 'swiper/css/pagination';
30
+ import 'swiper/css/navigation';
31
+
32
+ const props: any = defineProps({
33
+ srcName: {
34
+ type: String,
35
+ default: 'imgUrl'
36
+ },
37
+ id: {
38
+ type: String,
39
+ default: 'PhotoViewer'
40
+ },
41
+ initialSlide: {
42
+ type: Number,
43
+ default: 0
44
+ },
45
+ files: {
46
+ type: Array as () => any[],
47
+ default: () => []
48
+ }
49
+ });
50
+ </script>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1 @@
1
+ <template><div></div></template>
@@ -0,0 +1,431 @@
1
+ <template>
2
+ <ul :class="`${root ? '' : 'nl-ms'}`">
3
+ <li v-if="root" class="pr">
4
+ <div ui-form="@a sz: co:risk type:none :block" class="full-w">
5
+ <div style="left: 0.75em" :ui-form-tips="state.error.ontips">{{ state.error.tips }}</div>
6
+ </div>
7
+ </li>
8
+ <li v-for="node in state.tree" :key="node[options.id || 'id']" style="cursor: pointer">
9
+ <div ui-flex="row xm" :class="block ? 'ux-hover' : ''">
10
+ <div
11
+ @click="node._fold = !node._fold"
12
+ :class="`triangle ${node._fold ? '' : 'icon-rotate'}`"
13
+ :style="{ visibility: node[options.children || 'children']?.length ? 'visible' : 'hidden' }"
14
+ ></div>
15
+ <div ui-flex="row lm" class="flex-block" @click="select(node)">
16
+ <!-- 多选 -->
17
+ <label v-if="multiple" ui-form="@a type:checkbox sz:m" :class="block ? 'full' : ''">
18
+ <input
19
+ type="checkbox"
20
+ :disabled="node.disabled"
21
+ :checked="getCheckedStatus(node) === 'all'"
22
+ :class="getCheckedStatus(node) === 'part' ? 'part-checked' : ''"
23
+ @change="(e: any) => nodeClick(node, e.target.checked)"
24
+ />
25
+ <div ui-form-prefix="">
26
+ <template v-for="(p, idx) in node.prefix" :key="idx">
27
+ <div v-if="isObject(p)" v-bind="p.attrs">
28
+ <div v-html="isFunction(p.value) ? p.value(node) : p.value"></div>
29
+ </div>
30
+ <div v-else v-html="isFunction(p) ? p(node) : p"></div>
31
+ </template>
32
+ <slot name="prefix" :data="node"></slot>
33
+ </div>
34
+ <span v-bind="node.attrs" class="ux-hover nx-ss" style="color: unset; border-radius: 6px">{{ node[options.label || 'label'] }}</span>
35
+ <div v-if="!multiple" ui-form-suffix="">
36
+ <template v-for="(s, idx) in node.suffix" :key="idx">
37
+ <div v-if="isObject(s)" v-bind="s.attrs">
38
+ <div v-html="isFunction(s.value) ? s.value(node) : s.value"></div>
39
+ </div>
40
+ <div v-else v-html="isFunction(s) ? s(node) : s"></div>
41
+ </template>
42
+ <slot name="suffix" :data="node"></slot>
43
+ </div>
44
+ </label>
45
+ <!-- 单选 -->
46
+ <template v-if="!multiple">
47
+ <div ui-form-prefix="">
48
+ <template v-for="(p, idx) in node.prefix" :key="idx">
49
+ <div v-if="isObject(p)" v-bind="p.attrs">
50
+ <div v-html="isFunction(p.value) ? p.value(node) : p.value"></div>
51
+ </div>
52
+ <div v-else v-html="isFunction(p) ? p(node) : p"></div>
53
+ </template>
54
+ <slot name="prefix" :data="node"></slot>
55
+ </div>
56
+ <span
57
+ style="border-radius: 6px"
58
+ class="ux-hover nx-ss"
59
+ :class="node[options.id || 'id'] === (isArray(props.modelValue) ? props.modelValue[0] : props.modelValue) ? 'co-main' : ''"
60
+ @click="nodeClick(node)"
61
+ >{{ node[options.label || 'label'] }}</span
62
+ >
63
+ <div ui-form-suffix="">
64
+ <template v-for="(s, idx) in node.suffix" :key="idx">
65
+ <div v-if="isObject(s)" v-bind="s.attrs">
66
+ <div v-html="isFunction(s.value) ? s.value(node) : s.value"></div>
67
+ </div>
68
+ <div v-else v-html="isFunction(s) ? s(node) : s"></div>
69
+ </template>
70
+ <slot name="suffix" :data="node"></slot>
71
+ </div>
72
+ </template>
73
+ </div>
74
+ <slot name="tools" :data="node"></slot>
75
+ </div>
76
+ <template v-if="node[options.children || 'children']?.length > 0 && node._fold">
77
+ <TreeNode :key="node[options.id || 'id']" v-model="state.selected" v-bind="$props" :treeData="node[options.children || 'children']" :root="false" @select="tranSelect">
78
+ <template #tools="{ data = {} }: any = {}">
79
+ <slot name="tools" :data="data"></slot>
80
+ </template>
81
+ <template #suffix="{ data = {} }: any = {}">
82
+ <slot name="suffix" :data="data"></slot>
83
+ </template>
84
+ <template #prefix="{ data = {} }: any = {}">
85
+ <slot name="prefix" :data="data"></slot>
86
+ </template>
87
+ </TreeNode>
88
+ </template>
89
+ </li>
90
+ </ul>
91
+ </template>
92
+
93
+ <script lang="ts" setup>
94
+ import { reactive, computed, ref, watch } from 'vue';
95
+ import TreeNode from './Tree.vue';
96
+ import { isObject, isFunction, isArray, deepcopy } from '@fekit/utils';
97
+ import { useRoute, useRouter } from 'vue-router';
98
+
99
+ /**
100
+ * 入参说明
101
+ * modelValue 值
102
+ * theme 主题
103
+ * sz 尺寸
104
+ * co 颜色
105
+ * options 字段配置信息
106
+ * expandedKeys 默认展开的节点
107
+ * multiple 是否复选
108
+ * treeData 树形结构数据
109
+ * autoExpandSelected 是否展开选中的节点
110
+ * defaultExpandAll 是否默认展开所有子节点
111
+ * checkStrictly multiple 状态下节点选择完全受控(父子节点选中状态不再关联)
112
+ * root 是否为跟节点(外部不需要传入)
113
+ * line 是否有线条
114
+ */
115
+ // 类型
116
+ interface Props {
117
+ modelValue?: Array<string | number> | string | number;
118
+ options?: PropsOptions;
119
+ theme?: string;
120
+ sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
121
+ co?: string;
122
+ expandedKeys?: Array<string | number>;
123
+ autoExpandSelected?: boolean;
124
+ defaultExpandAll?: boolean;
125
+ multiple?: boolean;
126
+ treeData?: Array<Record<string, any>>;
127
+ checkStrictly?: boolean;
128
+ // attrs?: Record<string, any>;
129
+ root?: boolean;
130
+ line?: boolean;
131
+ interacted?: boolean;
132
+ rules?: Array<object>;
133
+ block?: boolean;
134
+ }
135
+
136
+ interface PropsOptions {
137
+ label?: string;
138
+ id?: string;
139
+ children?: string;
140
+ }
141
+
142
+ const props: any = withDefaults(defineProps<Props>(), {
143
+ modelValue: '',
144
+ expandedKeys: () => [],
145
+ theme: '@a',
146
+ sz: '',
147
+ co: '',
148
+ autoExpandSelected: false,
149
+ defaultExpandAll: true,
150
+ multiple: false,
151
+ checkStrictly: false,
152
+ treeData: () => [],
153
+ options: () => ({ label: 'label', id: 'id', children: 'children' }),
154
+ // attrs: () => ({}),
155
+ root: true,
156
+ line: false,
157
+ interacted: false,
158
+ rules: () => [],
159
+ block: false
160
+ });
161
+
162
+ /*
163
+ 优先级
164
+ 展开传入的expandkeys节点 - expandedKeys数组不为空 -- 传入的需要展开的子节点是当前节点或者是当前节点的子节点
165
+ 展开所有 - defaultExpandAll = true && expandedKeys数组为空
166
+ 展开选中节点 - autoExpandSelected = true && defaultExpandAll = false && expandedKeys数组为空
167
+ */
168
+
169
+ const route = useRoute();
170
+ const router = useRouter();
171
+
172
+ const emit = defineEmits(['update:modelValue', 'select']);
173
+ const change = (item: Array<string | number>) => {
174
+ const _item = props.multiple ? item : isArray(item) ? item[0] : item;
175
+ emit('update:modelValue', _item);
176
+ };
177
+
178
+ // 判断当前节点&&当前节点的子节点,是否存在于目标节点数组中
179
+ const handleExpand = (targetKeys: Array<string | number>, node: any) => {
180
+ if (targetKeys.includes(node[props.options.id])) {
181
+ return true;
182
+ } else {
183
+ // 递归判断子节点是否存在于目标节点数组
184
+ let childNodeVerify = false;
185
+ const childKey: string = props.options.children || 'children';
186
+ const idKey = props.options.id || 'id';
187
+ const deepMap = (opts: any) => {
188
+ opts.map((o: any) => {
189
+ if (targetKeys.includes(o[idKey])) {
190
+ childNodeVerify = true;
191
+ } else {
192
+ if (o[childKey]?.length > 0) deepMap(o[childKey]);
193
+ }
194
+ });
195
+ };
196
+ if (node[childKey]?.length > 0) deepMap(node[childKey]);
197
+ return childNodeVerify;
198
+ }
199
+ };
200
+
201
+ /*
202
+ 寻找某个节点对应的子节点
203
+ type=bottom 只查找最底部子节点
204
+ type=all 查找此节点以下所有子节点
205
+ */
206
+ const searchChildNode = (n: any, type = 'bottom') => {
207
+ const childNodes: any = [];
208
+ const childKey = props.options.children || 'children';
209
+ // 递归查找子节点
210
+ const deepMap = (opts: any) => {
211
+ if (opts[childKey]?.length > 0) {
212
+ opts[childKey].map((item: any) => {
213
+ if (type === 'all') childNodes.push(opts[props.options.id || 'id']);
214
+ deepMap(item);
215
+ });
216
+ } else {
217
+ childNodes.push(opts[props.options.id || 'id']);
218
+ }
219
+ };
220
+ deepMap(n);
221
+ return childNodes;
222
+ };
223
+
224
+ const state: any = reactive({
225
+ selected: [] as Array<string | number>,
226
+ // 错误
227
+ error: {},
228
+ // 用户是否交互
229
+ interacted: false,
230
+ tree: [] as Array<Record<string, any>>
231
+ });
232
+
233
+ watch(
234
+ () => props.treeData,
235
+ () => {
236
+ const _tree = deepcopy(props.treeData);
237
+ state.tree = _tree?.map((n: any) => {
238
+ if (props.expandedKeys?.length > 0) {
239
+ n._fold = handleExpand(props.expandedKeys, n);
240
+ } else if (props.defaultExpandAll) {
241
+ n._fold = true;
242
+ } else if (props.autoExpandSelected) {
243
+ n._fold = handleExpand(state.selected, n);
244
+ } else {
245
+ n._fold = false;
246
+ }
247
+ return n;
248
+ });
249
+ },
250
+ { deep: true, immediate: true }
251
+ );
252
+
253
+ // 获取节点勾选状态
254
+ const getCheckedStatus = (node: any) => {
255
+ if (props.multiple) {
256
+ if (!props.checkStrictly) {
257
+ const childNodes = searchChildNode(node);
258
+ const all = childNodes.every((n: any) => state.selected.includes(n));
259
+ const part = childNodes.some((n: any) => state.selected.includes(n));
260
+ return !all && !part ? 'none' : all ? 'all' : 'part';
261
+ } else {
262
+ return state.selected.includes(node[props.options.id || 'id']) ? 'all' : 'none';
263
+ }
264
+ } else {
265
+ return '';
266
+ }
267
+ };
268
+
269
+ // 节点点击事件
270
+ const nodeClick = (node: any, checked = true) => {
271
+ const idKey: string = props.options.id || 'id';
272
+ if (props.multiple) {
273
+ const childNodes = searchChildNode(node, props.checkStrictly ? 'all' : 'boottom');
274
+ if (checked) {
275
+ state.selected = [...new Set([...state.selected, ...childNodes])];
276
+ } else {
277
+ state.selected = state.selected.filter((n: any) => !childNodes.includes(n));
278
+ }
279
+ } else {
280
+ // 单选
281
+ state.selected = [node[idKey]];
282
+ }
283
+ };
284
+
285
+ const select = (node: any) => {
286
+ emit('select', { node, data: state.selected, route, router });
287
+ };
288
+
289
+ const tranSelect = ({ node, data, route, router }: any) => {
290
+ emit('select', { node, data, route, router });
291
+ };
292
+
293
+ watch(
294
+ [() => props.modelValue, () => state.selected],
295
+ (n: any, o: any) => {
296
+ if (n[0] !== o[0]) {
297
+ state.selected = isArray(props.modelValue) ? props.modelValue : props.modelValue ? [props.modelValue] : [];
298
+ }
299
+ if (n[1] !== o[1]) {
300
+ change(state.selected);
301
+ }
302
+ },
303
+ { deep: true, immediate: true }
304
+ );
305
+
306
+ // 校验
307
+ watch(
308
+ [() => state.interacted, () => state.selected],
309
+ async () => {
310
+ if (state.interacted || props.interacted) {
311
+ // 提示文案
312
+ let tips = '';
313
+ // 错误类型
314
+ let type = '';
315
+ // 验证结果
316
+ let verify = '';
317
+
318
+ if (state.selected?.length) {
319
+ // 有值
320
+ let hasError = false;
321
+ // 遍历校验规则
322
+ for (const rule of props.rules) {
323
+ if (rule && rule.validator) {
324
+ const yes = !(await rule.validator(state.selected));
325
+ if (yes) {
326
+ tips = rule.msg;
327
+ type = rule.type;
328
+ hasError = true;
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ verify = hasError ? 'no' : 'ok';
334
+ } else {
335
+ // 无值
336
+ let hasError = false;
337
+ // 遍历校验规则
338
+ for (const rule of props.rules) {
339
+ if (rule) {
340
+ tips = rule.msg;
341
+ type = rule.type;
342
+ if (rule.required) {
343
+ hasError = true;
344
+ break;
345
+ }
346
+ }
347
+ }
348
+ verify = hasError ? 'no' : '';
349
+ }
350
+ if (verify !== 'no' && props.tips) {
351
+ tips = props.tips;
352
+ }
353
+ // 是否显示提示
354
+ const ontips = (verify === 'no' && tips) || props.tips ? 'show' : '';
355
+ // 状态颜色
356
+ const co = verify === 'no' ? 'co:' + type : props.co ? 'co:' + props.co : '';
357
+ state.error = { tips, ontips, verify, co };
358
+ } else {
359
+ state.error = { tips: props.tips, ontips: props.tips ? 'show' : '', verify: '', co: '' };
360
+ }
361
+ },
362
+ { deep: true, immediate: true }
363
+ );
364
+ </script>
365
+ <style lang="scss" scoped>
366
+ .triangle {
367
+ position: relative;
368
+ width: 1.25em;
369
+ height: 1em;
370
+ margin-right: -0.25em;
371
+ &::after {
372
+ position: absolute;
373
+ display: inline-block;
374
+ content: '';
375
+ width: 0;
376
+ height: 0;
377
+ border-width: 0.35em 0 0.35em 0.35em;
378
+ border-style: solid;
379
+ border-color: transparent;
380
+ border-left-color: transparent;
381
+ border-left-color: currentColor;
382
+ top: 0.2em;
383
+ left: 0.1em;
384
+ transform: rotate(90deg);
385
+ transition: all 0.25s;
386
+ border-radius: 30%;
387
+ }
388
+ &.icon-rotate {
389
+ &::after {
390
+ transform: rotate(0deg);
391
+ }
392
+ }
393
+ }
394
+ [fekit-json-tree] {
395
+ &,
396
+ * {
397
+ user-select: none;
398
+ line-height: 1.5;
399
+ }
400
+ }
401
+
402
+ // 线条
403
+ [fekit-json-tree-line~='1'] {
404
+ position: relative;
405
+
406
+ &::after {
407
+ position: absolute;
408
+ content: '';
409
+ width: 1px;
410
+ left: 0.5em;
411
+ top: -0.5em;
412
+ bottom: 1.25em;
413
+ background-color: currentColor;
414
+ opacity: 0.15;
415
+ }
416
+
417
+ & > li {
418
+ position: relative;
419
+ &::before {
420
+ content: '';
421
+ position: absolute;
422
+ width: 0.5em;
423
+ height: 1px;
424
+ background: currentColor;
425
+ left: -0.5em;
426
+ top: 1.25em;
427
+ opacity: 0.15;
428
+ }
429
+ }
430
+ }
431
+ </style>