type info
This commit is contained in:
23
src/components/ClipboardButton.vue
Normal file
23
src/components/ClipboardButton.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { copyToClipboard } from '@/utils';
|
||||||
|
import { ClipboardIcon } from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const doCopy = () => {
|
||||||
|
if (!props.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copyToClipboard(props.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="btn-icon" title="Copy to clipboard" @click="doCopy">
|
||||||
|
<ClipboardIcon />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { default as ClipboardButton } from './ClipboardButton.vue';
|
||||||
export { default as LoadingSpinner } from './LoadingSpinner.vue';
|
export { default as LoadingSpinner } from './LoadingSpinner.vue';
|
||||||
export { default as Modal } from './Modal.vue';
|
export { default as Modal } from './Modal.vue';
|
||||||
export { default as SliderCheckbox } from './SliderCheckbox.vue';
|
export { default as SliderCheckbox } from './SliderCheckbox.vue';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SliderCheckbox } from '@/components';
|
|||||||
import { SortableHeader, useSort } from '@/components/table';
|
import { SortableHeader, useSort } 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 { ShoppingCartIcon, TrashIcon } 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 { ScanResult, getHistoryQuartils } from '.';
|
import { ScanResult, getHistoryQuartils } from '.';
|
||||||
@@ -23,6 +23,7 @@ type Result = {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items?: ScanResult[];
|
items?: ScanResult[];
|
||||||
|
infoOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -35,7 +36,8 @@ const scoreFormater = new Intl.NumberFormat("en-US", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
items: () => []
|
items: () => [],
|
||||||
|
infoOnly: false
|
||||||
});
|
});
|
||||||
defineEmits<Emits>();
|
defineEmits<Emits>();
|
||||||
|
|
||||||
@@ -69,7 +71,9 @@ const { sortedArray, headerProps } = useSort<Result>(computed(() => props.items
|
|||||||
defaultSortDirection: 'desc'
|
defaultSortDirection: 'desc'
|
||||||
})
|
})
|
||||||
const getLineColor = (result: Result) => {
|
const getLineColor = (result: Result) => {
|
||||||
if (result.profit < (threshold.value / 100)) {
|
if (props.infoOnly) {
|
||||||
|
return '';
|
||||||
|
} else if (result.profit < (threshold.value / 100)) {
|
||||||
return 'line-red';
|
return 'line-red';
|
||||||
} else if (result.sell > 0 && result.sell <= result.q1) {
|
} else if (result.sell > 0 && result.sell <= result.q1) {
|
||||||
return 'line-blue';
|
return 'line-blue';
|
||||||
@@ -81,7 +85,7 @@ const getLineColor = (result: Result) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex mb-2 mt-4">
|
<div v-if="!infoOnly" class="flex mb-2 mt-4">
|
||||||
<div class="flex justify-self-end ms-auto">
|
<div class="flex justify-self-end ms-auto">
|
||||||
<TaxInput />
|
<TaxInput />
|
||||||
<div class="end">
|
<div class="end">
|
||||||
@@ -112,7 +116,7 @@ const getLineColor = (result: Result) => {
|
|||||||
<SortableHeader v-bind="headerProps" sortKey="q3">Q3</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="q3">Q3</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="profit">Profit</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="profit">Profit</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="score">Score</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="score">Score</SortableHeader>
|
||||||
<th></th>
|
<th v-if="!infoOnly"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -127,9 +131,9 @@ const getLineColor = (result: Result) => {
|
|||||||
<td class="text-right">{{ formatIsk(r.q3) }}</td>
|
<td class="text-right">{{ formatIsk(r.q3) }}</td>
|
||||||
<td class="text-right">{{ percentFormater.format(r.profit) }}</td>
|
<td class="text-right">{{ percentFormater.format(r.profit) }}</td>
|
||||||
<td class="text-right">{{ scoreFormater.format(r.score) }}</td>
|
<td class="text-right">{{ scoreFormater.format(r.score) }}</td>
|
||||||
<td class="text-right">
|
<td v-if="!infoOnly" class="text-right">
|
||||||
<button class="btn-icon me-1" @click="$emit('buy', r.type, r.buy, r.sell)"><ShoppingCartIcon /></button>
|
<button class="btn-icon me-1" @click="$emit('buy', r.type, r.buy, r.sell)"><ShoppingCartIcon /></button>
|
||||||
<button class="btn-icon me-1" @click="$emit('remove', r.type)"><TrashIcon /></button>
|
<button class="btn-icon me-1" @click="$emit('remove', r.type)"><BookmarkSlashIcon /></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarketOrderHistory, MarketType } from "@/market";
|
import { MarketOrderHistory, MarketType, MarketTypePrice, getHistory, jitaId } from "@/market";
|
||||||
import { usePocketBase, watchCollection } from "@/pocketbase";
|
import { usePocketBase, watchCollection } from "@/pocketbase";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { RecordModel } from "pocketbase";
|
import { RecordModel } from "pocketbase";
|
||||||
@@ -31,6 +31,16 @@ export const useMarketScanStore = defineStore(marketScans, () => {
|
|||||||
marketScan.value = await pb.collection(marketScans).create({ owner: pb.authStore.model!.id, types });
|
marketScan.value = await pb.collection(marketScans).create({ owner: pb.authStore.model!.id, types });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const addType = async (type: number) => {
|
||||||
|
if (!types.value.includes(type)) {
|
||||||
|
await setTypes([...types.value, type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const removeType = async (type: number) => {
|
||||||
|
if (types.value.includes(type)) {
|
||||||
|
await setTypes(types.value.filter(t => t !== type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watchCollection<MarketScan>(marketScans, '*', data => {
|
watchCollection<MarketScan>(marketScans, '*', data => {
|
||||||
if (data.action === 'delete') {
|
if (data.action === 'delete') {
|
||||||
@@ -40,5 +50,7 @@ export const useMarketScanStore = defineStore(marketScans, () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
onMounted(async () => marketScan.value = await pb.collection(marketScans).getFirstListItem<MarketScan>('').catch(() => undefined));
|
onMounted(async () => marketScan.value = await pb.collection(marketScans).getFirstListItem<MarketScan>('').catch(() => undefined));
|
||||||
return { types, setTypes };
|
return { types, setTypes, addType, removeType };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createResult = async (id: number, price: MarketTypePrice): Promise<ScanResult> => ({ history: await getHistory(jitaId, id), ...price });
|
||||||
@@ -82,7 +82,7 @@ watchEffect(async () => {
|
|||||||
@apply btn-icon px-2;
|
@apply btn-icon px-2;
|
||||||
}
|
}
|
||||||
&.open>:deep(div.header) {
|
&.open>:deep(div.header) {
|
||||||
@apply bg-slate-600 rounded-t;
|
@apply rounded-t-md bg-slate-600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -45,20 +45,25 @@ const moveUp = () => {
|
|||||||
}
|
}
|
||||||
scrollTo(currentIndex.value);
|
scrollTo(currentIndex.value);
|
||||||
}
|
}
|
||||||
const select = (type: MarketType) => {
|
const select = (type?: MarketType) => {
|
||||||
log.debug('Select:', type);
|
log.debug('Select:', type);
|
||||||
value.value = type;
|
value.value = type;
|
||||||
currentIndex.value = -1;
|
currentIndex.value = -1;
|
||||||
suggestions.value = [];
|
suggestions.value = [];
|
||||||
|
isOpen.value = false;
|
||||||
|
name.value = type?.name ?? '';
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (currentIndex.value >= 0 && currentIndex.value < suggestions.value.length) {
|
if (currentIndex.value >= 0 && currentIndex.value < suggestions.value.length) {
|
||||||
const v = suggestions.value[currentIndex.value];
|
const v = suggestions.value[currentIndex.value];
|
||||||
|
|
||||||
value.value = v;
|
select(v);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
} else if (props.modelValue === undefined && suggestions.value.length > 0) {
|
} else if (props.modelValue === undefined && suggestions.value.length > 0) {
|
||||||
value.value = suggestions.value[0];
|
select(suggestions.value[0]);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +113,7 @@ watchEffect(async () => {
|
|||||||
|
|
||||||
<style scoped lang="postcss">
|
<style scoped lang="postcss">
|
||||||
.fake-input {
|
.fake-input {
|
||||||
@apply w-96 flex border bg-slate-500 rounded px-1;
|
@apply w-96 flex border bg-slate-500 rounded px-1 py-0.5;
|
||||||
|
|
||||||
&:has(> input:focus-visible) {
|
&:has(> input:focus-visible) {
|
||||||
outline: -webkit-focus-ring-color auto 1px;
|
outline: -webkit-focus-ring-color auto 1px;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { copyToClipboard } from '@/utils';
|
import { ClipboardButton } from '@/components';
|
||||||
import { ClipboardIcon } from '@heroicons/vue/24/outline';
|
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -21,7 +20,13 @@ withDefaults(defineProps<Props>(), {
|
|||||||
<img v-if="id" :src="`https://images.evetech.net/types/${id}/icon`" class="inline-block w-5 h-5 me-1" />
|
<img v-if="id" :src="`https://images.evetech.net/types/${id}/icon`" class="inline-block w-5 h-5 me-1" />
|
||||||
<template v-if="name">
|
<template v-if="name">
|
||||||
{{ name }}
|
{{ name }}
|
||||||
<button v-if="!hideCopy" class="btn-icon" @click="copyToClipboard(name)"><ClipboardIcon class="relative top-0.5 !w-4 !h-4" /></button>
|
<ClipboardButton v-if="!hideCopy" :value="name" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="postcss">
|
||||||
|
button:deep(>svg) {
|
||||||
|
@apply relative top-0.5 !w-4 !h-4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,9 @@ import { RouterLink, RouterView } from 'vue-router';
|
|||||||
<template>
|
<template>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="flex border-b-2 border-emerald-500">
|
<div class="flex border-b-2 border-emerald-500">
|
||||||
|
<RouterLink :to="{name: 'market-type'}" class="tab">
|
||||||
|
<span>Item Info</span>
|
||||||
|
</RouterLink>
|
||||||
<RouterLink to="/market/scan" class="tab">
|
<RouterLink to="/market/scan" class="tab">
|
||||||
<span>Scan</span>
|
<span>Scan</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MarketType, MarketTypePrice, getHistory, getMarketTypes, jitaId, useApraisalStore } from "@/market";
|
import { MarketType, MarketTypeInput, MarketTypePrice, getHistory, getMarketTypes, jitaId, useApraisalStore } from "@/market";
|
||||||
import { ScanResult, ScanResultTable, useMarketScanStore } from '@/market/scan';
|
import { ScanResult, ScanResultTable, createResult, useMarketScanStore } from '@/market/scan';
|
||||||
import { BuyModal } from '@/market/track';
|
import { BuyModal } from '@/market/track';
|
||||||
import MarketTypeInput from "@/market/type/MarketTypeInput.vue";
|
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
|
||||||
@@ -57,12 +56,7 @@ watch(() => markeyScanStore.types, async t => {
|
|||||||
|
|
||||||
const prices = await apraisalStore.getPrices(await getMarketTypes(typesToLoad));
|
const prices = await apraisalStore.getPrices(await getMarketTypes(typesToLoad));
|
||||||
|
|
||||||
items.value = [...items.value, ...(await Promise.all(typesToLoad.map(async i => {
|
items.value = [...items.value, ...(await Promise.all(typesToLoad.map(i => createResult(i, prices.find(p => p.type.id === i) as MarketTypePrice))))];
|
||||||
const price = prices.find(p => p.type.id === i) as MarketTypePrice;
|
|
||||||
const history = await getHistory(jitaId, i);
|
|
||||||
|
|
||||||
return { id: i, history, ...price };
|
|
||||||
})))];
|
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
101
src/pages/market/TypeInfo.vue
Normal file
101
src/pages/market/TypeInfo.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ClipboardButton } from '@/components';
|
||||||
|
import { MarketType, MarketTypeInput, getMarketType, useApraisalStore } from "@/market";
|
||||||
|
import { ScanResultTable, createResult, useMarketScanStore } from '@/market/scan';
|
||||||
|
import { BuyModal } from '@/market/track';
|
||||||
|
import { BookmarkIcon, BookmarkSlashIcon, ShoppingCartIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { computedAsync } from '@vueuse/core/index.cjs';
|
||||||
|
import log from "loglevel";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const buyModal = ref<typeof BuyModal>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const item = ref<MarketType>();
|
||||||
|
const inputItem = ref<MarketType>();
|
||||||
|
|
||||||
|
const apraisalStore = useApraisalStore();
|
||||||
|
const price = computedAsync(() => item.value ? apraisalStore.getPrice(item.value) : undefined);
|
||||||
|
const markeyScanStore = useMarketScanStore();
|
||||||
|
const result = computedAsync(async () => item.value && price.value ? await createResult(item.value?.id, price.value) : undefined);
|
||||||
|
|
||||||
|
const isTracked = computed(() => item.value ? markeyScanStore.types.includes(item.value.id) : false);
|
||||||
|
const toogleTracking = () => {
|
||||||
|
if (!item.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isTracked.value) {
|
||||||
|
markeyScanStore.removeType(item.value.id);
|
||||||
|
} else {
|
||||||
|
markeyScanStore.addType(item.value.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = () => {
|
||||||
|
if (!inputItem.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: 'market-type',
|
||||||
|
params: {
|
||||||
|
type: inputItem.value.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(useRoute(), async route => {
|
||||||
|
if (route.params.type) {
|
||||||
|
const id = parseInt(typeof route.params.type === 'string' ? route.params.type : route.params.type[0]);
|
||||||
|
|
||||||
|
item.value = await getMarketType(id);
|
||||||
|
inputItem.value = item.value;
|
||||||
|
log.info('Loaded item:', item.value);
|
||||||
|
} else {
|
||||||
|
item.value = undefined;
|
||||||
|
inputItem.value = undefined;
|
||||||
|
log.info('No item to load');
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="grid mb-2 mt-4">
|
||||||
|
<div class="w-auto flex">
|
||||||
|
<span>Item: </span>
|
||||||
|
<MarketTypeInput class="ms-2" v-model="inputItem" @submit="view"/>
|
||||||
|
<button class="justify-self-end ms-2" @click="view">View</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="item">
|
||||||
|
<hr>
|
||||||
|
<div class="p-2 mb-4 flex">
|
||||||
|
<img v-if="item.id" :src="`https://images.evetech.net/types/${item.id}/icon?size=64`" class="type-image inline-block me-2" />
|
||||||
|
<div class="inline-block align-top">
|
||||||
|
<div class="flex">
|
||||||
|
<span class="text-lg font-semibold">{{ item.name }}</span>
|
||||||
|
<div class="ms-auto">
|
||||||
|
<ClipboardButton class="ms-1" :value="item.name" />
|
||||||
|
<button v-if="price" class="btn-icon ms-1" @click="buyModal?.open(item, { 'Buy': price.buy, 'Sell': price.sell })"><ShoppingCartIcon /></button>
|
||||||
|
<button class="btn-icon ms-1" :title="isTracked ? 'Untrack' : 'Track'" @click="toogleTracking">
|
||||||
|
<BookmarkSlashIcon v-if="isTracked" />
|
||||||
|
<BookmarkIcon v-else />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="item.description" class="text-sm">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ScanResultTable v-if="result" :items="[result]" infoOnly />
|
||||||
|
</template>
|
||||||
|
<BuyModal ref="buyModal" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="postcss">
|
||||||
|
img.type-image {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,7 +5,8 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
{ path: '/login', name: 'login', component: () => import('@/pages/Login.vue') },
|
{ path: '/login', name: 'login', component: () => import('@/pages/Login.vue') },
|
||||||
{ path: '/reprocess', component: () => import('@/pages/Reprocess.vue') },
|
{ path: '/reprocess', component: () => import('@/pages/Reprocess.vue') },
|
||||||
{ path: '/market', component: () => import('@/pages/Market.vue'), children: [
|
{ path: '/market', component: () => import('@/pages/Market.vue'), children: [
|
||||||
{ path: '', redirect: '/market/scan' },
|
{ path: '', redirect: '/market/type' },
|
||||||
|
{ path: 'type/:type?', name: 'market-type', component: () => import('@/pages/market/TypeInfo.vue') },
|
||||||
{ path: 'scan', component: () => import('@/pages/market/Scan.vue') },
|
{ path: 'scan', component: () => import('@/pages/market/Scan.vue') },
|
||||||
{ path: 'track', component: () => import('@/pages/market/Track.vue') },
|
{ path: 'track', component: () => import('@/pages/market/Track.vue') },
|
||||||
] },
|
] },
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
input[type=search] {
|
input[type=search] {
|
||||||
@apply search-cancel:appearance-none search-cancel:w-4 search-cancel:h-4 search-cancel:bg-[url('/svg/search-cancel.svg')];
|
@apply search-cancel:appearance-none search-cancel:w-4 search-cancel:h-4 search-cancel:bg-[url('/svg/search-cancel.svg')];
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
@apply p-0 border-none bg-transparent hover:text-slate-400 hover:bg-transparent;
|
@apply p-0 border-none bg-transparent hover:text-slate-400 hover:bg-transparent;
|
||||||
> svg {
|
> svg {
|
||||||
|
|||||||
Reference in New Issue
Block a user