New eveal #32
@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
autoClose: true
|
autoClose: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = defineModel<boolean>('open', {default: false});
|
||||||
const root = ref<HTMLElement | null>(null);
|
const root = ref<HTMLElement | null>(null);
|
||||||
const floating = ref<HTMLElement | null>(null);
|
const floating = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
ReprocessingApi,
|
ReprocessingApi,
|
||||||
RuleBookApi,
|
RuleBookApi,
|
||||||
RuleScriptApi,
|
RuleScriptApi,
|
||||||
TransactionApi
|
TransactionApi,
|
||||||
|
UniverseApi
|
||||||
} from "@/generated/mammon";
|
} from "@/generated/mammon";
|
||||||
|
|
||||||
export const mammonUrl = import.meta.env.VITE_MAMMON_URL;
|
export const mammonUrl = import.meta.env.VITE_MAMMON_URL;
|
||||||
@@ -106,3 +107,4 @@ export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInst
|
|||||||
export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const reprocessingApi = new ReprocessingApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const reprocessingApi = new ReprocessingApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
|
export const universeApi = new UniverseApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||||
|
import {MarketTypeLabel} from '@/market/type';
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {MarketLocation} from '../location';
|
||||||
|
import IskLabel from '../IskLabel.vue';
|
||||||
|
import {ComparedItem} from './compare';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
result?: ComparedItem[];
|
||||||
|
locations?: MarketLocation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
result: () => [],
|
||||||
|
locations: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
const buyKey = (id: number): `buy:${number}` => `buy:${id}`;
|
||||||
|
const sellKey = (id: number): `sell:${number}` => `sell:${id}`;
|
||||||
|
|
||||||
|
type SortableItem = ComparedItem & Record<`buy:${number}` | `sell:${number}`, number>;
|
||||||
|
|
||||||
|
const rows = computed<SortableItem[]>(() => props.result.map(item => {
|
||||||
|
const sortable = {...item} as SortableItem;
|
||||||
|
|
||||||
|
props.locations.forEach(l => {
|
||||||
|
const price = item.prices[l.id];
|
||||||
|
sortable[buyKey(l.id)] = price?.buy ?? 0;
|
||||||
|
sortable[sellKey(l.id)] = price?.sell ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortable;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {sortedArray, headerProps} = useSort(rows, {
|
||||||
|
defaultSortKey: 'name',
|
||||||
|
defaultSortDirection: 'asc'
|
||||||
|
});
|
||||||
|
|
||||||
|
const bestBuy = (row: ComparedItem): number | undefined => {
|
||||||
|
let best: number | undefined;
|
||||||
|
let bestValue = -Infinity;
|
||||||
|
props.locations.forEach(l => {
|
||||||
|
const cell = row.prices[l.id];
|
||||||
|
if (cell && cell.buy > bestValue) {
|
||||||
|
bestValue = cell.buy;
|
||||||
|
best = l.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return bestValue > 0 ? best : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bestSell = (row: ComparedItem): number | undefined => {
|
||||||
|
let best: number | undefined;
|
||||||
|
let bestValue = -Infinity;
|
||||||
|
props.locations.forEach(l => {
|
||||||
|
const cell = row.prices[l.id];
|
||||||
|
if (cell && cell.sell > bestValue) {
|
||||||
|
bestValue = cell.sell;
|
||||||
|
best = l.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return bestValue > 0 ? best : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totals = computed(() => new Map(props.locations.map(l => [l.id, props.result.reduce((acc, row) => {
|
||||||
|
const cell = row.prices[l.id];
|
||||||
|
return {
|
||||||
|
buy: acc.buy + (cell?.buy ?? 0),
|
||||||
|
sell: acc.sell + (cell?.sell ?? 0),
|
||||||
|
};
|
||||||
|
}, {buy: 0, sell: 0})])));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VirtualScrollTable :list="sortedArray" :itemHeight="33" :headerHeight="66" :footerHeight="33" bottom="1rem">
|
||||||
|
<template #default="{ list }">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="name" rowspan="2">Item</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="quantity" rowspan="2">Qty</SortableHeader>
|
||||||
|
<th v-for="location in locations" :key="location.id" colspan="2" class="text-center border-l border-slate-500" :title="location.name">
|
||||||
|
{{ location.systemName }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<template v-for="location in locations" :key="location.id">
|
||||||
|
<SortableHeader v-bind="headerProps" :sortKey="buyKey(location.id)" class="border-l border-slate-500">Buy</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" :sortKey="sellKey(location.id)">Sell</SortableHeader>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in list" :key="r.data.typeId">
|
||||||
|
<td>
|
||||||
|
<MarketTypeLabel :id="r.data.typeId" :name="r.data.name" />
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ r.data.quantity.toLocaleString() }}</td>
|
||||||
|
<template v-for="location in locations" :key="location.id">
|
||||||
|
<td class="text-right border-l border-slate-500" :class="r.data.prices[location.id] ? (bestBuy(r.data) === location.id ? 'bg-emerald-500' : 'bg-amber-900') : ''">
|
||||||
|
<IskLabel v-if="r.data.prices[location.id]" :amount="r.data.prices[location.id].buy" :colored="false" />
|
||||||
|
<span v-else>-</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right" :class="r.data.prices[location.id] ? (bestSell(r.data) === location.id ? 'bg-emerald-500' : 'bg-amber-900') : ''">
|
||||||
|
<IskLabel v-if="r.data.prices[location.id]" :amount="r.data.prices[location.id].sell" :colored="false" />
|
||||||
|
<span v-else>-</span>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="font-semibold">Total</td>
|
||||||
|
<template v-for="location in locations" :key="location.id">
|
||||||
|
<td class="text-right border-l border-slate-500"><IskLabel :amount="totals.get(location.id)?.buy ?? 0" :colored="false" /></td>
|
||||||
|
<td class="text-right"><IskLabel :amount="totals.get(location.id)?.sell ?? 0" :colored="false" /></td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<span>No items found</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VirtualScrollTable>
|
||||||
|
</template>
|
||||||
@@ -8,4 +8,4 @@ export type MarketTypePrice = {
|
|||||||
orderCount: number;
|
orderCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PriceGetter = (types: MarketType[]) => Promise<MarketTypePrice[]>;
|
export type PriceGetter = (types: MarketType[], locationId?: number) => Promise<MarketTypePrice[]>;
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export const useAppraisalStore = defineStore('appraisal', () => {
|
|||||||
|
|
||||||
const getPricesUncached = getMammonPrices;
|
const getPricesUncached = getMammonPrices;
|
||||||
|
|
||||||
const getPrice = async (type: MarketType, regionId?: number): Promise<MarketTypePrice> => (await getPrices([type], regionId))[0];
|
const getPrice = async (type: MarketType, locationId?: number): Promise<MarketTypePrice> => (await getPrices([type], locationId))[0];
|
||||||
const getPrices = async (types: MarketType[], regionId?: number): Promise<MarketTypePrice[]> => {
|
const getPrices = async (types: MarketType[], locationId?: number): Promise<MarketTypePrice[]> => {
|
||||||
const cached: MarketTypePrice[] = [];
|
const cached: MarketTypePrice[] = [];
|
||||||
const uncached: MarketType[] = [];
|
const uncached: MarketType[] = [];
|
||||||
const rId = regionId ?? jitaId;
|
const lId = locationId ?? jitaId;
|
||||||
|
|
||||||
types.forEach(t => {
|
types.forEach(t => {
|
||||||
const cachedPrice = cache.get(rId, t.id);
|
const cachedPrice = cache.get(lId, t.id);
|
||||||
|
|
||||||
if (cachedPrice) {
|
if (cachedPrice) {
|
||||||
cached.push(cachedPrice);
|
cached.push(cachedPrice);
|
||||||
@@ -33,12 +33,12 @@ export const useAppraisalStore = defineStore('appraisal', () => {
|
|||||||
const batches: Promise<MarketTypePrice[]>[] = [];
|
const batches: Promise<MarketTypePrice[]>[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
|
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
|
||||||
batches.push(getPricesUncached(uncached.slice(i, i + BATCH_SIZE)));
|
batches.push(getPricesUncached(uncached.slice(i, i + BATCH_SIZE), lId));
|
||||||
}
|
}
|
||||||
|
|
||||||
const prices = (await Promise.all(batches)).flat();
|
const prices = (await Promise.all(batches)).flat();
|
||||||
|
|
||||||
prices.forEach(p => cache.set(rId, p.type.id, p));
|
prices.forEach(p => cache.set(lId, p.type.id, p));
|
||||||
return [ ...cached, ...prices ];
|
return [ ...cached, ...prices ];
|
||||||
}
|
}
|
||||||
return cached;
|
return cached;
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {marketApi} from '@/mammon';
|
||||||
|
import {getMarketTypes, MarketType} from '../type';
|
||||||
|
import {MarketTypePrice} from './MarketTypePrice';
|
||||||
|
|
||||||
|
export type LocationPrice = {
|
||||||
|
buyUnit: number;
|
||||||
|
sellUnit: number;
|
||||||
|
buy: number;
|
||||||
|
sell: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComparedItem = {
|
||||||
|
typeId: number;
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
volume: number;
|
||||||
|
prices: Record<number, LocationPrice>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PricesForLocation = (types: MarketType[], locationId: number) => Promise<MarketTypePrice[]>;
|
||||||
|
|
||||||
|
export const compareAppraisal = async (
|
||||||
|
raw: string,
|
||||||
|
locationIds: number[],
|
||||||
|
getPrices: PricesForLocation
|
||||||
|
): Promise<ComparedItem[]> => {
|
||||||
|
if (!raw?.trim() || locationIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stacks = await marketApi.parseStacks(raw).then(r => r.data);
|
||||||
|
|
||||||
|
if (stacks.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantities = new Map(stacks.map(s => [s.marketTypeId, s.quantity]));
|
||||||
|
const types = await getMarketTypes(stacks.map(s => s.marketTypeId));
|
||||||
|
|
||||||
|
const rows = new Map<number, ComparedItem>(types.map(t => [t.id, {
|
||||||
|
typeId: t.id,
|
||||||
|
name: t.name,
|
||||||
|
quantity: quantities.get(t.id) ?? 0,
|
||||||
|
volume: t.volume,
|
||||||
|
prices: {},
|
||||||
|
}]));
|
||||||
|
|
||||||
|
const pricesByLocation = await Promise.all(
|
||||||
|
locationIds.map(async locationId => [locationId, await getPrices(types, locationId)] as const)
|
||||||
|
);
|
||||||
|
|
||||||
|
pricesByLocation.forEach(([locationId, prices]) => prices.forEach(p => {
|
||||||
|
const row = rows.get(p.type.id);
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
row.prices[locationId] = {
|
||||||
|
buyUnit: p.buy,
|
||||||
|
sellUnit: p.sell,
|
||||||
|
buy: p.buy * row.quantity,
|
||||||
|
sell: p.sell * row.quantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...rows.values()];
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export * from './MarketTypePrice';
|
export * from './MarketTypePrice';
|
||||||
export * from './appraisal';
|
export * from './appraisal';
|
||||||
export * from './appraise';
|
export * from './appraise';
|
||||||
|
export * from './compare';
|
||||||
|
|
||||||
export { default as AppraisalResultTable } from './AppraisalResultTable.vue';
|
export { default as AppraisalResultTable } from './AppraisalResultTable.vue';
|
||||||
|
export { default as CompareResultTable } from './CompareResultTable.vue';
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {marketApi} from '@/mammon/mammonService';
|
import {marketApi} from '@/mammon/mammonService';
|
||||||
import {MarketTypePrice, PriceGetter} from './MarketTypePrice';
|
import {MarketTypePrice, PriceGetter} from './MarketTypePrice';
|
||||||
|
|
||||||
export const getMammonPrices: PriceGetter = async types => {
|
export const getMammonPrices: PriceGetter = async (types, locationId) => {
|
||||||
if (types.length === 0) {
|
if (types.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const typesById = new Map(types.map(t => [t.id, t]));
|
const typesById = new Map(types.map(t => [t.id, t]));
|
||||||
const response = await marketApi.currentPrices(types.map(t => t.id));
|
const response = await marketApi.currentPrices(types.map(t => t.id), locationId);
|
||||||
|
|
||||||
return response.data.reduce<MarketTypePrice[]>((prices, p) => {
|
return response.data.reduce<MarketTypePrice[]>((prices, p) => {
|
||||||
const type = typesById.get(p.marketTypeId);
|
const type = typesById.get(p.marketTypeId);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './RegionalMarketCache';
|
export * from './RegionalMarketCache';
|
||||||
export * from './history';
|
export * from './history';
|
||||||
|
export * from './location';
|
||||||
export * from './order';
|
export * from './order';
|
||||||
export * from './tax';
|
export * from './tax';
|
||||||
export * from './type';
|
export * from './type';
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {universeApi} from '@/mammon/mammonService';
|
||||||
|
import type {MarketLocationResponse} from '@/generated/mammon';
|
||||||
|
|
||||||
|
export type MarketLocation = MarketLocationResponse;
|
||||||
|
|
||||||
|
const cache = new Map<number, MarketLocation>();
|
||||||
|
|
||||||
|
export const searchMarketLocations = async (search: string): Promise<MarketLocation[]> => {
|
||||||
|
if (search.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const locations = await universeApi.searchLocations(search).then(r => r.data);
|
||||||
|
locations.forEach(l => cache.set(l.id, l));
|
||||||
|
return locations;
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {vOnClickOutside} from '@vueuse/components';
|
||||||
|
import {watchDebounced} from '@vueuse/core';
|
||||||
|
import {ref, watch} from 'vue';
|
||||||
|
import {MarketLocation, searchMarketLocations} from './MarketLocation';
|
||||||
|
|
||||||
|
const modelValue = defineModel<MarketLocation>();
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const search = ref(modelValue.value?.name ?? '');
|
||||||
|
const suggestions = ref<MarketLocation[]>([]);
|
||||||
|
|
||||||
|
const select = (location: MarketLocation) => {
|
||||||
|
modelValue.value = location;
|
||||||
|
search.value = location.name;
|
||||||
|
suggestions.value = [];
|
||||||
|
isOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => modelValue.value, v => {
|
||||||
|
search.value = v?.name ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
watchDebounced(search, async value => {
|
||||||
|
const term = value.trim();
|
||||||
|
|
||||||
|
if (!isOpen.value || term.length < 3 || term === modelValue.value?.name) {
|
||||||
|
suggestions.value = [];
|
||||||
|
} else {
|
||||||
|
suggestions.value = await searchMarketLocations(term);
|
||||||
|
}
|
||||||
|
}, {debounce: 300});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div @click="isOpen = true" v-on-click-outside="() => isOpen = false">
|
||||||
|
<input type="text" class="w-96" v-model="search" placeholder="Search a market location…" />
|
||||||
|
<div v-if="isOpen && suggestions.length > 0" class="z-20 absolute w-96">
|
||||||
|
<div class="rounded-b bg-slate-500 max-h-72 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="s in suggestions"
|
||||||
|
:key="s.id"
|
||||||
|
class="px-1 py-0.5 cursor-pointer whitespace-nowrap overflow-hidden hover:bg-slate-700"
|
||||||
|
@click="select(s)"
|
||||||
|
>
|
||||||
|
{{ s.name }} <span class="text-slate-300">({{ s.regionName }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './MarketLocation';
|
||||||
|
export { default as MarketLocationInput } from './MarketLocationInput.vue';
|
||||||
@@ -1 +1 @@
|
|||||||
export const jitaId = 10000002;
|
export const jitaId = 60003760;
|
||||||
@@ -18,6 +18,9 @@ import {routeNames} from '@/routes';
|
|||||||
<RouterLink :to="{name: routeNames.appraise}" class="tab">
|
<RouterLink :to="{name: routeNames.appraise}" class="tab">
|
||||||
<span>Appraisal</span>
|
<span>Appraisal</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{name: routeNames.compareAppraisal}" class="tab">
|
||||||
|
<span>Compare</span>
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {Dropdown} from '@/components';
|
||||||
import {AppraisalItemValue, AppraisalResultTable, appraise} from '@/market/appraisal';
|
import {AppraisalItemValue, AppraisalResultTable, appraise} from '@/market/appraisal';
|
||||||
import {ref} from 'vue';
|
import {ref} from 'vue';
|
||||||
|
|
||||||
const items = ref('');
|
const items = ref('');
|
||||||
|
const showInputs = ref(true);
|
||||||
|
|
||||||
const result = ref<AppraisalItemValue[]>([]);
|
const result = ref<AppraisalItemValue[]>([]);
|
||||||
|
|
||||||
@@ -10,7 +12,9 @@ const send = async () => result.value = await appraise(items.value);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-stretch mt-4">
|
<Dropdown inline :auto-close="false" v-model:open="showInputs" class="mt-4">
|
||||||
|
<template #button>Items</template>
|
||||||
|
<div class="flex items-stretch mt-2">
|
||||||
<div class="flex-1 mx-1">
|
<div class="flex-1 mx-1">
|
||||||
<span>Items</span>
|
<span>Items</span>
|
||||||
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
|
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
|
||||||
@@ -19,6 +23,7 @@ const send = async () => result.value = await appraise(items.value);
|
|||||||
<div class="grid my-2">
|
<div class="grid my-2">
|
||||||
<button class="justify-self-end" @click="send" :disabled="!items.trim()">Send</button>
|
<button class="justify-self-end" @click="send" :disabled="!items.trim()">Send</button>
|
||||||
</div>
|
</div>
|
||||||
|
</Dropdown>
|
||||||
<template v-if="result.length > 0">
|
<template v-if="result.length > 0">
|
||||||
<hr />
|
<hr />
|
||||||
<div class="grid mt-2">
|
<div class="grid mt-2">
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {Dropdown} from '@/components';
|
||||||
|
import {compareAppraisal, ComparedItem, CompareResultTable, useAppraisalStore} from '@/market/appraisal';
|
||||||
|
import {MarketLocation, MarketLocationInput} from '@/market/location';
|
||||||
|
|
||||||
|
const store = useAppraisalStore();
|
||||||
|
|
||||||
|
const items = ref('');
|
||||||
|
const from = ref<MarketLocation>();
|
||||||
|
const to = ref<MarketLocation>();
|
||||||
|
const showInputs = ref(true);
|
||||||
|
|
||||||
|
const result = ref<ComparedItem[]>([]);
|
||||||
|
const resultLocations = ref<MarketLocation[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
const selected = [from.value, to.value].filter((l): l is MarketLocation => l !== undefined);
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
result.value = await compareAppraisal(items.value, selected.map(l => l.id), store.getPrices);
|
||||||
|
resultLocations.value = selected;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="showInputs" class="mt-4">
|
||||||
|
<template #button>Items & locations</template>
|
||||||
|
<div class="flex items-stretch mt-2">
|
||||||
|
<div class="flex-1 mx-1">
|
||||||
|
<span>Items</span>
|
||||||
|
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-6 mx-1 mt-2">
|
||||||
|
<div>
|
||||||
|
<span>From</span>
|
||||||
|
<div class="mt-1">
|
||||||
|
<MarketLocationInput v-model="from" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>To</span>
|
||||||
|
<div class="mt-1">
|
||||||
|
<MarketLocationInput v-model="to" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid my-2">
|
||||||
|
<button class="justify-self-end" @click="send" :disabled="!items.trim() || !from || !to || loading">Send</button>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
<template v-if="result.length > 0">
|
||||||
|
<hr />
|
||||||
|
<div class="grid mt-2">
|
||||||
|
<CompareResultTable :result="result" :locations="resultLocations" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -16,6 +16,7 @@ export const routeNames = {
|
|||||||
editRuleBook: 'edit-rule-book',
|
editRuleBook: 'edit-rule-book',
|
||||||
marketTypes: 'market-types',
|
marketTypes: 'market-types',
|
||||||
appraise: 'appraise',
|
appraise: 'appraise',
|
||||||
|
compareAppraisal: 'compare-appraisal',
|
||||||
about: 'about',
|
about: 'about',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
{path: 'scan', component: () => import('@/pages/market/Scan.vue')},
|
{path: 'scan', component: () => import('@/pages/market/Scan.vue')},
|
||||||
{path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
|
{path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
|
||||||
{path: 'appraisal', name: routeNames.appraise, component: () => import('@/pages/market/Appraise.vue')},
|
{path: 'appraisal', name: routeNames.appraise, component: () => import('@/pages/market/Appraise.vue')},
|
||||||
|
{path: 'appraisal/compare', name: routeNames.compareAppraisal, component: () => import('@/pages/market/CompareAppraisal.vue')},
|
||||||
]},
|
]},
|
||||||
|
|
||||||
{path: '/reprocess', component: () => import('@/pages/Reprocess.vue')},
|
{path: '/reprocess', component: () => import('@/pages/Reprocess.vue')},
|
||||||
|
|||||||
Reference in New Issue
Block a user