Add JSON viewer component for displaying game master data

This commit is contained in:
2026-01-29 03:55:00 +00:00
parent c6fc7894dc
commit 418c2cb20f

View File

@@ -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>