|
| 1 | +<script setup lang="ts"> |
| 2 | +import { cloneDeep } from 'es-toolkit/compat'; |
| 3 | +import type { TreeNode } from 'primevue/treenode'; |
| 4 | +import type { FileTree } from '~/shared/file-tree'; |
| 5 | +import { updateSelectedKey, type Get } from '~/shared/file-tree/utils'; |
| 6 | +
|
| 7 | +type Props = { get: Get, count: number | null }; |
| 8 | +const { get, count } = defineProps<Props>(); |
| 9 | +
|
| 10 | +const fullTabs = ref(cloneDeep(get.tabs)); |
| 11 | +watch(() => get, g => fullTabs.value = cloneDeep(g.tabs)); |
| 12 | +
|
| 13 | +const filtered_fileTree = computed<FileTree>(() => get.fileTree); |
| 14 | +
|
| 15 | +const nodes = computed<TreeNode[]>(() => { |
| 16 | + let nodes = []; |
| 17 | +
|
| 18 | + let key = 0; |
| 19 | + for (const datum of filtered_fileTree.value.data) { |
| 20 | + let node: TreeNode = { |
| 21 | + key: (key++).toString(), label: `[${datum.count}] ${datum.repo} #${datum.pkg}`, children: [], |
| 22 | + }; |
| 23 | + let count_fmt = 0; |
| 24 | + let count_clippy_warn = 0; |
| 25 | + let count_clippy_error = 0; |
| 26 | + for (const report of datum.raw_reports) { |
| 27 | + node.children?.push({ |
| 28 | + key: (key++).toString(), |
| 29 | + label: `[${report.count}] ${report.file}`, |
| 30 | + data: report.file |
| 31 | + }); |
| 32 | +
|
| 33 | + } |
| 34 | + node.data = { |
| 35 | + user: datum.user, repo: datum.repo, pkg: datum.pkg, |
| 36 | + total: datum.count, fmt: count_fmt, clippy_warn: count_clippy_warn, clippy_error: count_clippy_error |
| 37 | + }; |
| 38 | + nodes.push(node); |
| 39 | + } |
| 40 | + return nodes; |
| 41 | +}); |
| 42 | +
|
| 43 | +const selectedKey = ref({}); |
| 44 | +watch(() => ({ key: selectedKey.value, n: nodes.value, ft: filtered_fileTree.value }), |
| 45 | + ({ key, n, ft }) => { |
| 46 | + const val = updateSelectedKey(key, n, ft); |
| 47 | + if (val !== undefined) { |
| 48 | + get.tabs = val.results; |
| 49 | + get.selectedTab = val.selectedTab; |
| 50 | + } else { |
| 51 | + // display full diagnostics if none is selected or something is not found |
| 52 | + get.tabs = cloneDeep(fullTabs.value); |
| 53 | + } |
| 54 | + }); |
| 55 | +
|
| 56 | +function resetSelectKey() { |
| 57 | + selectedKey.value = {}; |
| 58 | + get.tabs = cloneDeep(fullTabs.value); |
| 59 | +} |
| 60 | +
|
| 61 | +// true means keeping file tree panel open (thus shows left arrow icon to indicate close) |
| 62 | +const displayFileTree = ref(true); |
| 63 | +const displayFileTreeIcon = computed<string>(() => displayFileTree.value ? "pi pi-angle-double-left" : "pi pi-angle-double-right"); |
| 64 | +
|
| 65 | +// true means keeping filter panel open (thus shows up arrow icon to indicate close) |
| 66 | +const displayFilters = defineModel<boolean>("filters", { default: true }); |
| 67 | +const displayFiltersIcon = computed<string>(() => displayFilters.value ? "pi pi-angle-double-up" : "pi pi-angle-double-down"); |
| 68 | +
|
| 69 | +onMounted(() => { |
| 70 | + document.addEventListener("keydown", ({ code }: KeyboardEvent) => { |
| 71 | + if (code === "Space") displayFileTree.value = !displayFileTree.value; |
| 72 | + else if (code === "Escape") displayFilters.value = !displayFilters.value; |
| 73 | + else if (code === "ArrowLeft") displayFileTree.value = false; |
| 74 | + else if (code === "ArrowRight") displayFileTree.value = true; |
| 75 | + else if (code === "ArrowUp") displayFilters.value = false; |
| 76 | + else if (code === "ArrowDown") displayFilters.value = true; |
| 77 | + }); |
| 78 | +}); |
| 79 | +
|
| 80 | +const { viewportHeight } = storeToRefs(useStyleStore()); |
| 81 | +const heightCodePanel = computed(() => { |
| 82 | + const height = viewportHeight.value; |
| 83 | + // add more space to scroll codeblock panel to the bottom if filters exist |
| 84 | + const adjust = displayFilters.value ? 100 : 0; |
| 85 | + return `${height * 0.85 - adjust}px`; |
| 86 | +}); |
| 87 | +</script> |
| 88 | + |
| 89 | +<template> |
| 90 | + <div class="fileViewPanel"> |
| 91 | + |
| 92 | + <div class="fileViewNavi" v-if="displayFileTree"> |
| 93 | + <div style="height: 3.2rem; display: flex; justify-content: space-between; align-items: center;"> |
| 94 | + <div style="display: flex; justify-content: left; gap: 8px;"> |
| 95 | + <div style="margin-left: 10px;"> |
| 96 | + <Button class="btn" :icon="displayFileTreeIcon" severity="secondary" variant="text" |
| 97 | + @click="() => displayFileTree = !displayFileTree" /> |
| 98 | + </div> |
| 99 | + <div> |
| 100 | + <Button class="btn" :icon="displayFiltersIcon" severity="secondary" variant="text" |
| 101 | + @click="() => displayFilters = !displayFilters" /> |
| 102 | + </div> |
| 103 | + </div> |
| 104 | + <div v-if="count" style="padding-right: 0.6rem;"> |
| 105 | + <b style="margin-right: 10px;">Total Count:</b> |
| 106 | + <Button class="btn" severity="danger" @click="resetSelectKey"> {{ count }} </Button> |
| 107 | + </div> |
| 108 | + </div> |
| 109 | + |
| 110 | + <ScrollPanel class="fileViewMenu" :style="{ height: heightCodePanel }"> |
| 111 | + <PackageFileMenu :nodes="nodes" :selectedKey="selectedKey" @update:selectedKey="selectedKey = $event" /> |
| 112 | + </ScrollPanel> |
| 113 | + </div> |
| 114 | + |
| 115 | + <div class="fileViewResult"> |
| 116 | + <Tabs :value="get.selectedTab" scrollable> |
| 117 | + <TabList> |
| 118 | + <Tab v-for="tab in get.tabs" :value="tab.kind" :disabled="tab.disabled"> |
| 119 | + {{ tab.kind }} |
| 120 | + <span class="tabBadge"> |
| 121 | + <Badge :value="tab.raw.length" :severity="tab.severity" /> |
| 122 | + </span> |
| 123 | + </Tab> |
| 124 | + </TabList> |
| 125 | + <TabPanels> |
| 126 | + <TabPanel v-for="tab in get.tabs" :value="tab.kind"> |
| 127 | + <ScrollPanel :dt="{ bar: { background: '{primary.color}' } }" :style="{ height: heightCodePanel }"> |
| 128 | + <CodeBlock :snippets="tab.raw" :lang="tab.lang" /> |
| 129 | + </ScrollPanel> |
| 130 | + </TabPanel> |
| 131 | + </TabPanels> |
| 132 | + </Tabs> |
| 133 | + </div> |
| 134 | + |
| 135 | + </div> |
| 136 | +</template> |
| 137 | + |
| 138 | +<style scoped> |
| 139 | +.tabBadge { |
| 140 | + vertical-align: super; |
| 141 | + --p-badge-padding: 0.2rem; |
| 142 | + --p-badge-font-size: normal; |
| 143 | + --p-badge-height: 1rem; |
| 144 | + --p-badge-min-width: 1.5rem; |
| 145 | + --p-badge-secondary-color: grey; |
| 146 | +} |
| 147 | +
|
| 148 | +.fileViewPanel { |
| 149 | + display: flex; |
| 150 | +} |
| 151 | +
|
| 152 | +.fileViewNavi { |
| 153 | + flex: 0 0 25%; |
| 154 | + padding-left: 0.25rem; |
| 155 | + padding-right: 0.5rem; |
| 156 | + /* flex-grow, flex-shrink, flex-basis */ |
| 157 | + /* 左边div不扩展也不收缩,基础宽度为10% */ |
| 158 | +} |
| 159 | +
|
| 160 | +.fileViewMenu { |
| 161 | + flex: 1; |
| 162 | + height: 92vh; |
| 163 | + /* 允许不含空格的单词在任何地方换行 */ |
| 164 | + word-break: break-all; |
| 165 | + font-size: smaller; |
| 166 | +} |
| 167 | +
|
| 168 | +.fileViewResult { |
| 169 | + flex: 1; |
| 170 | + overflow-x: auto; |
| 171 | + overflow-y: auto; |
| 172 | + padding: 0rem 0.5rem 0rem 1rem; |
| 173 | + /* 控制代码块容器的 padding: 上、右、下、左 */ |
| 174 | + --p-tabs-tabpanel-padding: 0.35rem 0rem 0 0; |
| 175 | + /* 右边div占据剩余空间 */ |
| 176 | + /* 可以省略flex-grow为1,因为默认值就是1 */ |
| 177 | +
|
| 178 | + /* 选中标签页的底部块的高度 */ |
| 179 | + --p-tabs-active-bar-height: 3.2px; |
| 180 | +} |
| 181 | +
|
| 182 | +.btn { |
| 183 | + height: 2.4rem; |
| 184 | +} |
| 185 | +</style> |
0 commit comments