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

2041 lines
51 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="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"
/>
</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="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
>
<el-icon class="tag-edit-icon" size="14"><Edit /></el-icon>
</el-tag>
<el-button type="text" @click="clearAll" class="clear-button">
<el-icon><RefreshLeft /></el-icon>
清除所有
</el-button>
</div>
</div>
<!-- 查询结果区域 -->
<div class="results-section" v-if="showResults">
<div
v-if="filteredData.length"
class="result-card-list-wrap"
style="position: relative"
>
<el-button
v-if="selectedCompareList.length > 0"
type="warning"
class="compare-btn-float"
@click="onCompare"
circle
>
参数<br />对比
</el-button>
<div class="result-card-list">
<el-card
v-for="item in filteredData"
:key="item.productNumber"
class="result-card"
:class="{ active: item.selected }"
@click="toggleSelect(item)"
>
<div class="result-card-checkbox-wrap" @click.stop>
<el-checkbox
v-model="item.selected"
class="result-card-checkbox"
/>
</div>
<img
:src="item.image"
class="result-card-img"
:alt="item.name"
@click.stop="toggleSelect(item)"
/>
<div class="result-card-info">
<div class="result-card-name">{{ item.name }}</div>
<div class="result-card-code">{{ item.productNumber }}</div>
<div class="result-card-spec">
{{ item.specificationModel }}
</div>
<div class="result-card-car-model">
适用车型: {{ item.carModel }}
</div>
</div>
</el-card>
</div>
</div>
<div v-if="filteredData.length === 0" class="no-results">
<el-empty description="没有匹配的结果"></el-empty>
</div>
</div>
<!-- 参数对比弹窗 - 使用query.vue的实现但保留step2的样式 -->
<el-dialog
v-model="compareDialogVisible"
title="参数对比"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<!-- 表格对比区,图片放在表头下方 -->
<div class="compare-table-wrap">
<el-table :data="compareTableData" border style="width: 100%">
<el-table-column
prop="param"
label="参数"
width="180"
class-name="param-col"
align="center"
header-align="center"
/>
<el-table-column
v-for="item in selectedCompareList"
:key="item.productNumber"
:label="item.productNumber"
:prop="item.productNumber"
align="center"
header-align="center"
>
<template #header="{ column }">
<div
style="
display: flex;
flex-direction: column;
align-items: center;
"
>
<img
v-if="
selectedCompareList.find(
(i) => i.productNumber === column.label
)?.image
"
:src="
selectedCompareList.find(
(i) => i.productNumber === column.label
).image
"
style="
width: 60px;
height: 40px;
object-fit: contain;
margin-top: 4px;
border-radius: 4px;
border: 1px solid #eee;
"
:alt="
selectedCompareList.find(
(i) => i.productNumber === column.label
).name
"
/>
<span>{{ column.label }}</span>
<span style="font-size: 12px; color: #666">{{
selectedCompareList.find(
(i) => i.productNumber === column.label
).name
}}</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
<div class="btn-row">
<el-button @click="emit('prev-step')">上一步</el-button>
<el-button type="primary" @click="onNextStep">下一步</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from "vue";
import {
carTypeOptions,
allParts, // 保留原始数据,但实际查询会使用新的查询逻辑
} from "@/data/stepMockData";
import CarModelDialog from "@/components/CarModelDialog.vue";
import { ElMessage, ElEmpty } from "element-plus";
// 接收props和定义emit保留step2的原有逻辑
const props = defineProps({
form: Object,
selectedCarType: String,
});
const emit = defineEmits(["update:form", "prev-step", "next-step"]);
// 彻底双向绑定保留step2的原有逻辑
const selectedCarTypeProxy = computed({
get() {
return props.form?.selectedCarType ?? "";
},
set(val) {
if (props.form) {
props.form.selectedCarType = val;
emit("update:form", props.form);
}
},
});
// 从query.vue引入的查询相关变量
const currentInput = ref("");
const searchInput = ref(null);
const inputWidth = ref(600);
const selectedConditionIndex = ref(-1);
// 建议相关
const suggestions = ref([]);
const showSuggestions = ref(false);
const activeSuggestionIndex = ref(-1);
const possibleFields = ref([]);
const activePossibleFieldIndex = ref(-1);
// 解析后的条件和查询结果
const parsedConditions = ref([]);
const showResults = ref(false);
const filteredData = ref([]); // 使用query.vue的filteredData替代原有的queryResultList
// 对比相关整合query.vue的逻辑但保留step2的样式和部分逻辑
const compareDialogVisible = ref(false);
const selectedCompareList = computed(() =>
filteredData.value.filter((i) => i.selected)
);
// 生成对比表格数据 - 采用query.vue的实现
const compareTableData = computed(() => {
if (selectedCompareList.value.length === 0) return [];
// 需要对比的参数列表
const paramsToCompare = [
"品名/物料名称",
"品号",
"图号",
"品号-规格型号",
"车型",
"型号",
"材质",
"采购属性",
"车轮踏面形式",
"油漆制造商",
];
// 字段映射关系
const fieldMap = {
"品名/物料名称": "name",
品号: "productNumber",
图号: "drawingNumber",
"品号-规格型号": "specificationModel",
车型: "carModel",
型号: "model",
材质: "material",
采购属性: "procurementAttributes",
车轮踏面形式: "wheelTread",
油漆制造商: "paintManufacturer",
};
// 构建对比数据
return paramsToCompare.map((param) => {
const row = { param };
selectedCompareList.value.forEach((item) => {
row[item.productNumber] = item[fieldMap[param]] || "-";
});
return row;
});
});
// 定义可查询的字段 - 来自query.vue
const allFields = [
{ key: "productNumber", label: "品号" },
{ key: "name", label: "品名/物料名称" },
{ key: "drawingNumber", label: "图号" },
{ key: "specificationModel", label: "品号-规格型号" },
{ key: "carModel", label: "车型" },
{ key: "model", label: "型号" },
{ key: "material", label: "材质" },
{ key: "procurementAttributes", label: "采购属性" },
{ key: "wheelTread", label: "车轮踏面形式" },
{ key: "paintManufacturer", label: "油漆制造商" },
{ key: "image", label: "图片" },
];
// 模拟数据 - 来自query.vue实际项目中可能从API获取
const mockData = [
{
productNumber: "D311299000045-00005",
name: "非动力车轴(带喷漆)",
drawingNumber: "ZQ1022009-001",
specificationModel: "(GBTA2 ZQ/LRS EA4T)(Weckerle)-四方股份",
carModel: "CR400AF",
model: "GBTA2",
material: null,
procurementAttributes: "自采",
wheelTread: null,
paintManufacturer: "Weckerle",
image: "/images/cars/1_1_1.jpg",
selected: false,
},
{
productNumber: "D311299000044-00005",
name: "动力车轴(带喷漆)",
drawingNumber: "ZQ1022009-004",
specificationModel: "(GBDA2A ZQ/LRS EA4T)(Weckerle)-四方股份",
carModel: "CR400AF",
model: "GBDA2A",
material: null,
procurementAttributes: "自采",
wheelTread: null,
paintManufacturer: "Weckerle",
image: "/images/cars/1_1_2.jpg",
selected: false,
},
{
productNumber: "D311102000025-00022",
name: "非动力车轮(带降噪板+喷漆)",
drawingNumber: "ZQ1014001-100(ZQ1014001-090)",
specificationModel: "(GBTW21B 等级6 ZQ /LRS ER8)(天津纵横)Weckerle-CRC",
carModel: "CR400BF",
model: "GBTW21B",
material: "ER8",
procurementAttributes: "自采",
wheelTread: "LMB10",
paintManufacturer: "Weckerle",
image: "/images/cars/1_2_1.jpg",
selected: false,
},
{
productNumber: "D311102000025-00021",
name: "非动力车轮(带降噪板+喷漆)",
drawingNumber: "ZQ1014001-100(ZQ1014001-090)",
specificationModel: "(GBTW21B 等级3 ZQ /LRS ER8)(天津纵横)Weckerle-CRC",
carModel: "CR400BF",
model: "GBTW21B",
material: "ER8",
procurementAttributes: "自采",
wheelTread: "LMB10",
paintManufacturer: "Weckerle",
image: "/images/cars/1_2.jpg",
selected: false,
},
{
productNumber: "D311199000035-00037",
name: "动力车轮(带喷漆)",
drawingNumber: "ZQ1014001-090",
specificationModel: "(GBDW21B 等级5 ZQ/LRS ER8)Weckerle-CRC",
carModel: "CR400BF",
model: "GBDW21B",
material: null,
procurementAttributes: "自采",
wheelTread: "LMB10",
paintManufacturer: "Weckerle",
image: "/images/cars/1_2_1_1.jpg",
selected: false,
},
{
productNumber: "D311199020097-00006",
name: "精加工动力车轮",
drawingNumber: "CCD2001-03-21-0001-DA",
specificationModel: "GBDW21B 等级5 ZQ/LRS ER8-CRC",
carModel: "CR400BF",
model: "GBDW21B",
material: null,
procurementAttributes: "自采",
wheelTread: "LMB10",
paintManufacturer: "Weckerle",
image: "/images/cars/2.jpg",
selected: false,
},
{
productNumber: "D311102010007-00037",
name: "非动力车轮(带降噪板)",
drawingNumber: "TKD0601-00-00-03",
specificationModel: "(GBTW21B 等级5 ZQ/LRS ER8)(天津纵横)-CRC",
carModel: "CR400BF",
model: "GBDW21B",
material: "ER8",
procurementAttributes: "自采",
wheelTread: "LMB10",
paintManufacturer: null,
image: "/images/cars/3.jpg",
selected: false,
},
{
productNumber: "D325000000014-10001",
name: "右轴箱组成",
drawingNumber: "ZQ1014001-117",
specificationModel: "检修",
carModel: "CR400BF",
model: null,
material: null,
procurementAttributes: null,
wheelTread: null,
paintManufacturer: null,
image: "/images/cars/4.jpg",
selected: false,
},
];
// 字段值关联映射 - 用于智能提示
const fieldValueMap = {
productNumber: [
"D311299000045-00005",
"D311299000044-00005",
"D311102000025-00022",
"D311102000025-00021",
"D311199000035-00037",
"D311199020097-00006",
"D311102010007-00037",
"D325000000014-10001",
],
name: [
"非动力车轴(带喷漆)",
"动力车轴(带喷漆)",
"非动力车轮(带降噪板+喷漆)",
"动力车轮(带喷漆)",
"精加工动力车轮",
"非动力车轮(带降噪板)",
"右轴箱组成",
],
drawingNumber: [
"ZQ1022009-001",
"ZQ1022009-004",
"ZQ1014001-100(ZQ1014001-090)",
"ZQ1014001-090",
"CCD2001-03-21-0001-DA",
"TKD0601-00-00-03",
"ZQ1014001-117",
],
specificationModel: [
"(GBTA2 ZQ/LRS EA4T)(Weckerle)-四方股份",
"(GBDA2A ZQ/LRS EA4T)(Weckerle)-四方股份",
"(GBTW21B 等级6 ZQ /LRS ER8)(天津纵横)Weckerle-CRC",
"(GBTW21B 等级3 ZQ /LRS ER8)(天津纵横)Weckerle-CRC",
"(GBDW21B 等级5 ZQ/LRS ER8)Weckerle-CRC",
"(GBTW21B 等级5 ZQ/LRS ER8)(天津纵横)-CRC",
"GBDW21B 等级5 ZQ/LRS ER8-CRC",
"检修",
],
carModel: ["CR400AF", "CR400BF"],
model: ["GBTA2", "GBDA2A", "GBTW21B", "GBDW21B"],
material: ["ER8"],
procurementAttributes: ["自采"],
wheelTread: ["LMB10"],
paintManufacturer: ["Weckerle"],
image: [
"/images/cars/1_1_1.jpg",
"/images/cars/1_1_2.jpg",
"/images/cars/1_2_1.jpg",
"/images/cars/1_2.jpg",
"/images/cars/1_2_1_1.jpg",
"/images/cars/2.jpg",
"/images/cars/3.jpg",
"/images/cars/4.jpg",
],
};
// 收集所有可能的值用于匹配
const allValues = ref([]);
// 初始化所有可能的值
const initializeValues = () => {
const values = [];
allFields.forEach((field) => {
const uniqueValues = [
...new Set(
mockData.map((item) => item[field.key] ?? "") // 将null转换为空字符串
),
];
uniqueValues.forEach((value) => {
// 过滤空值,避免空提示
if (value !== "") {
values.push({
value: String(value),
fieldKey: field.key,
fieldLabel: field.label,
weight: fieldValueMap[field.key]?.includes(String(value)) ? 2 : 1,
});
}
});
});
allValues.value = values;
};
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);
}, 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 parts = inputValue.split(/[;]/);
let currentLength = 0;
for (let i = 0; i < parts.length; i++) {
// 加上分号的长度
const partLength = parts[i].length + 1;
if (cursorPos <= currentLength + partLength) {
return i;
}
currentLength += partLength;
}
return parts.length - 1;
};
// 处理输入框容器点击
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();
};
// 聚焦到指定的条件标签
const focusConditionTag = (index) => {
const parts = currentInput.value.split(";");
if (index < parts.length) {
// 将光标定位到该条件
selectedConditionIndex.value = index;
const inputEl = searchInput.value.$el.querySelector("input");
if (!inputEl) return;
// 计算该条件在输入框中的位置
let material = 0;
for (let i = 0; i < index; i++) {
material += parts[i].length + 1; // +1 是分号的长度
}
// 设置光标位置
setTimeout(() => {
inputEl.focus();
inputEl.setSelectionRange(material, material + parts[index].length);
// 触发该条件的建议显示
triggerConditionSuggestions(parts[index].trim());
}, 0);
}
};
// 触发特定条件的建议显示
const triggerConditionSuggestions = (conditionText) => {
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.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 {
// 只有值,没有字段,使用原有逻辑
handleInput(currentInput.value);
}
};
// 处理输入事件,实时生成建议
const handleInput = (value) => {
// 获取当前正在输入的条件部分(分号后面的部分)
const parts = value.split(";");
const currentPart = parts[parts.length - 1].trim();
// 检查是否在编辑已有条件
const cursorIndex = findCursorConditionIndex();
if (cursorIndex >= 0 && cursorIndex < parts.length - 1) {
// 正在编辑中间的条件,使用特殊处理
triggerConditionSuggestions(parts[cursorIndex].trim());
return;
}
// 始终显示建议框,除非输入为空
showSuggestions.value = value.trim() !== "";
// 如果当前部分为空,不显示具体建议
if (!currentPart) {
suggestions.value = [];
possibleFields.value = [];
return;
}
// 检查当前是否在输入字段还是值
const isEditingField = !currentPart.includes(":");
// 查找匹配的字段和值
let fieldSuggestions = [];
let valueSuggestions = [];
if (isEditingField) {
// 正在输入字段或值的开始部分,同时查找字段和值
fieldSuggestions = allFields
.filter((field) =>
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.find((f) => f.key === val.fieldKey),
};
}
valueFieldCounts[val.fieldKey].count += val.weight;
});
// 提取并排序可能的字段
possibleFields.value = Object.values(valueFieldCounts)
.filter((item) => item.count > 1) // 只显示有足够匹配度的字段
.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.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 selectPossibleField = (field) => {
const parts = currentInput.value.split(";");
let targetIndex =
selectedConditionIndex.value >= 0
? selectedConditionIndex.value
: parts.length - 1;
// 确保索引有效
if (targetIndex < 0 || targetIndex >= parts.length) {
targetIndex = parts.length - 1;
}
// 获取当前部分的值
let targetPart = parts[targetIndex] || "";
// 在当前值前添加字段名和冒号
parts[targetIndex] = `${field.label}:${targetPart}`;
// 重新拼接条件,清理多余分号
currentInput.value = parts.join(";").replace(/;;+/g, ";").trim();
// 保持建议框显示,允许继续编辑
setTimeout(() => {
handleInput(currentInput.value);
}, 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));
};
// 选择建议项 - 支持替换已有条件的值
const selectSuggestion = (item) => {
const parts = currentInput.value.split(";");
let targetIndex =
selectedConditionIndex.value >= 0
? selectedConditionIndex.value
: parts.length - 1;
// 确保索引有效
if (targetIndex < 0 || targetIndex >= parts.length) {
targetIndex = parts.length - 1;
}
let targetPart = parts[targetIndex] || "";
if (item.type === "field") {
// 字段建议:直接替换为「字段:」格式
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 {
// 无字段时直接处理值
parts[targetIndex] = item.label;
}
}
// 重新拼接条件,清理多余分号
currentInput.value = parts.join(";").replace(/;;+/g, ";").trim();
// 保持建议框显示,允许继续编辑
setTimeout(() => {
handleInput(currentInput.value);
}, 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.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;
};
// 处理查询 - 采用query.vue的实现
const handleSearch = () => {
if (!currentInput.value.trim()) return;
// 解析条件
parsedConditions.value = parseConditions(currentInput.value);
// 执行过滤创建副本并添加selected属性
filteredData.value = filterData(parsedConditions.value).map((item) => ({
...item,
selected: false,
}));
// 显示结果
showResults.value = true;
// 隐藏建议
showSuggestions.value = false;
possibleFields.value = [];
};
// 根据条件过滤数据
const filterData = (conditions) => {
return mockData.filter((item) => {
return conditions.every((condition) => {
if (!condition.valid) return false;
// 如果指定了字段
if (condition.field) {
const fieldValue = item[condition.field];
return checkCondition(fieldValue, condition.value);
}
// 没有指定字段,检查所有字段
else {
return allFields.some((field) => {
const fieldValue = item[field.key];
return checkCondition(fieldValue, condition.value);
});
}
});
});
};
// 检查单个条件
const checkCondition = (fieldValue, conditionValue) => {
// 将null转换为空字符串处理
const processedValue = fieldValue ?? "";
const processedCondition = conditionValue ?? "";
// 后续逻辑使用processedValue和processedCondition
const operators = [
{ symbol: ">=", func: (a, b) => a >= b },
{ symbol: "<=", func: (a, b) => a <= b },
{ symbol: "!=", func: (a, b) => a != b },
{ symbol: ">", func: (a, b) => a > b },
{ symbol: "<", func: (a, b) => a < b },
{ symbol: "=", func: (a, b) => a == b },
];
for (const op of operators) {
if (processedCondition.startsWith(op.symbol)) {
const value = processedCondition.substring(op.symbol.length).trim();
if (!isNaN(Number(processedValue)) && !isNaN(Number(value))) {
return op.func(Number(processedValue), Number(value));
}
return op.func(String(processedValue), value);
}
}
return String(processedValue)
.toLowerCase()
.includes(processedCondition.toLowerCase());
};
// 移除单个条件
const removeCondition = (index) => {
const parts = currentInput.value.split(/[;]/); // 同时匹配中英文分号
parts.splice(index, 1);
currentInput.value = parts
.join(";")
.replace(/[;]+/g, ";")
.trim();
parsedConditions.value = parseConditions(currentInput.value);
filteredData.value = filterData(parsedConditions.value).map((item) => ({
...item,
selected: false,
}));
// 重置选中的条件索引
if (selectedConditionIndex.value === index) {
selectedConditionIndex.value = -1;
}
};
// 清除所有查询条件和结果
const clearAll = () => {
currentInput.value = "";
parsedConditions.value = [];
filteredData.value = [];
showResults.value = false;
showSuggestions.value = false;
possibleFields.value = [];
selectedConditionIndex.value = -1;
if (searchInput.value) {
searchInput.value.focus();
}
};
// 保留step2的原有生命周期钩子
onMounted(() => {
initializeValues();
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);
}
);
// 保留step2的原有功能方法
function toggleSelect(item) {
// 统计已选中的数量
const selectedCount = filteredData.value.filter((i) => i.selected).length;
if (!item.selected && selectedCount >= 3) {
ElMessage.warning("最多只能选择3个卡片进行对比");
return;
}
item.selected = !item.selected;
}
function onCompare() {
if (selectedCompareList.value.length === 0) {
ElMessage.warning("请先选择要对比的卡片");
return;
}
compareDialogVisible.value = true;
}
function onNextStep() {
const selected = filteredData.value.filter((i) => i.selected);
if (selected.length !== 1) {
ElMessage.warning("请且只能选择一个卡片进入下一步");
return;
}
// 写入form
if (props.form) {
props.form.selectedCarType = selectedCarTypeProxy.value;
props.form.selectedPartInfo = {
code: selected[0].productNumber,
label: selected[0].name,
image: selected[0].image,
type: selected[0].model,
feature: selected[0].specificationModel,
};
emit("update:form", props.form);
}
emit("next-step");
}
</script>
<style scoped lang="scss">
// 保留step2的原有样式
.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: 24px;
margin-top: 24px;
}
.result-card {
width: 220px;
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-checkbox-wrap {
position: absolute;
left: 20px;
top: 22px;
z-index: 2;
}
.result-card-checkbox {
position: static;
}
.result-card-img {
width: 100%;
height: 120px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #eee;
margin-bottom: 12px;
transition: transform 0.2s;
}
.result-card:hover .result-card-img,
.result-card.active .result-card-img {
transform: scale(1.08);
}
.result-card-info {
text-align: left;
}
.result-card-name {
font-size: 15px;
color: #333;
font-weight: 500;
margin-bottom: 4px;
}
.result-card-code {
font-size: 13px;
color: #888;
}
.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;
}
.compare-btn-float {
position: absolute;
right: 20px;
top: 170px;
z-index: 10;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(33, 86, 243, 0.12);
transition: box-shadow 0.2s, background 0.2s;
font-size: 14px;
}
.compare-btn-float:hover {
background: #ffe9c6;
color: #999;
box-shadow: 0 4px 16px rgba(255, 153, 0, 0.18);
}
.result-card-list-wrap {
position: relative;
}
.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;
}
.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;
}
.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;
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: 30px !important;
position: relative;
}
.condition-tag:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tag-field {
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-edit-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #409eff;
opacity: 0.7;
}
.results-section {
overflow: hidden;
padding: 0 10px;
}
.results-table {
width: 100%;
}
.no-results {
padding: 50px 0;
text-align: center;
}
.clear-button {
color: #666;
}
.result-card-spec {
font-size: 12px;
color: #666;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.result-card-car-model {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.compare-table-wrap {
margin-top: 16px;
max-height: 500px;
overflow-y: auto;
}
@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%;
}
}
</style>