oxy-uni-ui 1.0.1 → 1.1.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 (54) hide show
  1. package/LICENSE +1 -1
  2. package/attributes.json +1 -1
  3. package/components/common/abstracts/variable.scss +32 -4
  4. package/components/common/util.ts +44 -0
  5. package/components/oxy-checkbox/index.scss +36 -6
  6. package/components/oxy-checkbox/oxy-checkbox.vue +5 -4
  7. package/components/oxy-checkbox/types.ts +2 -1
  8. package/components/oxy-col-picker/index.scss +18 -15
  9. package/components/oxy-col-picker/oxy-col-picker.vue +28 -3
  10. package/components/oxy-col-picker/types.ts +12 -0
  11. package/components/oxy-corner/index.scss +138 -0
  12. package/components/oxy-corner/oxy-corner.vue +66 -0
  13. package/components/oxy-corner/types.ts +43 -0
  14. package/components/oxy-drop-menu/index.scss +4 -0
  15. package/components/oxy-drop-menu/oxy-drop-menu.vue +5 -3
  16. package/components/oxy-drop-menu/types.ts +1 -1
  17. package/components/oxy-drop-menu-item/index.scss +4 -4
  18. package/components/oxy-drop-menu-item/oxy-drop-menu-item.vue +2 -0
  19. package/components/oxy-file-list/index.scss +83 -0
  20. package/components/oxy-file-list/oxy-file-list.vue +213 -0
  21. package/components/oxy-file-list/types.ts +54 -0
  22. package/components/oxy-list/index.scss +4 -0
  23. package/components/oxy-list/oxy-list.vue +125 -0
  24. package/components/oxy-list/types.ts +50 -0
  25. package/components/oxy-slider/index.scss +2 -2
  26. package/components/oxy-swiper/index.scss +1 -2
  27. package/components/oxy-textarea/oxy-textarea.vue +0 -4
  28. package/components/oxy-tree/components/tree-node-content.vue +72 -0
  29. package/components/oxy-tree/index.scss +61 -0
  30. package/components/oxy-tree/index.ts +51 -0
  31. package/components/oxy-tree/oxy-tree.vue +289 -0
  32. package/components/oxy-tree/types.ts +48 -0
  33. package/components/oxy-upload/images/audio.png +0 -0
  34. package/components/oxy-upload/images/excle.png +0 -0
  35. package/components/oxy-upload/images/other.png +0 -0
  36. package/components/oxy-upload/images/pdf.png +0 -0
  37. package/components/oxy-upload/images/pic.png +0 -0
  38. package/components/oxy-upload/images/txt.png +0 -0
  39. package/components/oxy-upload/images/video.png +0 -0
  40. package/components/oxy-upload/images/word.png +0 -0
  41. package/components/oxy-upload/index.scss +50 -0
  42. package/components/oxy-upload/oxy-upload.vue +93 -7
  43. package/components/oxy-upload/types.ts +22 -1
  44. package/components/oxy-virtual-scroll/index.scss +35 -0
  45. package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +184 -0
  46. package/components/oxy-virtual-scroll/types.ts +65 -0
  47. package/components/oxy-virtual-scroll/virtual-scroll.ts +81 -0
  48. package/global.d.ts +3 -0
  49. package/locale/lang/ar-SA.ts +2 -1
  50. package/locale/lang/en-US.ts +2 -1
  51. package/locale/lang/zh-CN.ts +2 -1
  52. package/package.json +1 -1
  53. package/tags.json +1 -1
  54. package/web-types.json +1 -1
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <view class="oxy-tree-node-content" :style="getNodeStyle()" :class="computedClass" @click="handleNodeClick">
3
+ <oxy-icon @click.stop="toggleExpand" name="fill-arrow-down" class="oxy-tree-node-icon" size="22px"></oxy-icon>
4
+ <oxy-checkbox
5
+ v-if="treeProps?.showCheckbox"
6
+ :modelValue="node.checked"
7
+ :disabled="node.disabled"
8
+ :indeterminate="node.immediate"
9
+ shape="square"
10
+ ></oxy-checkbox>
11
+ <view class="oxy-tree-node">
12
+ <slot :node="node" :data="node.data">
13
+ {{ node.label }}
14
+ </slot>
15
+ </view>
16
+ </view>
17
+ </template>
18
+
19
+ <script lang="ts">
20
+ export default {
21
+ name: 'oxy-tree-node-content'
22
+ }
23
+ </script>
24
+
25
+ <script lang="ts" setup>
26
+ import { computed, toRefs, type Ref } from 'vue'
27
+ import type { TextProps, TreeNode } from '../types'
28
+ import { inject } from 'vue'
29
+ // 获取组件的 props 和 emit 函数
30
+ const emit = defineEmits<{
31
+ (e: 'click', node: TreeNode): void
32
+ (e: 'toggle-expand', node: TreeNode, flag: boolean): void
33
+ (e: 'toggle-checked', node: TreeNode, flag: boolean, isClick: boolean): void
34
+ }>()
35
+
36
+ const props = defineProps<{
37
+ node: TreeNode
38
+ }>()
39
+ const treeProps = inject<TextProps>('treeProps')
40
+ const currentNode = inject<Ref<TreeNode | undefined>>('currentNode')
41
+
42
+ function getNodeStyle() {
43
+ return {
44
+ paddingLeft: `${(props.node.level - 1) * (treeProps?.indent || 16)}px`
45
+ }
46
+ }
47
+
48
+ function toggleExpand() {
49
+ emit('toggle-expand', props.node, !props.node.expanded)
50
+ }
51
+
52
+ const computedClass = computed(() => {
53
+ const currentValue = currentNode?.value
54
+ return {
55
+ expanded: props.node.expanded,
56
+ checked: props.node.checked,
57
+ 'is-leaf': props.node.isLeaf,
58
+ immediate: props.node.immediate,
59
+ 'is-current': currentValue === props.node,
60
+ 'is-disabled': props.node.disabled
61
+ }
62
+ })
63
+
64
+ const handleNodeClick = () => {
65
+ emit('click', props.node)
66
+ emit('toggle-checked', props.node, !props.node.checked, true)
67
+ }
68
+ </script>
69
+
70
+ <style lang="scss" scoped>
71
+ @import '../index.scss';
72
+ </style>
@@ -0,0 +1,61 @@
1
+ @import '../common/abstracts/variable';
2
+ @import '../common/abstracts/mixin';
3
+
4
+ .oxy-tree-node-content {
5
+ line-height: 44px;
6
+ height: 44px;
7
+ display: flex;
8
+ width: fit-content;
9
+ width: 100%;
10
+ white-space: nowrap;
11
+ align-items: center;
12
+
13
+ &.expanded {
14
+ .oxy-tree-node-icon {
15
+ transform: rotate(0deg);
16
+ }
17
+ }
18
+
19
+ &.is-leaf {
20
+ .oxy-tree-node-icon {
21
+ opacity: 0;
22
+ }
23
+ }
24
+
25
+ &.is-current {
26
+ background-color: $-tree-node-current-bg;
27
+ color: $-tree-node-current-color;
28
+ }
29
+
30
+ &.is-disabled {
31
+ color: $-tree-node-disabled-color;
32
+ }
33
+
34
+ :deep(.oxy-checkbox) {
35
+ margin-right: 6px;
36
+
37
+ .oxy-checkbox__label {
38
+ display: none;
39
+ }
40
+
41
+ .oxy-checkbox__shape {
42
+ transition: none;
43
+ }
44
+
45
+ .oxy-checkbox__indeterminate {
46
+ transition: none;
47
+ }
48
+
49
+ .oxy-checkbox__check {
50
+ transition: none;
51
+ }
52
+ }
53
+
54
+ .oxy-tree-node-icon {
55
+ transform: rotate(-90deg);
56
+ }
57
+
58
+ .oxy-tree-node {
59
+ flex: 1;
60
+ }
61
+ }
@@ -0,0 +1,51 @@
1
+ import type { TextProps, RawTreeNode } from './types'
2
+
3
+ export const useTreeMethods = (props: TextProps) => {
4
+ function getDisabled(node: RawTreeNode): boolean {
5
+ return node.disabled ?? false
6
+ }
7
+
8
+ function getChildren(node: RawTreeNode): RawTreeNode[] {
9
+ return node[props.childrenKey] ?? []
10
+ }
11
+
12
+ function getLabel(node: RawTreeNode): string {
13
+ return node[props.labelKey] ?? ''
14
+ }
15
+
16
+ function getKey(node: RawTreeNode | undefined): string {
17
+ if (!node) {
18
+ return ''
19
+ }
20
+ return (node[props.valueKey] ?? '') as string
21
+ }
22
+
23
+ return {
24
+ getDisabled,
25
+ getChildren,
26
+ getLabel,
27
+ getKey
28
+ }
29
+ }
30
+ /**
31
+ * 判断两个 Set 中的元素是否完全相同(不考虑顺序)
32
+ * @param {Set} setA - 第一个 Set
33
+ * @param {Set} setB - 第二个 Set
34
+ * @returns {boolean} 两个 Set 是否相同
35
+ */
36
+ export function isSetsEqual(setA: Set<any>, setB: Set<any>) {
37
+ // 步骤1:如果大小不同,直接返回 false
38
+ if (setA.size !== setB.size) {
39
+ return false
40
+ }
41
+
42
+ // 步骤2:遍历 setA 的所有元素,检查 setB 是否都包含
43
+ for (const item of setA) {
44
+ if (!setB.has(item)) {
45
+ return false // 有元素不匹配,返回 false
46
+ }
47
+ }
48
+
49
+ // 所有元素匹配,返回 true
50
+ return true
51
+ }
@@ -0,0 +1,289 @@
1
+ <template>
2
+ <view class="oxy-tree" :class="customClass" :style="customStyle">
3
+ <oxy-virtual-scroll v-if="data.length" :data="flattenTree" :height="maxHeight" item-height="44px" :scrollX="true">
4
+ <template #item="{ item }">
5
+ <treeNodeContent :node="item" @toggle-expand="toggleExpand" @toggle-checked="toggleChecked" @click="handleClick">
6
+ <template #default="{ node, data }">
7
+ <slot :node="node" :data="data"></slot>
8
+ </template>
9
+ </treeNodeContent>
10
+ </template>
11
+ </oxy-virtual-scroll>
12
+ <view v-else>
13
+ <oxy-status-tip image="content" :tip="emptyText" />
14
+ </view>
15
+ </view>
16
+ </template>
17
+
18
+ <script lang="ts">
19
+ export default {
20
+ name: 'oxy-tree',
21
+ options: {
22
+ virtualHost: true,
23
+ addGlobalClass: true,
24
+ styleIsolation: 'shared'
25
+ }
26
+ }
27
+ </script>
28
+
29
+ <script lang="ts" setup>
30
+ import { computed, nextTick, provide, ref, toRefs, watch } from 'vue'
31
+ import type { RawTreeNode, TreeNode } from './types'
32
+ import { textProps } from './types'
33
+ import treeNodeContent from './components/tree-node-content.vue'
34
+ import { isSetsEqual, useTreeMethods } from '.'
35
+
36
+ // 树结构类型定义
37
+ type Tree = {
38
+ treeNodeMap: Map<string, TreeNode>
39
+ levelTreeNodeMap: Map<number, TreeNode[]>
40
+ maxLevel: number
41
+ treeNodes: TreeNode[]
42
+ }
43
+
44
+ // 获取组件的 props 和 emit 函数
45
+ const props = defineProps(textProps)
46
+ const { modelValue, data, defaultExpandedKeys, expandAll, showCheckbox, checkStrictly, selectionLeafOnly } = toRefs(props)
47
+ const emit = defineEmits<{
48
+ (e: 'node-click', node: TreeNode): void
49
+ (e: 'update:modelValue', value: string[] | string | undefined): void
50
+ (e: 'node-check', node: TreeNode): void
51
+ (e: 'node-expand', node: TreeNode): void
52
+ (e: 'node-collapse', node: TreeNode): void
53
+ }>()
54
+
55
+ const { getDisabled, getChildren, getLabel, getKey } = useTreeMethods(props)
56
+ const expandedKeySet = ref<Set<string>>(new Set())
57
+ const hiddenNodeKeySet = ref<Set<string>>(new Set())
58
+ const checkedKeys = ref<Set<string>>(new Set())
59
+ const immediateKeySet = ref<Set<string>>(new Set())
60
+ const currentNode = ref<TreeNode>()
61
+ provide('treeProps', props)
62
+ provide('currentNode', currentNode)
63
+
64
+ const tree = ref<Tree>()
65
+
66
+ const flattenTree = computed<TreeNode[]>(() => {
67
+ const expandedKeys = expandedKeySet.value
68
+ const hiddenKeys = hiddenNodeKeySet.value
69
+ const flattenNodes: TreeNode[] = []
70
+ const nodes = (tree.value && tree.value.treeNodes) || []
71
+ function traverse() {
72
+ const stack: TreeNode[] = []
73
+ for (let i = nodes.length - 1; i >= 0; --i) {
74
+ stack.push(nodes[i])
75
+ }
76
+ while (stack.length) {
77
+ const node = stack.pop()
78
+ if (!node) continue
79
+ if (!hiddenKeys.has(node.key)) {
80
+ flattenNodes.push(node)
81
+ }
82
+ if (expandedKeys.has(node.key)) {
83
+ const children = node.children
84
+ if (children) {
85
+ const length = children.length
86
+ for (let i = length - 1; i >= 0; --i) {
87
+ stack.push(children[i])
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ traverse()
94
+ return flattenNodes
95
+ })
96
+ const createTree = (data: RawTreeNode[]) => {
97
+ const treeNodeMap = new Map<string, TreeNode>()
98
+ const levelTreeNodeMap = new Map<number, TreeNode[]>()
99
+
100
+ let maxLevel = 1
101
+
102
+ function traverse(nodes: RawTreeNode[], level = 1, parent?: TreeNode): TreeNode[] {
103
+ const siblings: TreeNode[] = []
104
+ nodes.forEach((rawNode, index) => {
105
+ const value = getKey(rawNode)
106
+ const node: TreeNode = {
107
+ level,
108
+ key: value,
109
+ data: rawNode,
110
+ index,
111
+ isLast: index === nodes.length - 1,
112
+ expanded: expandedKeySet.value.has(value),
113
+ label: getLabel(rawNode),
114
+ parent: parent,
115
+ isLeaf: false
116
+ }
117
+ if (showCheckbox.value) {
118
+ node.checked = checkedKeys.value.has(value)
119
+ }
120
+ if (expandAll.value) {
121
+ node.expanded = true
122
+ expandedKeySet.value.add(value)
123
+ }
124
+ const children = getChildren(rawNode)
125
+ node.disabled = getDisabled(rawNode)
126
+ node.isLeaf = !children || children.length === 0
127
+ if (children && children.length) {
128
+ node.children = traverse(children, level + 1, node)
129
+ }
130
+ siblings.push(node)
131
+ treeNodeMap.set(value, node)
132
+ if (!levelTreeNodeMap.has(level)) {
133
+ levelTreeNodeMap.set(level, [])
134
+ }
135
+ levelTreeNodeMap.get(level)!.push(node)
136
+ })
137
+
138
+ if (level > maxLevel) {
139
+ maxLevel = level
140
+ }
141
+ return siblings
142
+ }
143
+ const treeNodes = traverse(data)
144
+
145
+ return {
146
+ treeNodeMap,
147
+ levelTreeNodeMap,
148
+ maxLevel,
149
+ treeNodes
150
+ }
151
+ }
152
+
153
+ const toggleExpand = (node: TreeNode, flag: boolean) => {
154
+ node.expanded = flag
155
+ const key = node.key
156
+ if (node.expanded) {
157
+ expandedKeySet.value.add(key)
158
+ emit('node-expand', node)
159
+ } else {
160
+ expandedKeySet.value.delete(key)
161
+ emit('node-collapse', node)
162
+ }
163
+ }
164
+ const toggleChecked = (node: TreeNode, flag: boolean, isClick: boolean = false) => {
165
+ if (!showCheckbox.value || !node || node.disabled) {
166
+ return
167
+ }
168
+
169
+ function toggle(currentNode: TreeNode) {
170
+ const key = currentNode.key
171
+ currentNode.checked = flag
172
+ currentNode.immediate = false
173
+
174
+ if (currentNode.checked && (!selectionLeafOnly.value || currentNode.isLeaf)) {
175
+ checkedKeys.value.add(key)
176
+ } else {
177
+ checkedKeys.value.delete(key)
178
+ }
179
+ if (!checkStrictly.value && currentNode.children) {
180
+ currentNode.children.forEach((child) => {
181
+ toggle(child)
182
+ })
183
+ }
184
+ }
185
+ toggle(node)
186
+ !checkStrictly.value && updateParentNode(node)
187
+ updateValue()
188
+ isClick && emit('node-check', node)
189
+ }
190
+ const handleClick = (node: TreeNode) => {
191
+ emit('node-click', node)
192
+
193
+ if (node.disabled) return
194
+ currentNode.value = node
195
+
196
+ if (!showCheckbox.value) {
197
+ updateValue()
198
+ }
199
+ }
200
+ const updateParentNode = (node: TreeNode) => {
201
+ if (!node.parent) return
202
+ updateNode(node.parent)
203
+ updateParentNode(node.parent)
204
+ }
205
+ const updateNode = (node: TreeNode | undefined) => {
206
+ if (!node) return
207
+ if (node.children?.every((item) => item.checked)) {
208
+ node.checked = true
209
+ node.immediate = false
210
+ !node.disabled && (!selectionLeafOnly.value || node.isLeaf) && checkedKeys.value.add(node.key)
211
+ } else if (node.children?.every((item) => !item.checked && !item.immediate)) {
212
+ node.checked = false
213
+ node.immediate = false
214
+ checkedKeys.value.delete(node.key)
215
+ } else {
216
+ node.checked = false
217
+ node.immediate = true
218
+ checkedKeys.value.delete(node.key)
219
+ !node.disabled && immediateKeySet.value.add(node.key)
220
+ }
221
+ }
222
+
223
+ const expandNode = (node: TreeNode | undefined) => {
224
+ if (!node) return
225
+ !node.isLeaf && expandedKeySet.value.add(node.key)
226
+ node.expanded = true
227
+ if (node.parent) {
228
+ expandNode(node.parent)
229
+ }
230
+ }
231
+ const initValue = () => {
232
+ if (showCheckbox.value) {
233
+ if (isSetsEqual(checkedKeys.value, new Set(modelValue.value as string[]))) return
234
+ checkedKeys.value = new Set(modelValue.value as string[])
235
+
236
+ Array.isArray(modelValue.value) &&
237
+ modelValue.value.map((value) => {
238
+ const node = tree.value?.treeNodeMap.get(value as string)
239
+ if (node) {
240
+ toggleChecked(node, true)
241
+ }
242
+ })
243
+ } else {
244
+ currentNode.value = tree.value?.treeNodeMap.get(modelValue.value as string)
245
+ expandNode(currentNode.value?.parent)
246
+ }
247
+ }
248
+ const initDefaultExpandedKeys = () => {
249
+ defaultExpandedKeys.value.map((key) => {
250
+ expandNode(tree.value?.treeNodeMap.get(key as string))
251
+ })
252
+ }
253
+ const updateValue = () => {
254
+ if (showCheckbox.value) {
255
+ emit('update:modelValue', Array.from(checkedKeys.value))
256
+ } else {
257
+ emit('update:modelValue', currentNode.value?.key)
258
+ }
259
+ }
260
+ watch(
261
+ () => data.value,
262
+ () => {
263
+ tree.value = createTree(data.value)
264
+ nextTick(() => {
265
+ checkedKeys.value = new Set()
266
+ initValue()
267
+ initDefaultExpandedKeys()
268
+ })
269
+ },
270
+ {
271
+ immediate: true
272
+ }
273
+ )
274
+ watch(
275
+ () => modelValue.value,
276
+ () => {
277
+ initValue()
278
+ }
279
+ )
280
+ defineExpose({
281
+ toggleExpand,
282
+ toggleChecked,
283
+ tree
284
+ })
285
+ </script>
286
+
287
+ <style lang="scss" scoped>
288
+ @import './index.scss';
289
+ </style>
@@ -0,0 +1,48 @@
1
+ import type { ExtractPropTypes } from 'vue'
2
+ import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp } from '../common/props'
3
+
4
+ // 原始树节点类型
5
+ export type RawTreeNode = {
6
+ disabled?: boolean
7
+ [key: string]: any
8
+ }
9
+
10
+ // 处理后的树节点类型
11
+ export type TreeNode = {
12
+ level: number
13
+ key: string
14
+ data: RawTreeNode
15
+ index: number
16
+ isLast: boolean
17
+ expanded: boolean
18
+ label: string
19
+ parent?: TreeNode
20
+ children?: TreeNode[]
21
+ disabled?: boolean
22
+ isLeaf: boolean
23
+ checked?: boolean
24
+ immediate?: boolean
25
+ isCurrent?: boolean
26
+ }
27
+
28
+ export const textProps = {
29
+ ...baseProps,
30
+ data: makeArrayProp<RawTreeNode>(),
31
+ showCheckbox: makeBooleanProp(false),
32
+ childrenKey: makeStringProp('children'),
33
+ labelKey: makeStringProp('name'),
34
+ valueKey: makeStringProp('id'),
35
+ defaultExpandedKeys: makeArrayProp<string>(),
36
+ expandAll: makeBooleanProp(false),
37
+ checkStrictly: makeBooleanProp(false),
38
+ modelValue: {
39
+ type: [Array<string>, String],
40
+ default: ''
41
+ },
42
+ emptyText: makeStringProp('暂无数据'),
43
+ maxHeight: makeStringProp('300px'),
44
+ indent: makeNumberProp(16),
45
+ selectionLeafOnly: makeBooleanProp(false)
46
+ }
47
+
48
+ export type TextProps = ExtractPropTypes<typeof textProps>
@@ -46,6 +46,10 @@
46
46
  }
47
47
  }
48
48
 
49
+ @include e(evoke-file) {
50
+ border-radius: 8px;
51
+ }
52
+
49
53
  @include e(evoke-num) {
50
54
  font-size: 14px;
51
55
  line-height: 14px;
@@ -172,4 +176,50 @@
172
176
  width: 100%;
173
177
  height: 100%;
174
178
  }
179
+
180
+ @include e(preview-file) {
181
+ margin-bottom: 32px;
182
+ .oxy-upload__status-content:not(.oxy-upload__mask) {
183
+ border: 1px solid $-color-border;
184
+ box-sizing: border-box;
185
+ border-radius: 8px;
186
+ .oxy-upload__picture {
187
+ border-radius: 8px;
188
+ }
189
+ .oxy-upload__picture-icon {
190
+ width: 32px;
191
+ height: 32px;
192
+ }
193
+ }
194
+ .oxy-upload__mask {
195
+ border-radius: 8px;
196
+ }
197
+ :deep(.oxy-tooltip) {
198
+ width: 100%;
199
+ .oxy-tooltip__inner {
200
+ width: fit-content;
201
+ white-space: pre-wrap;
202
+ word-break: break-all;
203
+ }
204
+ }
205
+ .oxy-upload____status-content-name {
206
+ display: block;
207
+ width: 100%;
208
+ font-size: $-upload-file-fs;
209
+ color: $-upload-file-color;
210
+ box-sizing: border-box;
211
+ padding: 0 4px;
212
+ text-align: center;
213
+ margin-top: 8px;
214
+ white-space: nowrap;
215
+ overflow: hidden;
216
+ text-overflow: ellipsis;
217
+ }
218
+ }
219
+
220
+ &.oxy-upload__position-right {
221
+ .oxy-upload__evoke {
222
+ margin-right: 12px;
223
+ }
224
+ }
175
225
  }