search item
This commit is contained in:
@@ -20,5 +20,15 @@ export const getMarketTypes = async (types: (string | number)[]): Promise<Market
|
|||||||
} else if (types.length === 1 && typeof types[0] === "number") {
|
} else if (types.length === 1 && typeof types[0] === "number") {
|
||||||
return [(await apiAxiosInstance.get<MarketType>(`/sde/types/${types[0]}/`)).data];
|
return [(await apiAxiosInstance.get<MarketType>(`/sde/types/${types[0]}/`)).data];
|
||||||
}
|
}
|
||||||
return (await apiAxiosInstance.post<MarketType[]>("/sde/types/search", types.map(t => [typeof t === "number" ? 'id' : "name", t]))).data;
|
return (await apiAxiosInstance.post<MarketType[]>("/sde/types/search", types.map(t => {
|
||||||
|
if (typeof t === "number") {
|
||||||
|
return { id: t };
|
||||||
|
} else {
|
||||||
|
return { name: t };
|
||||||
|
}
|
||||||
|
}))).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const searchMarketTypes = async (search: string): Promise<MarketType[]> => {
|
||||||
|
return (await apiAxiosInstance.post<MarketType[]>("/sde/types/search", [{ name_i: search }])).data;
|
||||||
|
}
|
||||||
102
src/market/type/MarketTypeInput.vue
Normal file
102
src/market/type/MarketTypeInput.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useFocus, useVirtualList, useVModel } from '@vueuse/core';
|
||||||
|
import { nextTick, ref, watch, watchEffect } from 'vue';
|
||||||
|
import { MarketType, searchMarketTypes } from './MarketType';
|
||||||
|
import MarketTypeLabel from "./MarketTypeLabel.vue";
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: MarketType;
|
||||||
|
}
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value?: MarketType): void;
|
||||||
|
(e: 'submit'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const value = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const input = ref<HTMLInputElement>();
|
||||||
|
const {focused} = useFocus(input);
|
||||||
|
|
||||||
|
const name = ref('');
|
||||||
|
const suggestions = ref<MarketType[]>([]);
|
||||||
|
const currentIndex = ref(-1);
|
||||||
|
const {list, scrollTo, containerProps, wrapperProps} = useVirtualList(suggestions, {
|
||||||
|
itemHeight: 24,
|
||||||
|
overscan: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
const moveDown = () => {
|
||||||
|
if (currentIndex.value < 0 || currentIndex.value >= suggestions.value.length - 1) {
|
||||||
|
currentIndex.value = 0;
|
||||||
|
} else if (currentIndex.value < suggestions.value.length - 1) {
|
||||||
|
currentIndex.value++;
|
||||||
|
}
|
||||||
|
scrollTo(currentIndex.value);
|
||||||
|
}
|
||||||
|
const moveUp = () => {
|
||||||
|
if (currentIndex.value <= 0) {
|
||||||
|
currentIndex.value = suggestions.value.length - 1;
|
||||||
|
} else if (currentIndex.value > 0) {
|
||||||
|
currentIndex.value--;
|
||||||
|
}
|
||||||
|
scrollTo(currentIndex.value);
|
||||||
|
}
|
||||||
|
const select = (type: MarketType) => {
|
||||||
|
value.value = type;
|
||||||
|
currentIndex.value = -1;
|
||||||
|
suggestions.value = [];
|
||||||
|
}
|
||||||
|
const submit = async () => {
|
||||||
|
if (currentIndex.value >= 0 && currentIndex.value < suggestions.value.length) {
|
||||||
|
const v = suggestions.value[currentIndex.value];
|
||||||
|
|
||||||
|
value.value = v;
|
||||||
|
await nextTick();
|
||||||
|
} else if (props.modelValue === undefined && suggestions.value.length > 0) {
|
||||||
|
value.value = suggestions.value[0];
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('submit');
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, async v => {
|
||||||
|
if (v === undefined) {
|
||||||
|
name.value = '';
|
||||||
|
} else {
|
||||||
|
name.value = v.name;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watchEffect(async () => {
|
||||||
|
const search = name.value.split('\t')[0];
|
||||||
|
|
||||||
|
if (!focused.value || search.length < 3) {
|
||||||
|
suggestions.value = [];
|
||||||
|
} else {
|
||||||
|
suggestions.value = await searchMarketTypes(search);
|
||||||
|
}
|
||||||
|
currentIndex.value = -1;
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input ref="input" type="text" class="w-96" v-model="name" @keyup.enter="submit" @keyup.down="moveDown" @keyup.up="moveUp" />
|
||||||
|
<div v-if="suggestions.length > 1" class="z-10 absolute w-96">
|
||||||
|
<div v-bind="containerProps" style="height: 300px">
|
||||||
|
<div v-bind="wrapperProps">
|
||||||
|
<div v-for="s in list" :key="s.index" class="hover:bg-slate-700" :class="{'bg-slate-500': s.index !== currentIndex, 'bg-emerald-500': s.index === currentIndex}" @click="$emit('update:modelValue', s.data)">
|
||||||
|
<MarketTypeLabel :id="s.data.id" :name="s.data.name" class="whitespace-nowrap overflow-hidden cursor-pointer" hideCopy @click="select(s.data)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -6,11 +6,13 @@ import { ClipboardIcon } from '@heroicons/vue/24/outline';
|
|||||||
interface Props {
|
interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
|
hideCopy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
name: "",
|
name: "",
|
||||||
id: 0
|
id: 0,
|
||||||
|
hideCopy: false
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ 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 class="btn-icon-stroke" @click="copyToClipboard(name)"><ClipboardIcon class="relative top-0.5 !w-4 !h-4" /></button>
|
<button v-if="!hideCopy" class="btn-icon-stroke" @click="copyToClipboard(name)"><ClipboardIcon class="relative top-0.5 !w-4 !h-4" /></button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './MarketType';
|
export * from './MarketType';
|
||||||
export { default as MarketTypeLabel } from './MarketTypeLabel.vue';
|
export { default as MarketTypeLabel } from './MarketTypeLabel.vue';
|
||||||
|
export { default as MarketTypeInput } from './MarketTypeInput.vue';
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MarketType, MarketTypePrice, getHistory, getMarketType, getMarketTypes, jitaId, useApraisalStore } from "@/market";
|
import { MarketType, MarketTypePrice, getHistory, getMarketTypes, jitaId, useApraisalStore } from "@/market";
|
||||||
import { ScanResult, ScanResultTable, useMarketScanStore } from '@/market/scan';
|
import { ScanResult, ScanResultTable, 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';
|
||||||
|
|
||||||
|
|
||||||
const buyModal = ref<typeof BuyModal>();
|
const buyModal = ref<typeof BuyModal>();
|
||||||
|
|
||||||
const item = ref("");
|
const item = ref<MarketType>();
|
||||||
|
|
||||||
const apraisalStore = useApraisalStore();
|
const apraisalStore = useApraisalStore();
|
||||||
const markeyScanStore = useMarketScanStore();
|
const markeyScanStore = useMarketScanStore();
|
||||||
@@ -18,7 +19,7 @@ const addOrRelaod = async (type: MarketType) => {
|
|||||||
getHistory(jitaId, typeID),
|
getHistory(jitaId, typeID),
|
||||||
apraisalStore.getPrice(type)
|
apraisalStore.getPrice(type)
|
||||||
]);
|
]);
|
||||||
const item = {
|
const itm = {
|
||||||
type,
|
type,
|
||||||
history,
|
history,
|
||||||
buy: price.buy,
|
buy: price.buy,
|
||||||
@@ -27,20 +28,19 @@ const addOrRelaod = async (type: MarketType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (items.value.some(i => i.type.id === typeID)) {
|
if (items.value.some(i => i.type.id === typeID)) {
|
||||||
items.value = items.value.map(i => i.type.id === typeID ? item : i);
|
items.value = items.value.map(i => i.type.id === typeID ? itm : i);
|
||||||
} else {
|
} else {
|
||||||
items.value = [ ...items.value, item];
|
items.value = [ ...items.value, itm];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const addItem = async () => {
|
const addItem = async () => {
|
||||||
const type = await getMarketType(item.value.split('\t')[0]);
|
if (!item.value) {
|
||||||
|
// TODO error
|
||||||
item.value = "";
|
|
||||||
if (!type) {
|
|
||||||
// TODO: Show error
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addOrRelaod(type);
|
|
||||||
|
addOrRelaod(item.value);
|
||||||
|
item.value = undefined;
|
||||||
}
|
}
|
||||||
const removeItem = (type: MarketType) => {
|
const removeItem = (type: MarketType) => {
|
||||||
items.value = items.value.filter(i => i.type.id !== type.id);
|
items.value = items.value.filter(i => i.type.id !== type.id);
|
||||||
@@ -68,9 +68,9 @@ watch(() => markeyScanStore.types, async t => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid mb-2 mt-4">
|
<div class="grid mb-2 mt-4">
|
||||||
<div class="w-auto">
|
<div class="w-auto flex">
|
||||||
<span>Item: </span>
|
<span>Item: </span>
|
||||||
<input type="text" class="w-96" v-model="item" @keyup.enter="addItem" />
|
<MarketTypeInput class="ms-2" v-model="item" @submit="addItem"/>
|
||||||
<button class="justify-self-end ms-2" @click="addItem">Add</button>
|
<button class="justify-self-end ms-2" @click="addItem">Add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user