443 lines
15 KiB
Vue
443 lines
15 KiB
Vue
<template>
|
|
<div>
|
|
<div v-if="!diffs" class="p-5"><d-skeleton></d-skeleton></div>
|
|
<d-splitter v-if="diffs" class="splitter-border" style="height: 100%">
|
|
<template v-slot:DSplitterPane>
|
|
<d-splitter-pane
|
|
collapseDirection="before"
|
|
size="320px"
|
|
minSize="320px"
|
|
:collapsible="true"
|
|
@size-change="setSplitterCollapse"
|
|
@collapsed-change="collapsedChange"
|
|
>
|
|
<div class="pane-content">
|
|
<div v-if="diffTree" style="min-width: 320px" class="left-tree pl-5 pr-5 pt-5">
|
|
<d-search
|
|
class="mb10"
|
|
style="width: 100%"
|
|
is-keyup-search
|
|
placeholder="查找文件"
|
|
:delay="100"
|
|
@search="onSearch"
|
|
></d-search>
|
|
<d-tree @node-click="onTreeClick" ref="treeRef" :data="diffTree" class="mt-2">
|
|
<template #content="{ nodeData }">
|
|
<div class="ml-2" :title="nodeData?.file_path">{{ nodeData?.file_path }}</div>
|
|
</template>
|
|
<template #icon="{ nodeData }">
|
|
<span class="inline-block w-5">
|
|
<Icon v-if="nodeData.isLeaf" name="gt-file-c" />
|
|
<Icon v-else :name="nodeData.expanded ? 'gt-folder-open-c' : 'gt-folder-c'" />
|
|
</span>
|
|
</template>
|
|
</d-tree>
|
|
</div>
|
|
</div>
|
|
</d-splitter-pane>
|
|
<d-splitter-pane minSize="15%">
|
|
<div class="pane-content">
|
|
<div class="flex-1 pt-5 bg-[#f6f6f8]">
|
|
<div class="flex px-5 justify-between items-center">
|
|
<div class="flex items-center">
|
|
<StatusSelect v-model:value="selected" />
|
|
<div class="flex items-center ml-4">
|
|
<div class="text-[#2D2D2E] flex items-center">
|
|
共
|
|
<div class="text-[#517ca0] mx-1">{{ diffs.changes?.length }}</div>
|
|
个文件变更
|
|
</div>
|
|
<p class="text-green-500 ml-4">+{{ diffs.added_lines }}</p>
|
|
<p class="text-red-500 ml-1">-{{ diffs.removed_lines }}</p>
|
|
</div>
|
|
<span class="w-px h-5 bg-gray-300 mx-4"></span>
|
|
<div class="flex items-center gap-2">
|
|
<d-dropdown style="width: 150px" :position="['bottom-start']" align="start" close-scope="all">
|
|
<div class="flex items-center justify-between gap-2 cursor-pointer">
|
|
<div class="commitTitle text-[#707a87] hover:text-[#2d2d2e]">
|
|
{{ preCommitName }}
|
|
</div>
|
|
<Icon name="gt-select-arrow-down-12" size="12px"></Icon>
|
|
</div>
|
|
<template #menu>
|
|
<ul>
|
|
<li
|
|
class="p-2 cursor-pointer hover:bg-[#f6f6f8]"
|
|
v-for="item in firstCommitOptions"
|
|
:key="item.value"
|
|
:value="item.value"
|
|
:name="item.name"
|
|
@click="
|
|
preCommit = item.value;
|
|
baseLineChange();
|
|
"
|
|
>
|
|
<div class="px-2">
|
|
<div style="font-weight: 700">{{ item.name }}</div>
|
|
<div v-if="item.head_commit_sha">
|
|
{{ item.head_commit_sha.slice(0, 8) }}
|
|
</div>
|
|
<div v-if="item.created_at">{{ formatTime(item.created_at) }}</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</d-dropdown>
|
|
<Icon name="gt-exit" size="12px"></Icon>
|
|
<d-dropdown style="width: 150px" :position="['bottom-start']" align="start">
|
|
<div class="flex items-center justify-between gap-2 cursor-pointer">
|
|
<div class="commitTitle text-[#707a87] hover:text-[#2d2d2e]">
|
|
{{ afterCommitName }}
|
|
</div>
|
|
<Icon name="gt-select-arrow-down-12" size="12px"></Icon>
|
|
</div>
|
|
<template #menu>
|
|
<ul>
|
|
<li
|
|
class="p-2 cursor-pointer hover:bg-[#f6f6f8]"
|
|
v-for="item in lastCommitOptions"
|
|
:key="item.value"
|
|
:value="item.value"
|
|
:name="item.name"
|
|
@click="
|
|
afterCommit = item.value;
|
|
baseLineChange();
|
|
"
|
|
>
|
|
<div class="px-2">
|
|
<div style="font-weight: 700">{{ item.name }}</div>
|
|
<div v-if="item.head_commit_sha">
|
|
{{ item.head_commit_sha.slice(0, 8) }}
|
|
</div>
|
|
<div v-if="item.created_at">{{ formatTime(item.created_at) }}</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</d-dropdown>
|
|
</div>
|
|
<div class="ml-4"></div>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<d-popover content="展开/收起全部文件" trigger="hover" :position="['top']">
|
|
<span class="flex items-center cursor-pointer" @click="handleExpand">
|
|
<Icon name="gt-frame-expand2"></Icon>
|
|
</span>
|
|
</d-popover>
|
|
<span class="w-px h-5 bg-gray-300 mx-3"></span>
|
|
<d-popover content="设置" trigger="hover" :position="['top']">
|
|
<ShowFormat />
|
|
</d-popover>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex">
|
|
<div v-if="diffsloading" class="p-5 h-80 w-[100%]"><d-skeleton></d-skeleton></div>
|
|
<DataPanel
|
|
class="filter-diff-list"
|
|
:loading="diffsloading"
|
|
:empty="!filterDiffs?.length"
|
|
:skeleton="diffsloading"
|
|
:card="false"
|
|
>
|
|
<div class="flex-1 codeblock p-5 pt-0" :key="`${preCommit}-${afterCommit}`" v-show="!diffsloading">
|
|
<div
|
|
:class="{ selectItem: selectedFile === item.file_path }"
|
|
class="mb-3 overflow-auto rounded-[8px]"
|
|
style="box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.09)"
|
|
v-for="(item, index) in filterDiffs"
|
|
:key="item.file_path"
|
|
@click="codelistClick(item.file_path)"
|
|
>
|
|
<DiffView
|
|
:index="index"
|
|
:repoId="repoId"
|
|
:diffObj="item"
|
|
:discussions="diss?.length ? diss.find((x) => x.path === item.file_path) : null"
|
|
:showIssue="true"
|
|
:allowComment="canComment"
|
|
:showViewSource="true"
|
|
:showReadBtn="true"
|
|
@onMergeAble="$emit('onMergeAble')"
|
|
@foldChange="(e) => (item.fold = e)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</DataPanel>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</d-splitter-pane>
|
|
</template>
|
|
</d-splitter>
|
|
<!-- 勿删 Status 枚举需要 -->
|
|
<div
|
|
class="hidden text-[#fa9841] bg-[#fa984133] bg-[#3ac29533] bg-[#f66f6a33] bg-[#71757f33] text-[#3ac295] text-[#f66f6a] text-[#71757f]"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { computed, ref, toValue, watch, reactive, onMounted, provide, inject } from 'vue';
|
|
import { useReq } from '@/utils/hooks/useReq';
|
|
import { changes } from '@/api/merge/index';
|
|
import ShowFormat from './ShowFormat.vue';
|
|
import StatusSelect from './StatusSelect.vue';
|
|
import { changesTrees, versions, getDis } from '@/api/merge/index';
|
|
import DiffView from './DiffView.vue';
|
|
import { useAccountStore } from '@/stores/user';
|
|
import { escapeResData } from '@/utils';
|
|
import { diffFilter } from './support';
|
|
import { repoInfoStore } from '@/stores/Repo';
|
|
import { useMrChangeStore } from '@/stores/merge';
|
|
import { formatTime } from './support';
|
|
|
|
const mrChangeStore = useMrChangeStore();
|
|
const user = useAccountStore();
|
|
const accountInfo = user.accountInfo;
|
|
const arts_id = accountInfo?.arts_id;
|
|
const { isPrivate, isVisitor } = repoInfoStore();
|
|
const canComment = computed(() => {
|
|
if (isPrivate) return isVisitor;
|
|
else return !!arts_id;
|
|
});
|
|
|
|
//
|
|
const repoId = inject('repoId');
|
|
const iid = inject('iid');
|
|
provide('arts_id', arts_id);
|
|
provide('accountInfo', accountInfo);
|
|
const mrInfo = inject('mrInfo');
|
|
provide('source_branch', mrInfo?.value?.source_branch);
|
|
|
|
// 筛选
|
|
const selected = ref([]);
|
|
const keyWord = ref('');
|
|
const allFold = ref(false);
|
|
const handleExpand = () => {
|
|
diffs?.value?.changes?.forEach((x) => (x.collapsed = allFold.value));
|
|
allFold.value = !allFold.value;
|
|
};
|
|
|
|
// 基线
|
|
const preCommit = ref(null);
|
|
const afterCommit = ref(null);
|
|
provide('preCommit', preCommit);
|
|
provide('afterCommit', afterCommit);
|
|
const preCommitName = computed(() => {
|
|
return commitOptions?.value?.find((x) => x.value === preCommit.value)?.name || '';
|
|
});
|
|
const afterCommitName = computed(() => {
|
|
return commitOptions?.value?.find((x) => x.value === afterCommit.value)?.name || '';
|
|
});
|
|
|
|
const from_diff_id = computed(() => {
|
|
if (!commitOptions?.value?.length) return null;
|
|
if (preCommit.value === -1 && afterCommit.value === commitOptions.value[0].value) return null;
|
|
return afterCommit.value;
|
|
});
|
|
const to_diff_id = computed(() => {
|
|
if (preCommit.value === -1) return null;
|
|
return preCommit.value;
|
|
});
|
|
const { data: commitOptions } = useReq(versions, reactive({ repoId, iid }), null, (res) => {
|
|
const data = res.map((x, i) => ({ ...x, value: x.id }));
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i === 0) data[i].name = '最新版本';
|
|
else data[i].name = '版本' + (data.length - i);
|
|
}
|
|
return [...data, { value: -1, name: '基线' }];
|
|
});
|
|
watch(commitOptions, () => {
|
|
if (!commitOptions.value?.length) return;
|
|
//
|
|
preCommit.value = -1;
|
|
afterCommit.value = commitOptions.value[0].value;
|
|
});
|
|
const firstCommitOptions = computed(() => {
|
|
if (!commitOptions.value?.length) return [];
|
|
const firstOptions = commitOptions.value.filter((x) => x.value < afterCommit.value);
|
|
return [...firstOptions];
|
|
});
|
|
const lastCommitOptions = computed(() => {
|
|
if (!preCommit.value || !commitOptions.value?.length) return [];
|
|
const afterOptions = commitOptions.value.filter((x) => x.value > preCommit.value);
|
|
return [...afterOptions];
|
|
});
|
|
const baseLineDateNow = ref(+new Date());
|
|
const baseLineChange = () => {
|
|
loadDiffTree();
|
|
loadDiff();
|
|
baseLineDateNow.value = +new Date();
|
|
};
|
|
|
|
// diffs files tree
|
|
const formatTree = (childrenProp, pickProps) => (sourceList) => {
|
|
if (!Array.isArray(sourceList)) return sourceList;
|
|
return sourceList.map((node) => {
|
|
return {
|
|
...node,
|
|
children: formatTree(childrenProp, pickProps)(node[childrenProp]),
|
|
...pickProps(node)
|
|
};
|
|
});
|
|
};
|
|
const findTreeNode = (id, nodes) => {
|
|
for (const node of nodes) {
|
|
if (node.id === id) return node;
|
|
if (node.children?.length) {
|
|
const deepRes = findTreeNode(id, node.children);
|
|
if (deepRes) return deepRes;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const diffTree = ref(null);
|
|
const loadDiffTree = () => {
|
|
changesTrees(
|
|
reactive({
|
|
repoId,
|
|
iid,
|
|
view: 'simple',
|
|
from_diff_id,
|
|
to_diff_id
|
|
})
|
|
)
|
|
.then((res) => {
|
|
const data = escapeResData(res);
|
|
const pickProps = (node) => ({
|
|
label: node.title,
|
|
id: node.file_path,
|
|
expanded: true,
|
|
selected: false
|
|
});
|
|
diffTree.value = formatTree('items', pickProps)(data.tree);
|
|
})
|
|
.catch((e) => {});
|
|
};
|
|
//
|
|
const treeRef = ref();
|
|
const onSearch = (value) => {
|
|
treeRef.value.treeFactory.searchTree(value, { isFilter: true });
|
|
};
|
|
const selectedFile = ref('');
|
|
const onTreeClick = (node) => {
|
|
selectedFile.value = node?.file_path;
|
|
const thenode = diffs?.value?.changes?.find((x) => x.file_path === selectedFile.value);
|
|
if (thenode) thenode.fold = false;
|
|
|
|
if (node.childNodeCount > 0) {
|
|
node.expanded = !node.expanded;
|
|
}
|
|
//
|
|
};
|
|
const codelistClick = (file_path) => {
|
|
selectedFile.value = file_path;
|
|
//
|
|
const thenode = findTreeNode(file_path, diffTree.value);
|
|
//
|
|
treeRef.value.treeFactory.selectNode(thenode);
|
|
};
|
|
|
|
// diffs (文件)
|
|
const diffs = ref();
|
|
const diffsloading = ref(false);
|
|
const loadDiff = () => {
|
|
diffsloading.value = true;
|
|
const diffsParams = reactive({
|
|
repoId,
|
|
iid,
|
|
view: 'simple',
|
|
ignore_whitespace_change: mrChangeStore.ignore_whitespace_change,
|
|
from_diff_id,
|
|
to_diff_id
|
|
});
|
|
changes(diffsParams)
|
|
.then((res) => {
|
|
diffsloading.value = false;
|
|
const data = escapeResData(res);
|
|
diffs.value = data;
|
|
//
|
|
})
|
|
.catch((e) => {
|
|
diffsloading.value = false;
|
|
});
|
|
};
|
|
const filterDiffs = computed(() => {
|
|
if (!diffs?.value?.changes) return [];
|
|
return diffs.value.changes?.filter((m) => diffFilter(m, selected));
|
|
});
|
|
//
|
|
|
|
// diss
|
|
const { data: diss } = useReq(getDis, reactive({ repoId, iid }));
|
|
|
|
watch(
|
|
() => mrChangeStore.ignore_whitespace_change,
|
|
() => {
|
|
diffs.value = [];
|
|
loadDiff();
|
|
}
|
|
);
|
|
|
|
onMounted(() => {
|
|
loadDiffTree();
|
|
loadDiff();
|
|
});
|
|
|
|
const collapsedChange = (e) => {
|
|
if (e) setSplitterCollapse('4px');
|
|
else setSplitterCollapse('320px');
|
|
};
|
|
const setSplitterCollapse = (left) => {
|
|
setTimeout(() => {
|
|
document.querySelector('.devui-splitter__collapse').style.left = left;
|
|
}, 0);
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import 'devui-theme/styles-var/devui-var.scss';
|
|
.codeblock {
|
|
// background-color: $devui-global-bg;
|
|
min-height: calc(100vh - 144px - 104px - 52px);
|
|
}
|
|
|
|
.selectItem {
|
|
:deep(.devui-code-review__header) {
|
|
background-image: linear-gradient(to right, rgba(103, 136, 242, 0.2), transparent);
|
|
}
|
|
}
|
|
|
|
.left-tree {
|
|
border-right: 1px solid $devui-dividing-line;
|
|
position: sticky;
|
|
top: 10px;
|
|
|
|
min-height: calc(100vh - 144px - 104px);
|
|
}
|
|
|
|
.splitter-border {
|
|
:deep(.resizable .devui-splitter__collapse) {
|
|
position: fixed;
|
|
left: 320px;
|
|
top: 50%;
|
|
}
|
|
:deep(.none-resizable .devui-splitter__collapse) {
|
|
position: fixed;
|
|
left: 4px;
|
|
top: 50%;
|
|
}
|
|
}
|
|
|
|
.commitTitle {
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.filter-diff-list {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
</style>
|