pinia+track

This commit is contained in:
2023-09-20 21:42:31 +02:00
parent d64cb69f1e
commit dabadaa1c9
16 changed files with 216 additions and 113 deletions

53
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1",
"axios": "^1.4.0",
"pinia": "^2.1.6",
"pocketbase": "^0.18.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
@@ -1719,6 +1720,56 @@
"node": ">=0.10.0"
}
},
"node_modules/pinia": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz",
"integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==",
"dependencies": {
"@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.3.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -2111,7 +2162,7 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -14,6 +14,7 @@
"@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1",
"axios": "^1.4.0",
"pinia": "^2.1.6",
"pocketbase": "^0.18.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4"

View File

@@ -37,7 +37,7 @@ useEventListener('keyup', e => {
<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>
<div class="justify-self-center m-auto" @click.stop>
<slot />
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { providePocketBase } from '@/pocketbase';
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
@@ -7,6 +8,7 @@ import './style.css';
const app = createApp(App);
const pb = providePocketBase(app);
const pinia = createPinia();
const router = createRouter({
history: createWebHistory(),
routes,
@@ -20,6 +22,7 @@ router.beforeEach(async to => {
}
});
app.use(pinia);
app.use(router);
app.mount('#app');

View File

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

View File

@@ -1,4 +1,8 @@
import { MarketOrderHistory, MarketType } from "@/market";
import { usePocketBase, watchCollection } from "@/pocketbase";
import { defineStore } from "pinia";
import { RecordModel } from "pocketbase";
import { computed, onMounted, ref } from "vue";
export type ScanResult = {
type: MarketType;
@@ -7,3 +11,33 @@ export type ScanResult = {
sell: number
}
interface MarketScan extends RecordModel {
owner: string;
types: number[];
};
const marketScans = 'marketScans';
export const useMarkeyScanStore = defineStore(marketScans, () => {
const pb = usePocketBase();
const marketScan = ref<MarketScan>();
const types = computed(() => marketScan.value?.types ?? []);
const setTypes = async (types: number[]) => {
if (marketScan.value?.id) {
pb.collection(marketScans).update(marketScan.value.id, { owner: pb.authStore.model!.id, types });
} else {
pb.collection(marketScans).create({ owner: pb.authStore.model!.id, types });
}
}
watchCollection<MarketScan>(marketScans, '*', data => {
if (data.action === 'delete') {
marketScan.value = undefined;
} else if (!marketScan.value || data.record.id === marketScan.value.id) {
marketScan.value = data.record;
}
});
onMounted(async () => marketScan.value = await pb.collection(marketScans).getFirstListItem<MarketScan>('').catch(() => undefined));
return { types, setTypes };
});

View File

@@ -2,16 +2,11 @@
import Modal from '@/Modal.vue';
import { formatIsk } from '@/formaters';
import { MarketType } from '@/market';
import { useTrackedItemsStorage } from '@/market/track';
import { ref } from 'vue';
import { useTrackedItemStore } from './track';
interface Emit {
(e: 'added'): void;
}
const emit = defineEmits<Emit>();
const itemsStorage = useTrackedItemsStorage();
const trackedItemStore = useTrackedItemStore();
const modalOpen = ref<boolean>(false);
const type = ref<MarketType>();
@@ -43,24 +38,7 @@ const add = () => {
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');
trackedItemStore.addTrackedItem(id, count.value, price.value);
modalOpen.value = false;
}
@@ -69,7 +47,7 @@ defineExpose({ open });
<template>
<Modal v-model:open="modalOpen">
<div class="p-4 bg-slate-800 rounded mt-20 flex">
<div class="p-4 bg-slate-800 rounded flex">
<div class="flex me-2">
<span>Price: </span>
<div class="ms-2">

View File

@@ -1,16 +1,11 @@
<script setup lang="ts">
import Modal from '@/Modal.vue';
import { MarketType } from '@/market';
import { useTrackedItemsStorage } from '@/market/track';
import { ref } from 'vue';
import { useTrackedItemStore } from './track';
interface Emit {
(e: 'removed'): void;
}
const emit = defineEmits<Emit>();
const itemsStorage = useTrackedItemsStorage();
const trackedItemStore = useTrackedItemStore();
const modalOpen = ref<boolean>(false);
const type = ref<MarketType>();
@@ -29,26 +24,7 @@ const remove = () => {
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');
trackedItemStore.removeTrackedItem(id, count.value);
modalOpen.value = false;
}
@@ -57,7 +33,7 @@ defineExpose({ open });
<template>
<Modal v-model:open="modalOpen">
<div class="p-4 bg-slate-800 rounded mt-20 flex">
<div class="p-4 bg-slate-800 rounded flex">
<div class="flex me-2 mb-auto">
<span>Count: </span>
<input class="ms-2" type="number" min="0" step="1" v-model="count" />

View File

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

View File

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

58
src/market/track/track.ts Normal file
View File

@@ -0,0 +1,58 @@
import { useCollection, usePocketBase } from "@/pocketbase";
import { createSharedComposable, useLocalStorage } from '@vueuse/core';
import { defineStore } from "pinia";
import { RecordModel } from "pocketbase";
import { computed } from "vue";
export type TrackedMarketItemStorage = {
typeID: number;
count: number;
averagePrice: number;
}
interface TrackedMarketItem extends RecordModel {
owner: string;
typeID: number;
count: number;
averagePrice: number;
}
const marketTrackings = 'marketTrackings';
export const useTrackedItemsStorage = createSharedComposable(() => useLocalStorage<TrackedMarketItemStorage[]>('market-track-items', []));
export const useTrackedItemStore = defineStore(marketTrackings, () => {
const pb = usePocketBase();
const trackedItems = useCollection<TrackedMarketItem>(marketTrackings);
const items = computed(() => trackedItems);
const addTrackedItem = async (typeID: number, count: number, averagePrice: number) => {
const oldItem = trackedItems.value.find(i => i.typeID === typeID);
if (oldItem?.id) {
pb.collection(marketTrackings).update(oldItem.id, {
...oldItem,
count: count + oldItem.count,
averagePrice: ((averagePrice * count) + (oldItem.averagePrice * oldItem.count)) / (count + oldItem.count)
});
} else {
pb.collection(marketTrackings).create({ owner: pb.authStore.model!.id, typeID, count, averagePrice});
}
};
const removeTrackedItem = async (typeID: number, count: number) => {
const oldItem = trackedItems.value.find(i => i.typeID === typeID);
if (!oldItem?.id) {
return;
} else if (oldItem.count > count) {
pb.collection(marketTrackings).update(oldItem.id, {
...oldItem,
count: oldItem.count - count
});
} else {
pb.collection(marketTrackings).delete(oldItem.id);
}
};
return { items, addTrackedItem, removeTrackedItem };
});

View File

@@ -19,12 +19,12 @@ const login = async () => {
<div class="p-4 mx-auto mt-10 grid justify-center gap-2 w-64">
<div class="grid">
Login:
<input type="text" autocomplete="username" v-model="username" />
<input type="text" name="username" v-model="username" />
</div>
<div class="grid">
Password:
<input type="password" autocomplete="password" v-model="password" />
<input type="password" name="password" v-model="password" />
</div>
<button class="justify-self-end" @click="login" >Login</button>
<button class="justify-self-end" name="login" @click="login">Login</button>
</div>
</template>

View File

@@ -1,21 +1,15 @@
<script setup lang="ts">
import { MarketType, MarketTypePrice, getHistory, getMarketType, getMarketTypes, getPrice, getPrices, jitaId } from "@/market";
import { BuyModal, ScanResult, ScanResultTable } from '@/market/scan';
import { usePocketBase } from "@/pocketbase";
import { onMounted, ref, watch } from 'vue';
import { ScanResult, ScanResultTable, useMarkeyScanStore } from '@/market/scan';
import { BuyModal } from '@/market/track';
import { ref, watch } from 'vue';
const pb = usePocketBase();
type MarketScan = {
id?: string;
owner: string;
types: number[];
}
const buyModal = ref<typeof BuyModal>();
const item = ref("");
const markeyScanStore = useMarkeyScanStore();
const items = ref<ScanResult[]>([]);
const addOrRelaod = async (type: MarketType) => {
const typeID = type.id;
@@ -43,35 +37,24 @@ const addItem = async () => {
addOrRelaod(type);
}
const getMarketScan = () => pb.collection('marketScans').getFirstListItem<MarketScan>("").catch(() => null);
watch(items, async itms => {
const types = itms.map(i => i.type.id);
const marketScan = await getMarketScan();
watch(items, async itms => markeyScanStore.setTypes(itms.map(i => i.type.id)));
watch(() => markeyScanStore.types, async t => {
const typesToLoad = t.filter(t => !items.value.some(i => i.type.id === t));
if (marketScan?.id) {
pb.collection('marketScans').update(marketScan.id, { owner: pb.authStore.model!.id, types });
} else {
pb.collection('marketScans').create({ owner: pb.authStore.model!.id, types });
}
});
onMounted(async () => {
const marketScan = await getMarketScan();
if (!marketScan || marketScan.types.length === 0) {
if (typesToLoad.length === 0) {
return;
}
const prices = await getPrices(await getMarketTypes(marketScan.types));
const prices = await getPrices(await getMarketTypes(typesToLoad));
items.value = await Promise.all(marketScan.types.map(async i => {
items.value = [...items.value, ...(await Promise.all(typesToLoad.map(async i => {
const price = prices.find(p => p.type.id === i) as MarketTypePrice;
const history = await getHistory(jitaId, i);
return { id: i, history, ...price };
}));
});
})))];
}, { immediate: true });
</script>
<template>

View File

@@ -1,31 +1,27 @@
<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';
import { BuyModal, SellModal, TrackResultTable, TrackedItem, useTrackedItemStore } from '@/market/track';
import { ref, watch } from 'vue';
const buyModal = ref<typeof BuyModal>();
const sellModal = ref<typeof SellModal>();
const itemsStorage = useTrackedItemsStorage();
const trackedItemStore = useTrackedItemStore();
const items = ref<TrackedItem[]>([]);
const relaod = async () => {
if (itemsStorage.value.length === 0) {
watch(() => trackedItemStore.items.value, async itms => {
if (itms.length === 0) {
return;
}
const prices = await getPrices(await getMarketTypes(itemsStorage.value.map(i => i.typeID)));
const prices = await getPrices(await getMarketTypes(itms.map(i => i.typeID)));
items.value = itemsStorage.value.map(i => {
items.value = itms.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>
@@ -34,8 +30,8 @@ onMounted(relaod);
<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" />
<BuyModal ref="buyModal" />
<SellModal ref="sellModal" />
</template>
</div>
</template>

View File

@@ -0,0 +1,31 @@
import { usePocketBase } from "@/pocketbase";
import { RecordModel, RecordSubscription, UnsubscribeFunc } from "pocketbase";
import { Ref, computed, onMounted, onUnmounted, ref } from "vue";
export const watchCollection = <T extends RecordModel = RecordModel>(collection: string, query: string, callback: (data: RecordSubscription<T>) => void) => {
const pb = usePocketBase();
let unsubscribe: UnsubscribeFunc = () => Promise.resolve();
onMounted(async () => {
unsubscribe = await pb.collection(collection).subscribe<T>(query, callback);
});
onUnmounted(() => unsubscribe());
};
export const useCollection = <T extends RecordModel = RecordModel>(collection: string) => {
const pb = usePocketBase();
const list = ref<T[]>([]) as Ref<T[]>;
watchCollection<T>(collection, '*', data => {
if (data.action === 'delete') {
list.value = list.value.filter(i => i.id !== data.record.id);
} else if (data.action === 'update') {
list.value = list.value.map(i => i.id === data.record.id ? data.record : i);
} else if (data.action === 'create') {
list.value = [...list.value, data.record];
}
});
onMounted(async () => list.value = await pb.collection(collection).getFullList<T>().catch(() => [] as T[]));
return computed(() => list.value);
}

View File

@@ -1 +1,2 @@
export * from './collection';
export * from './pocketbase';