优化供应商搜索下拉与卡片操作

供应商画像选择器改为支持输入筛选的动态下拉框,输入关键词实时匹配并刷新画像;删除暂未实现的收藏方案按钮,保留已接入操作。
This commit is contained in:
kx_guo 2026-09-09 15:51:43 +08:00
parent 69a1433a86
commit d05be62e02
2 changed files with 153 additions and 19 deletions

View File

@ -403,14 +403,6 @@ function generateReport() {
> >
查看标准详情 查看标准详情
</button> </button>
<button
v-if="card.type === 'solution'"
type="button"
disabled
title="功能开发中"
>
📌 收藏方案
</button>
<button <button
v-if="card.type === 'solution'" v-if="card.type === 'solution'"
type="button" type="button"

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue"; import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { fetchSupplierOptions } from "@/api/suppliers"; import { fetchSupplierOptions } from "@/api/suppliers";
@ -10,6 +10,11 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
const supplierNames = ref<string[]>([]); const supplierNames = ref<string[]>([]);
const selected = ref(""); const selected = ref("");
const supplierQuery = ref("");
const supplierResults = ref<string[]>([]);
const supplierDropdownOpen = ref(false);
const supplierSearching = ref(false);
let supplierSearchTimer: ReturnType<typeof setTimeout> | undefined;
const profile = ref<SupplierProfile | null>(null); const profile = ref<SupplierProfile | null>(null);
const loading = ref(false); const loading = ref(false);
const error = ref(""); const error = ref("");
@ -89,11 +94,64 @@ async function loadOptions() {
typeof route.query.supplier === "string" typeof route.query.supplier === "string"
? route.query.supplier ? route.query.supplier
: (response.data.data?.defaultSupplier ?? supplierNames.value[0] ?? ""); : (response.data.data?.defaultSupplier ?? supplierNames.value[0] ?? "");
supplierQuery.value = selected.value;
supplierResults.value = supplierNames.value;
if (selected.value) await loadProfile(); if (selected.value) await loadProfile();
} catch { } catch {
error.value = "供应商列表加载失败,请重试。"; error.value = "供应商列表加载失败,请重试。";
} }
} }
function openSupplierDropdown() {
supplierDropdownOpen.value = true;
if (!supplierQuery.value || supplierQuery.value === selected.value) {
supplierResults.value = supplierNames.value;
}
}
function searchSuppliers(value: string) {
if (supplierSearchTimer) clearTimeout(supplierSearchTimer);
const keyword = value.trim();
if (!keyword) {
supplierResults.value = supplierNames.value;
supplierSearching.value = false;
return;
}
const local = supplierNames.value.filter((name) => name.includes(keyword));
supplierResults.value = local;
supplierSearching.value = true;
supplierSearchTimer = setTimeout(async () => {
try {
const response = await fetchSupplierOptions(keyword);
supplierResults.value =
response.data.data?.items.map((item) => item.name) ?? [];
} catch {
supplierResults.value = local;
} finally {
supplierSearching.value = false;
}
}, 180);
}
function onSupplierInput(event: Event) {
supplierQuery.value = (event.target as HTMLInputElement).value;
supplierDropdownOpen.value = true;
searchSuppliers(supplierQuery.value);
}
function selectSupplier(name: string) {
selected.value = name;
supplierQuery.value = name;
supplierDropdownOpen.value = false;
chooseSupplier();
}
function closeSupplierDropdown() {
window.setTimeout(() => {
supplierDropdownOpen.value = false;
if (selected.value) supplierQuery.value = selected.value;
}, 120);
}
async function loadProfile() { async function loadProfile() {
if (!selected.value) return; if (!selected.value) return;
loading.value = true; loading.value = true;
@ -116,11 +174,15 @@ function chooseSupplier() {
void loadProfile(); void loadProfile();
} }
onMounted(() => void loadOptions()); onMounted(() => void loadOptions());
onBeforeUnmount(() => {
if (supplierSearchTimer) clearTimeout(supplierSearchTimer);
});
watch( watch(
() => route.query.supplier, () => route.query.supplier,
(value) => { (value) => {
if (typeof value === "string" && value !== selected.value) { if (typeof value === "string" && value !== selected.value) {
selected.value = value; selected.value = value;
supplierQuery.value = value;
void loadProfile(); void loadProfile();
} }
}, },
@ -132,16 +194,46 @@ watch(
<div class="supplier-inner"> <div class="supplier-inner">
<header class="supplier-head"> <header class="supplier-head">
<h1>供应商深度画像</h1> <h1>供应商深度画像</h1>
<select <div class="supplier-combobox" @focusout="closeSupplierDropdown">
v-model="selected" <input
class="supplier-select" :value="supplierQuery"
aria-label="选择供应商" class="supplier-select"
@change="chooseSupplier" type="text"
> role="combobox"
<option v-for="name in supplierNames" :key="name" :value="name"> aria-label="搜索并选择供应商"
{{ name }} aria-autocomplete="list"
</option> :aria-expanded="supplierDropdownOpen"
</select> placeholder="输入供应商名称"
@focus="openSupplierDropdown"
@input="onSupplierInput"
/>
<div
v-if="supplierDropdownOpen"
class="supplier-options"
role="listbox"
>
<button
v-for="name in supplierResults"
:key="name"
type="button"
class="supplier-option"
:class="{ active: name === selected }"
role="option"
:aria-selected="name === selected"
@mousedown.prevent="selectSupplier(name)"
>
{{ name }}
</button>
<span v-if="supplierSearching" class="supplier-option-state"
>搜索中…</span
>
<span
v-else-if="!supplierResults.length"
class="supplier-option-state"
>未找到匹配供应商</span
>
</div>
</div>
<span v-if="profile" class="tier-badge" :class="tierClass">{{ <span v-if="profile" class="tier-badge" :class="tierClass">{{
tierLabel tierLabel
}}</span> }}</span>
@ -418,6 +510,8 @@ watch(
font-weight: 650; font-weight: 650;
} }
.supplier-select { .supplier-select {
width: 180px;
min-height: 34px;
padding: 7px 14px; padding: 7px 14px;
border: 1px solid #e4e4e2; border: 1px solid #e4e4e2;
border-radius: 5px; border-radius: 5px;
@ -428,6 +522,47 @@ watch(
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
} }
.supplier-combobox {
position: relative;
z-index: 5;
}
.supplier-combobox .supplier-select {
cursor: text;
}
.supplier-options {
position: absolute;
top: calc(100% + 4px);
right: 0;
left: 0;
max-height: 260px;
overflow-y: auto;
border: 1px solid var(--color-border-strong);
border-radius: 5px;
padding: 4px 0;
background: #fff;
box-shadow: var(--shadow-lg);
}
.supplier-option {
display: block;
width: 100%;
border: 0;
padding: 8px 12px;
background: #fff;
color: var(--color-text-secondary);
font-size: 12px;
text-align: left;
}
.supplier-option:hover,
.supplier-option.active {
background: var(--color-accent-subtle);
color: var(--color-accent);
}
.supplier-option-state {
display: block;
padding: 9px 12px;
color: var(--color-text-faint);
font-size: 11px;
}
.version-note { .version-note {
color: #999; color: #999;
font-size: 11px; font-size: 11px;
@ -700,6 +835,13 @@ watch(
order: 3; order: 3;
width: 100%; width: 100%;
} }
.supplier-combobox {
order: 3;
width: 100%;
}
.supplier-combobox .supplier-select {
width: 100%;
}
.version-note { .version-note {
width: 100%; width: 100%;
} }