@yuhufe/wtool-vdiff 0.0.7 → 0.0.8

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 (39) hide show
  1. package/README.md +140 -41
  2. package/dist/DiffFiles/DiffFiles.vue.d.ts +12 -0
  3. package/dist/DiffFiles/DiffList/DiffList.vue.d.ts +94 -0
  4. package/dist/DiffFiles/FileExplore/FileExplore.vue.d.ts +510 -0
  5. package/dist/DiffFiles/FileExplore/FileSearch.vue.d.ts +11 -0
  6. package/dist/DiffFiles/FileExplore/fileTree.d.ts +17 -0
  7. package/dist/DiffFiles/createDiffFiles.d.ts +13 -0
  8. package/dist/DiffFiles/index.d.ts +3 -0
  9. package/dist/DiffFiles/types.d.ts +12 -0
  10. package/dist/DiffFiles/useDiffFiles.d.ts +23 -0
  11. package/dist/DiffViewer/DiffViewer.vue.d.ts +15 -4
  12. package/dist/DiffViewer/TopBar.vue.d.ts +2 -0
  13. package/dist/DiffViewer/const.d.ts +7 -0
  14. package/dist/DiffViewer/utils/autoHeight.d.ts +3 -1
  15. package/dist/DiffViewer/utils/patch2Pair.d.ts +10 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/types.d.ts +15 -5
  18. package/dist/wtool-vdiff.cjs.js +42 -39
  19. package/dist/wtool-vdiff.es.js +9306 -5945
  20. package/package.json +6 -7
  21. package/src/DiffFiles/DiffFiles.vue +97 -18
  22. package/src/DiffFiles/DiffList/DiffList.vue +125 -0
  23. package/src/DiffFiles/FileExplore/FileExplore.vue +165 -7
  24. package/src/DiffFiles/FileExplore/FileSearch.vue +81 -0
  25. package/src/DiffFiles/FileExplore/fileTree.ts +102 -0
  26. package/src/DiffFiles/createDiffFiles.ts +70 -0
  27. package/src/DiffFiles/index.ts +11 -0
  28. package/src/DiffFiles/types.ts +15 -0
  29. package/src/DiffFiles/useDiffFiles.ts +56 -0
  30. package/src/DiffViewer/DiffViewer.vue +22 -8
  31. package/src/DiffViewer/MonacoDiffViewer.vue +21 -2
  32. package/src/DiffViewer/TopBar.vue +23 -4
  33. package/src/DiffViewer/const.ts +10 -0
  34. package/src/DiffViewer/utils/autoHeight.ts +83 -29
  35. package/src/DiffViewer/utils/patch2Pair.ts +37 -9
  36. package/src/assets/file.svg +4 -0
  37. package/src/assets/folder.svg +4 -0
  38. package/src/index.ts +1 -0
  39. package/src/types.ts +17 -5
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yuhufe/wtool-vdiff",
3
3
  "private": false,
4
- "version": "0.0.7",
4
+ "version": "0.0.8",
5
5
  "type": "module",
6
6
  "description": "Monaco diff viewer as Vue 3 Web Component",
7
7
  "keywords": [
@@ -12,7 +12,7 @@
12
12
  "web-component"
13
13
  ],
14
14
  "scripts": {
15
- "dev": "pnpm run dev:site",
15
+ "vite": "pnpm run dev:site",
16
16
  "dev:lib": "vite build --watch --mode development",
17
17
  "build:site": "vite build --mode production --config site/vite.config.ts",
18
18
  "dev:site": "vite --config site/vite.config.ts",
@@ -31,20 +31,19 @@
31
31
  },
32
32
  "author": "",
33
33
  "license": "ISC",
34
- "peerDependencies": {
35
- "vue": "^3.5.0"
36
- },
37
34
  "dependencies": {
38
35
  "@yuhufe/web-ui": "workspace:*",
39
36
  "@yuhufe/web-common": "workspace:*",
40
37
  "@monaco-editor/loader": "^1.6.1",
41
38
  "diff": "^9.0.0",
42
- "monaco-editor": "^0.53.0"
39
+ "es-toolkit": "^1.49.0",
40
+ "monaco-editor": "^0.53.0",
41
+ "@wsfe/vue-tree": "^4.1.1"
43
42
  },
44
43
  "devDependencies": {
45
44
  "@vitejs/plugin-vue": "^5.2.0",
46
45
  "less": "4.6.4",
47
- "typescript": "^5.9.3",
46
+ "typescript": "^6.0.3",
48
47
  "vite": "^7.3.1",
49
48
  "vite-plugin-css-injected-by-js": "5.0.1",
50
49
  "vite-plugin-dts": "^4.5.3",
@@ -1,32 +1,97 @@
1
1
  <template>
2
2
  <div class="diff-files-wrap">
3
- <Toolbar class="toolbar" />
4
3
  <div class="content-wrap">
5
- <FileExplore :diffFiles="diffFiles" />
6
- <div class="filelist-viewer-wrap"></div>
4
+ <FileExplore class="file-explore" :diffFiles="files" @select-file="emit('select-file', $event)" />
5
+ <div class="filelist-viewer-wrap">
6
+ <DiffList
7
+ :diffFiles="files"
8
+ :fileOverScan="fileOverScan"
9
+ :fileViewMap="fileViewMap"
10
+ :viewerStyle="viewerStyle"
11
+ @viewStateChange="freshViewState"
12
+ />
13
+ </div>
7
14
  </div>
8
15
  </div>
9
16
  </template>
10
17
 
11
18
  <script setup lang="ts">
12
- import { ref, computed, nextTick } from 'vue'
13
-
14
- import { loadingDirective } from '@yuhufe/web-ui'
15
- import type { DiffEditorOptions, WtoolDiffViewerProps, ModelOptions } from '../types'
19
+ import { onBeforeMount, reactive } from 'vue'
20
+ import type { WtoolDiffFilesProps } from '../types'
16
21
 
17
22
  import FileExplore from './FileExplore/FileExplore.vue'
23
+ import { useDiffFiles, formatDiffFiles } from './useDiffFiles'
24
+ import type { DiffFileState, FileItem } from './types'
25
+ import type { DiffFileSelection } from './FileExplore/fileTree'
26
+ import DiffList from './DiffList/DiffList.vue'
27
+ import { autoHeight, height2Num } from '@/DiffViewer/utils/autoHeight'
28
+
29
+ const props = withDefaults(defineProps<WtoolDiffFilesProps>(), {
30
+ diffFiles: () => [],
31
+ fileOverScan: 30,
32
+ viewerStyle: () => ({}),
33
+ })
18
34
 
19
- const vLoading = loadingDirective
20
- const loading = ref(true)
35
+ const { files, fileMap } = formatDiffFiles(props.diffFiles)
36
+ const DEFAULT_CONTEXT_LINE_COUNT = 3
21
37
 
22
- const props = withDefaults(
23
- defineProps<{
24
- diffFiles: WtoolDiffViewerProps[]
25
- }>(),
26
- {
27
- diffFiles: () => [],
38
+ const calculateViewerHeight = (file: FileItem) => {
39
+ const state = fileViewMap[file.fullPath]
40
+ if (props.viewerStyle.height) {
41
+ return height2Num(props.viewerStyle.height)
28
42
  }
43
+
44
+ const fileHeight = (() => {
45
+ const heightRange = {
46
+ minHeight: '0px',
47
+ maxHeight: '500px',
48
+ ...props.viewerStyle,
49
+ }
50
+ const height = autoHeight({
51
+ id: file.fullPath,
52
+ patch: file.diffPatch,
53
+ pair: file.diffPair,
54
+ minHeight: heightRange.minHeight,
55
+ maxHeight: heightRange.maxHeight,
56
+ unchangedVisiable: state.rawed,
57
+ unchangedCtxLineNum: DEFAULT_CONTEXT_LINE_COUNT,
58
+ unchangedMinLineNum: 1,
59
+ })
60
+
61
+ return height
62
+ })()
63
+
64
+ return fileHeight
65
+ }
66
+ const fileViewMap = reactive(
67
+ Object.fromEntries(
68
+ files.map(file => [
69
+ file.fullPath,
70
+ {
71
+ height: 0,
72
+ viewed: false,
73
+ rawed: false,
74
+ },
75
+ ])
76
+ )
29
77
  )
78
+ onBeforeMount(() => {
79
+ // 初始计算高度
80
+ Object.entries(fileViewMap).forEach(([key, viewFile]) => {
81
+ freshViewState(fileMap[key])
82
+ })
83
+ })
84
+
85
+ const emit = defineEmits<{
86
+ 'select-file': [selection: DiffFileSelection]
87
+ }>()
88
+ useDiffFiles({ isMaster: true })
89
+
90
+ const freshViewState = (file: FileItem, newViewState = {}) => {
91
+ const state = fileViewMap[file.fullPath]
92
+ Object.assign(state, newViewState)
93
+ state.height = calculateViewerHeight(file)
94
+ }
30
95
  </script>
31
96
 
32
97
  <style scoped>
@@ -36,12 +101,26 @@ const props = withDefaults(
36
101
  display: flex;
37
102
  flex-direction: column;
38
103
 
39
- .toolbar {
40
- flex-shrink: 0;
41
- }
42
104
  .content-wrap {
43
105
  flex: 1;
106
+ min-height: 0;
107
+ display: flex;
44
108
  overflow: hidden;
109
+
110
+ .file-explore {
111
+ width: 300px;
112
+ min-width: 220px;
113
+ max-width: 40%;
114
+ flex: 0 0 300px;
115
+ border-right: 1px solid #dfe3e8;
116
+ box-sizing: border-box;
117
+ }
118
+
119
+ .filelist-viewer-wrap {
120
+ min-width: 0;
121
+ flex: 1;
122
+ overflow: hidden;
123
+ }
45
124
  }
46
125
  }
47
126
  </style>
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <VirtualScroll
3
+ ref="virtualScrollRef"
4
+ class="diff-list-wrap"
5
+ :items="diffFiles"
6
+ :itemSize="getItemSize"
7
+ keyField="fullPath"
8
+ :overscan="fileOverScan"
9
+ >
10
+ <template #default="{ item: file }">
11
+ <div
12
+ class="file-wrap"
13
+ :class="{ 'file-wrap--selected': selectedFileKey === file.fullPath }"
14
+ :style="{ paddingBottom: `${FILE_GAP}px` }"
15
+ >
16
+ <DiffViewer
17
+ :fileId="file.fullPath"
18
+ :diffPair="file.diffPair"
19
+ :diffPatch="file.diffPatch"
20
+ :viewerStyle="getViewerStyle(file)"
21
+ @viewStateChange="handleViewStateChange(file, $event)"
22
+ />
23
+ </div>
24
+ </template>
25
+ </VirtualScroll>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { nextTick, ref } from 'vue'
30
+ import { VirtualScroll, useVirtual } from '@yuhufe/web-ui'
31
+ import type { DiffFileState, FileItem } from '../types'
32
+ import { useDiffFiles } from '../useDiffFiles'
33
+ import type { WtoolDiffViewerStyle } from '@/types'
34
+ import DiffViewer from '@/DiffViewer/DiffViewer.vue'
35
+ import { HEIGHT_TOP_BAR } from '@/DiffViewer/const'
36
+
37
+ const FILE_GAP = 12
38
+ const VIEWER_BORDER_HEIGHT = 2
39
+ const BASE_HEIGHT = HEIGHT_TOP_BAR + VIEWER_BORDER_HEIGHT + FILE_GAP
40
+
41
+ const props = withDefaults(
42
+ defineProps<{
43
+ diffFiles: FileItem[]
44
+ fileOverScan?: number
45
+ fileViewMap: Record<string, DiffFileState>
46
+ viewerStyle?: WtoolDiffViewerStyle
47
+ }>(),
48
+ {
49
+ diffFiles: () => [],
50
+ fileOverScan: 30,
51
+ fileViewMap: () => ({}),
52
+ viewerStyle: () => ({}),
53
+ }
54
+ )
55
+
56
+ const emit = defineEmits<{
57
+ 'view-state-change': [file: FileItem, state: Partial<DiffFileState>]
58
+ }>()
59
+
60
+ const virtualScrollRef = ref(null)
61
+ const { funcs } = useVirtual()
62
+ const { funcs: diffFilesFuncs, registerFunc } = useDiffFiles()
63
+ const { selectedFileKey } = diffFilesFuncs
64
+
65
+ const selectFile = async (fullPath: string) => {
66
+ const index = props.diffFiles.findIndex(file => file.fullPath === fullPath)
67
+ if (index < 0) {
68
+ selectedFileKey.value = ''
69
+ return
70
+ }
71
+
72
+ selectedFileKey.value = fullPath
73
+ await nextTick()
74
+ funcs.virtualer?.value.scrollToIndex(index, { align: 'start' })
75
+ }
76
+
77
+ registerFunc({ selectFile })
78
+
79
+ const getViewerStyle = (file: FileItem): WtoolDiffViewerStyle => {
80
+ const fileViewState = props.fileViewMap[file.fullPath]
81
+
82
+ return {
83
+ ...props.viewerStyle,
84
+ height: `${fileViewState?.height || 0}px`,
85
+ viewed: fileViewState?.viewed ?? false,
86
+ rawed: fileViewState?.rawed ?? false,
87
+ }
88
+ }
89
+
90
+ const getItemSize = (file: FileItem): number => {
91
+ let { height, viewed } = props.fileViewMap[file.fullPath]
92
+ // 此时代码隐藏
93
+ if (viewed) {
94
+ height = 0
95
+ }
96
+ return height + BASE_HEIGHT
97
+ }
98
+
99
+ const handleViewStateChange = (file: FileItem, state: Partial<DiffFileState>) => {
100
+ emit('view-state-change', file, state)
101
+ funcs.resizeItem(file.fullPath, getItemSize(file))
102
+ }
103
+ </script>
104
+
105
+ <style scoped>
106
+ .diff-list-wrap {
107
+ width: 100%;
108
+ height: 100%;
109
+ overflow-y: auto;
110
+ overflow-x: hidden;
111
+ scrollbar-width: thin;
112
+ scrollbar-gutter: stable;
113
+ padding: 8px;
114
+ box-sizing: border-box;
115
+
116
+ .file-wrap {
117
+ height: 100%;
118
+ box-sizing: border-box;
119
+ }
120
+
121
+ .file-wrap--selected :deep(.diff-viewer-wrap) {
122
+ border-color: #1677ff;
123
+ }
124
+ }
125
+ </style>
@@ -1,23 +1,181 @@
1
1
  <template>
2
- <div class="file-explore-wrap"></div>
2
+ <div class="file-explore-wrap">
3
+ <FileSearch />
4
+ <div class="file-tree">
5
+ <VTree
6
+ ref="treeRef"
7
+ selectable
8
+ enable-leaf-only
9
+ :unselect-on-click="false"
10
+ :default-expand-all="true"
11
+ :animation="false"
12
+ :node-min-height="32"
13
+ :node-indent="8"
14
+ :render-node-amount="60"
15
+ :buffer-node-amount="20"
16
+ empty-text="No changed files"
17
+ @selected-change="handleSelectedChange"
18
+ >
19
+ <template #node="{ node }">
20
+ <div class="file-tree-node" :title="node.fullPath || node.title" @click="handleNodeClick(node)">
21
+ <img class="file-tree-node__icon" :src="node.isDirectory ? folderIcon : fileIcon" alt="" />
22
+ <span class="file-tree-node__label">{{ node.title }}</span>
23
+ </div>
24
+ </template>
25
+ </VTree>
26
+ </div>
27
+ </div>
3
28
  </template>
4
29
 
5
30
  <script setup lang="ts">
6
- import { ref, computed, nextTick, shallowRef } from 'vue'
31
+ import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
32
+ import VTree, { type TreeNode } from '@wsfe/vue-tree'
33
+ import '@wsfe/vue-tree/style.css'
7
34
 
8
- import { loadingDirective } from '@yuhufe/web-ui'
9
- import type { FileTree, WtoolDiffViewerProps, ModelOptions } from '@/types'
35
+ import fileIcon from '@/assets/file.svg'
36
+ import folderIcon from '@/assets/folder.svg'
37
+ import FileSearch from './FileSearch.vue'
38
+ import { buildDiffFileTree, filterDiffFileTree, type DiffFileSelection, type DiffFileTreeNode } from './fileTree'
39
+ import { type FileItem } from '../types'
40
+ import { useDiffFiles } from '../useDiffFiles'
10
41
 
11
42
  const props = withDefaults(
12
43
  defineProps<{
13
- diffFiles: WtoolDiffViewerProps[]
44
+ diffFiles: FileItem[]
14
45
  }>(),
15
46
  {
16
47
  diffFiles: () => [],
17
48
  }
18
49
  )
19
50
 
20
- const filesData = shallowRef<FileTree[]>([])
51
+ const emit = defineEmits<{
52
+ 'select-file': [selection: DiffFileSelection]
53
+ }>()
54
+
55
+ const { funcs: diffFilesFuncs, registerFunc } = useDiffFiles()
56
+
57
+ type VTreeInstance = InstanceType<typeof VTree>
58
+ type RenderedTreeNode = TreeNode & DiffFileTreeNode
59
+
60
+ const treeRef = ref<VTreeInstance | null>(null)
61
+ const filesData = shallowRef<DiffFileTreeNode[]>([])
62
+ const searchKeyword = ref('')
63
+ const selectedFileKey = ref('')
64
+
65
+ const applySearch = (keyword: string) => {
66
+ const filteredTree = filterDiffFileTree(filesData.value, keyword)
67
+ treeRef.value?.setData(filteredTree)
68
+ }
69
+ registerFunc({
70
+ searchKeyword,
71
+ selectedFileKey,
72
+ filterTree: applySearch,
73
+ })
74
+
75
+ const loadTreeData = async () => {
76
+ filesData.value = buildDiffFileTree(props.diffFiles)
77
+
78
+ await nextTick()
79
+ treeRef.value?.clearSelected()
80
+ applySearch(searchKeyword.value)
81
+ }
82
+
83
+ const selectFile = (fullPath: string) => {
84
+ void diffFilesFuncs.selectFile?.(fullPath)
85
+ emit('select-file', { fullPath })
86
+ }
87
+
88
+ const handleNodeClick = node => {
89
+ node = node as RenderedTreeNode
90
+ if (node.isDirectory) {
91
+ treeRef.value?.setExpand(node.id, !node.expand, false)
92
+ return
93
+ }
94
+
95
+ selectFile(node.fullPath)
96
+ }
97
+
98
+ const handleSelectedChange = (node: RenderedTreeNode | null) => {
99
+ if (!node || node.isDirectory || diffFilesFuncs.selectedFileKey.value === node.fullPath) return
100
+
101
+ selectFile(node.fullPath)
102
+ }
103
+
104
+ onMounted(() => {
105
+ loadTreeData()
106
+ })
21
107
  </script>
22
108
 
23
- <style scoped></style>
109
+ <style lang="less" scoped>
110
+ .file-explore-wrap {
111
+ display: flex;
112
+ flex-direction: column;
113
+ width: 100%;
114
+ height: 100%;
115
+ min-width: 0;
116
+ min-height: 0;
117
+ overflow: hidden;
118
+ background: #fff;
119
+ }
120
+
121
+ .file-tree {
122
+ flex: 1 1 0;
123
+ width: 100%;
124
+ height: 0;
125
+ min-height: 0;
126
+ overflow: hidden;
127
+ color: #252a31;
128
+ font-size: 13px;
129
+ }
130
+
131
+ .file-tree-node {
132
+ display: flex;
133
+ align-items: center;
134
+ width: 100%;
135
+ min-width: 0;
136
+ padding-right: 8px;
137
+ box-sizing: border-box;
138
+ font-size: 14px;
139
+
140
+ .file-tree-node__icon {
141
+ width: 16px;
142
+ height: 16px;
143
+ margin-right: 7px;
144
+ flex: 0 0 16px;
145
+ }
146
+
147
+ .file-tree-node__label {
148
+ min-width: 0;
149
+ overflow: hidden;
150
+ text-overflow: ellipsis;
151
+ white-space: nowrap;
152
+ }
153
+ }
154
+
155
+ .file-explore-wrap {
156
+ :deep(.vtree-tree__wrapper) {
157
+ .vtree-tree-node__title {
158
+ min-width: 0;
159
+ flex: 1;
160
+ overflow: hidden;
161
+ cursor: pointer;
162
+ margin-left: 0;
163
+ padding-left: 2px;
164
+ }
165
+
166
+ .vtree-tree-node__title_selected {
167
+ color: #1261a6;
168
+ background: #e7f2fc;
169
+ }
170
+
171
+ .vtree-tree__empty-text_default {
172
+ color: #747b85;
173
+ font-size: 13px;
174
+ }
175
+
176
+ .vtree-tree__scroll-area {
177
+ scrollbar-width: thin;
178
+ }
179
+ }
180
+ }
181
+ </style>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div class="file-search">
3
+ <input
4
+ class="file-search__input"
5
+ type="search"
6
+ v-model="searchKeyword"
7
+ :placeholder="placeholder"
8
+ autocomplete="off"
9
+ spellcheck="false"
10
+ @input="handleInput"
11
+ />
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { debounce } from 'es-toolkit'
17
+ import { useDiffFiles } from '../useDiffFiles'
18
+
19
+ withDefaults(
20
+ defineProps<{
21
+ placeholder?: string
22
+ }>(),
23
+ {
24
+ placeholder: 'Search files',
25
+ }
26
+ )
27
+
28
+ const emit = defineEmits<{
29
+ 'update:modelValue': [value: string]
30
+ }>()
31
+
32
+ const { funcs } = useDiffFiles()
33
+ const { searchKeyword } = funcs
34
+
35
+ const handleInput = debounce((event: Event) => {
36
+ funcs.filterTree((event.target as HTMLInputElement).value)
37
+ }, 300)
38
+ </script>
39
+
40
+ <style lang="less" scoped>
41
+ .file-search {
42
+ flex: 0 0 auto;
43
+ padding: 8px 9px;
44
+ border-bottom: 1px solid #dfe3e8;
45
+ box-sizing: border-box;
46
+ background: #fff;
47
+ }
48
+
49
+ .file-search__input {
50
+ width: 100%;
51
+ height: 32px;
52
+ padding: 4px 9px;
53
+ border: 1px solid #cbd2d9;
54
+ border-radius: 4px;
55
+ box-sizing: border-box;
56
+ color: #252a31;
57
+ background: #fff;
58
+ font: inherit;
59
+ font-size: 13px;
60
+ letter-spacing: 0;
61
+ outline: none;
62
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
63
+
64
+ &::placeholder {
65
+ color: #8a929c;
66
+ }
67
+
68
+ &:hover {
69
+ border-color: #7aaed8;
70
+ }
71
+
72
+ &:focus {
73
+ border-color: #2979b8;
74
+ box-shadow: 0 0 0 2px rgb(41 121 184 / 16%);
75
+ }
76
+
77
+ &::-webkit-search-cancel-button {
78
+ cursor: pointer;
79
+ }
80
+ }
81
+ </style>
@@ -0,0 +1,102 @@
1
+ import { treeUtil, strUtil } from '@yuhufe/web-common'
2
+ import type { DiffFile } from '../../types'
3
+ import type { FileItem } from '../types'
4
+
5
+ export interface DiffFileSelection {
6
+ fullPath: string
7
+ }
8
+ export interface DiffFileTreeNode extends DiffFile {
9
+ id: string
10
+ title: string
11
+ name: string
12
+ fullPath: string
13
+ folderPath?: string
14
+ type?: string
15
+ isDirectory: boolean
16
+ children?: DiffFileTreeNode[]
17
+ }
18
+
19
+ function createDirectory(name: string, fullPath: string): DiffFileTreeNode {
20
+ return {
21
+ id: fullPath,
22
+ title: name,
23
+ name,
24
+ fullPath,
25
+ isDirectory: true,
26
+ children: [],
27
+ }
28
+ }
29
+
30
+ function finalizeDirectory(node: DiffFileTreeNode): DiffFileTreeNode {
31
+ const children = node.children?.map(child => (child.isDirectory ? finalizeDirectory(child) : child)) || []
32
+ let directory: DiffFileTreeNode = {
33
+ id: node.id,
34
+ title: node.title,
35
+ name: node.name,
36
+ fullPath: node.fullPath,
37
+ isDirectory: true,
38
+ children,
39
+ }
40
+
41
+ while (directory.children?.length === 1 && directory.children[0].isDirectory) {
42
+ const child = directory.children[0]
43
+ const name = `${directory.name}/${child.name}`
44
+
45
+ directory = {
46
+ id: child.id,
47
+ title: name,
48
+ name,
49
+ fullPath: child.fullPath,
50
+ isDirectory: true,
51
+ children: child.children,
52
+ }
53
+ }
54
+
55
+ return directory
56
+ }
57
+
58
+ // 格式:{ filePath: 'aaa/bbb/ccc', isDirectory: true, children: [] }
59
+ export function buildDiffFileTree(files: FileItem[]): DiffFileTreeNode[] {
60
+ const root = createDirectory('', '')
61
+
62
+ files.forEach(file => {
63
+ const { fullPath, folderPath = '', name: filename } = file
64
+ const segments = folderPath.split('/').filter(Boolean)
65
+ let parent = root
66
+ let directoryPath = ''
67
+
68
+ segments.forEach(segment => {
69
+ directoryPath = directoryPath ? `${directoryPath}/${segment}` : segment
70
+ let directory = parent.children?.find((child: any) => child.isDirectory && child.name === segment)
71
+
72
+ if (!directory) {
73
+ directory = createDirectory(segment, directoryPath)
74
+ parent.children?.push(directory)
75
+ }
76
+
77
+ parent = directory
78
+ })
79
+
80
+ parent.children?.push({
81
+ ...file,
82
+ id: fullPath,
83
+ title: filename || '',
84
+ name: filename,
85
+ fullPath,
86
+ isDirectory: false,
87
+ } as any)
88
+ })
89
+
90
+ return root.children!.map(child => (child.isDirectory ? finalizeDirectory(child) : child))
91
+ }
92
+
93
+ export function filterDiffFileTree(fileTree: DiffFileTreeNode[], keyword: string): DiffFileTreeNode[] {
94
+ if (!keyword?.trim()) return fileTree
95
+
96
+ const filteredTree = treeUtil.filter(fileTree, node => {
97
+ const hasMatch = [node.title, node.fullPath].find(content => strUtil.like(content, keyword))
98
+ return hasMatch || Boolean(node.children?.length)
99
+ })
100
+
101
+ return (filteredTree || []) as DiffFileTreeNode[]
102
+ }