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/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1", "@vueuse/integrations": "^10.2.1",
"axios": "^1.4.0", "axios": "^1.4.0",
"pinia": "^2.1.6",
"pocketbase": "^0.18.0", "pocketbase": "^0.18.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
@@ -1719,6 +1720,56 @@
"node": ">=0.10.0" "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": { "node_modules/pirates": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -2111,7 +2162,7 @@
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@@ -14,6 +14,7 @@
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1", "@vueuse/integrations": "^10.2.1",
"axios": "^1.4.0", "axios": "^1.4.0",
"pinia": "^2.1.6",
"pocketbase": "^0.18.0", "pocketbase": "^0.18.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.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="fixed inset-0" @click="isOpen = false">
<div class="absolute bg-black opacity-80 inset-0 z-0" /> <div class="absolute bg-black opacity-80 inset-0 z-0" />
<div class="absolute grid inset-0"> <div class="absolute grid inset-0">
<div class="justify-self-center" @click.stop> <div class="justify-self-center m-auto" @click.stop>
<slot /> <slot />
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -1,4 +1,8 @@
import { MarketOrderHistory, MarketType } from "@/market"; 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 = { export type ScanResult = {
type: MarketType; type: MarketType;
@@ -7,3 +11,33 @@ export type ScanResult = {
sell: number 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 Modal from '@/Modal.vue';
import { formatIsk } from '@/formaters'; import { formatIsk } from '@/formaters';
import { MarketType } from '@/market'; import { MarketType } from '@/market';
import { useTrackedItemsStorage } from '@/market/track';
import { ref } from 'vue'; import { ref } from 'vue';
import { useTrackedItemStore } from './track';
interface Emit {
(e: 'added'): void;
}
const emit = defineEmits<Emit>(); const trackedItemStore = useTrackedItemStore();
const itemsStorage = useTrackedItemsStorage();
const modalOpen = ref<boolean>(false); const modalOpen = ref<boolean>(false);
const type = ref<MarketType>(); const type = ref<MarketType>();
@@ -43,24 +38,7 @@ const add = () => {
return; return;
} }
const oldItem = itemsStorage.value.find(i => i.typeID === id); trackedItemStore.addTrackedItem(id, count.value, price.value);
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; modalOpen.value = false;
} }
@@ -69,7 +47,7 @@ defineExpose({ open });
<template> <template>
<Modal v-model:open="modalOpen"> <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"> <div class="flex me-2">
<span>Price: </span> <span>Price: </span>
<div class="ms-2"> <div class="ms-2">

View File

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

View File

@@ -1,6 +1,7 @@
export * from './TrackedItem'; 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 SellModal } from './SellModal.vue';
export { default as TrackResultTable } from './TrackResultTable.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="p-4 mx-auto mt-10 grid justify-center gap-2 w-64">
<div class="grid"> <div class="grid">
Login: Login:
<input type="text" autocomplete="username" v-model="username" /> <input type="text" name="username" v-model="username" />
</div> </div>
<div class="grid"> <div class="grid">
Password: Password:
<input type="password" autocomplete="password" v-model="password" /> <input type="password" name="password" v-model="password" />
</div> </div>
<button class="justify-self-end" @click="login" >Login</button> <button class="justify-self-end" name="login" @click="login">Login</button>
</div> </div>
</template> </template>

View File

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

View File

@@ -1,31 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { MarketTypePrice, getMarketTypes, getPrices } from "@/market"; import { MarketTypePrice, getMarketTypes, getPrices } from "@/market";
import { BuyModal } from '@/market/scan'; import { BuyModal, SellModal, TrackResultTable, TrackedItem, useTrackedItemStore } from '@/market/track';
import { SellModal, TrackResultTable, TrackedItem, useTrackedItemsStorage } from '@/market/track'; import { ref, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';
const buyModal = ref<typeof BuyModal>(); const buyModal = ref<typeof BuyModal>();
const sellModal = ref<typeof SellModal>(); const sellModal = ref<typeof SellModal>();
const itemsStorage = useTrackedItemsStorage(); const trackedItemStore = useTrackedItemStore();
const items = ref<TrackedItem[]>([]); const items = ref<TrackedItem[]>([]);
const relaod = async () => { watch(() => trackedItemStore.items.value, async itms => {
if (itemsStorage.value.length === 0) { if (itms.length === 0) {
return; 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; const price = prices.find(p => p.type.id === i.typeID) as MarketTypePrice;
return { ...i, ...price }; return { ...i, ...price };
}); });
}; })
watch(items, itms => itemsStorage.value = itms.map(i => ({ typeID: i.type.id, count: i.count, averagePrice: i.averagePrice })));
onMounted(relaod);
</script> </script>
@@ -34,8 +30,8 @@ onMounted(relaod);
<template v-if="items.length > 0"> <template v-if="items.length > 0">
<hr /> <hr />
<TrackResultTable :items="items" @buy="(type, price, buy, sell) => buyModal?.open(type, { 'Price': price, 'Buy': buy, 'Sell': sell })" @sell="type => sellModal?.open(type)" /> <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" /> <BuyModal ref="buyModal" />
<SellModal ref="sellModal" @removed="relaod" /> <SellModal ref="sellModal" />
</template> </template>
</div> </div>
</template> </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'; export * from './pocketbase';