This commit is contained in:
2023-09-19 16:04:32 +02:00
parent 6587e4f522
commit 7c645b0d0b
15 changed files with 430 additions and 24 deletions

59
src/Modal.vue Normal file
View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { useEventListener, useVModel } from '@vueuse/core';
import { watch } from 'vue';
interface Props {
open: boolean;
}
interface Emit {
(e: 'update:open', value: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
});
const emit = defineEmits<Emit>();
const isOpen = useVModel(props, 'open', emit, {passive: true});
watch(isOpen, value => {
if (value) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}
});
useEventListener('keyup', e => {
if (e.key === 'Escape') {
isOpen.value = false;
}
});
</script>
<template>
<Transition name="fade">
<template v-if="isOpen">
<div class="fixed inset-0" @click="isOpen = false">
<div class="absolute bg-black opacity-80 inset-0 z-0" />
<div class="absolute grid inset-0">
<div class="justify-self-center" @click.stop>
<slot />
</div>
</div>
</div>
</template>
</Transition>
</template>
<style scoped>
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 100ms ease-out;
}
</style>

15
src/market/appraisal.ts Normal file
View File

@@ -0,0 +1,15 @@
import { evepraisalAxiosInstance } from '@/service';
import { MarketType } from "./type";
export type MarketTypePrice = {
type: MarketType;
buy: number,
sell: number
}
export const getPrice = async (type: MarketType): Promise<MarketTypePrice> => (await getPrices([type]))[0];
export const getPrices = async (types: MarketType[]): Promise<MarketTypePrice[]> => (await evepraisalAxiosInstance.post(`/appraisal.json?market=jita&persist=no&raw_textarea=${types.map(t => t.name).join("%0A")}`)).data.appraisal.items.map((item: any) => ({
type: types.find(t => t.name === item.typeName),
buy: item.prices.buy.max,
sell: item.prices.sell.min
}));

View File

@@ -1,4 +1,6 @@
export * from './MarketOrderHistory';
export * from './market';
export * from './type';
export * from './MarketOrderHistory';
export * from './appraisal';
export * from './market';

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import Modal from '@/Modal.vue';
import { formatIsk } from '@/formaters';
import { MarketType } from '@/market';
import { useTrackedItemsStorage } from '@/market/track';
import { ref } from 'vue';
interface Emit {
(e: 'added'): void;
}
const emit = defineEmits<Emit>();
const itemsStorage = useTrackedItemsStorage();
const modalOpen = ref<boolean>(false);
const type = ref<MarketType>();
const suggestions = ref<Record<string, number>>({});
const price = ref(1000000);
const count = ref(1);
const open = (t: MarketType, s?: Record<string, number> | number) => {
type.value = t;
count.value = 1;
if (typeof s === 'number') {
suggestions.value = {};
price.value = s;
} else if (s) {
suggestions.value = s;
price.value = Object.values(s)[0];
} else {
suggestions.value = {};
price.value = 1000000;
}
modalOpen.value = true;
}
const add = () => {
const id = type.value?.id;
if (!id) {
modalOpen.value = false;
return;
}
const oldItem = itemsStorage.value.find(i => i.typeID === id);
if (oldItem) {
const item = {
typeID: id,
count: count.value + oldItem.count,
averagePrice: ((price.value * count.value) + (oldItem.averagePrice * oldItem.count)) / (count.value + oldItem.count)
};
itemsStorage.value = itemsStorage.value.map(i => i.typeID === id ? item : i);
} else {
const item = {
typeID: id,
count: count.value,
averagePrice: price.value
};
itemsStorage.value = [ ...itemsStorage.value, item ];
}
emit('added');
modalOpen.value = false;
}
defineExpose({ open });
</script>
<template>
<Modal v-model:open="modalOpen">
<div class="p-4 bg-slate-800 rounded mt-20 flex">
<div class="flex me-2">
<span>Price: </span>
<div class="ms-2">
<input type="number" min="0" step="1" v-model="price" />
<div class="px-2 mt-2 bg-slate-600 hover:bg-slate-700 border rounded cursor-pointer" v-for="(p, n) of suggestions" :key="n" @click="price = p">{{ n }}: {{ formatIsk(p) }}</div>
</div>
</div>
<div class="flex me-2 mb-auto">
<span>Count: </span>
<input class="ms-2" type="number" min="0" step="1" v-model="count" />
</div>
<button class="mb-auto" @click="add">Add</button>
</div>
</Modal>
</template>

View File

@@ -2,7 +2,7 @@
import { formatIsk, percentFormater } from '@/formaters';
import { MarketType, MarketTypeLabel } from "@/market";
import { SortableHeader, useSort } from '@/table';
import { ArrowPathIcon } from '@heroicons/vue/24/outline';
import { ArrowPathIcon, ShoppingCartIcon } from '@heroicons/vue/24/outline';
import { useStorage } from '@vueuse/core';
import { computed, ref } from 'vue';
import { ScanResult, getHistoryQuartils } from '.';
@@ -26,6 +26,7 @@ interface Props {
interface Emits {
(e: 'relaod', type: MarketType): void;
(e: 'relaodAll'): void;
(e: 'buy', type: MarketType, buy: number, sell: number): void;
}
const props = withDefaults(defineProps<Props>(), {
@@ -113,7 +114,8 @@ const getLineColor = (result: Result) => {
<td class="text-right">{{ formatIsk(r.q3) }}</td>
<td class="text-right">{{ percentFormater.format(r.profit) }}</td>
<td class="text-right">
<button class="btn-icon-stroke" @click="$emit('relaod', r.type)"><ArrowPathIcon /></button>
<button class="btn-icon-stroke me-1" @click="$emit('buy', r.type, r.buy, r.sell)"><ShoppingCartIcon /></button>
<button class="btn-icon-stroke me-1" @click="$emit('relaod', r.type)"><ArrowPathIcon /></button>
</td>
</tr>
</tbody>

View File

@@ -1,4 +1,6 @@
export * from './HistoryQuartils';
export * from './scan';
export { default as BuyModal } from './BuyModal.vue';
export { default as ScanResultTable } from './ScanResultTable.vue';

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import Modal from '@/Modal.vue';
import { MarketType } from '@/market';
import { useTrackedItemsStorage } from '@/market/track';
import { ref } from 'vue';
interface Emit {
(e: 'removed'): void;
}
const emit = defineEmits<Emit>();
const itemsStorage = useTrackedItemsStorage();
const modalOpen = ref<boolean>(false);
const type = ref<MarketType>();
const count = ref(1);
const open = (t: MarketType) => {
type.value = t;
count.value = 1;
modalOpen.value = true;
}
const remove = () => {
const id = type.value?.id;
if (!id) {
modalOpen.value = false;
return;
}
const oldItem = itemsStorage.value.find(i => i.typeID === id);
if (!oldItem) {
modalOpen.value = false;
return;
}
const c = oldItem.count - count.value;
if (c > 0) {
const item = {
typeID: id,
count: oldItem.count - count.value,
averagePrice: oldItem.averagePrice
};
itemsStorage.value = itemsStorage.value.map(i => i.typeID === id ? item : i);
} else {
itemsStorage.value = itemsStorage.value.filter(i => i.typeID !== id);
}
emit('removed');
modalOpen.value = false;
}
defineExpose({ open });
</script>
<template>
<Modal v-model:open="modalOpen">
<div class="p-4 bg-slate-800 rounded mt-20 flex">
<div class="flex me-2 mb-auto">
<span>Count: </span>
<input class="ms-2" type="number" min="0" step="1" v-model="count" />
</div>
<button class="mb-auto" @click="remove">Remove</button>
</div>
</Modal>
</template>

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import { formatIsk, percentFormater } from '@/formaters';
import { MarketType, MarketTypeLabel } from "@/market";
import { SortableHeader, useSort } from '@/table';
import { MinusIcon, PlusIcon } from '@heroicons/vue/24/outline';
import { useStorage } from '@vueuse/core';
import { computed, ref } from 'vue';
import { TrackedItem } from '.';
type Result = {
type: MarketType;
typeID: number;
name: string;
buy: number;
sell: number;
price: number;
count: number;
precentProfit: number;
iskProfit: number;
}
interface Props {
items?: TrackedItem[];
}
interface Emits {
(e: 'buy', type: MarketType, price: number, buy: number, sell: number): void;
(e: 'sell', type: MarketType): void;
}
const props = withDefaults(defineProps<Props>(), {
items: () => []
});
defineEmits<Emits>();
const threshold = useStorage('market-track-threshold', 10);
const filter = ref("");
const { sortedArray, headerProps } = useSort<Result>(computed(() => props.items
.filter(r => r.type.name.toLowerCase().includes(filter.value.toLowerCase()))
.map(r => {
const precentProfit = (r.sell / r.buy) - 1;
return {
type: r.type,
typeID: r.type.id,
name: r.type.name,
buy: r.buy,
sell: r.sell,
price: r.averagePrice,
count: r.count,
precentProfit,
iskProfit: r.averagePrice * precentProfit
};
})), {
defaultSortKey: 'name',
defaultSortDirection: 'asc'
})
const getLineColor = (result: Result) => {
if (result.precentProfit >= (threshold.value / 100)) {
return 'line-green';
}
return '';
}
</script>
<template>
<div class="flex">
<div class="flex justify-self-end mb-2 mt-4 ms-auto">
<div class="end">
<span>Profit Threshold: </span>
<input type="number" min="0" max="1000" step="1" v-model="threshold" />
</div>
<div class="end">
<span>Filter: </span>
<input type="search" class="w-96" v-model="filter" >
</div>
</div>
</div>
<table>
<thead>
<tr>
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="buy">Buy</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="sell">Sell</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="price">Bought Price</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="count">Bought Amount</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="precentProfit">Profit (%)</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="iskProfit">Profit (ISK)</SortableHeader>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="r in sortedArray" :key="r.typeID" :class="getLineColor(r)">
<td>
<MarketTypeLabel :id="r.typeID" :name="r.name" />
</td>
<td class="text-right">{{ formatIsk(r.buy) }}</td>
<td class="text-right">{{ formatIsk(r.sell) }}</td>
<td class="text-right">{{ formatIsk(r.price) }}</td>
<td class="text-right">{{ r.count }}</td>
<td class="text-right">{{ percentFormater.format(r.precentProfit) }}</td>
<td class="text-right">{{ formatIsk(r.iskProfit) }}</td>
<td class="text-right">
<button class="btn-icon-stroke me-1" @click="$emit('buy', r.type, r.price, r.buy, r.sell)"><PlusIcon /></button>
<button class="btn-icon-stroke me-1" @click="$emit('sell', r.type)"><MinusIcon /></button>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
div.end {
@apply justify-self-end ms-2;
}
</style>

View File

@@ -0,0 +1,9 @@
import { MarketType } from "@/market";
export type TrackedItem = {
type: MarketType;
count: number;
averagePrice: number;
buy: number,
sell: number
}

View File

@@ -0,0 +1,6 @@
export * from './TrackedItem';
export * from './storage';
export { default as SellModal } from './SellModal.vue';
export { default as TrackResultTable } from './TrackResultTable.vue';

View File

@@ -0,0 +1,9 @@
import { createSharedComposable, useLocalStorage } from '@vueuse/core';
export type TrackedMarketItemStorage = {
typeID: number;
count: number;
averagePrice: number;
}
export const useTrackedItemsStorage = createSharedComposable(() => useLocalStorage<TrackedMarketItemStorage[]>('market-track-items', []));

View File

@@ -5,7 +5,7 @@ import { RouterLink, RouterView } from 'vue-router';
<template>
<div class="mt-4">
<div class="flex border-b-2">
<div class="flex border-b-2 border-emerald-500">
<RouterLink to="/market/scan" class="tab">
<span>Scan</span>
</RouterLink>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { MarketOrderHistory, MarketType, getHistory, getMarketType, getMarketTypes, jitaId } from "@/market";
import { ScanResult, ScanResultTable } from '@/market/scan';
import { evepraisalAxiosInstance } from '@/service';
import { MarketOrderHistory, MarketType, MarketTypePrice, getHistory, getMarketType, getMarketTypes, getPrice, getPrices, jitaId } from "@/market";
import { BuyModal, ScanResult, ScanResultTable } from '@/market/scan';
import { useStorage } from '@vueuse/core';
import { onMounted, ref, watch } from 'vue';
@@ -10,6 +9,8 @@ type MarketItemStorage = {
history: MarketOrderHistory[];
}
const buyModal = ref<typeof BuyModal>();
const item = ref("");
/**
* @deprecated use itemsStorage instead
@@ -23,13 +24,13 @@ const addOrRelaod = async (type: MarketType) => {
const typeID = type.id;
const [history, price] = await Promise.all([
getHistory(jitaId, typeID),
evepraisalAxiosInstance.post(`/appraisal.json?market=jita&persist=no&raw_textarea=${type.name}`)
getPrice(type)
]);
const item = {
type,
history,
buy: price.data.appraisal.items[0].prices.buy.max,
sell: price.data.appraisal.items[0].prices.sell.min
buy: price.buy,
sell: price.sell
};
if (items.value.some(i => i.type.id === typeID)) {
@@ -61,21 +62,15 @@ onMounted(async () => {
return;
}
const types = await getMarketTypes(itemsStorage.value.map(i => i.typeID));
const prices: any = (await evepraisalAxiosInstance.post(`/appraisal.json?market=jita&persist=no&raw_textarea=${types.map(t => t.name).join("%0A")}`)).data;
const prices = await getPrices(await getMarketTypes(itemsStorage.value.map(i => i.typeID)));
items.value = itemsStorage.value.map(i => {
const type = types.find(t => t.id === i.typeID) as MarketType;
const price = prices.appraisal.items.find((p: any) => p.typeID === i.typeID);
const price = prices.find(p => p.type.id === i.typeID) as MarketTypePrice;
return {
...i,
type: type,
buy: price.prices.buy.max,
sell: price.prices.sell.min
};
return { ...i, ...price };
});
});
</script>
<template>
@@ -88,6 +83,7 @@ onMounted(async () => {
</div>
<template v-if="items.length > 0">
<hr />
<ScanResultTable :items="items" @relaod="type => addOrRelaod(type)" @relaodAll="reloadAll" />
<ScanResultTable :items="items" @relaod="type => addOrRelaod(type)" @relaodAll="reloadAll" @buy="(type, buy, sell) => buyModal?.open(type, { 'Buy': buy, 'Sell': sell })" />
<BuyModal ref="buyModal" />
</template>
</template>

View File

@@ -1,6 +1,41 @@
<script setup lang="ts">
import { MarketTypePrice, getMarketTypes, getPrices } from "@/market";
import { BuyModal } from '@/market/scan';
import { SellModal, TrackResultTable, TrackedItem, useTrackedItemsStorage } from '@/market/track';
import { onMounted, ref, watch } from 'vue';
const buyModal = ref<typeof BuyModal>();
const sellModal = ref<typeof SellModal>();
const itemsStorage = useTrackedItemsStorage();
const items = ref<TrackedItem[]>([]);
const relaod = async () => {
if (itemsStorage.value.length === 0) {
return;
}
const prices = await getPrices(await getMarketTypes(itemsStorage.value.map(i => i.typeID)));
items.value = itemsStorage.value.map(i => {
const price = prices.find(p => p.type.id === i.typeID) as MarketTypePrice;
return { ...i, ...price };
});
};
watch(items, itms => itemsStorage.value = itms.map(i => ({ typeID: i.type.id, count: i.count, averagePrice: i.averagePrice })));
onMounted(relaod);
</script>
<template>
<div></div>
<div class="mt-4">
<template v-if="items.length > 0">
<hr />
<TrackResultTable :items="items" @buy="(type, price, buy, sell) => buyModal?.open(type, { 'Price': price, 'Buy': buy, 'Sell': sell })" @sell="type => sellModal?.open(type)" />
<BuyModal ref="buyModal" @added="relaod" />
<SellModal ref="sellModal" @removed="relaod" />
</template>
</div>
</template>

View File

@@ -9,7 +9,7 @@ const links = [
</script>
<template>
<aside class="fixed top-0 left-0 z-40 w-64 h-screen transition-transform -translate-x-full sm:translate-x-0">
<aside class="fixed top-0 left-0 w-64 h-screen transition-transform -translate-x-full sm:translate-x-0">
<div class="h-full px-3 py-4 overflow-y-auto bg-slate-700">
<ul class="space-y-2 font-medium">
<li v-for="link in links" :key="link.name">