@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.
- package/README.md +140 -41
- package/dist/DiffFiles/DiffFiles.vue.d.ts +12 -0
- package/dist/DiffFiles/DiffList/DiffList.vue.d.ts +94 -0
- package/dist/DiffFiles/FileExplore/FileExplore.vue.d.ts +510 -0
- package/dist/DiffFiles/FileExplore/FileSearch.vue.d.ts +11 -0
- package/dist/DiffFiles/FileExplore/fileTree.d.ts +17 -0
- package/dist/DiffFiles/createDiffFiles.d.ts +13 -0
- package/dist/DiffFiles/index.d.ts +3 -0
- package/dist/DiffFiles/types.d.ts +12 -0
- package/dist/DiffFiles/useDiffFiles.d.ts +23 -0
- package/dist/DiffViewer/DiffViewer.vue.d.ts +15 -4
- package/dist/DiffViewer/TopBar.vue.d.ts +2 -0
- package/dist/DiffViewer/const.d.ts +7 -0
- package/dist/DiffViewer/utils/autoHeight.d.ts +3 -1
- package/dist/DiffViewer/utils/patch2Pair.d.ts +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/types.d.ts +15 -5
- package/dist/wtool-vdiff.cjs.js +42 -39
- package/dist/wtool-vdiff.es.js +9306 -5945
- package/package.json +6 -7
- package/src/DiffFiles/DiffFiles.vue +97 -18
- package/src/DiffFiles/DiffList/DiffList.vue +125 -0
- package/src/DiffFiles/FileExplore/FileExplore.vue +165 -7
- package/src/DiffFiles/FileExplore/FileSearch.vue +81 -0
- package/src/DiffFiles/FileExplore/fileTree.ts +102 -0
- package/src/DiffFiles/createDiffFiles.ts +70 -0
- package/src/DiffFiles/index.ts +11 -0
- package/src/DiffFiles/types.ts +15 -0
- package/src/DiffFiles/useDiffFiles.ts +56 -0
- package/src/DiffViewer/DiffViewer.vue +22 -8
- package/src/DiffViewer/MonacoDiffViewer.vue +21 -2
- package/src/DiffViewer/TopBar.vue +23 -4
- package/src/DiffViewer/const.ts +10 -0
- package/src/DiffViewer/utils/autoHeight.ts +83 -29
- package/src/DiffViewer/utils/patch2Pair.ts +37 -9
- package/src/assets/file.svg +4 -0
- package/src/assets/folder.svg +4 -0
- package/src/index.ts +1 -0
- 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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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": "^
|
|
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="
|
|
6
|
-
<div class="filelist-viewer-wrap"
|
|
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 {
|
|
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
|
|
20
|
-
const
|
|
35
|
+
const { files, fileMap } = formatDiffFiles(props.diffFiles)
|
|
36
|
+
const DEFAULT_CONTEXT_LINE_COUNT = 3
|
|
21
37
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
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"
|
|
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 {
|
|
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
|
|
9
|
-
import
|
|
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:
|
|
44
|
+
diffFiles: FileItem[]
|
|
14
45
|
}>(),
|
|
15
46
|
{
|
|
16
47
|
diffFiles: () => [],
|
|
17
48
|
}
|
|
18
49
|
)
|
|
19
50
|
|
|
20
|
-
const
|
|
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
|
|
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
|
+
}
|