✨ Add JSON viewer component for displaying game master data
This commit is contained in:
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="fileContent"
|
||||||
|
class="content-viewer"
|
||||||
|
:class="{
|
||||||
|
'dark-mode': preferences.darkMode,
|
||||||
|
'line-wrap': preferences.lineWrap
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<RecycleScroller
|
||||||
|
v-if="displayLines.length > 1000"
|
||||||
|
ref="virtualScroller"
|
||||||
|
class="scroller"
|
||||||
|
:items="displayLines"
|
||||||
|
:item-size="lineHeight"
|
||||||
|
key-field="lineNumber"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div
|
||||||
|
:data-line="item.lineNumber"
|
||||||
|
:class="[
|
||||||
|
'line',
|
||||||
|
{ selected: selectedLines.has(item.lineNumber) },
|
||||||
|
{ 'highlight-match': item.hasMatch },
|
||||||
|
{ 'current-result': isCurrentResult(item.lineNumber) }
|
||||||
|
]"
|
||||||
|
@click="toggleLineSelection(item.lineNumber, $event)"
|
||||||
|
>
|
||||||
|
<span v-if="preferences.showLineNumbers" class="line-number">
|
||||||
|
{{ item.lineNumber }}
|
||||||
|
</span>
|
||||||
|
<pre v-highlight="highlightConfig"><code>{{ item.content }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecycleScroller>
|
||||||
|
|
||||||
|
<div v-else class="lines-container">
|
||||||
|
<div
|
||||||
|
v-for="line in displayLines"
|
||||||
|
:key="line.lineNumber"
|
||||||
|
:data-line="line.lineNumber"
|
||||||
|
:class="[
|
||||||
|
'line',
|
||||||
|
{ selected: selectedLines.has(line.lineNumber) },
|
||||||
|
{ 'highlight-match': line.hasMatch },
|
||||||
|
{ 'current-result': isCurrentResult(line.lineNumber) }
|
||||||
|
]"
|
||||||
|
@click="toggleLineSelection(line.lineNumber, $event)"
|
||||||
|
>
|
||||||
|
<span v-if="preferences.showLineNumbers" class="line-number">
|
||||||
|
{{ line.lineNumber }}
|
||||||
|
</span>
|
||||||
|
<pre v-highlight="highlightConfig"><code>{{ line.content }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="fileTooLarge" class="warning-banner">
|
||||||
|
⚠️ File exceeds 10,000 lines. Showing first 10,000 lines only. Use search
|
||||||
|
or filters to find specific content.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, toRef, ref } from 'vue';
|
||||||
|
import { RecycleScroller } from 'vue-virtual-scroller';
|
||||||
|
import { useLineSelection } from '../../composables/useLineSelection.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
displayLines: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
fileContent: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
selectedFile: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
fileTooLarge: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
darkMode: false,
|
||||||
|
lineWrap: false,
|
||||||
|
showLineNumbers: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
searchResults: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
currentResultIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
highlightConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
theme: 'github',
|
||||||
|
language: 'json'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
lineHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayLines = toRef(props, 'displayLines');
|
||||||
|
const fileContent = toRef(props, 'fileContent');
|
||||||
|
const selectedFile = toRef(props, 'selectedFile');
|
||||||
|
|
||||||
|
const virtualScroller = ref(null);
|
||||||
|
|
||||||
|
const { selectedLines, toggleLineSelection } = useLineSelection(
|
||||||
|
displayLines,
|
||||||
|
fileContent,
|
||||||
|
selectedFile
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCurrentResult = lineNumber => {
|
||||||
|
if (!props.searchResults.length) return false;
|
||||||
|
const currentLine = props.searchResults[props.currentResultIndex] + 1;
|
||||||
|
return lineNumber === currentLine;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
selectedLines,
|
||||||
|
toggleLineSelection,
|
||||||
|
virtualScroller
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-viewer {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-viewer.dark-mode {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-color: #333;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-viewer.line-wrap pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller,
|
||||||
|
.lines-container {
|
||||||
|
max-height: 65vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line.selected {
|
||||||
|
background: rgba(0, 123, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line.highlight-match {
|
||||||
|
background: rgba(255, 234, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line.current-result {
|
||||||
|
outline: 1px solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
color: #999;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-banner {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border-top: 1px solid #ffeeba;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user