123 lines
2.4 KiB
Vue
123 lines
2.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
|
|
export interface TableColumn {
|
|
key: string
|
|
label: string
|
|
width?: string
|
|
}
|
|
|
|
const props = defineProps<{
|
|
columns: TableColumn[]
|
|
rows: Record<string, unknown>[]
|
|
sortKey?: string
|
|
sortDir?: 'asc' | 'desc'
|
|
}>()
|
|
|
|
const emits = defineEmits<{
|
|
sort: [key: string]
|
|
}>()
|
|
|
|
const sorted = computed(() => {
|
|
if (!props.sortKey) return props.rows
|
|
const key = props.sortKey
|
|
const dir = props.sortDir === 'desc' ? -1 : 1
|
|
return [...props.rows].sort((a, b) => {
|
|
const av = a[key] as number | string
|
|
const bv = b[key] as number | string
|
|
if (av < bv) return -1 * dir
|
|
if (av > bv) return 1 * dir
|
|
return 0
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="table-renderer">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th
|
|
v-for="col in columns"
|
|
:key="col.key"
|
|
:style="{ width: col.width }"
|
|
@click="emits('sort', col.key)"
|
|
class="sortable"
|
|
>
|
|
{{ col.label }}
|
|
<span v-if="sortKey === col.key" class="sort-indicator">
|
|
{{ sortDir === 'desc' ? '▼' : '▲' }}
|
|
</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(row, i) in sorted" :key="i">
|
|
<td v-for="col in columns" :key="col.key">
|
|
{{ row[col.key] }}
|
|
</td>
|
|
</tr>
|
|
<tr v-if="rows.length === 0">
|
|
<td :colspan="columns.length" class="empty">No detections yet</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.table-renderer {
|
|
overflow: auto;
|
|
height: 100%;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
th {
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--surface-2);
|
|
color: var(--text-secondary);
|
|
font-weight: 600;
|
|
text-align: left;
|
|
padding: var(--space-2) var(--space-3);
|
|
border-bottom: var(--panel-border);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
th:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.sort-indicator {
|
|
font-size: 9px;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
td {
|
|
padding: var(--space-1) var(--space-3);
|
|
border-bottom: 1px solid var(--surface-3);
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
tr:hover td {
|
|
background: var(--surface-3);
|
|
}
|
|
|
|
.empty {
|
|
color: var(--text-dim);
|
|
text-align: center;
|
|
padding: var(--space-6);
|
|
}
|
|
</style>
|