zhiqi/src/views/order/intention/search.vue

3460 lines
96 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="step2-container">
<div class="main-row">
<div class="right-content">
<div>
<div class="search-box-container">
<div class="page-header">
<h1>品号查询</h1>
</div>
<!-- 主查询区域 -->
<div class="search-box">
<div
class="custom-input-wrapper"
:style="{ width: inputWidth + 'px' }"
@click="handleInputContainerClick"
>
<el-input
v-model="currentInput"
ref="searchInput"
placeholder="输入查询条件,例如: 品号:D311299000045-00005;..."
class="main-input"
@input="handleInput"
@keydown.enter="handleSearch"
@keydown.arrow-down="focusFirstSuggestion"
@keydown.escape="hideSuggestions"
@keydown="handleKeydown"
@click="handleInputClick"
@mouseup="handleInputMouseUp"
>
<!-- 输入框内添加话筒图标,暂时不用-->
<!-- <template #append>
<el-icon
class="voice-icon"
@click.stop="showVoicePopup = true"
>
<Microphone />
</el-icon>
</template> -->
</el-input>
</div>
<!-- 下拉建议框 -->
<div
v-if="
showSuggestions &&
(suggestions.length > 0 || possibleFields.length > 0)
"
class="suggestions-dropdown"
:style="{ width: inputWidth + 'px' }"
>
<!-- 可能的字段提示项 -->
<div
v-if="possibleFields.length > 0"
class="possible-fields-section"
>
<div class="possible-fields-header">
您输入的条件可能属于以下字段:
</div>
<div
v-for="(field, index) in possibleFields"
:key="'field-' + index"
class="possible-field-item"
:class="{ active: activePossibleFieldIndex === index }"
@click="selectPossibleField(field)"
@mouseenter="activePossibleFieldIndex = index"
@keydown.enter.stop="selectPossibleField(field)"
@keydown.arrow-down.stop="navigatePossibleFields('down')"
@keydown.arrow-up.stop="navigatePossibleFields('up')"
tabindex="0"
>
<span class="field-type">[字段]</span>
<span>{{ field.label }}</span>
<span class="field-hint">: </span>
</div>
</div>
<div
v-for="(item, index) in suggestions"
:key="index"
class="suggestion-item"
:class="{
active:
activeSuggestionIndex ===
index +
(possibleFields.length > 0
? 1 + possibleFields.length
: 0),
}"
@click="selectSuggestion(item)"
@mouseenter="
activeSuggestionIndex =
index +
(possibleFields.length > 0
? 1 + possibleFields.length
: 0)
"
@keydown.enter.stop="selectSuggestion(item)"
@keydown.arrow-down.stop="navigateSuggestions('down')"
@keydown.arrow-up.stop="navigateSuggestions('up')"
tabindex="0"
>
<span v-if="item.type === 'field'" class="field-type"
>[字段]</span
>
<span v-if="item.type === 'value'" class="value-type"
>[值]</span
>
<span>{{ item.label }}</span>
<span v-if="item.type === 'field'" class="field-hint"
>:
</span>
</div>
</div>
<!-- 查询按钮 -->
<el-button
type="primary"
class="search-button"
@click="handleSearch"
>
<el-icon><Search /></el-icon>
查询
</el-button>
</div>
<!-- 关键差异信息提示 -->
<div v-if="showKeyDiffHint" class="key-diff-hint">
以下商品列表中,关键差异信息:
<span
v-for="(item, index) in keyDiffFields"
:key="index"
class="diff-field"
@click="addDiffFieldToInput(item)"
>
{{ item.label }}
<template v-if="index < keyDiffFields.length - 1"> 、</template>
</span>
</div>
<!-- 条件标签展示 -->
<div v-if="parsedConditions.length > 0" class="conditions-tags">
<el-tag
v-for="(cond, index) in parsedConditions"
:key="index"
closable
:type="cond.valid ? 'success' : 'danger'"
@close="removeCondition(index)"
@click="focusConditionTag(index)"
class="condition-tag"
>
<span v-if="cond.field" class="tag-field"
>{{ cond.fieldLabel }}:</span
>
<span
class="tag-value"
@click.stop="focusConditionTag(index)"
>{{ cond.value }}</span
>
<!-- 将编辑图标替换为"精准"文字,并添加点击事件 -->
<span
class="tag-precise"
@click.stop="addPreciseCondition(index)"
>精准</span
>
</el-tag>
<el-button link @click="clearAll" class="clear-button">
<el-icon><RefreshLeft /></el-icon>
清除所有
</el-button>
</div>
<!-- 精准查询参数展示区域 -->
<div v-if="preciseConditions.length > 0" class="precise-conditions">
<div class="precise-conditions-label">选择精准查询参数:</div>
<div class="precise-tags">
<el-tag
v-for="(cond, index) in preciseConditions"
:key="'precise-' + index"
closable
type="primary"
@close="removePreciseCondition(index)"
class="precise-tag"
>
<span v-if="cond.field" class="tag-field"
>{{ cond.fieldLabel }}:</span
>
<span class="tag-value">{{ cond.value }}</span>
</el-tag>
</div>
</div>
</div>
<!-- 查询结果区域 -->
<div class="results-section" v-if="showResults">
<div
v-if="currentPageData.length"
class="result-card-list-wrap"
style="position: relative"
>
<!-- 参数对比按钮 - 随滚动条移动 -->
<el-button
v-if="selectedCompareList.length === 1"
type="primary"
class="compare-btn-float detail"
@click="onCompare"
circle
>
查看<br />详情
</el-button>
<el-button
v-else-if="selectedCompareList.length > 1"
type="warning"
class="compare-btn-float compare"
@click="onCompare"
circle
>
参数<br />对比
</el-button>
<div class="result-card-list">
<el-card
v-for="item in currentPageData"
:key="item.partNumber"
class="result-card"
:class="{ active: item.selected }"
@click="toggleSelect(item)"
>
<div class="result-card-info">
<div class="result-card-name" :title="item.partName">
{{ item.partName }}
</div>
<div
class="result-card-parameter"
:title="`品号:${item.partNumber}`"
>
<span>品号:</span>{{ item.partNumber }}
</div>
<div
class="result-card-parameter"
:title="`品号-规格型号:${item.partNumberSpec}`"
>
<span>品号-规格型号: </span>{{ item.partNumberSpec }}
</div>
<div
class="result-card-parameter text-truncate"
:title="`车型:${item.trainModel}`"
>
<span>车型:</span> {{ item.trainModel }}
</div>
</div>
</el-card>
</div>
</div>
<div v-if="currentPageData.length === 0" class="no-results">
<el-empty description="没有匹配的结果"></el-empty>
</div>
<!-- 分页控件 -->
<div class="pagination-container" v-if="showResults">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[8, 16, 32]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalItems"
></el-pagination>
<!-- 确定按钮 -->
<el-button
type="primary"
class="confirm-button"
@click="handleConfirm"
:disabled="selectedCount !== 1"
>
确定
</el-button>
</div>
</div>
<!-- 参数对比弹窗 -->
<el-dialog
v-model="compareDialogVisible"
width="1200px"
top="40px"
:close-on-click-modal="false"
:fullscreen="isFullscreen"
draggable
class="compare-dialog"
>
<template #header>
<div class="compare-dialog-header">
<span class="compare-dialog-title">{{
displayCompareList && displayCompareList.length === 1
? "查看详情"
: "参数对比"
}}</span>
<div class="compare-dialog-actions">
<el-button text size="small" @click="toggleFullscreen">
<el-icon :size="18">
<full-screen v-if="!isFullscreen" />
<cancel-full-screen v-else />
</el-icon>
<span class="action-text">
{{ isFullscreen ? "退出全屏" : "全屏" }}
</span>
</el-button>
</div>
</div>
</template>
<!-- 仅看不同项按钮 -->
<div
v-if="displayCompareList && displayCompareList.length > 1"
class="compare-filter-options"
style="margin: -45px 80px 16px; text-align: left"
>
<el-button
type="primary"
@click="showOnlyDifferences = !showOnlyDifferences"
>
{{ showOnlyDifferences ? "显示全部" : "仅看不同项" }}
</el-button>
</div>
<!-- 表格对比区,添加横向滚动 -->
<div class="compare-table-wrap">
<div class="compare-table-scroll">
<el-table
:data="filteredCompareTableData"
border
:height="isFullscreen ? '90vh' : '60vh'"
style="width: 100%; min-width: 600px"
>
<el-table-column
prop="param"
label="参数"
width="180"
class-name="param-col"
align="center"
header-align="center"
fixed="left"
/>
<el-table-column
v-for="(item, index) in displayCompareList"
:key="item.partNumber"
:label="item.partNumber"
:prop="item.partNumber"
align="center"
header-align="center"
min-width="300"
>
<template #header="{ column }">
<div
class="compare-header-draggable"
draggable="true"
@dragstart="onHeaderDragStart(index)"
@dragover.prevent
@drop="onHeaderDrop(index)"
>
<span
v-if="
item.isLatest &&
displayCompareList &&
displayCompareList.length > 1
"
class="new-tag"
>
<img
src="/images/cars/new.png"
alt="最新品号"
class="new-tag-img"
/>
</span>
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
"
>
<span>
{{ column.label }}
</span>
<span style="font-size: 12px; color: #666">{{
displayCompareList.find(
(i) => i.partNumber === column.label
)?.partName || "-"
}}</span>
</div>
</div>
</template>
<!-- 单元格内容,不同的值标红 -->
<template #default="scope">
<span
:class="{
'different-value': isValueDifferent(scope.row.param),
}"
>
{{ scope.row[scope.column.property] || "-" }}
</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-dialog>
<!-- 选择确认提示弹窗 -->
<el-dialog
v-model="confirmDialogVisible"
title="选择确认"
width="400px"
:show-close="false"
>
<div class="confirm-message">
您选择的品号为:{{ selectedProductNumber }}
</div>
<template #footer>
<el-button type="primary" @click="confirmDialogVisible = false">
确定
</el-button>
</template>
</el-dialog>
<!-- 语音输入弹窗 -->
<el-dialog
v-model="showVoicePopup"
title="语音输入"
width="400px"
:show-close="true"
>
<div class="voice-popup-content">
正式环境中提供语音输入检索功能demo中仅做示意。
</div>
<template #footer>
<el-button type="primary" @click="showVoicePopup = false">
我知道了
</el-button>
</template>
</el-dialog>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
nextTick,
getCurrentInstance,
} from "vue";
import {
searchHint,
search as searchProducts,
differenceWords,
compare,
} from "@/api/order";
import { ElMessage, ElEmpty, ElDialog } from "element-plus";
import { Close } from "@element-plus/icons-vue";
const { proxy } = getCurrentInstance();
const { display_field } = proxy.useDict("display_field");
// 接收props和定义emit
const props = defineProps({
form: Object,
selectedCarType: String,
});
const emit = defineEmits(["update:form"]);
// 双向绑定
const selectedCarTypeProxy = computed({
get() {
return props.form?.selectedCarType ?? "";
},
set(val) {
if (props.form) {
props.form.selectedCarType = val;
emit("update:form", props.form);
}
},
});
// 查询相关变量
const currentInput = ref("");
const searchInput = ref(null);
const inputWidth = ref(600);
const selectedConditionIndex = ref(-1);
const lastInputValue = ref(""); // 存储上一次的输入值,用于检测条件是否被修改
const isConditionMoved = ref(false); // 标记条件是否刚刚被移动,用于避免使用本地缓存
const disableLocalSuggestions = ref(false); // smart 返回为空时,禁止本地兜底建议
// 建议相关
const suggestions = ref([]);
const showSuggestions = ref(false);
const activeSuggestionIndex = ref(-1);
const possibleFields = ref([]);
const activePossibleFieldIndex = ref(-1);
const allFields = ref([]);
const fieldValueMap = ref({});
const allValues = ref([]);
const fieldsFromApi = ref(false); // 标记字段是否来自接口
const hintTimer = ref(null);
const pendingSearchTimer = ref(null);
const differenceTimer = ref(null);
const selectedItems = ref(new Map());
// 语音输入弹窗控制
const showVoicePopup = ref(false);
// 解析后的条件和查询结果
const parsedConditions = ref([]);
// 精准查询条件
const preciseConditions = ref([]);
const showResults = ref(false);
const filteredData = ref([]);
// 分页相关变量
const currentPage = ref(1);
const pageSize = ref(8);
const totalItems = ref(0);
// 计算当前页数据
const currentPageData = computed(() => {
return filteredData.value;
});
// 计算选中的商品数量
const selectedCount = computed(() => selectedItems.value.size);
// 选中的商品品号
const selectedProductNumber = computed(() => {
if (selectedItems.value.size !== 1) return "";
const iterator = selectedItems.value.values().next();
const first = iterator.value;
return first ? first.partNumber : "";
});
// 全屏相关
const isFullscreen = ref(false);
// 切换全屏/还原的方法
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value;
};
// 对比相关
const compareDialogVisible = ref(false);
const selectedCompareList = computed(() =>
Array.from(selectedItems.value.values())
);
// 仅显示不同项的开关
const showOnlyDifferences = ref(false);
// 存储参数差异信息的映射
const parameterDifferences = ref({});
// 确认弹窗
const confirmDialogVisible = ref(false);
// 关键差异字段相关
const keyDiffFields = ref([]);
const showKeyDiffHint = ref(false);
// 计算排序后的对比列表(最新品号在前)
const sortedCompareList = computed(() => {
// 复制数组以避免修改原始数据
const list = [...selectedCompareList.value];
// 排序规则:
// 1取品号最后的 5 位数字,如 00001
// 2第一位数字越大越靠后
// 3后四位数字整体越大越靠前
list.sort((a, b) => {
const aMatch = a.partNumber?.match(/-(\d{5})$/);
const bMatch = b.partNumber?.match(/-(\d{5})$/);
if (!aMatch && !bMatch) return 0;
if (!aMatch) return 1;
if (!bMatch) return -1;
const aSuffix = aMatch[1];
const bSuffix = bMatch[1];
const aFirst = parseInt(aSuffix.charAt(0), 10);
const bFirst = parseInt(bSuffix.charAt(0), 10);
// 第一位数字越大越靠后 => 升序
if (!Number.isNaN(aFirst) && !Number.isNaN(bFirst) && aFirst !== bFirst) {
return aFirst - bFirst;
}
// 后四位整体越大越靠前 => 降序
const aRest = parseInt(aSuffix.slice(1), 10);
const bRest = parseInt(bSuffix.slice(1), 10);
if (Number.isNaN(aRest) || Number.isNaN(bRest)) return 0;
return bRest - aRest;
});
// 标记最新的品号(排序后的第一个)
if (list.length > 0) {
list.forEach((item, index) => {
item.isLatest = index === 0;
});
}
return list;
});
// 对比列展示顺序(默认跟随 sortedCompareList可通过拖拽调整
const compareOrder = ref([]);
const displayCompareList = computed(() => {
const baseList = sortedCompareList.value;
if (!baseList.length) return [];
if (!compareOrder.value.length) {
return baseList;
}
const map = new Map(baseList.map((item) => [item.partNumber, item]));
const ordered = [];
compareOrder.value.forEach((partNumber) => {
if (map.has(partNumber)) {
ordered.push(map.get(partNumber));
map.delete(partNumber);
}
});
return ordered.concat(Array.from(map.values()));
});
// sortedCompareList 变化时,同步默认的列顺序
watch(
() => sortedCompareList.value.map((item) => item.partNumber),
(newOrder) => {
compareOrder.value = newOrder;
},
{ immediate: true }
);
// 列拖拽相关
const draggingColumnIndex = ref(null);
const onHeaderDragStart = (index) => {
draggingColumnIndex.value = index;
};
const onHeaderDrop = (targetIndex) => {
const from = draggingColumnIndex.value;
if (from === null || from === targetIndex) return;
const order = [...compareOrder.value];
const [moved] = order.splice(from, 1);
order.splice(targetIndex, 0, moved);
compareOrder.value = order;
draggingColumnIndex.value = null;
};
const compareTableData = ref([]);
const diffFieldKeys = ref(new Set());
const getItemKey = (item) => item?.partNumber ?? item?.id ?? item?.uuid ?? "";
const getDictLabel = (dictRef, value) => {
if (!dictRef) return null;
const list = dictRef.value ?? [];
const target = list.find((item) => item.value === value);
return target ? target.label : null;
};
const getFieldLabelFromPart = (part = "") => {
const colonIndex = part.indexOf(":");
return colonIndex > 0 ? part.substring(0, colonIndex).trim() : null;
};
const getUsedFieldLabels = (excludeIndex = -1) => {
const labels = new Set();
const parts = currentInput.value.split(/[;]/);
parts.forEach((part, idx) => {
if (idx === excludeIndex) return;
const label = getFieldLabelFromPart(part.trim());
if (label) labels.add(label);
});
return labels;
};
const fieldLabelExists = (label, excludeIndex = -1) =>
getUsedFieldLabels(excludeIndex).has(label);
const focusFieldLabel = (label) => {
const parts = currentInput.value.split(/[;]/);
const idx = parts.findIndex(
(part) => getFieldLabelFromPart(part.trim()) === label
);
if (idx !== -1) {
focusConditionTag(idx);
}
};
const getFieldLabelByKey = (key) => {
if (!key) return "-";
const dictLabel = getDictLabel(display_field, key);
if (dictLabel) return dictLabel;
const field = allFields.value.find((item) => item.key === key);
return field?.label || key;
};
const updateParameterDifferenceFlags = (entries) => {
const differences = {};
entries.forEach((entry) => {
differences[entry.label] = diffFieldKeys.value.has(entry.key);
});
parameterDifferences.value = differences;
};
const getCompareFieldEntries = () => {
const dictList = display_field?.value ?? [];
if (dictList.length > 0) {
return dictList
.filter((item) => item.value)
.map((item) => ({
key: item.value,
label: item.label || item.value,
}));
}
return allFields.value.map((field) => ({
key: field.key,
label: field.label || field.key,
}));
};
// 过滤后的对比表格数据(根据"仅看不同项"选项)
const filteredCompareTableData = computed(() => {
if (!showOnlyDifferences.value) {
return compareTableData.value;
}
// 只返回有差异的参数项
return compareTableData.value.filter(
(row) => parameterDifferences.value[row.param]
);
});
// 判断某个参数是否有差异(只要有差异就标红)
const isValueDifferent = (param) => {
// 如果参数有任何差异,就标红
return parameterDifferences.value[param];
};
// 初始化所有可能的值
const initializeValues = () => {
const values = [];
allFields.value.forEach((field) => {
const uniqueValues = [
...new Set(
(fieldValueMap.value[field.key] || []).map((item) =>
item === null || item === undefined ? "" : String(item)
)
),
];
uniqueValues.forEach((value) => {
if (value !== "") {
values.push({
value,
fieldKey: field.key,
fieldLabel: field.label,
weight: 2,
});
}
});
});
allValues.value = values;
};
const mergeFieldMetadata = (fields = []) => {
if (!Array.isArray(fields) || fields.length === 0) return;
const fieldMapCache = new Map(
allFields.value.map((item) => [item.key, item])
);
fields.forEach((item) => {
const label = item.fieldName || item.fieldLabel || item.label;
const key = item.fieldKey || item.key;
if (!label || !key) return;
if (!fieldMapCache.has(key)) {
fieldMapCache.set(key, { key, label });
}
// 接口明确返回 fieldValues 时,统一以接口为准:
// - 空数组:清空本地缓存,避免出现“幽灵”值
// - 非空数组:合并去重
if (Array.isArray(item.fieldValues)) {
if (item.fieldValues.length === 0) {
fieldValueMap.value[key] = [];
} else {
const existingValues = fieldValueMap.value[key] || [];
fieldValueMap.value[key] = Array.from(
new Set([...existingValues, ...item.fieldValues])
);
}
}
});
allFields.value = Array.from(fieldMapCache.values());
initializeValues();
};
// 点击差异字段时添加到输入框
const addDiffFieldToInput = (field) => {
// 检查字段是否已存在
if (fieldLabelExists(field.label)) {
// 如果字段已存在,定位到该字段位置
focusFieldLabel(field.label);
return;
}
// 字段不存在,添加新字段
// 检查输入框末尾是否需要加分号
let separator = "";
if (
currentInput.value.trim() !== "" &&
!currentInput.value.trim().endsWith(";") &&
!currentInput.value.trim().endsWith("")
) {
separator = ";";
}
// 添加字段到输入框
currentInput.value = `${currentInput.value}${separator}${field.label}:`;
// 触发输入事件和搜索
setTimeout(() => {
handleInput(currentInput.value, { skipDifference: true });
triggerRealTimeSearch();
// 聚焦输入框并将光标定位到字段后的适当位置
if (searchInput.value) {
searchInput.value.focus();
const inputEl = searchInput.value.$el.querySelector("input");
if (inputEl) {
inputEl.setSelectionRange(
currentInput.value.length,
currentInput.value.length
);
}
}
}, 0);
};
const getCurrentInputPart = () => {
const parts = currentInput.value.split(/[;]/);
return parts[parts.length - 1].trim();
};
const handleKeydown = (e) => {
// 检查是否按下了分号键
if (e.key === ";" || e.key === "" || e.keyCode === 186) {
handleSemicolon(e);
}
};
// 处理分号输入
const handleSemicolon = (e) => {
// 如果是空值或最后一个字符已经是分号,则不处理
if (!currentInput.value.trim() || currentInput.value.endsWith(";")) {
e.preventDefault();
return;
}
// 延迟处理,确保分号已添加到输入框
setTimeout(() => {
// 触发输入事件以更新建议
handleInput(currentInput.value, { skipDifference: true });
// 分号输入后触发实时查询
triggerRealTimeSearch();
// 条件输入完成(有分号)时调用差异查询
fetchDifferenceRecommendations();
}, 0);
};
// 查找光标所在的条件位置
const findCursorConditionIndex = () => {
if (!searchInput.value) return -1;
const inputEl = searchInput.value.$el.querySelector("input");
if (!inputEl) return -1;
const cursorPos = inputEl.selectionStart;
const inputValue = currentInput.value;
// 修复:处理中英文分号的正则表达式
const semicolons = [];
const semicolonRegex = /[;]/g;
let match;
// 收集所有分号的位置
while ((match = semicolonRegex.exec(inputValue)) !== null) {
semicolons.push(match.index);
}
// 确定光标所在的条件索引
for (let i = 0; i < semicolons.length; i++) {
if (cursorPos <= semicolons[i]) {
return i;
}
}
// 如果光标在最后一个分号之后,返回最后一个条件索引
return semicolons.length;
};
// 处理输入框容器点击
const handleInputContainerClick = (e) => {
// 不需要处理箭头相关逻辑了
};
// 处理输入框点击事件 - 支持点击已输入值显示下拉
const handleInputClick = () => {
const index = findCursorConditionIndex();
const parts = currentInput.value.split(";");
if (index >= 0 && index < parts.length) {
const part = parts[index].trim();
if (part) {
selectedConditionIndex.value = index;
// 触发该条件的建议显示
triggerConditionSuggestions(part);
}
}
};
// 处理输入框鼠标抬起事件 - 支持选择文本后显示下拉
const handleInputMouseUp = () => {
handleInputClick();
};
// 聚焦到指定的条件标签 - 修复问题2
const focusConditionTag = (index) => {
const parts = currentInput.value.split(/[;]/);
if (index < 0 || index >= parts.length) return;
// 将光标定位到该条件
selectedConditionIndex.value = index;
const inputEl = searchInput.value?.$el.querySelector("input");
if (!inputEl) return;
// 计算该条件在输入框中的位置
let cursorPos = 0;
for (let i = 0; i < index; i++) {
cursorPos += parts[i].length + 1; // +1 是分号的长度
}
// 设置光标位置到该条件的末尾
setTimeout(() => {
inputEl.focus();
inputEl.setSelectionRange(
cursorPos + parts[index].length,
cursorPos + parts[index].length
);
// 触发该条件的建议显示
triggerConditionSuggestions(parts[index].trim());
}, 0);
};
// 触发特定条件的建议显示
const triggerConditionSuggestions = (conditionText) => {
// 当 smart 返回为空时,禁止本地兜底建议
if (disableLocalSuggestions.value) {
suggestions.value = [];
possibleFields.value = [];
showSuggestions.value = false;
return;
}
if (!conditionText) {
suggestions.value = [];
possibleFields.value = [];
return;
}
const colonIndex = conditionText.indexOf(":");
showSuggestions.value = true;
if (colonIndex > -1) {
// 已有字段和值,显示该字段的可能值
const fieldLabel = conditionText.substring(0, colonIndex).trim();
const valuePart = conditionText.substring(colonIndex + 1).trim();
const field = allFields.value.find((f) => f.label === fieldLabel);
if (field) {
// 显示该字段的所有可能值
const fieldValues = allValues.value
.filter((item) => item.fieldKey === field.key)
.map((item) => ({
type: "value",
label: item.value,
value: item.value,
fieldKey: item.fieldKey,
fieldLabel: item.fieldLabel,
weight: item.weight,
}));
// 按匹配度排序
fieldValues.sort((a, b) => {
const aMatch = a.label.toLowerCase().includes(valuePart.toLowerCase())
? 0
: 1;
const bMatch = b.label.toLowerCase().includes(valuePart.toLowerCase())
? 0
: 1;
return aMatch - bMatch || a.label.localeCompare(b.label);
});
suggestions.value = fieldValues.slice(0, 8);
possibleFields.value = []; // 已有字段时不显示可能的字段
} else {
// 字段无效,显示所有值
const allValueSuggestions = allValues.value
.filter((item) =>
String(item.value).toLowerCase().includes(conditionText.toLowerCase())
)
.map((item) => ({
type: "value",
label: item.value,
value: item.value,
fieldKey: item.fieldKey,
fieldLabel: item.fieldLabel,
weight: item.weight,
}));
suggestions.value = allValueSuggestions.slice(0, 8);
possibleFields.value = [];
}
} else {
// 只有值,没有字段,直接查找匹配的建议(避免递归调用)
const cursorIndex = findCursorConditionIndex();
const usedLabels = getUsedFieldLabels(cursorIndex);
// 查找匹配的字段和值
const fieldSuggestions = allFields.value
.filter(
(field) =>
!usedLabels.has(field.label) &&
field.label.toLowerCase().includes(conditionText.toLowerCase())
)
.map((field) => ({
type: "field",
label: field.label,
value: field.key,
weight: 3,
}));
const valueSuggestions = allValues.value
.filter((item) =>
String(item.value).toLowerCase().includes(conditionText.toLowerCase())
)
.map((item) => ({
type: "value",
label: item.value,
value: item.value,
fieldKey: item.fieldKey,
fieldLabel: item.fieldLabel,
weight: item.weight,
}));
// 检查是否有值强烈匹配某个字段
const valueFieldCounts = {};
valueSuggestions.forEach((val) => {
if (!valueFieldCounts[val.fieldKey]) {
valueFieldCounts[val.fieldKey] = {
count: 0,
label: val.fieldLabel,
field: allFields.value.find((f) => f.key === val.fieldKey),
};
}
valueFieldCounts[val.fieldKey].count += val.weight;
});
// 只有在没有从接口获取字段时才设置
if (!fieldsFromApi.value) {
possibleFields.value = Object.values(valueFieldCounts)
.filter(
(item) =>
item.count > 1 && item.field && !usedLabels.has(item.field.label)
)
.sort((a, b) => b.count - a.count)
.map((item) => item.field);
}
// 合并建议并按相关性排序
let allSuggestions = [...fieldSuggestions, ...valueSuggestions];
allSuggestions.sort((a, b) => {
if (b.weight !== a.weight) {
return b.weight - a.weight;
}
const aMatch = a.label.toLowerCase().indexOf(conditionText.toLowerCase());
const bMatch = b.label.toLowerCase().indexOf(conditionText.toLowerCase());
if (aMatch !== bMatch) {
return aMatch - bMatch;
}
return a.label.length - b.label.length;
});
// 去重并限制数量
const uniqueSuggestions = [];
const seenLabels = new Set();
allSuggestions.forEach((suggestion) => {
if (!seenLabels.has(suggestion.label)) {
seenLabels.add(suggestion.label);
uniqueSuggestions.push(suggestion);
}
});
suggestions.value = uniqueSuggestions.slice(0, 8);
}
// 无论是字段还是值,只要光标移动到某个条件,都实时调用 searchHint
// 使用当前这一段文本作为 keyword且不依赖之前的本地缓存
if (conditionText && conditionText.trim()) {
// 允许本次 smart 接口返回覆盖本地字段/值缓存
disableLocalSuggestions.value = false;
fieldsFromApi.value = false;
scheduleSearchHint(conditionText.trim());
}
};
// 根据当前输入更新建议列表(不触发远程请求)
const updateSuggestionsFromValue = (value) => {
// 当 smart 返回为空时,禁止本地兜底建议
if (disableLocalSuggestions.value) {
suggestions.value = [];
possibleFields.value = [];
showSuggestions.value = false;
return;
}
const normalizedValue = value ?? "";
// 获取当前正在输入的条件部分(分号后面的部分)
const parts = normalizedValue.split(/[;]/);
const currentPart = parts[parts.length - 1].trim();
// 检查是否在编辑已有条件
const cursorIndex = findCursorConditionIndex();
const usedLabels = getUsedFieldLabels(cursorIndex);
if (cursorIndex >= 0 && cursorIndex < parts.length - 1) {
// 正在编辑中间的条件,使用特殊处理
triggerConditionSuggestions(parts[cursorIndex].trim());
return;
}
// 始终显示建议框,除非输入为空
showSuggestions.value = normalizedValue.trim() !== "";
// 如果当前部分为空,不显示具体建议
if (!currentPart) {
suggestions.value = [];
possibleFields.value = [];
return;
}
// 检查当前是否在输入字段还是值
const isEditingField = !currentPart.includes(":");
// 查找匹配的字段和值
let fieldSuggestions = [];
let valueSuggestions = [];
if (isEditingField) {
// 正在输入字段或值的开始部分,同时查找字段和值
fieldSuggestions = allFields.value
.filter(
(field) =>
!usedLabels.has(field.label) &&
field.label.toLowerCase().includes(currentPart.toLowerCase())
)
.map((field) => ({
type: "field",
label: field.label,
value: field.key,
weight: 3, // 字段建议权重更高
}));
// 查找可能匹配的值,并检查是否有强关联的字段
const potentialValues = allValues.value
.filter((item) =>
String(item.value).toLowerCase().includes(currentPart.toLowerCase())
)
.map((item) => ({
type: "value",
label: item.value,
value: item.value,
fieldKey: item.fieldKey,
fieldLabel: item.fieldLabel,
weight: item.weight,
}));
// 检查是否有值强烈匹配某个字段
const valueFieldCounts = {};
potentialValues.forEach((val) => {
if (!valueFieldCounts[val.fieldKey]) {
valueFieldCounts[val.fieldKey] = {
count: 0,
label: val.fieldLabel,
field: allFields.value.find((f) => f.key === val.fieldKey),
};
}
valueFieldCounts[val.fieldKey].count += val.weight;
});
// 提取并排序可能的字段(只有在没有从接口获取字段时才设置)
if (!fieldsFromApi.value) {
possibleFields.value = Object.values(valueFieldCounts)
.filter(
(item) =>
item.count > 1 && item.field && !usedLabels.has(item.field.label)
) // 只显示有足够匹配度的字段且未使用
.sort((a, b) => b.count - a.count) // 按匹配度排序
.map((item) => item.field);
}
valueSuggestions = potentialValues;
} else {
// 已经有字段,只查找值
const colonIndex = currentPart.indexOf(":");
const fieldLabel = currentPart.substring(0, colonIndex).trim();
const valuePart = currentPart.substring(colonIndex + 1).trim();
// 查找对应的字段
const field = allFields.value.find((f) => f.label === fieldLabel);
if (field) {
// 只查找该字段的值
valueSuggestions = allValues.value
.filter(
(item) =>
item.fieldKey === field.key &&
String(item.value).toLowerCase().includes(valuePart.toLowerCase())
)
.map((item) => ({
type: "value",
label: item.value,
value: item.value,
fieldKey: item.fieldKey,
fieldLabel: item.fieldLabel,
weight: item.weight,
}));
} else {
// 字段无效,查找所有值
valueSuggestions = allValues.value
.filter((item) =>
String(item.value).toLowerCase().includes(currentPart.toLowerCase())
)
.map((item) => ({
type: "value",
label: item.value,
value: item.value,
fieldKey: item.fieldKey,
fieldLabel: item.fieldLabel,
weight: item.weight,
}));
}
possibleFields.value = []; // 已有字段时不显示可能的字段
}
// 合并建议并按相关性排序
let allSuggestions = [...fieldSuggestions, ...valueSuggestions];
// 根据权重和匹配程度排序
allSuggestions.sort((a, b) => {
// 权重高的排在前面
if (b.weight !== a.weight) {
return b.weight - a.weight;
}
// 更精确的匹配排在前面
const aMatch = a.label.toLowerCase().indexOf(currentPart.toLowerCase());
const bMatch = b.label.toLowerCase().indexOf(currentPart.toLowerCase());
if (aMatch !== bMatch) {
return aMatch - bMatch;
}
// 短的排在前面
return a.label.length - b.label.length;
});
// 去重并限制数量
const uniqueSuggestions = [];
const seenLabels = new Set();
allSuggestions.forEach((suggestion) => {
if (!seenLabels.has(suggestion.label)) {
seenLabels.add(suggestion.label);
uniqueSuggestions.push(suggestion);
}
});
suggestions.value = uniqueSuggestions.slice(0, 8);
// 重置激活的建议索引
activeSuggestionIndex.value = -1;
activePossibleFieldIndex.value = -1;
};
// 处理输入事件,实时生成建议并调度远程提示
const handleInput = (value, options = {}) => {
const normalizedValue = value ?? "";
// 如果是在跳过处理的情况下(避免无限循环),直接返回
if (options.skipReorder) {
lastInputValue.value = normalizedValue;
return;
}
// 检测用户是否编辑了中间的条件,如果改变了,将该条件移到最后一个位置,并清除原位置
if (lastInputValue.value && normalizedValue !== lastInputValue.value) {
const lastParts = lastInputValue.value
.split(/[;]/)
.map((p) => p.trim())
.filter((p) => p);
const currentParts = normalizedValue
.split(/[;]/)
.map((p) => p.trim())
.filter((p) => p);
const cursorIndex = findCursorConditionIndex();
// 如果正在编辑中间的条件(不是最后一个)
if (cursorIndex >= 0 && cursorIndex < currentParts.length - 1) {
const lastPart = lastParts[cursorIndex] || "";
const currentPart = currentParts[cursorIndex] || "";
// 如果该条件的内容发生了变化,将其移到最后一个位置,并清除原位置
if (lastPart !== currentPart) {
// 获取被编辑的条件
const editedPart = currentPart.trim();
// 保存原条件的索引,用于清理精准查询条件
const oldConditionIndex = cursorIndex;
// 移除原位置的条件,并将编辑后的条件添加到最后一个位置
const newParts = currentParts.filter(
(_, index) => index !== cursorIndex
);
// 如果编辑后的条件不为空,才添加到末尾
if (editedPart) {
newParts.push(editedPart);
}
const newValue = newParts.join(";");
// 标记条件刚刚被移动,避免使用本地缓存
isConditionMoved.value = true;
// 更新上一次的输入值(避免再次触发重排序)
lastInputValue.value = newValue;
// 更新输入框的值(使用 skipReorder 避免无限循环)
currentInput.value = newValue;
// 重新解析条件,确保使用最新的条件顺序
parsedConditions.value = parseConditions(newValue);
// 清理精准查询条件:如果原条件有精准查询标记,需要移除或更新
if (preciseConditions.value.length > 0) {
// 找到引用原索引的精准查询条件
const preciseIndex = preciseConditions.value.findIndex(
(cond) => cond.originalIndex === oldConditionIndex
);
if (preciseIndex !== -1) {
// 如果编辑后的条件不为空,尝试更新精准查询条件的索引
if (editedPart) {
// 重新解析编辑后的条件,看是否能匹配到精准查询条件
const editedCondition = parseConditions(editedPart)[0];
if (editedCondition) {
// 查找新条件在新列表中的索引
const newIndex = parsedConditions.value.findIndex(
(c) =>
c.field === editedCondition.field &&
c.value === editedCondition.value &&
c.fieldLabel === editedCondition.fieldLabel
);
if (newIndex !== -1) {
// 更新精准查询条件的索引
preciseConditions.value[preciseIndex] = {
...preciseConditions.value[preciseIndex],
...editedCondition,
originalIndex: newIndex,
};
} else {
// 如果找不到匹配的条件,移除精准查询条件
preciseConditions.value.splice(preciseIndex, 1);
}
} else {
// 如果解析失败,移除精准查询条件
preciseConditions.value.splice(preciseIndex, 1);
}
} else {
// 如果编辑后的条件为空,移除精准查询条件
preciseConditions.value.splice(preciseIndex, 1);
}
}
// 同步其他精准查询条件的索引
syncPreciseConditionsIndex();
}
// 清空建议,不从本地缓存获取
suggestions.value = [];
possibleFields.value = [];
fieldsFromApi.value = false;
// 重新定位光标到最后一个条件
nextTick(() => {
if (searchInput.value) {
const inputEl = searchInput.value.$el.querySelector("input");
if (inputEl) {
// 计算光标应该在的位置(最后一个条件的末尾)
const cursorPos = newValue.length;
inputEl.setSelectionRange(cursorPos, cursorPos);
}
}
// 重置标志,允许下次使用缓存
setTimeout(() => {
isConditionMoved.value = false;
}, 100);
});
// 触发实时搜索,使用最新的条件顺序
if (newValue.trim()) {
triggerRealTimeSearch();
}
// 触发搜索提示(只针对最后一个条件)
const finalPart = editedPart;
if (finalPart && !options.skipFetch) {
scheduleSearchHint(finalPart);
}
return;
}
}
}
// 更新上一次的输入值
lastInputValue.value = normalizedValue;
// 如果输入为空,清空所有条件和相关状态
if (!normalizedValue.trim()) {
parsedConditions.value = [];
preciseConditions.value = [];
disableLocalSuggestions.value = false; // 清空时恢复允许使用接口返回
isConditionMoved.value = false; // 重置条件移动标志
fieldsFromApi.value = false;
possibleFields.value = [];
suggestions.value = [];
keyDiffFields.value = [];
showKeyDiffHint.value = false;
if (differenceTimer.value) {
clearTimeout(differenceTimer.value);
differenceTimer.value = null;
}
return;
}
// 同步更新 parsedConditions确保 tag 显示与输入框内容一致
const newConditions = parseConditions(normalizedValue);
parsedConditions.value = newConditions;
// 清理不再存在的精准查询条件
if (preciseConditions.value.length > 0) {
preciseConditions.value = preciseConditions.value.filter((preciseCond) => {
// 检查精准查询条件是否在新的条件列表中存在
const exists = newConditions.some(
(cond) =>
cond.field === preciseCond.field &&
cond.value === preciseCond.value &&
cond.fieldLabel === preciseCond.fieldLabel
);
return exists;
});
}
// 同步精准查询条件的索引
syncPreciseConditionsIndex();
updateSuggestionsFromValue(normalizedValue);
if (options.skipFetch) {
return;
}
const parts = normalizedValue.split(/[;]/);
const currentPart = parts[parts.length - 1]?.trim();
if (currentPart) {
scheduleSearchHint(currentPart);
}
// 移除实时调用 differenceWords只在条件完成有分号或点击查询时调用
};
const fetchSearchHints = async (keyword) => {
if (!keyword) return;
try {
// 始终基于最新的 currentInput.value 解析条件,确保使用最新的条件顺序
parsedConditions.value = parseConditions(currentInput.value);
// 检测当前正在编辑的条件索引
const cursorIndex = findCursorConditionIndex();
const parts = currentInput.value.split(/[;]/);
const activeIndex =
cursorIndex >= 0 && cursorIndex < parts.length
? cursorIndex
: parts.length - 1;
// 如果正在编辑中间的条件,需要重新排序:将正在编辑的条件移到最后一个位置
let conditionsToProcess = [...parsedConditions.value];
let reorderedInputWord = currentInput.value.trim();
if (activeIndex >= 0 && activeIndex < conditionsToProcess.length - 1) {
// 正在编辑中间的条件,需要重新排序
const [editedCondition] = conditionsToProcess.splice(activeIndex, 1);
conditionsToProcess.push(editedCondition);
// 重新构建 inputWord将被编辑的条件移到最后一个位置
const reorderedParts = parts.map((p) => p.trim()).filter((p) => p);
if (activeIndex < reorderedParts.length) {
const [editedPart] = reorderedParts.splice(activeIndex, 1);
reorderedParts.push(editedPart);
reorderedInputWord = reorderedParts.join(";");
}
}
// 构建 fieldConditions使用重新排序后的条件顺序
const fieldConditions = conditionsToProcess
.filter((condition) => {
// 保留有值的条件,或者有字段名但没有值的条件(如 "车型:"
return condition.value || (condition.fieldLabel && condition.field);
})
.map((condition) => {
// 1字段 + 值:结构化查询
if (condition.valid && condition.field && condition.value) {
return {
fieldName: condition.field,
fieldValue: condition.value,
keyword: "",
queryType: "FUZZY",
};
}
// 2只有字段如 "车型:"fieldName 为字段 keyfieldValue 为空
if (condition.valid && condition.field && !condition.value) {
return {
fieldName: condition.field,
fieldValue: "",
keyword: "",
queryType: "FUZZY",
};
}
// 3只有值没有字段需要区分是从下拉框选择的还是纯手输的
// 如果值在 allValues 中存在,说明可能是从下拉框选择的,放在 fieldValue 里
if (!condition.field && condition.value && !condition.fieldLabel) {
const isValueFromSuggestion = allValues.value.some(
(item) => String(item.value) === String(condition.value)
);
if (isValueFromSuggestion) {
// 从下拉框选择的值,放在 fieldValue 里
return {
fieldName: "",
fieldValue: condition.value,
keyword: "",
queryType: "FUZZY",
};
}
// 否则继续走方案4放在 keyword 里
}
// 4其它情况例如纯手输、无法识别字段作为关键字查询
// 纯手输的情况:没有 fieldLabel 和 field应该放在 keyword 里
const keyword = condition.fieldLabel
? `${condition.fieldLabel}:${condition.value}`.replace(/:$/, "")
: condition.value;
return {
fieldName: "",
fieldValue: "",
keyword,
queryType: "FUZZY",
};
});
const payload = {
fieldConditions,
inputWord: reorderedInputWord,
page: 0,
size: pageSize.value,
};
const res = await searchHint(payload);
const pageData = res?.data ?? res ?? {};
const hintList = Array.isArray(pageData?.content)
? pageData.content
: Array.isArray(pageData)
? pageData
: Array.isArray(res?.data)
? res.data
: Array.isArray(res)
? res
: [];
// 若接口无数据:严格按照本次 smart 接口结果处理,不做任何兜底或本地缓存回退
if (!hintList.length) {
disableLocalSuggestions.value = true; // smart 无数据时禁止本地兜底
suggestions.value = [];
possibleFields.value = [];
fieldsFromApi.value = false;
showSuggestions.value = false;
return;
}
// smart 有返回数据,则允许后续基于本次返回的字段做联想
disableLocalSuggestions.value = false;
// 处理返回的数据:合并字段元数据(仅来自接口)
mergeFieldMetadata(hintList);
// 从返回的数据中提取字段信息用于"可能属于的字段"下拉框
// 优先根据光标位置确定当前正在编辑的条件,再决定使用哪一段文本
// 注意parts、cursorIndex、activeIndex 已在函数开始处声明,这里直接使用
const currentPart = parts[activeIndex]?.trim() || "";
const isEditingExistingField =
cursorIndex >= 0 && cursorIndex < parts.length - 1;
if (
!isEditingExistingField &&
!currentPart.includes(":") &&
hintList.length > 0
) {
// 提取所有字段作为可能的字段选项(所有 fieldName
const fieldsFromHint = hintList
.filter((item) => item.fieldName && item.fieldKey)
.map((item) => ({
key: item.fieldKey,
label: item.fieldName,
}));
// 去重,只保留未使用的字段
const usedLabels = getUsedFieldLabels(cursorIndex);
const uniqueFields = fieldsFromHint.filter(
(field) => !usedLabels.has(field.label)
);
// 如果当前输入有值,过滤出包含该值的字段
if (currentPart) {
const valueQuery = currentPart.includes(":")
? currentPart.split(":").slice(1).join(":").trim()
: currentPart;
const valueQueryLower = valueQuery.toLowerCase();
const matchingFields = uniqueFields.filter((field) => {
const fieldItem = hintList.find(
(item) => item.fieldKey === field.key
);
if (!fieldItem || !Array.isArray(fieldItem.fieldValues)) return false;
return fieldItem.fieldValues.some((value) =>
String(value).toLowerCase().includes(valueQueryLower)
);
});
// 如果有关联的字段,优先显示这些字段
possibleFields.value =
matchingFields.length > 0 ? matchingFields : uniqueFields;
} else {
possibleFields.value = uniqueFields;
}
// 标记字段来自接口
fieldsFromApi.value = true;
} else {
possibleFields.value = [];
fieldsFromApi.value = false;
}
// 基于接口返回的 fieldValues 直接构建值建议,仅展示接口值、不做数量限制
const valueQuery = currentPart.includes(":")
? currentPart.split(":").slice(1).join(":").trim()
: currentPart;
const valueQueryLower = (valueQuery || "").toLowerCase();
// 如果当前条件已经选定字段(例如 "车型:"),则只展示该字段对应的值
let activeFieldKey = "";
if (currentPart.includes(":")) {
const fieldLabel = currentPart.split(":")[0].trim();
// 先从本地字段缓存中找
const localField = allFields.value.find(
(f) => f.label === fieldLabel || f.key === fieldLabel
);
if (localField) {
activeFieldKey = localField.key;
} else {
// 再从接口返回的字段中找
const apiField = hintList.find(
(item) =>
item.fieldName === fieldLabel ||
item.fieldKey === fieldLabel ||
item.label === fieldLabel
);
if (apiField) {
activeFieldKey = apiField.fieldKey || apiField.key || "";
}
}
}
const matchedValueSuggestions = [];
const fallbackValueSuggestions = [];
hintList.forEach((item) => {
if (!Array.isArray(item.fieldValues)) return;
const fieldKey = item.fieldKey || item.key || "";
// 若已经识别出当前正在编辑的字段,则只保留该字段的值
if (activeFieldKey && fieldKey !== activeFieldKey) return;
const fieldLabel = item.fieldName || item.label || item.fieldLabel || "";
item.fieldValues.forEach((val) => {
const strVal =
val === null || val === undefined ? "" : String(val).trim();
if (!strVal) return;
const matched =
!valueQueryLower || strVal.toLowerCase().includes(valueQueryLower);
const target = matched
? matchedValueSuggestions
: fallbackValueSuggestions;
target.push({
type: "value",
label: strVal,
value: strVal,
fieldKey,
fieldLabel,
weight: matched ? 5 : 3, // 匹配结果权重大,不匹配时作为兜底展示
});
});
});
// 如果没有匹配到值,则使用接口返回的全部值做兜底展示
const apiValueSuggestions =
matchedValueSuggestions.length > 0
? matchedValueSuggestions
: fallbackValueSuggestions;
// 只用接口返回的值作为下拉建议,并去重
const seen = new Set();
const uniqueSuggestions = [];
apiValueSuggestions.forEach((s) => {
const key = s.label; // 按展示文本去重
if (seen.has(key)) return;
seen.add(key);
uniqueSuggestions.push(s);
});
suggestions.value = uniqueSuggestions;
// 如果有字段或有值建议,只要其中之一存在就展示下拉
showSuggestions.value =
uniqueSuggestions.length > 0 || possibleFields.value.length > 0;
} catch (error) {
console.error("searchHint error:", error);
}
};
const scheduleSearchHint = (keyword) => {
if (hintTimer.value) {
clearTimeout(hintTimer.value);
}
if (!keyword) return;
hintTimer.value = setTimeout(() => {
fetchSearchHints(keyword);
}, 300);
};
const scheduleDifferenceFetch = () => {
if (differenceTimer.value) {
clearTimeout(differenceTimer.value);
}
if (!currentInput.value.trim()) return;
differenceTimer.value = setTimeout(() => {
fetchDifferenceRecommendations();
}, 500);
};
// 选择可能的字段 - 修复问题4
const selectPossibleField = (field) => {
// 始终在当前条件上操作,不修改其他条件
const parts = currentInput.value.split(/[;]/);
let targetIndex = parts.length - 1;
if (fieldLabelExists(field.label, targetIndex)) {
ElMessage.warning(`${field.label} 已选择,请直接修改对应的值`);
focusFieldLabel(field.label);
return;
}
// 如果最后一个条件为空,使用前一个
if (parts.length > 0 && parts[targetIndex].trim() === "") {
targetIndex = Math.max(0, parts.length - 2);
}
// 获取当前部分的值
let targetPart = parts[targetIndex] || "";
// 检查是否已有字段
if (targetPart.includes(":")) {
// 已有字段,添加新条件
const newPart = `${field.label}:`;
parts.push(newPart);
targetIndex = parts.length - 1;
} else {
// 没有字段,在当前值前添加字段名和冒号
// 如果当前值本身已经等于字段名,避免出现“字段名:字段名”的情况
const trimmedPart = (targetPart || "").trim();
if (!trimmedPart || trimmedPart === field.label) {
parts[targetIndex] = `${field.label}:`;
} else {
parts[targetIndex] = `${field.label}:${targetPart}`;
}
}
// 重新拼接条件,清理多余分号
currentInput.value = parts.join(";").replace(/;;+/g, ";").trim();
// 保持建议框显示,允许继续编辑
setTimeout(() => {
handleInput(currentInput.value, { skipDifference: true });
// 选择字段后触发实时查询
triggerRealTimeSearch();
}, 0);
// 聚焦输入框并将光标定位到字段后的适当位置
searchInput.value.focus();
const inputEl = searchInput.value.$el.querySelector("input");
if (inputEl) {
// 计算光标应该在的位置(字段名+冒号之后)
let cursorPos = 0;
for (let i = 0; i < targetIndex; i++) {
cursorPos += parts[i].length + 1; // +1 是分号的长度
}
cursorPos += field.label.length + 1; // 字段名长度 + 冒号
inputEl.setSelectionRange(cursorPos, cursorPos);
}
};
// 导航可能的字段
const navigatePossibleFields = (direction) => {
if (!showSuggestions.value || possibleFields.value.length === 0) return;
if (direction === "down") {
activePossibleFieldIndex.value =
(activePossibleFieldIndex.value + 1) % possibleFields.value.length;
} else {
activePossibleFieldIndex.value =
(activePossibleFieldIndex.value - 1 + possibleFields.value.length) %
possibleFields.value.length;
}
// 滚动到激活的字段
const fieldElements = document.querySelectorAll(".possible-field-item");
if (fieldElements[activePossibleFieldIndex.value]) {
fieldElements[activePossibleFieldIndex.value].scrollIntoView({
block: "nearest",
});
}
};
// 提取运算符
const getOperator = (conditionValue) => {
const operators = [">=", "<=", "!=", ">", "<", "="];
return operators.find((op) => conditionValue.startsWith(op));
};
// 选择建议项
// 修改selectSuggestion函数在选择值后自动添加分号
const selectSuggestion = (item) => {
// 使用光标位置确定当前正在编辑的条件索引
const targetIndex = findCursorConditionIndex();
if (targetIndex === -1) return;
const parts = currentInput.value.split(/[;]/);
let targetPart = parts[targetIndex] || "";
if (item.type === "field") {
if (fieldLabelExists(item.label, targetIndex)) {
ElMessage.warning(`${item.label} 已选择,请直接修改对应的值`);
focusFieldLabel(item.label);
return;
}
// 字段建议:直接替换为「字段:」格式(不自动加分号,等待输入值)
parts[targetIndex] = `${item.label}:`;
} else {
// 值建议处理:选择值后自动添加分号
const colonIndex = targetPart.indexOf(":");
const operator = getOperator(targetPart.split(":").pop() || "");
if (colonIndex > -1) {
// 已有字段部分
const fieldPart = targetPart.substring(0, colonIndex + 1);
if (operator) {
// 带有运算符的情况,保留运算符结构
const operatorIndex = targetPart.indexOf(operator, colonIndex);
const prefix = targetPart.substring(0, operatorIndex + operator.length);
parts[targetIndex] = `${prefix}${item.label}`;
} else {
// 普通值替换
parts[targetIndex] = `${fieldPart}${item.label}`;
}
} else {
// 无字段时,如果值建议项包含字段信息,自动补充字段名
if (item.fieldLabel) {
// 自动补充字段名和冒号
parts[targetIndex] = `${item.fieldLabel}:${item.label}`;
} else {
// 如果没有字段信息,直接使用值
parts[targetIndex] = item.label;
}
}
// 核心:值选择后自动添加分号(避免重复添加)
const lastChar = parts[targetIndex].slice(-1);
if (![",", ";", ""].includes(lastChar)) {
parts[targetIndex] += ";"; // 使用英文分号统一分隔
}
}
// 重新拼接条件,清理多余分号
currentInput.value = parts.join(";").replace(/;;+/g, ";").trim();
// 检查是否添加了分号(选择值建议时会自动添加分号)
const hasAddedSemicolon =
item.type === "value" && parts[targetIndex].trim().endsWith(";");
// 保持建议框显示,允许继续编辑
setTimeout(() => {
handleInput(currentInput.value, { skipDifference: true });
// 选择建议项后触发实时查询
triggerRealTimeSearch();
// 如果选择值建议并添加了分号,调用差异查询
if (hasAddedSemicolon) {
fetchDifferenceRecommendations();
}
}, 0);
// 聚焦输入框并将光标定位到当前条件末尾(分号后)
searchInput.value.focus();
const inputEl = searchInput.value.$el.querySelector("input");
if (inputEl) {
// 计算光标应该在的位置(分号后面)
let cursorPos = 0;
for (let i = 0; i < targetIndex; i++) {
cursorPos += parts[i].length + 1; // +1 是分号的长度
}
cursorPos += parts[targetIndex].length; // 光标定位到分号后
inputEl.setSelectionRange(cursorPos, cursorPos);
}
};
// 导航建议项(上下箭头)
const navigateSuggestions = (direction) => {
if (!showSuggestions.value || suggestions.value.length === 0) return;
const totalPossibleFields = possibleFields.value.length;
const totalItems =
suggestions.value.length +
(totalPossibleFields > 0 ? 1 + totalPossibleFields : 0);
if (direction === "down") {
activeSuggestionIndex.value =
(activeSuggestionIndex.value + 1) % totalItems;
} else {
activeSuggestionIndex.value =
(activeSuggestionIndex.value - 1 + totalItems) % totalItems;
}
// 如果有可选字段,处理导航逻辑
if (totalPossibleFields > 0) {
// 导航进入字段区域
if (activeSuggestionIndex.value <= totalPossibleFields) {
activePossibleFieldIndex.value = activeSuggestionIndex.value - 1;
activeSuggestionIndex.value = -1;
return;
} else {
activePossibleFieldIndex.value = -1;
activeSuggestionIndex.value -= 1 + totalPossibleFields;
}
}
// 滚动到激活的建议项
const suggestionElements = document.querySelectorAll(".suggestion-item");
if (suggestionElements[activeSuggestionIndex.value]) {
suggestionElements[activeSuggestionIndex.value].scrollIntoView({
block: "nearest",
});
}
};
// 聚焦第一个建议项
const focusFirstSuggestion = () => {
if (showSuggestions.value) {
if (possibleFields.value.length > 0) {
// 先聚焦到可能的字段
activePossibleFieldIndex.value = 0;
const firstField = document.querySelector(".possible-field-item");
if (firstField) firstField.focus();
} else if (suggestions.value.length > 0) {
// 再聚焦到建议项
activeSuggestionIndex.value = 0;
const firstElement = document.querySelector(".suggestion-item");
if (firstElement) firstElement.focus();
}
}
};
// 隐藏建议
const hideSuggestions = () => {
showSuggestions.value = false;
possibleFields.value = [];
};
// 解析查询条件
const parseConditions = (input) => {
const conditions = [];
const parts = input
.split(/[;]/) // 同时匹配中英文分号
.map((part) => part.trim())
.filter((part) => part);
parts.forEach((part) => {
// 检查是否包含字段名(冒号分隔)
const colonIndex = part.indexOf(":");
if (colonIndex > 0) {
const fieldLabel = part.substring(0, colonIndex).trim();
const valuePart = part.substring(colonIndex + 1).trim();
// 查找对应的字段key
const field = allFields.value.find((f) => f.label === fieldLabel);
conditions.push({
fieldLabel,
field: field ? field.key : null,
value: valuePart,
valid: !!field,
});
} else {
// 没有字段名,默认为值查询
conditions.push({
fieldLabel: null,
field: null,
value: part,
valid: true,
});
}
});
return conditions;
};
const getPreciseIndexSet = () => {
const preciseIndexSet = new Set();
preciseConditions.value.forEach((condition) => {
if (typeof condition.originalIndex === "number") {
preciseIndexSet.add(condition.originalIndex);
} else {
const idx = parsedConditions.value.findIndex(
(item) =>
item.field === condition.field && item.value === condition.value
);
if (idx !== -1) {
preciseIndexSet.add(idx);
}
}
});
return preciseIndexSet;
};
// 同步精准查询条件在 parsedConditions 中的索引,使其在修改条件后仍然生效
const syncPreciseConditionsIndex = () => {
if (!preciseConditions.value.length) return;
preciseConditions.value = preciseConditions.value.map((cond) => {
const idx = parsedConditions.value.findIndex(
(item) => item.field === cond.field && item.value === cond.value
);
return {
...cond,
originalIndex: idx !== -1 ? idx : cond.originalIndex,
};
});
};
const buildFieldConditionsPayload = () => {
const preciseIndexSet = getPreciseIndexSet();
// 始终基于最新的 currentInput.value 解析条件,确保使用最新的条件顺序
const latestConditions = parseConditions(currentInput.value);
return latestConditions
.filter((condition) => {
// 保留有值的条件,或者有字段名但没有值的条件(如 "车型:"
return condition.value || (condition.fieldLabel && condition.field);
})
.map((condition, index) => {
// 计算原始索引(用于精准查询)
// 精准查询条件是基于字段和值匹配的,所以需要找到对应的原始索引
const originalIndex = parsedConditions.value.findIndex(
(c) =>
c.fieldLabel === condition.fieldLabel &&
c.value === condition.value &&
c.field === condition.field
);
// 如果找不到原始索引,使用当前索引
const finalIndex = originalIndex !== -1 ? originalIndex : index;
const queryType = preciseIndexSet.has(finalIndex) ? "EXACT" : "FUZZY";
// 1字段 + 值:结构化查询
if (condition.valid && condition.field && condition.value) {
return {
fieldName: condition.field,
fieldValue: condition.value,
keyword: "",
queryType,
};
}
// 2只有字段如 "车型:"fieldName 为字段 keyfieldValue 为空
if (condition.valid && condition.field && !condition.value) {
return {
fieldName: condition.field,
fieldValue: "",
keyword: "",
queryType,
};
}
// 3只有值没有字段需要区分是从下拉框选择的还是纯手输的
// 如果值在 allValues 中存在,说明可能是从下拉框选择的,放在 fieldValue 里
if (!condition.field && condition.value && !condition.fieldLabel) {
const isValueFromSuggestion = allValues.value.some(
(item) => String(item.value) === String(condition.value)
);
if (isValueFromSuggestion) {
// 从下拉框选择的值,放在 fieldValue 里
return {
fieldName: "",
fieldValue: condition.value,
keyword: "",
queryType,
};
}
// 否则继续走方案4放在 keyword 里
}
// 4其它情况例如纯手输、无法识别字段作为关键字查询
const keyword = condition.fieldLabel
? `${condition.fieldLabel}:${condition.value}`.replace(/:$/, "")
: condition.value;
return {
fieldName: "",
fieldValue: "",
keyword,
queryType,
};
});
};
// 为 searchHint 构建 fieldConditions
const buildFieldConditionsForHint = () => {
// 始终基于最新的 currentInput.value 解析条件,确保使用最新的条件顺序
const latestConditions = parseConditions(currentInput.value);
return latestConditions
.filter((condition) => {
// 保留有值的条件,或者有字段名但没有值的条件(如 "车型:"
return condition.value || (condition.fieldLabel && condition.field);
})
.map((condition) => {
// 如果明确选择了字段(有 field 且 valid使用 fieldName 和 fieldValue
if (condition.field && condition.valid) {
return {
fieldName: condition.field, // 使用 fieldKey
fieldValue: condition.value || "", // 对应的 value可能为空
keyword: "",
queryType: "FUZZY",
};
}
// 如果有字段名但没有值(如 "车型:"),使用字段名作为 keyword
if (condition.fieldLabel && !condition.value) {
return {
fieldName: "",
fieldValue: "",
keyword: condition.fieldLabel,
queryType: "FUZZY",
};
}
// 不确定输入的值是 label 还是 value全部放 keyword
return {
fieldName: "",
fieldValue: "",
keyword: condition.value,
queryType: "FUZZY",
};
});
};
const executeSearch = async ({ page = currentPage.value } = {}) => {
if (pendingSearchTimer.value) {
clearTimeout(pendingSearchTimer.value);
pendingSearchTimer.value = null;
}
const payload = {
fieldConditions: buildFieldConditionsPayload(),
inputWord: currentInput.value.trim(),
page: Math.max(page - 1, 0),
size: pageSize.value,
};
try {
const res = await searchProducts(payload);
const pageData = res?.data ?? res ?? {};
const list = pageData?.content ?? [];
const mappedList = list.map((item) => {
const key = getItemKey(item);
const isSelected = key ? selectedItems.value.has(key) : false;
return {
...item,
selected: isSelected,
};
});
const updatedSelectedMap = new Map(selectedItems.value);
mappedList.forEach((item) => {
const key = getItemKey(item);
if (key && updatedSelectedMap.has(key)) {
updatedSelectedMap.set(key, { ...item, selected: true });
}
});
selectedItems.value = updatedSelectedMap;
filteredData.value = mappedList;
totalItems.value = pageData?.totalElements ?? list.length;
const serverPage = pageData?.number ?? Math.max(page - 1, 0);
currentPage.value = serverPage + 1;
pageSize.value = pageData?.size ?? pageSize.value;
showResults.value = true;
} catch (error) {
console.error("search error:", error);
}
};
const scheduleSearchExecution = (page = 1) => {
if (pendingSearchTimer.value) {
clearTimeout(pendingSearchTimer.value);
}
pendingSearchTimer.value = setTimeout(() => {
executeSearch({ page });
}, 500);
};
const fetchDifferenceRecommendations = async () => {
if (!currentInput.value.trim()) {
keyDiffFields.value = [];
showKeyDiffHint.value = false;
return;
}
try {
const res = await differenceWords({
fieldConditions: buildFieldConditionsPayload(),
inputWord: currentInput.value.trim(),
});
const diffList = Array.isArray(res?.data)
? res.data
: Array.isArray(res)
? res
: [];
mergeFieldMetadata(diffList);
keyDiffFields.value = diffList.slice(0, 5).map((item) => ({
label: item.fieldName || "",
key: item.fieldKey,
}));
showKeyDiffHint.value =
keyDiffFields.value.length > 0 && currentInput.value.trim() !== "";
} catch (error) {
console.error("differenceWords error:", error);
}
};
const fetchCompareTable = async () => {
const idList = selectedCompareList.value
.map((item) => item.partNumber)
.filter(Boolean);
const ids = idList.join(",");
if (!ids) {
compareTableData.value = [];
parameterDifferences.value = {};
return;
}
try {
const res = await compare({ ids });
const fieldList = Array.isArray(res?.data)
? res.data
: Array.isArray(res)
? res
: [];
const responseKeys = fieldList
.map((item) => item.fieldKey || item.key || item.param)
.filter(Boolean);
diffFieldKeys.value = new Set(responseKeys);
const fieldEntries = getCompareFieldEntries();
const rows = fieldEntries.map((entry) => {
const row = { param: entry.label };
sortedCompareList.value.forEach((product) => {
const value = product[entry.key];
row[product.partNumber] =
value !== undefined && value !== null && value !== "" ? value : "-";
});
return row;
});
compareTableData.value = rows;
updateParameterDifferenceFlags(fieldEntries);
} catch (error) {
console.error("compare error:", error);
compareTableData.value = [];
parameterDifferences.value = {};
}
};
// 添加精准查询条件
const addPreciseCondition = (index) => {
const condition = parsedConditions.value[index];
// 检查是否已经添加到精准查询条件中
const exists = preciseConditions.value.some(
(cond) => cond.field === condition.field && cond.value === condition.value
);
if (!exists) {
preciseConditions.value.push({ ...condition, originalIndex: index });
executeSearch({ page: 1 });
// 添加精准查询条件后调用差异查询
fetchDifferenceRecommendations();
}
};
// 移除精准查询条件
const removePreciseCondition = (index) => {
preciseConditions.value.splice(index, 1);
executeSearch({ page: 1 });
};
// 处理查询
const handleSearch = async () => {
// 解析条件
parsedConditions.value = parseConditions(currentInput.value);
// 同步精准查询条件在新解析结果中的索引,不清空精准条件
syncPreciseConditionsIndex();
selectedItems.value = new Map();
// 执行查询与关键差异词推荐
await executeSearch({ page: 1 });
await fetchDifferenceRecommendations();
// 隐藏建议
showSuggestions.value = false;
possibleFields.value = [];
};
// 触发实时查询
const triggerRealTimeSearch = () => {
// 只有当有有效条件时才触发实时查询
if (currentInput.value.trim()) {
parsedConditions.value = parseConditions(currentInput.value);
// 输入实时变化时,同步精准条件索引
syncPreciseConditionsIndex();
scheduleSearchExecution(1);
// 移除实时调用差异查询,只在条件完成或点击查询时调用
}
};
// 移除单个条件
const removeCondition = (index) => {
const parts = currentInput.value.split(/[;]/);
parts.splice(index, 1);
currentInput.value = parts
.join(";")
.replace(/[;]+/g, ";")
.trim();
parsedConditions.value = parseConditions(currentInput.value);
// 检查是否有精准查询条件引用了这个索引,如果有则一并删除
const preciseIndex = preciseConditions.value.findIndex(
(cond) => cond.originalIndex === index
);
if (preciseIndex !== -1) {
preciseConditions.value.splice(preciseIndex, 1);
}
// 重新查询
executeSearch({ page: 1 });
// 移除条件后不调用差异查询,只在条件完成或点击查询时调用
// 重置选中的条件索引
if (selectedConditionIndex.value === index) {
selectedConditionIndex.value = -1;
}
};
// 清除所有查询条件和结果
const clearAll = () => {
currentInput.value = "";
lastInputValue.value = ""; // 重置上一次的输入值
isConditionMoved.value = false; // 重置条件移动标志
parsedConditions.value = [];
preciseConditions.value = [];
filteredData.value = [];
totalItems.value = 0;
currentPage.value = 1;
showResults.value = false;
showSuggestions.value = false;
possibleFields.value = [];
fieldsFromApi.value = false; // 重置接口字段标记
selectedConditionIndex.value = -1;
// 重置关键差异提示
keyDiffFields.value = [];
showKeyDiffHint.value = false;
selectedItems.value = new Map();
if (differenceTimer.value) {
clearTimeout(differenceTimer.value);
differenceTimer.value = null;
}
if (searchInput.value) {
searchInput.value.focus();
}
handleSearch();
};
// 分页相关方法
const handleSizeChange = (val) => {
pageSize.value = val;
currentPage.value = 1; // 重置为第一页
executeSearch({ page: 1 });
};
const handleCurrentChange = (val) => {
currentPage.value = val;
executeSearch({ page: val });
// 滚动到顶部
window.scrollTo({ top: 0, behavior: "smooth" });
};
// 生命周期钩子
onMounted(() => {
initializeValues();
handleSearch();
updateInputWidth();
window.addEventListener("resize", updateInputWidth);
// 自动聚焦输入框
setTimeout(() => {
if (searchInput.value) {
searchInput.value.focus();
}
}, 100);
// 点击页面其他地方隐藏建议
document.addEventListener("click", (e) => {
if (
!e.target.closest(".search-box") &&
!e.target.closest(".suggestions-dropdown")
) {
showSuggestions.value = false;
}
});
});
// 监听输入框宽度变化
const updateInputWidth = () => {
if (searchInput.value) {
inputWidth.value = searchInput.value.$el.clientWidth;
}
};
// 监听输入框宽度变化
watch(
() => currentInput.value,
() => {
// 延迟更新确保DOM已更新
setTimeout(() => {
updateInputWidth();
}, 0);
}
);
// 切换选择状态
function toggleSelect(item) {
const key = getItemKey(item);
if (!key) return;
const nextMap = new Map(selectedItems.value);
const isSelected = nextMap.has(key);
if (isSelected) {
nextMap.delete(key);
item.selected = false;
} else {
const snapshot = { ...item, selected: true };
nextMap.set(key, snapshot);
item.selected = true;
}
selectedItems.value = nextMap;
}
// 打开对比弹窗
const onCompare = async () => {
if (selectedCompareList.value.length === 0) {
ElMessage.warning("请先选择要对比的卡片");
return;
}
await fetchCompareTable();
compareDialogVisible.value = true;
};
// 确定按钮点击事件
function handleConfirm() {
if (selectedCount.value === 1) {
confirmDialogVisible.value = true;
}
}
</script>
<style scoped lang="scss">
// 原有样式保持不变
.step2-container {
background: #fff;
padding: 24px;
border-radius: 8px;
}
.main-row {
display: flex;
align-items: flex-start;
}
.right-content {
flex: 1;
}
.step {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.step-left {
flex: 1;
margin-right: 10px;
}
.step-title {
font-size: 18px;
margin: 15px 0;
color: #333;
font-weight: 500;
&.active {
color: #2156f3;
}
.reference-model {
font-size: 14px;
color: #666;
margin-top: 8px;
padding-left: 20px;
position: relative;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
background: #2156f3;
border-radius: 50%;
}
}
}
.image-box.right-align {
display: flex;
justify-content: flex-end;
align-items: flex-start;
margin-bottom: 16px;
}
.table-title {
font-weight: bold;
margin: 16px 0 8px 0;
color: #333;
}
.tree-table-row {
display: flex;
flex-direction: column;
border: 1px solid #bbb;
border-radius: 4px;
padding: 12px;
background: #fff;
}
.header-title {
flex: 1;
padding: 10px;
border-radius: 4px;
}
.header-code,
.header-name {
font-size: 14px;
color: #333;
margin-bottom: 5px;
}
.tab-container {
width: 100%;
}
.tab-header {
background: #f5f7fa;
border-radius: 4px;
padding: 16px;
margin-bottom: 20px;
}
.tab-header-item {
display: flex;
gap: 20px;
align-items: center;
}
.tab-header-image {
width: 200px;
height: 150px;
flex-shrink: 0;
}
.tab-header-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.tab-header-code,
.tab-header-name {
font-size: 14px;
color: #333;
line-height: 1.5;
}
.part-img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 4px;
border: 1px solid #eee;
}
.model-form {
max-width: 900px;
margin-top: 16px;
}
.btn-row {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 20px;
}
.main-img {
width: 580px;
height: 260px;
object-fit: contain;
border-radius: 8px;
border: 1px solid #eee;
}
.custom-tab-label {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 0;
}
.tab-label-image {
width: 80px;
height: 60px;
margin-bottom: 8px;
}
.tab-img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 4px;
border: 1px solid #eee;
}
.tab-label-info {
text-align: center;
}
.tab-label-code {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.tab-label-name {
font-size: 14px;
color: #333;
font-weight: 500;
}
:deep(.el-tabs__item) {
height: auto !important;
padding: 0 20px !important;
}
:deep(.el-tabs__nav) {
display: flex;
gap: 20px;
}
:deep(.el-tabs__item.is-active) {
.tab-label-code,
.tab-label-name {
color: var(--el-color-primary);
}
}
.tree-node {
transition: background 0.2s;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 4px;
display: flex;
align-items: center;
justify-content: space-between;
}
.tree-node.active {
color: #2156f3;
}
.tree-node:hover {
color: #2156f3;
}
.tab-content-flex {
display: flex;
min-height: 300px;
height: 400px;
max-height: 400px;
overflow: hidden;
}
.tab-left {
width: 260px;
border-right: 1px solid #eee;
padding-right: 16px;
overflow-y: auto;
height: 100%;
}
.tab-right {
flex: 1;
padding-left: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-y: auto;
height: 100%;
}
.right-button {
margin: 15px 0 0 260px;
}
.detail-img {
width: 180px;
height: 120px;
object-fit: contain;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 16px;
}
.replace-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.replace-item {
display: flex;
align-items: center;
gap: 16px;
padding: 8px;
border: 1px solid #eee;
border-radius: 4px;
cursor: pointer;
}
.replace-item.selected {
border-color: #2156f3;
}
.replace-img {
width: 80px;
height: 60px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #eee;
}
.no-data {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
color: #999;
font-size: 18px;
}
:deep(.el-collapse-item.is-disabled .el-collapse-item__header) {
cursor: default;
color: #303133;
&:hover {
background-color: transparent;
}
}
:deep(.el-collapse-item.is-disabled .el-collapse-item__arrow) {
display: none;
}
:deep(.el-collapse-item__header) {
padding: 0;
height: auto;
}
:deep(.el-collapse-item__content) {
padding: 0;
padding-left: 16px;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
.search-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.search-label {
font-size: 15px;
color: #666;
margin-right: 8px;
}
.car-type-text-list {
display: flex;
gap: 32px;
}
.car-type-text {
font-size: 16px;
color: #333;
cursor: pointer;
padding: 0 4px 4px 4px;
border-bottom: 2px solid transparent;
transition: color 0.2s, border 0.2s;
}
.car-type-text.active {
color: #2156f3;
border-bottom: 2px solid #2156f3;
font-weight: 500;
}
.search-divider {
border-bottom: 1px dashed #e5e5e5;
margin: 8px 0 16px 0;
}
.param-group-title {
font-size: 15px;
color: #333;
font-weight: 600;
margin: 12px 0 12px 0;
}
.param-form-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 24px 32px;
margin-bottom: 0;
position: relative;
}
.param-form-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 0;
}
.param-label {
font-size: 14px;
color: #666;
min-width: 60px;
text-align: right;
}
.param-input {
width: 120px;
}
.param-form-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
margin-left: auto;
min-width: 200px;
}
.param-btn {
min-width: 90px;
}
.result-card-list {
display: flex;
flex-wrap: wrap;
gap: 30px;
}
.result-card {
width: 280px;
position: relative;
padding-top: 16px;
box-sizing: border-box;
transition: box-shadow 0.2s, transform 0.15s, border-color 0.2s;
cursor: pointer;
border: 1px solid #eee;
}
.result-card:hover {
box-shadow: 0 4px 16px rgba(33, 86, 243, 0.12);
transform: translateY(-4px) scale(1.03);
border-color: #2156f3;
}
.result-card.active {
box-shadow: 0 6px 20px rgba(33, 86, 243, 0.18);
border-color: #2156f3;
background: #f5f8ff;
transform: scale(1.04);
}
.result-card-info {
text-align: left;
}
.result-card-name {
font-size: 16px;
color: #333;
font-weight: 500;
margin-bottom: 4px;
}
.result-card-parameter {
font-size: 13px;
color: #888;
overflow: visible;
white-space: normal;
padding: 2px;
max-width: 300px;
word-break: break-word;
span {
color: #555;
}
}
.compare-dialog-cards {
display: flex;
gap: 32px;
margin-bottom: 24px;
}
.compare-card {
width: 220px;
text-align: center;
box-sizing: border-box;
}
.compare-card-img {
width: 100%;
height: 120px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #eee;
margin-bottom: 12px;
}
.compare-card-info {
text-align: center;
}
.compare-card-name {
font-size: 15px;
color: #333;
font-weight: 500;
margin-bottom: 4px;
}
.compare-card-code {
font-size: 13px;
color: #888;
}
.compare-table-wrap {
margin-top: 16px;
max-width: 100%;
}
.compare-table-scroll {
overflow-x: auto;
padding-bottom: 8px; /* 预留滚动条空间避免遮挡内容 */
scrollbar-gutter: stable both-edges; /* 保留滚动条占位避免跳动 */
}
/* 参数对比按钮样式 - 随滚动条移动 */
.compare-btn-float {
position: fixed; /* 修改为fixed定位相对于视口固定 */
top: 260px;
right: 10px;
z-index: 100; /* 提高层级确保可见 */
width: 52px;
height: 52px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 8px;
box-shadow: 0 3px 15px rgba(33, 86, 243, 0.15);
transition: all 0.25s ease;
font-size: 13px;
line-height: 1.3;
}
.detail:hover {
background: #cde7ff;
color: #0043d3;
box-shadow: 0 5px 20px rgba(0, 162, 255, 0.2);
transform: translateY(-2px);
}
.compare:hover {
background: #fff3cd;
color: #d39e00;
box-shadow: 0 5px 20px rgba(255, 153, 0, 0.2);
transform: translateY(-2px);
}
.result-card-list-wrap {
position: relative;
padding-right: 20px;
}
.result-card-checkbox .el-checkbox__input.is-checked .el-checkbox__inner {
border-color: #2156f3 !important;
background-color: #2156f3 !important;
}
// 参数对比表格第一列背景色与表头一致
:deep(.el-table .param-col),
:deep(.el-table th.param-col) {
background: #f5f7fa !important;
}
// 固定列的样式优化
:deep(.el-table__fixed-left) {
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.05);
z-index: 2;
}
.search-box-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.search-box {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
position: relative;
}
.custom-input-wrapper {
position: relative;
flex-grow: 1;
}
.main-input {
width: 100%;
min-width: 300px;
font-size: 16px;
}
// 语音图标样式
:deep(.voice-icon) {
cursor: pointer;
color: #666;
margin-right: 8px;
transition: color 0.2s;
&:hover {
color: #2156f3;
}
}
.search-button {
white-space: nowrap;
}
.suggestions-dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-top: 5px;
max-height: 300px;
overflow-y: auto;
}
/* 可能的字段部分样式 */
.possible-fields-section {
border-bottom: 1px solid #e4e7ed;
padding-bottom: 5px;
margin-bottom: 5px;
}
.possible-fields-header {
padding: 8px 15px;
font-size: 14px;
color: #666;
background-color: #f5f7fa;
}
.possible-field-item {
padding: 10px 15px;
cursor: pointer;
transition: background-color 0.2s;
}
.possible-field-item:hover,
.possible-field-item.active {
background-color: #f0f7ff;
color: #1890ff;
}
.hint-item {
padding: 10px 15px;
background-color: #f0f7ff;
border-bottom: 1px solid #e4e7ed;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
gap: 8px;
color: #1890ff;
}
.hint-icon {
font-size: 14px;
}
.suggestion-item {
padding: 10px 15px;
cursor: pointer;
transition: background-color 0.2s;
}
.suggestion-item:hover,
.suggestion-item.active {
background-color: #f5f7fa;
}
.field-type,
.value-type {
display: inline-block;
width: 50px;
font-size: 12px;
color: #909399;
}
.field-hint {
color: #909399;
}
.conditions-tags {
margin: 15px 0;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.condition-tag {
cursor: pointer;
transition: all 0.2s;
padding-right: 60px !important; /* 增加右侧 padding 以容纳"精准"文字 */
position: relative;
}
.condition-tag:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tag-field {
font-weight: 500;
font-weight: 500;
}
.tag-value {
color: #409eff; /* 突出显示可更改的值 */
margin-right: 5px;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.tag-value:hover {
color: #1890ff;
}
/* 精准查询相关样式 */
.tag-precise {
position: absolute;
right: 20px; /* 为关闭按钮留出空间 */
top: 50%;
transform: translateY(-50%);
color: #409eff;
font-size: 12px;
cursor: pointer;
padding: 2px 4px;
border-radius: 2px;
background-color: rgba(64, 158, 255, 0.1);
transition: all 0.2s;
}
.tag-precise:hover {
background-color: rgba(64, 158, 255, 0.2);
color: #1890ff;
}
.precise-conditions {
margin: 15px 0;
}
.precise-conditions-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.precise-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.precise-tag {
cursor: default;
padding-right: 30px !important;
position: relative;
}
.results-section {
max-width: 1250px;
margin: 0 auto;
}
.no-results {
padding: 50px 0;
text-align: center;
}
.clear-button {
color: #666;
}
.compare-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 8px;
}
.compare-dialog-title {
font-weight: 600;
font-size: 16px;
color: #303133;
}
.compare-dialog-actions {
display: flex;
align-items: center;
gap: 8px;
}
.compare-dialog-actions .el-button {
display: inline-flex;
align-items: center;
gap: 4px;
}
.compare-dialog-actions .action-text {
margin-left: 2px;
}
.dialog-close-btn {
color: #909399;
}
/* 分页和确定按钮容器 */
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 20px;
padding: 10px 0;
border-top: 1px solid #eee;
}
/* 确定按钮样式 */
.confirm-button {
min-width: 100px;
margin-left: 15px;
}
/* 文字溢出处理样式 */
.text-truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
}
.text-truncate:hover {
/* 保持原样式不变但通过title属性显示完整内容 */
overflow: visible;
white-space: normal;
z-index: 10;
background-color: rgba(255, 255, 255, 0.95);
padding: 5px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
max-width: 300px;
word-break: break-word;
}
/* 确认弹窗样式 */
.confirm-message {
font-size: 16px;
text-align: center;
padding: 20px 0;
}
/* 不同参数值的样式 */
.different-value {
color: #ff4d4f; /* 红色 */
font-weight: 500;
}
/* 对比筛选选项样式 */
.compare-filter-options {
text-align: right;
padding-right: 10px;
}
/* 语音弹窗内容样式 */
.voice-popup-content {
font-size: 16px;
text-align: center;
padding: 30px 0;
line-height: 1.6;
}
.new-tag-img {
width: 35px;
height: 35px;
vertical-align: middle;
}
/* 强制本页卡片始终使用浅色背景避免受全局暗色模式影响 */
.result-card {
background-color: #fff !important;
}
::deep(.el-card.result-card) {
background-color: #fff !important;
}
/* 对比弹窗及其他弹窗的头部底部统一浅色背景避免暗色主题干扰 */
.compare-dialog {
::deep(.el-dialog__header),
::deep(.el-dialog__footer) {
background-color: #fff !important;
}
}
::deep(.el-dialog__header),
::deep(.el-dialog__footer) {
background-color: #fff;
}
/* 对比表头可拖拽的鼠标样式 */
.compare-header-draggable {
display: flex;
align-items: center;
justify-content: center;
cursor: move;
padding: 4px 0;
}
/* 关键差异信息提示样式 */
.key-diff-hint {
margin: -15px 0 15px 0;
padding: 8px 12px;
background-color: #f0f7ff;
border-left: 3px solid #2156f3;
border-radius: 4px;
font-size: 14px;
color: #1890ff;
}
.diff-field {
color: #2156f3;
font-weight: 500;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
transition: color 0.2s;
margin: 0 3px;
}
.diff-field:hover {
color: #096dd9;
}
@media (max-width: 768px) {
.search-box {
flex-direction: column;
align-items: stretch;
}
.main-input {
min-width: auto;
}
.search-button {
width: 100%;
}
.result-card {
width: 100%;
}
.pagination-container {
flex-direction: column;
gap: 15px;
align-items: stretch;
}
.confirm-button {
width: 100%;
}
}
</style>