feat(ui): add single selection and keyboard navigation to TagsSelect
Add `multiple` prop to support single-value selection Implement keyboard navigation (Up/Down/Enter) for dropdown options Replace native select elements with TagsSelect across views
This commit is contained in:
@@ -1,17 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
export type TagsSelectOption = {
|
export type TagsSelectOption = {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
name: string;
|
name: string;
|
||||||
|
label?: string;
|
||||||
subcategory?: string | null;
|
subcategory?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OptionRow = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CandidateRow = { type: 'option'; id: string; value: string; label: string } | { type: 'create'; id: string };
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
id: string;
|
id: string;
|
||||||
modelValue: string[];
|
modelValue: string[] | string;
|
||||||
options: TagsSelectOption[];
|
options: TagsSelectOption[];
|
||||||
|
multiple?: boolean;
|
||||||
max?: number;
|
max?: number;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
@@ -21,6 +31,7 @@ const props = withDefaults(
|
|||||||
createLabel?: string;
|
createLabel?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
multiple: true,
|
||||||
max: 0,
|
max: 0,
|
||||||
placeholder: '搜索或选择',
|
placeholder: '搜索或选择',
|
||||||
searchPlaceholder: '搜索',
|
searchPlaceholder: '搜索',
|
||||||
@@ -32,7 +43,7 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: string[]];
|
'update:modelValue': [value: string[] | string];
|
||||||
create: [name: string];
|
create: [name: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -40,19 +51,25 @@ const root = ref<HTMLElement | null>(null);
|
|||||||
const searchInput = ref<HTMLInputElement | null>(null);
|
const searchInput = ref<HTMLInputElement | null>(null);
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
|
const activeIndex = ref(-1);
|
||||||
|
|
||||||
const optionRows = computed(() =>
|
const optionRows = computed(() =>
|
||||||
props.options.map((option) => ({
|
props.options.map((option, index) => ({
|
||||||
value: String(option.id),
|
value: String(option.id),
|
||||||
label: option.subcategory ? `${option.name} · ${option.subcategory}` : option.name
|
label: option.label ?? (option.subcategory ? `${option.name} · ${option.subcategory}` : option.name),
|
||||||
|
id: `${props.id}-option-${index}`
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedValues = computed(() => new Set(props.modelValue));
|
const modelValues = computed(() => {
|
||||||
const maxReached = computed(() => props.max > 0 && props.modelValue.length >= props.max);
|
if (Array.isArray(props.modelValue)) return props.modelValue;
|
||||||
|
return props.modelValue ? [props.modelValue] : [];
|
||||||
|
});
|
||||||
|
const selectedValues = computed(() => new Set(modelValues.value));
|
||||||
|
const maxReached = computed(() => props.multiple && props.max > 0 && modelValues.value.length >= props.max);
|
||||||
|
|
||||||
const selectedRows = computed(() =>
|
const selectedRows = computed(() =>
|
||||||
props.modelValue
|
modelValues.value
|
||||||
.map((value) => optionRows.value.find((option) => option.value === value))
|
.map((value) => optionRows.value.find((option) => option.value === value))
|
||||||
.filter((option) => option !== undefined)
|
.filter((option) => option !== undefined)
|
||||||
);
|
);
|
||||||
@@ -69,16 +86,55 @@ const hasExactMatch = computed(() => {
|
|||||||
});
|
});
|
||||||
const canCreate = computed(() => props.allowCreate && createName.value !== '' && !hasExactMatch.value && !maxReached.value);
|
const canCreate = computed(() => props.allowCreate && createName.value !== '' && !hasExactMatch.value && !maxReached.value);
|
||||||
const createText = computed(() => props.createLabel.replace('{name}', createName.value));
|
const createText = computed(() => props.createLabel.replace('{name}', createName.value));
|
||||||
|
const optionsListId = computed(() => `${props.id}-options`);
|
||||||
|
const createOptionId = computed(() => `${props.id}-create`);
|
||||||
|
const candidateRows = computed<CandidateRow[]>(() => {
|
||||||
|
const rows: CandidateRow[] = filteredRows.value
|
||||||
|
.filter((option) => isSearchSelectable(option.value))
|
||||||
|
.map((option) => ({ type: 'option', id: option.id, value: option.value, label: option.label }));
|
||||||
|
|
||||||
|
if (canCreate.value) {
|
||||||
|
rows.push({ type: 'create', id: createOptionId.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
const activeCandidate = computed(() => candidateRows.value[activeIndex.value]);
|
||||||
|
const activeDescendant = computed(() => activeCandidate.value?.id);
|
||||||
|
|
||||||
|
function setDefaultActiveIndex() {
|
||||||
|
const keyword = createName.value.toLowerCase();
|
||||||
|
const exactIndex = keyword
|
||||||
|
? candidateRows.value.findIndex((candidate) => candidate.type === 'option' && candidate.label.toLowerCase() === keyword)
|
||||||
|
: -1;
|
||||||
|
activeIndex.value = exactIndex >= 0 ? exactIndex : candidateRows.value.length ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampActiveIndex() {
|
||||||
|
if (!candidateRows.value.length) {
|
||||||
|
activeIndex.value = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeIndex.value < 0) {
|
||||||
|
activeIndex.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeIndex.value = Math.min(activeIndex.value, candidateRows.value.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
async function openDropdown() {
|
async function openDropdown() {
|
||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
setDefaultActiveIndex();
|
||||||
searchInput.value?.focus();
|
searchInput.value?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDropdown() {
|
function closeDropdown() {
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
search.value = '';
|
search.value = '';
|
||||||
|
activeIndex.value = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDropdown() {
|
function toggleDropdown() {
|
||||||
@@ -89,25 +145,31 @@ function toggleDropdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(value: string) {
|
function updateValue(values: string[]) {
|
||||||
|
emit('update:modelValue', props.multiple ? values : (values[0] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectOption(value: string) {
|
||||||
|
if (!props.multiple) {
|
||||||
|
updateValue([value]);
|
||||||
|
closeDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedValues.value.has(value)) {
|
if (selectedValues.value.has(value)) {
|
||||||
emit(
|
updateValue(modelValues.value.filter((item) => item !== value));
|
||||||
'update:modelValue',
|
|
||||||
props.modelValue.filter((item) => item !== value)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!maxReached.value) {
|
if (!maxReached.value) {
|
||||||
emit('update:modelValue', [...props.modelValue, value]);
|
updateValue([...modelValues.value, value]);
|
||||||
|
search.value = '';
|
||||||
|
setDefaultActiveIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(value: string) {
|
function remove(value: string) {
|
||||||
emit(
|
updateValue(modelValues.value.filter((item) => item !== value));
|
||||||
'update:modelValue',
|
|
||||||
props.modelValue.filter((item) => item !== value)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOption() {
|
function createOption() {
|
||||||
@@ -116,6 +178,37 @@ function createOption() {
|
|||||||
search.value = '';
|
search.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSearchSelectable(value: string) {
|
||||||
|
return !props.multiple || (!selectedValues.value.has(value) && !maxReached.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveOption(option: OptionRow) {
|
||||||
|
const candidate = activeCandidate.value;
|
||||||
|
return candidate?.type === 'option' && candidate.value === option.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCreateActive() {
|
||||||
|
return activeCandidate.value?.type === 'create';
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveActive(delta: number) {
|
||||||
|
if (!candidateRows.value.length) return;
|
||||||
|
const nextIndex = activeIndex.value < 0 ? 0 : activeIndex.value + delta;
|
||||||
|
activeIndex.value = (nextIndex + candidateRows.value.length) % candidateRows.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitSearch() {
|
||||||
|
const candidate = activeCandidate.value;
|
||||||
|
if (candidate?.type === 'option') {
|
||||||
|
selectOption(candidate.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate?.type === 'create') {
|
||||||
|
createOption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onRootKeydown(event: KeyboardEvent) {
|
function onRootKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
@@ -135,10 +228,13 @@ onMounted(() => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('pointerdown', onDocumentPointerDown);
|
document.removeEventListener('pointerdown', onDocumentPointerDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(search, setDefaultActiveIndex);
|
||||||
|
watch(candidateRows, clampActiveIndex);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="root" class="tags-select" @keydown="onRootKeydown">
|
<div ref="root" class="tags-select" :class="{ 'tags-select--single': !multiple }" @keydown="onRootKeydown">
|
||||||
<button
|
<button
|
||||||
:id="id"
|
:id="id"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -175,19 +271,27 @@ onBeforeUnmount(() => {
|
|||||||
class="tags-select__search"
|
class="tags-select__search"
|
||||||
type="search"
|
type="search"
|
||||||
:placeholder="searchPlaceholder"
|
:placeholder="searchPlaceholder"
|
||||||
|
:aria-activedescendant="activeDescendant"
|
||||||
|
:aria-controls="optionsListId"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
role="combobox"
|
||||||
|
@keydown.enter.stop.prevent="commitSearch"
|
||||||
|
@keydown.down.stop.prevent="moveActive(1)"
|
||||||
|
@keydown.up.stop.prevent="moveActive(-1)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="tags-select__options" role="listbox" aria-multiselectable="true">
|
<div :id="optionsListId" class="tags-select__options" role="listbox" :aria-multiselectable="multiple ? 'true' : undefined">
|
||||||
<button
|
<button
|
||||||
v-for="option in filteredRows"
|
v-for="option in filteredRows"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
type="button"
|
type="button"
|
||||||
class="tags-select__option"
|
class="tags-select__option"
|
||||||
:class="{ selected: selectedValues.has(option.value) }"
|
:id="option.id"
|
||||||
|
:class="{ selected: selectedValues.has(option.value), active: isActiveOption(option) }"
|
||||||
role="option"
|
role="option"
|
||||||
:aria-selected="selectedValues.has(option.value)"
|
:aria-selected="selectedValues.has(option.value)"
|
||||||
:disabled="!selectedValues.has(option.value) && maxReached"
|
:disabled="!selectedValues.has(option.value) && maxReached"
|
||||||
@click="toggle(option.value)"
|
@click="selectOption(option.value)"
|
||||||
>
|
>
|
||||||
<span>{{ option.label }}</span>
|
<span>{{ option.label }}</span>
|
||||||
<span v-if="selectedValues.has(option.value)" class="tags-select__state">已选</span>
|
<span v-if="selectedValues.has(option.value)" class="tags-select__state">已选</span>
|
||||||
@@ -196,6 +300,8 @@ onBeforeUnmount(() => {
|
|||||||
v-if="canCreate"
|
v-if="canCreate"
|
||||||
type="button"
|
type="button"
|
||||||
class="tags-select__option tags-select__create"
|
class="tags-select__option tags-select__create"
|
||||||
|
:id="createOptionId"
|
||||||
|
:class="{ active: isCreateActive() }"
|
||||||
:disabled="creating"
|
:disabled="creating"
|
||||||
@click="createOption"
|
@click="createOption"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -251,11 +251,16 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tags-select__option:hover,
|
.tags-select__option:hover,
|
||||||
|
.tags-select__option.active,
|
||||||
.tags-select__option.selected {
|
.tags-select__option.selected {
|
||||||
background: #edf7ef;
|
background: #edf7ef;
|
||||||
color: #1f5c40;
|
color: #1f5c40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-select__option.active {
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(31, 111, 80, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.tags-select__option.selected {
|
.tags-select__option.selected {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
@@ -532,6 +537,10 @@ select {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-row .tags-select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-row input {
|
.inline-row input {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ const habitatForm = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectedConfig = computed(() => configTypes.find((item) => item.key === activeConfigType.value) ?? configTypes[0]);
|
const selectedConfig = computed(() => configTypes.find((item) => item.key === activeConfigType.value) ?? configTypes[0]);
|
||||||
|
const itemSelectOptions = computed(() => itemRows.value.map((item) => ({ id: item.id, name: item.name })));
|
||||||
|
const pokemonSelectOptions = computed(() =>
|
||||||
|
pokemonRows.value.map((pokemon) => ({ id: pokemon.id, name: pokemon.name, label: `#${pokemon.id} ${pokemon.name}` }))
|
||||||
|
);
|
||||||
|
|
||||||
function toIds(values: string[]): number[] {
|
function toIds(values: string[]): number[] {
|
||||||
return values.map(Number).filter((item) => Number.isInteger(item) && item > 0);
|
return values.map(Number).filter((item) => Number.isInteger(item) && item > 0);
|
||||||
@@ -503,10 +507,14 @@ onMounted(() => {
|
|||||||
<div class="field"><label for="pokemon-name">名字</label><input id="pokemon-name" v-model="pokemonForm.name" /></div>
|
<div class="field"><label for="pokemon-name">名字</label><input id="pokemon-name" v-model="pokemonForm.name" /></div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="pokemon-environment">喜欢的环境</label>
|
<label for="pokemon-environment">喜欢的环境</label>
|
||||||
<select id="pokemon-environment" v-model="pokemonForm.environmentId">
|
<TagsSelect
|
||||||
<option value="">请选择</option>
|
id="pokemon-environment"
|
||||||
<option v-for="item in options.environments" :key="item.id" :value="item.id">{{ item.name }}</option>
|
v-model="pokemonForm.environmentId"
|
||||||
</select>
|
:options="options.environments"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="请选择"
|
||||||
|
search-placeholder="搜索喜欢的环境"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="pokemon-skills">特长</label>
|
<label for="pokemon-skills">特长</label>
|
||||||
@@ -560,17 +568,25 @@ onMounted(() => {
|
|||||||
<div class="field"><label for="item-name">名称</label><input id="item-name" v-model="itemForm.name" /></div>
|
<div class="field"><label for="item-name">名称</label><input id="item-name" v-model="itemForm.name" /></div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="item-category">分类</label>
|
<label for="item-category">分类</label>
|
||||||
<select id="item-category" v-model="itemForm.categoryId">
|
<TagsSelect
|
||||||
<option value="">请选择</option>
|
id="item-category"
|
||||||
<option v-for="item in options.itemCategories" :key="item.id" :value="item.id">{{ item.name }}</option>
|
v-model="itemForm.categoryId"
|
||||||
</select>
|
:options="options.itemCategories"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="请选择"
|
||||||
|
search-placeholder="搜索分类"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="item-usage">用途</label>
|
<label for="item-usage">用途</label>
|
||||||
<select id="item-usage" v-model="itemForm.usageId">
|
<TagsSelect
|
||||||
<option value="">无</option>
|
id="item-usage"
|
||||||
<option v-for="item in options.itemUsages" :key="item.id" :value="item.id">{{ item.name }}</option>
|
v-model="itemForm.usageId"
|
||||||
</select>
|
:options="options.itemUsages"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="无"
|
||||||
|
search-placeholder="搜索用途"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="check-row">
|
<div class="check-row">
|
||||||
<label><input v-model="itemForm.dyeable" type="checkbox" /> 可染色</label>
|
<label><input v-model="itemForm.dyeable" type="checkbox" /> 可染色</label>
|
||||||
@@ -626,10 +642,14 @@ onMounted(() => {
|
|||||||
<h2>材料单</h2>
|
<h2>材料单</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="recipe-item">物品</label>
|
<label for="recipe-item">物品</label>
|
||||||
<select id="recipe-item" v-model="recipeForm.itemId">
|
<TagsSelect
|
||||||
<option value="">请选择</option>
|
id="recipe-item"
|
||||||
<option v-for="item in itemRows" :key="item.id" :value="String(item.id)">{{ item.name }}</option>
|
v-model="recipeForm.itemId"
|
||||||
</select>
|
:options="itemSelectOptions"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="请选择"
|
||||||
|
search-placeholder="搜索物品"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="recipe-methods">入手方式</label>
|
<label for="recipe-methods">入手方式</label>
|
||||||
@@ -646,10 +666,14 @@ onMounted(() => {
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>需要材料</label>
|
<label>需要材料</label>
|
||||||
<div v-for="(row, index) in recipeForm.materials" :key="index" class="inline-row">
|
<div v-for="(row, index) in recipeForm.materials" :key="index" class="inline-row">
|
||||||
<select v-model="row.itemId">
|
<TagsSelect
|
||||||
<option value="">请选择</option>
|
:id="`recipe-material-${index}`"
|
||||||
<option v-for="item in itemRows" :key="item.id" :value="String(item.id)">{{ item.name }}</option>
|
v-model="row.itemId"
|
||||||
</select>
|
:options="itemSelectOptions"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="请选择"
|
||||||
|
search-placeholder="搜索物品"
|
||||||
|
/>
|
||||||
<input v-model.number="row.quantity" type="number" min="1" />
|
<input v-model.number="row.quantity" type="number" min="1" />
|
||||||
<button type="button" @click="recipeForm.materials.splice(index, 1)">删除</button>
|
<button type="button" @click="recipeForm.materials.splice(index, 1)">删除</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -682,10 +706,14 @@ onMounted(() => {
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>配方</label>
|
<label>配方</label>
|
||||||
<div v-for="(row, index) in habitatForm.recipeItems" :key="index" class="inline-row">
|
<div v-for="(row, index) in habitatForm.recipeItems" :key="index" class="inline-row">
|
||||||
<select v-model="row.itemId">
|
<TagsSelect
|
||||||
<option value="">请选择</option>
|
:id="`habitat-recipe-item-${index}`"
|
||||||
<option v-for="item in itemRows" :key="item.id" :value="String(item.id)">{{ item.name }}</option>
|
v-model="row.itemId"
|
||||||
</select>
|
:options="itemSelectOptions"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="请选择"
|
||||||
|
search-placeholder="搜索物品"
|
||||||
|
/>
|
||||||
<input v-model.number="row.quantity" type="number" min="1" />
|
<input v-model.number="row.quantity" type="number" min="1" />
|
||||||
<button type="button" @click="habitatForm.recipeItems.splice(index, 1)">删除</button>
|
<button type="button" @click="habitatForm.recipeItems.splice(index, 1)">删除</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -694,10 +722,14 @@ onMounted(() => {
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>可出现的宝可梦</label>
|
<label>可出现的宝可梦</label>
|
||||||
<div v-for="(row, index) in habitatForm.pokemonAppearances" :key="index" class="appearance-row">
|
<div v-for="(row, index) in habitatForm.pokemonAppearances" :key="index" class="appearance-row">
|
||||||
<select v-model="row.pokemonId">
|
<TagsSelect
|
||||||
<option value="">Pokemon</option>
|
:id="`appearance-pokemon-${index}`"
|
||||||
<option v-for="item in pokemonRows" :key="item.id" :value="String(item.id)">#{{ item.id }} {{ item.name }}</option>
|
v-model="row.pokemonId"
|
||||||
</select>
|
:options="pokemonSelectOptions"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="Pokemon"
|
||||||
|
search-placeholder="搜索 Pokemon"
|
||||||
|
/>
|
||||||
<TagsSelect
|
<TagsSelect
|
||||||
:id="`appearance-maps-${index}`"
|
:id="`appearance-maps-${index}`"
|
||||||
v-model="row.mapIds"
|
v-model="row.mapIds"
|
||||||
|
|||||||
@@ -61,18 +61,26 @@ watch([tab, itemQuery], loadItems);
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="category">分类</label>
|
<label for="category">分类</label>
|
||||||
<select id="category" v-model="categoryId">
|
<TagsSelect
|
||||||
<option value="">全部</option>
|
id="category"
|
||||||
<option v-for="item in options.itemCategories" :key="item.id" :value="item.id">{{ item.name }}</option>
|
v-model="categoryId"
|
||||||
</select>
|
:options="options.itemCategories"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="全部"
|
||||||
|
search-placeholder="搜索分类"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="usage">用途</label>
|
<label for="usage">用途</label>
|
||||||
<select id="usage" v-model="usageId">
|
<TagsSelect
|
||||||
<option value="">全部</option>
|
id="usage"
|
||||||
<option v-for="item in options.itemUsages" :key="item.id" :value="item.id">{{ item.name }}</option>
|
v-model="usageId"
|
||||||
</select>
|
:options="options.itemUsages"
|
||||||
|
:multiple="false"
|
||||||
|
placeholder="全部"
|
||||||
|
search-placeholder="搜索用途"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@@ -54,12 +54,14 @@ watch(query, loadPokemon);
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="environment">喜欢的环境</label>
|
<label for="environment">喜欢的环境</label>
|
||||||
<select id="environment" v-model="environmentId">
|
<TagsSelect
|
||||||
<option value="">全部</option>
|
id="environment"
|
||||||
<option v-for="item in options.environments" :key="item.id" :value="item.id">
|
v-model="environmentId"
|
||||||
{{ item.name }}
|
:options="options.environments"
|
||||||
</option>
|
:multiple="false"
|
||||||
</select>
|
placeholder="全部"
|
||||||
|
search-placeholder="搜索喜欢的环境"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
Reference in New Issue
Block a user