virtual scroll table

This commit is contained in:
2024-05-21 13:29:09 +02:00
parent b81282b42e
commit 7a7dba010e
4 changed files with 154 additions and 72 deletions

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import { useElementBounding, useVirtualList } from '@vueuse/core';
import { computed, ref } from 'vue';
interface Props {
list?: any[];
itemHeight: number;
headerHeight?: number;
bottom?: string;
}
const props = withDefaults(defineProps<Props>(), {
list: () => [],
});
const { list: values, containerProps, wrapperProps } = useVirtualList(computed(() => props.list), {
itemHeight: () => props.itemHeight,
})
const tableTop = ref<HTMLSpanElement | null>(null);
const { bottom: offset } = useElementBounding(tableTop);
const ypx = computed(() => {
let y = (offset.value ?? 0) + 'px';
if (props.bottom) {
y = `calc(${y} + ${props.bottom})`;
}
return y;
})
const scrollBarTop = computed(() => {
const h = props.headerHeight ?? props.itemHeight ?? 0;
return h + 'px';
})
</script>
<template>
<span ref="tableTop" class="h-0" />
<div v-if="list.length > 0" v-bind="containerProps" class="table-container">
<table v-bind="wrapperProps">
<slot :list="values" />
</table>
</div>
<slot v-else name="empty" />
</template>
<style scoped lang="postcss">
div.table-container {
@apply bg-slate-600;
max-height: calc(100vh - v-bind(ypx));
:deep(>table) {
@apply bg-slate-800;
>thead {
@apply sticky top-0 z-10;
top: -1px;
}
}
&::-webkit-scrollbar-track {
margin-top: v-bind(scrollBarTop);
}
}
</style>

View File

@@ -1,3 +1,6 @@
export { default as SortableHeader } from './SortableHeader.vue';
export * from './sort'; export * from './sort';
export { default as SortableHeader } from './SortableHeader.vue';
export { default as VirtualScrollTable } from './VirtualScrollTable.vue';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { SortableHeader, useSort } from '@/components/table'; import { SortableHeader, useSort, VirtualScrollTable } from '@/components/table';
import { formatIsk, percentFormater } from '@/formaters'; import { formatIsk, percentFormater } from '@/formaters';
import { MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore } from "@/market"; import { MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore } from "@/market";
import { MinusIcon, PlusIcon } from '@heroicons/vue/24/outline'; import { MinusIcon, PlusIcon } from '@heroicons/vue/24/outline';
@@ -137,40 +137,47 @@ const getLineColor = (result: Result) => {
</div> </div>
</div> </div>
</div> </div>
<table> <VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
<thead> <template #default="{ list }">
<tr> <thead>
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader> <tr>
<SortableHeader v-bind="headerProps" sortKey="buy">Buy</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="sell">Sell</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="buy">Buy</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="price">Bought Price</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="sell">Sell</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="remaining">Remaining Amount</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="price">Bought Price</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="precentProfit">Profit (%)</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="remaining">Remaining Amount</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="iskProfit">Profit (ISK)</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="precentProfit">Profit (%)</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="buttons" unsortable /> <SortableHeader v-bind="headerProps" sortKey="iskProfit">Profit (ISK)</SortableHeader>
</tr> <SortableHeader v-bind="headerProps" sortKey="buttons" unsortable />
</thead> </tr>
<tbody> </thead>
<tr v-for="r in sortedArray" :key="r.id" :class="getLineColor(r)"> <tbody>
<td v-if="showColumn('name')"> <tr v-for="r in list" :key="r.data.id" :class="getLineColor(r.data)">
<div class="flex"> <td v-if="showColumn('name')">
<MarketTypeLabel :id="r.type.id" :name="r.name" /> <div class="flex">
<AcquisitionQuantilsTooltip :id="r.type.id" :buy="r.buy" :sell="r.sell" /> <MarketTypeLabel :id="r.data.type.id" :name="r.data.name" />
</div> <AcquisitionQuantilsTooltip :id="r.data.type.id" :buy="r.data.buy" :sell="r.data.sell" />
</td> </div>
<td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.buy) }}</td> </td>
<td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.sell) }}</td> <td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.data.buy) }}</td>
<td v-if="showColumn('price')" class="text-right">{{ formatIsk(r.price) }}</td> <td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.data.sell) }}</td>
<td v-if="showColumn('remaining')" class="text-right">{{ r.remaining }}/{{ r.quantity }}</td> <td v-if="showColumn('price')" class="text-right">{{ formatIsk(r.data.price) }}</td>
<td v-if="showColumn('precentProfit')" class="text-right">{{ percentFormater.format(r.precentProfit) }}</td> <td v-if="showColumn('remaining')" class="text-right">{{ r.data.remaining }}/{{ r.data.quantity }}</td>
<td v-if="showColumn('iskProfit')" class="text-right">{{ formatIsk(r.iskProfit) }}</td> <td v-if="showColumn('precentProfit')" class="text-right">{{ percentFormater.format(r.data.precentProfit) }}</td>
<td v-if="showColumn('buttons')" class="text-right"> <td v-if="showColumn('iskProfit')" class="text-right">{{ formatIsk(r.data.iskProfit) }}</td>
<button class="btn-icon me-1" @click="$emit('buy', r.acquisitions, r.price, r.buy, r.sell)"><PlusIcon /></button> <td v-if="showColumn('buttons')" class="text-right">
<button class="btn-icon me-1" @click="$emit('sell', r.acquisitions)"><MinusIcon /></button> <button class="btn-icon me-1" @click="$emit('buy', r.data.acquisitions, r.data.price, r.data.buy, r.data.sell)"><PlusIcon /></button>
</td> <button class="btn-icon me-1" @click="$emit('sell', r.data.acquisitions)"><MinusIcon /></button>
</tr> </td>
</tbody> </tr>
</table> </tbody>
</template>
<template #empty>
<div class="text-center mt-4">
<span>No items found</span>
</div>
</template>
</VirtualScrollTable>
</template> </template>
<style scoped lang="postcss"> <style scoped lang="postcss">

View File

@@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { SliderCheckbox } from '@/components'; import { SliderCheckbox } from '@/components';
import { SortableHeader, useSort } from '@/components/table'; import { SortableHeader, useSort, VirtualScrollTable } from '@/components/table';
import { formatIsk, percentFormater } from '@/formaters'; import { formatIsk, percentFormater } from '@/formaters';
import { MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore } from "@/market"; import { MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore } from "@/market";
import { BookmarkSlashIcon, ShoppingCartIcon } from '@heroicons/vue/24/outline'; import { BookmarkSlashIcon, ShoppingCartIcon } from '@heroicons/vue/24/outline';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { TrackingResult, getHistoryQuartils } from '.'; import { getHistoryQuartils } from './HistoryQuartils';
import { TrackingResult } from './tracking';
type Result = { type Result = {
type: MarketType; type: MarketType;
@@ -112,43 +113,50 @@ const getLineColor = (result: Result) => {
</div> </div>
<div class="end"> <div class="end">
<span>Filter: </span> <span>Filter: </span>
<input type="search" class="w-96" v-model="filter" > <input type="search" class="w-96" v-model="filter" />
</div> </div>
</div> </div>
</div> </div>
<table> <VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
<thead> <template #default="{ list }">
<tr> <thead>
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader> <tr>
<SortableHeader v-bind="headerProps" sortKey="buy">Buy</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="sell">Sell</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="buy">Buy</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="q1">Q1</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="sell">Sell</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="median">Median</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="q1">Q1</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="q3">Q3</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="median">Median</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="profit">Profit</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="q3">Q3</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="score">Score</SortableHeader> <SortableHeader v-bind="headerProps" sortKey="profit">Profit</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="buttons" unsortable /> <SortableHeader v-bind="headerProps" sortKey="score">Score</SortableHeader>
</tr> <SortableHeader v-bind="headerProps" sortKey="buttons" unsortable />
</thead> </tr>
<tbody> </thead>
<tr v-for="r in sortedArray" :key="r.typeID" :class="getLineColor(r)"> <tbody>
<td v-if="showColumn('name')"> <tr v-for="r in list" :key="r.data.typeID" :class="getLineColor(r.data)">
<MarketTypeLabel :id="r.typeID" :name="r.name" /> <td v-if="showColumn('name')">
</td> <MarketTypeLabel :id="r.data.typeID" :name="r.data.name" />
<td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.buy) }}</td> </td>
<td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.sell) }}</td> <td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.data.buy) }}</td>
<td v-if="showColumn('q1')" class="text-right">{{ formatIsk(r.q1) }}</td> <td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.data.sell) }}</td>
<td v-if="showColumn('median')" class="text-right">{{ formatIsk(r.median) }}</td> <td v-if="showColumn('q1')" class="text-right">{{ formatIsk(r.data.q1) }}</td>
<td v-if="showColumn('q3')" class="text-right">{{ formatIsk(r.q3) }}</td> <td v-if="showColumn('median')" class="text-right">{{ formatIsk(r.data.median) }}</td>
<td v-if="showColumn('profit')" class="text-right">{{ percentFormater.format(r.profit) }}</td> <td v-if="showColumn('q3')" class="text-right">{{ formatIsk(r.data.q3) }}</td>
<td v-if="showColumn('score')" class="text-right">{{ scoreFormater.format(r.score) }}</td> <td v-if="showColumn('profit')" class="text-right">{{ percentFormater.format(r.data.profit) }}</td>
<td v-if="showColumn('buttons')" class="text-right"> <td v-if="showColumn('score')" class="text-right">{{ scoreFormater.format(r.data.score) }}</td>
<button class="btn-icon me-1" title="Add acquisitions" @click="$emit('buy', r.type, r.buy, r.sell)"><ShoppingCartIcon /></button> <td v-if="showColumn('buttons')" class="text-right">
<button class="btn-icon me-1" title="Untrack" @click="$emit('remove', r.type)"><BookmarkSlashIcon /></button> <button class="btn-icon me-1" title="Add acquisitions" @click="$emit('buy', r.data.type, r.data.buy, r.data.sell)"><ShoppingCartIcon /></button>
</td> <button class="btn-icon me-1" title="Untrack" @click="$emit('remove', r.data.type)"><BookmarkSlashIcon /></button>
</tr> </td>
</tbody> </tr>
</table> </tbody>
</template>
<template #empty>
<div class="text-center mt-4">
<span>No items found</span>
</div>
</template>
</VirtualScrollTable>
</template> </template>
<style scoped lang="postcss"> <style scoped lang="postcss">