bug修复
This commit is contained in:
parent
d575852b50
commit
fd4f0c891c
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="tags-view-container" class="tags-view-container">
|
<div id="tags-view-container" class="tags-view-container">
|
||||||
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
|
<scroll-pane
|
||||||
|
ref="scrollPaneRef"
|
||||||
|
class="tags-view-wrapper"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
v-for="tag in visitedViews"
|
v-for="tag in visitedViews"
|
||||||
:key="tag.path"
|
:key="tag.path"
|
||||||
|
|
@ -14,39 +18,46 @@
|
||||||
>
|
>
|
||||||
{{ tag.title }}
|
{{ tag.title }}
|
||||||
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
|
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
|
||||||
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
|
<close
|
||||||
|
class="el-icon-close"
|
||||||
|
style="width: 1em; height: 1em; vertical-align: middle"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</scroll-pane>
|
</scroll-pane>
|
||||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
<ul
|
||||||
|
v-show="visible"
|
||||||
|
:style="{ left: left + 'px', top: top + 'px' }"
|
||||||
|
class="contextmenu"
|
||||||
|
>
|
||||||
<li @click="refreshSelectedTag(selectedTag)">
|
<li @click="refreshSelectedTag(selectedTag)">
|
||||||
<refresh-right style="width: 1em; height: 1em;" /> 刷新页面
|
<refresh-right style="width: 1em; height: 1em" /> 刷新页面
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||||
<close style="width: 1em; height: 1em;" /> 关闭当前
|
<close style="width: 1em; height: 1em" /> 关闭当前
|
||||||
</li>
|
</li>
|
||||||
<li @click="closeOthersTags">
|
<li @click="closeOthersTags">
|
||||||
<circle-close style="width: 1em; height: 1em;" /> 关闭其他
|
<circle-close style="width: 1em; height: 1em" /> 关闭其他
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||||
<back style="width: 1em; height: 1em;" /> 关闭左侧
|
<back style="width: 1em; height: 1em" /> 关闭左侧
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!isLastView()" @click="closeRightTags">
|
<li v-if="!isLastView()" @click="closeRightTags">
|
||||||
<right style="width: 1em; height: 1em;" /> 关闭右侧
|
<right style="width: 1em; height: 1em" /> 关闭右侧
|
||||||
</li>
|
</li>
|
||||||
<li @click="closeAllTags(selectedTag)">
|
<li @click="closeAllTags(selectedTag)">
|
||||||
<circle-close style="width: 1em; height: 1em;" /> 全部关闭
|
<circle-close style="width: 1em; height: 1em" /> 全部关闭
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ScrollPane from './ScrollPane'
|
import ScrollPane from "./ScrollPane";
|
||||||
import { getNormalPath } from '@/utils/ruoyi'
|
import { getNormalPath } from "@/utils/ruoyi";
|
||||||
import useTagsViewStore from '@/store/modules/tagsView'
|
import useTagsViewStore from "@/store/modules/tagsView";
|
||||||
import useSettingsStore from '@/store/modules/settings'
|
import useSettingsStore from "@/store/modules/settings";
|
||||||
import usePermissionStore from '@/store/modules/permission'
|
import usePermissionStore from "@/store/modules/permission";
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const top = ref(0);
|
const top = ref(0);
|
||||||
|
|
@ -64,75 +75,81 @@ const routes = computed(() => usePermissionStore().routes);
|
||||||
const theme = computed(() => useSettingsStore().theme);
|
const theme = computed(() => useSettingsStore().theme);
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
addTags()
|
addTags();
|
||||||
moveToCurrentTag()
|
moveToCurrentTag();
|
||||||
})
|
});
|
||||||
|
|
||||||
watch(visible, (value) => {
|
watch(visible, (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
document.body.addEventListener('click', closeMenu)
|
document.body.addEventListener("click", closeMenu);
|
||||||
} else {
|
} else {
|
||||||
document.body.removeEventListener('click', closeMenu)
|
document.body.removeEventListener("click", closeMenu);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initTags()
|
initTags();
|
||||||
addTags()
|
addTags();
|
||||||
})
|
});
|
||||||
|
|
||||||
function isActive(r) {
|
function isActive(r) {
|
||||||
return r.path === route.path
|
return r.path === route.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function activeStyle(tag) {
|
function activeStyle(tag) {
|
||||||
if (!isActive(tag)) return {};
|
if (!isActive(tag)) return {};
|
||||||
return {
|
return {
|
||||||
"background-color": theme.value,
|
"background-color": theme.value,
|
||||||
"border-color": theme.value
|
"border-color": theme.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAffix(tag) {
|
function isAffix(tag) {
|
||||||
return tag.meta && tag.meta.affix
|
return tag.meta && tag.meta.affix;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFirstView() {
|
function isFirstView() {
|
||||||
try {
|
try {
|
||||||
return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
|
return (
|
||||||
|
selectedTag.value.fullPath === "/index" ||
|
||||||
|
selectedTag.value.fullPath === visitedViews.value[1].fullPath
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLastView() {
|
function isLastView() {
|
||||||
try {
|
try {
|
||||||
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
|
return (
|
||||||
|
selectedTag.value.fullPath ===
|
||||||
|
visitedViews.value[visitedViews.value.length - 1].fullPath
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterAffixTags(routes, basePath = '') {
|
function filterAffixTags(routes, basePath = "") {
|
||||||
let tags = []
|
let tags = [];
|
||||||
routes.forEach(route => {
|
routes.forEach((route) => {
|
||||||
if (route.meta && route.meta.affix) {
|
if (route.meta && route.meta.affix) {
|
||||||
const tagPath = getNormalPath(basePath + '/' + route.path)
|
const tagPath = getNormalPath(basePath + "/" + route.path);
|
||||||
tags.push({
|
tags.push({
|
||||||
fullPath: tagPath,
|
fullPath: tagPath,
|
||||||
path: tagPath,
|
path: tagPath,
|
||||||
name: route.name,
|
name: route.name,
|
||||||
meta: { ...route.meta }
|
meta: { ...route.meta },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
const tempTags = filterAffixTags(route.children, route.path)
|
const tempTags = filterAffixTags(route.children, route.path);
|
||||||
if (tempTags.length >= 1) {
|
if (tempTags.length >= 1) {
|
||||||
tags = [...tags, ...tempTags]
|
tags = [...tags, ...tempTags];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return tags
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTags() {
|
function initTags() {
|
||||||
|
|
@ -141,15 +158,15 @@ function initTags() {
|
||||||
for (const tag of res) {
|
for (const tag of res) {
|
||||||
// Must have tag name
|
// Must have tag name
|
||||||
if (tag.name) {
|
if (tag.name) {
|
||||||
useTagsViewStore().addVisitedView(tag)
|
useTagsViewStore().addVisitedView(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTags() {
|
function addTags() {
|
||||||
const { name } = route
|
const { name } = route;
|
||||||
if (name) {
|
if (name) {
|
||||||
useTagsViewStore().addView(route)
|
useTagsViewStore().addView(route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,11 +177,11 @@ function moveToCurrentTag() {
|
||||||
scrollPaneRef.value.moveToTarget(r);
|
scrollPaneRef.value.moveToTarget(r);
|
||||||
// when query is different then update
|
// when query is different then update
|
||||||
if (r.fullPath !== route.fullPath) {
|
if (r.fullPath !== route.fullPath) {
|
||||||
useTagsViewStore().updateVisitedView(route)
|
useTagsViewStore().updateVisitedView(route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshSelectedTag(view) {
|
function refreshSelectedTag(view) {
|
||||||
|
|
@ -177,83 +194,83 @@ function refreshSelectedTag(view) {
|
||||||
function closeSelectedTag(view) {
|
function closeSelectedTag(view) {
|
||||||
proxy.$tab.closePage(view).then(({ visitedViews }) => {
|
proxy.$tab.closePage(view).then(({ visitedViews }) => {
|
||||||
if (isActive(view)) {
|
if (isActive(view)) {
|
||||||
toLastView(visitedViews, view)
|
toLastView(visitedViews, view);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeRightTags() {
|
function closeRightTags() {
|
||||||
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
|
proxy.$tab.closeRightPage(selectedTag.value).then((visitedViews) => {
|
||||||
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
if (!visitedViews.find((i) => i.fullPath === route.fullPath)) {
|
||||||
toLastView(visitedViews)
|
toLastView(visitedViews);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLeftTags() {
|
function closeLeftTags() {
|
||||||
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
|
proxy.$tab.closeLeftPage(selectedTag.value).then((visitedViews) => {
|
||||||
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
if (!visitedViews.find((i) => i.fullPath === route.fullPath)) {
|
||||||
toLastView(visitedViews)
|
toLastView(visitedViews);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeOthersTags() {
|
function closeOthersTags() {
|
||||||
router.push(selectedTag.value).catch(() => {});
|
router.push(selectedTag.value).catch(() => {});
|
||||||
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
||||||
moveToCurrentTag()
|
moveToCurrentTag();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAllTags(view) {
|
function closeAllTags(view) {
|
||||||
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
|
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
|
||||||
if (affixTags.value.some(tag => tag.path === route.path)) {
|
if (affixTags.value.some((tag) => tag.path === route.path)) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
toLastView(visitedViews, view)
|
toLastView(visitedViews, view);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLastView(visitedViews, view) {
|
function toLastView(visitedViews, view) {
|
||||||
const latestView = visitedViews.slice(-1)[0]
|
const latestView = visitedViews.slice(-1)[0];
|
||||||
if (latestView) {
|
if (latestView) {
|
||||||
router.push(latestView.fullPath)
|
router.push(latestView.fullPath);
|
||||||
} else {
|
} else {
|
||||||
// now the default is to redirect to the home page if there is no tags-view,
|
// now the default is to redirect to the home page if there is no tags-view,
|
||||||
// you can adjust it according to your needs.
|
// you can adjust it according to your needs.
|
||||||
if (view.name === 'Dashboard') {
|
if (view.name === "Dashboard") {
|
||||||
// to reload home page
|
// to reload home page
|
||||||
router.replace({ path: '/redirect' + view.fullPath })
|
router.replace({ path: "/redirect" + view.fullPath });
|
||||||
} else {
|
} else {
|
||||||
router.push('/')
|
router.push("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMenu(tag, e) {
|
function openMenu(tag, e) {
|
||||||
const menuMinWidth = 105
|
const menuMinWidth = 105;
|
||||||
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
|
const offsetLeft = proxy.$el.getBoundingClientRect().left; // container margin left
|
||||||
const offsetWidth = proxy.$el.offsetWidth // container width
|
const offsetWidth = proxy.$el.offsetWidth; // container width
|
||||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
||||||
const l = e.clientX - offsetLeft + 15 // 15: margin right
|
const l = e.clientX - offsetLeft + 15; // 15: margin right
|
||||||
|
|
||||||
if (l > maxLeft) {
|
if (l > maxLeft) {
|
||||||
left.value = maxLeft
|
left.value = maxLeft;
|
||||||
} else {
|
} else {
|
||||||
left.value = l
|
left.value = l;
|
||||||
}
|
}
|
||||||
|
|
||||||
top.value = e.clientY
|
top.value = e.clientY;
|
||||||
visible.value = true
|
visible.value = true;
|
||||||
selectedTag.value = tag
|
selectedTag.value = tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMenu() {
|
function closeMenu() {
|
||||||
visible.value = false
|
visible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
closeMenu()
|
closeMenu();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -261,9 +278,9 @@ function handleScroll() {
|
||||||
.tags-view-container {
|
.tags-view-container {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--tags-bg, #fff);
|
background: #fff;
|
||||||
border-bottom: 1px solid var(--tags-item-border, #d8dce5);
|
border-bottom: 1px solid var(--tags-item-border, #d8dce5);
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
.tags-view-wrapper {
|
.tags-view-wrapper {
|
||||||
.tags-view-item {
|
.tags-view-item {
|
||||||
|
|
@ -294,7 +311,7 @@ function handleScroll() {
|
||||||
border-color: #42b983;
|
border-color: #42b983;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: "";
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|
@ -318,7 +335,7 @@ function handleScroll() {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--tags-item-text, #333);
|
color: var(--tags-item-text, #333);
|
||||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
|
||||||
border: 1px solid var(--el-border-color-light, #e4e7ed);
|
border: 1px solid var(--el-border-color-light, #e4e7ed);
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
|
@ -344,11 +361,11 @@ function handleScroll() {
|
||||||
vertical-align: 2px;
|
vertical-align: 2px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
transform-origin: 100% 50%;
|
transform-origin: 100% 50%;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
transform: scale(.6);
|
transform: scale(0.6);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: -3px;
|
vertical-align: -3px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -486,6 +486,9 @@ const currentInput = ref("");
|
||||||
const searchInput = ref(null);
|
const searchInput = ref(null);
|
||||||
const inputWidth = ref(600);
|
const inputWidth = ref(600);
|
||||||
const selectedConditionIndex = ref(-1);
|
const selectedConditionIndex = ref(-1);
|
||||||
|
const lastInputValue = ref(""); // 存储上一次的输入值,用于检测条件是否被修改
|
||||||
|
const isConditionMoved = ref(false); // 标记条件是否刚刚被移动,用于避免使用本地缓存
|
||||||
|
const disableLocalSuggestions = ref(false); // smart 返回为空时,禁止本地兜底建议
|
||||||
|
|
||||||
// 建议相关
|
// 建议相关
|
||||||
const suggestions = ref([]);
|
const suggestions = ref([]);
|
||||||
|
|
@ -784,12 +787,19 @@ const mergeFieldMetadata = (fields = []) => {
|
||||||
fieldMapCache.set(key, { key, label });
|
fieldMapCache.set(key, { key, label });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(item.fieldValues) && item.fieldValues.length > 0) {
|
// 接口明确返回 fieldValues 时,统一以接口为准:
|
||||||
|
// - 空数组:清空本地缓存,避免出现“幽灵”值
|
||||||
|
// - 非空数组:合并去重
|
||||||
|
if (Array.isArray(item.fieldValues)) {
|
||||||
|
if (item.fieldValues.length === 0) {
|
||||||
|
fieldValueMap.value[key] = [];
|
||||||
|
} else {
|
||||||
const existingValues = fieldValueMap.value[key] || [];
|
const existingValues = fieldValueMap.value[key] || [];
|
||||||
fieldValueMap.value[key] = Array.from(
|
fieldValueMap.value[key] = Array.from(
|
||||||
new Set([...existingValues, ...item.fieldValues])
|
new Set([...existingValues, ...item.fieldValues])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
allFields.value = Array.from(fieldMapCache.values());
|
allFields.value = Array.from(fieldMapCache.values());
|
||||||
|
|
@ -955,6 +965,14 @@ const focusConditionTag = (index) => {
|
||||||
|
|
||||||
// 触发特定条件的建议显示
|
// 触发特定条件的建议显示
|
||||||
const triggerConditionSuggestions = (conditionText) => {
|
const triggerConditionSuggestions = (conditionText) => {
|
||||||
|
// 当 smart 返回为空时,禁止本地兜底建议
|
||||||
|
if (disableLocalSuggestions.value) {
|
||||||
|
suggestions.value = [];
|
||||||
|
possibleFields.value = [];
|
||||||
|
showSuggestions.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!conditionText) {
|
if (!conditionText) {
|
||||||
suggestions.value = [];
|
suggestions.value = [];
|
||||||
possibleFields.value = [];
|
possibleFields.value = [];
|
||||||
|
|
@ -1100,6 +1118,14 @@ const triggerConditionSuggestions = (conditionText) => {
|
||||||
|
|
||||||
// 根据当前输入更新建议列表(不触发远程请求)
|
// 根据当前输入更新建议列表(不触发远程请求)
|
||||||
const updateSuggestionsFromValue = (value) => {
|
const updateSuggestionsFromValue = (value) => {
|
||||||
|
// 当 smart 返回为空时,禁止本地兜底建议
|
||||||
|
if (disableLocalSuggestions.value) {
|
||||||
|
suggestions.value = [];
|
||||||
|
possibleFields.value = [];
|
||||||
|
showSuggestions.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedValue = value ?? "";
|
const normalizedValue = value ?? "";
|
||||||
// 获取当前正在输入的条件部分(分号后面的部分)
|
// 获取当前正在输入的条件部分(分号后面的部分)
|
||||||
const parts = normalizedValue.split(/[;;]/);
|
const parts = normalizedValue.split(/[;;]/);
|
||||||
|
|
@ -1270,20 +1296,109 @@ const updateSuggestionsFromValue = (value) => {
|
||||||
// 处理输入事件,实时生成建议并调度远程提示
|
// 处理输入事件,实时生成建议并调度远程提示
|
||||||
const handleInput = (value, options = {}) => {
|
const handleInput = (value, options = {}) => {
|
||||||
const normalizedValue = value ?? "";
|
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;
|
||||||
|
|
||||||
|
// 移除原位置的条件,并将编辑后的条件添加到最后一个位置
|
||||||
|
const newParts = currentParts.filter(
|
||||||
|
(_, index) => index !== cursorIndex
|
||||||
|
);
|
||||||
|
newParts.push(editedPart);
|
||||||
|
const newValue = newParts.join(";");
|
||||||
|
|
||||||
|
// 标记条件刚刚被移动,避免使用本地缓存
|
||||||
|
isConditionMoved.value = true;
|
||||||
|
|
||||||
|
// 更新上一次的输入值(避免再次触发重排序)
|
||||||
|
lastInputValue.value = newValue;
|
||||||
|
|
||||||
|
// 更新输入框的值(使用 skipReorder 避免无限循环)
|
||||||
|
currentInput.value = newValue;
|
||||||
|
|
||||||
|
// 重新解析条件,不使用缓存
|
||||||
|
parsedConditions.value = parseConditions(newValue);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触发搜索提示(只针对最后一个条件)
|
||||||
|
const finalPart = editedPart;
|
||||||
|
if (finalPart && !options.skipFetch) {
|
||||||
|
scheduleSearchHint(finalPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新上一次的输入值
|
||||||
|
lastInputValue.value = normalizedValue;
|
||||||
|
|
||||||
|
// 如果输入为空,清空所有条件和相关状态
|
||||||
if (!normalizedValue.trim()) {
|
if (!normalizedValue.trim()) {
|
||||||
|
parsedConditions.value = [];
|
||||||
|
preciseConditions.value = [];
|
||||||
|
disableLocalSuggestions.value = false; // 清空时恢复允许使用接口返回
|
||||||
|
isConditionMoved.value = false; // 重置条件移动标志
|
||||||
fieldsFromApi.value = false;
|
fieldsFromApi.value = false;
|
||||||
possibleFields.value = [];
|
possibleFields.value = [];
|
||||||
}
|
suggestions.value = [];
|
||||||
updateSuggestionsFromValue(normalizedValue);
|
|
||||||
if (!normalizedValue.trim()) {
|
|
||||||
keyDiffFields.value = [];
|
keyDiffFields.value = [];
|
||||||
showKeyDiffHint.value = false;
|
showKeyDiffHint.value = false;
|
||||||
if (differenceTimer.value) {
|
if (differenceTimer.value) {
|
||||||
clearTimeout(differenceTimer.value);
|
clearTimeout(differenceTimer.value);
|
||||||
differenceTimer.value = null;
|
differenceTimer.value = null;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSuggestionsFromValue(normalizedValue);
|
||||||
if (options.skipFetch) {
|
if (options.skipFetch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1342,23 +1457,18 @@ const fetchSearchHints = async (keyword) => {
|
||||||
? res
|
? res
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// 若接口无数据:若已有接口缓存,则保持缓存并继续用缓存给出建议;否则关闭下拉
|
// 若接口无数据:严格按照本次 smart 接口结果处理,不做任何兜底或本地缓存回退
|
||||||
if (!hintList.length) {
|
if (!hintList.length) {
|
||||||
if (!allFields.value.length) {
|
disableLocalSuggestions.value = true; // smart 无数据时禁止本地兜底
|
||||||
suggestions.value = [];
|
suggestions.value = [];
|
||||||
possibleFields.value = [];
|
possibleFields.value = [];
|
||||||
allFields.value = [];
|
|
||||||
allValues.value = [];
|
|
||||||
fieldValueMap.value = {};
|
|
||||||
fieldsFromApi.value = false;
|
fieldsFromApi.value = false;
|
||||||
showSuggestions.value = false;
|
showSuggestions.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 有缓存则继续用缓存生成建议
|
|
||||||
updateSuggestionsFromValue(currentInput.value);
|
// smart 有返回数据,则允许后续基于本次返回的字段做联想
|
||||||
showSuggestions.value = suggestions.value.length > 0;
|
disableLocalSuggestions.value = false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理返回的数据:合并字段元数据(仅来自接口)
|
// 处理返回的数据:合并字段元数据(仅来自接口)
|
||||||
mergeFieldMetadata(hintList);
|
mergeFieldMetadata(hintList);
|
||||||
|
|
@ -1500,7 +1610,9 @@ const fetchSearchHints = async (keyword) => {
|
||||||
uniqueSuggestions.push(s);
|
uniqueSuggestions.push(s);
|
||||||
});
|
});
|
||||||
suggestions.value = uniqueSuggestions;
|
suggestions.value = uniqueSuggestions;
|
||||||
showSuggestions.value = uniqueSuggestions.length > 0;
|
// 如果有字段或有值建议,只要其中之一存在就展示下拉
|
||||||
|
showSuggestions.value =
|
||||||
|
uniqueSuggestions.length > 0 || possibleFields.value.length > 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("searchHint error:", error);
|
console.error("searchHint error:", error);
|
||||||
}
|
}
|
||||||
|
|
@ -1554,8 +1666,14 @@ const selectPossibleField = (field) => {
|
||||||
targetIndex = parts.length - 1;
|
targetIndex = parts.length - 1;
|
||||||
} else {
|
} else {
|
||||||
// 没有字段,在当前值前添加字段名和冒号
|
// 没有字段,在当前值前添加字段名和冒号
|
||||||
|
// 如果当前值本身已经等于字段名,避免出现“字段名:字段名”的情况
|
||||||
|
const trimmedPart = (targetPart || "").trim();
|
||||||
|
if (!trimmedPart || trimmedPart === field.label) {
|
||||||
|
parts[targetIndex] = `${field.label}:`;
|
||||||
|
} else {
|
||||||
parts[targetIndex] = `${field.label}:${targetPart}`;
|
parts[targetIndex] = `${field.label}:${targetPart}`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 重新拼接条件,清理多余分号
|
// 重新拼接条件,清理多余分号
|
||||||
currentInput.value = parts.join(";").replace(/;;+/g, ";").trim();
|
currentInput.value = parts.join(";").replace(/;;+/g, ";").trim();
|
||||||
|
|
@ -1826,20 +1944,60 @@ const syncPreciseConditionsIndex = () => {
|
||||||
const buildFieldConditionsPayload = () => {
|
const buildFieldConditionsPayload = () => {
|
||||||
const preciseIndexSet = getPreciseIndexSet();
|
const preciseIndexSet = getPreciseIndexSet();
|
||||||
|
|
||||||
return parsedConditions.value
|
// 检测当前正在编辑的条件索引
|
||||||
.filter((condition) => condition.value)
|
const cursorIndex = findCursorConditionIndex();
|
||||||
|
const parts = currentInput.value.split(/[;;]/);
|
||||||
|
const activeIndex =
|
||||||
|
cursorIndex >= 0 && cursorIndex < parts.length
|
||||||
|
? cursorIndex
|
||||||
|
: parts.length - 1;
|
||||||
|
|
||||||
|
// 如果正在编辑中间的条件,需要重新排序:将正在编辑的条件移到最后一个位置
|
||||||
|
let conditionsToProcess = [...parsedConditions.value];
|
||||||
|
if (activeIndex >= 0 && activeIndex < conditionsToProcess.length - 1) {
|
||||||
|
const [editedCondition] = conditionsToProcess.splice(activeIndex, 1);
|
||||||
|
conditionsToProcess.push(editedCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditionsToProcess
|
||||||
|
.filter((condition) => {
|
||||||
|
// 保留有值的条件,或者有字段名但没有值的条件(如 "车型:")
|
||||||
|
return condition.value || (condition.fieldLabel && condition.field);
|
||||||
|
})
|
||||||
.map((condition, index) => {
|
.map((condition, index) => {
|
||||||
const queryType = preciseIndexSet.has(index) ? "EXACT" : "FUZZY";
|
// 计算原始索引(用于精准查询)
|
||||||
|
const originalIndex =
|
||||||
|
activeIndex >= 0 && activeIndex < parsedConditions.value.length - 1
|
||||||
|
? index === conditionsToProcess.length - 1
|
||||||
|
? activeIndex
|
||||||
|
: parsedConditions.value.findIndex(
|
||||||
|
(c) =>
|
||||||
|
c.fieldLabel === condition.fieldLabel &&
|
||||||
|
c.value === condition.value &&
|
||||||
|
c.field === condition.field
|
||||||
|
)
|
||||||
|
: index;
|
||||||
|
const queryType = preciseIndexSet.has(originalIndex) ? "EXACT" : "FUZZY";
|
||||||
|
|
||||||
if (condition.valid && condition.field) {
|
if (condition.valid && condition.field) {
|
||||||
return {
|
return {
|
||||||
fieldName: condition.field,
|
fieldName: condition.field,
|
||||||
fieldValue: condition.value,
|
fieldValue: condition.value || "",
|
||||||
keyword: "",
|
keyword: "",
|
||||||
queryType,
|
queryType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果有字段名但没有值(如 "车型:"),使用字段名作为 keyword
|
||||||
|
if (condition.fieldLabel && !condition.value) {
|
||||||
|
return {
|
||||||
|
fieldName: "",
|
||||||
|
fieldValue: "",
|
||||||
|
keyword: condition.fieldLabel,
|
||||||
|
queryType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const keyword = condition.fieldLabel
|
const keyword = condition.fieldLabel
|
||||||
? `${condition.fieldLabel}:${condition.value}`.replace(/:$/, "")
|
? `${condition.fieldLabel}:${condition.value}`.replace(/:$/, "")
|
||||||
: condition.value;
|
: condition.value;
|
||||||
|
|
@ -1855,19 +2013,47 @@ const buildFieldConditionsPayload = () => {
|
||||||
|
|
||||||
// 为 searchHint 构建 fieldConditions
|
// 为 searchHint 构建 fieldConditions
|
||||||
const buildFieldConditionsForHint = () => {
|
const buildFieldConditionsForHint = () => {
|
||||||
return parsedConditions.value
|
// 检测当前正在编辑的条件索引
|
||||||
.filter((condition) => condition.value)
|
const cursorIndex = findCursorConditionIndex();
|
||||||
|
const parts = currentInput.value.split(/[;;]/);
|
||||||
|
const activeIndex =
|
||||||
|
cursorIndex >= 0 && cursorIndex < parts.length
|
||||||
|
? cursorIndex
|
||||||
|
: parts.length - 1;
|
||||||
|
|
||||||
|
// 如果正在编辑中间的条件,需要重新排序:将正在编辑的条件移到最后一个位置
|
||||||
|
let conditionsToProcess = [...parsedConditions.value];
|
||||||
|
if (activeIndex >= 0 && activeIndex < conditionsToProcess.length - 1) {
|
||||||
|
const [editedCondition] = conditionsToProcess.splice(activeIndex, 1);
|
||||||
|
conditionsToProcess.push(editedCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditionsToProcess
|
||||||
|
.filter((condition) => {
|
||||||
|
// 保留有值的条件,或者有字段名但没有值的条件(如 "车型:")
|
||||||
|
return condition.value || (condition.fieldLabel && condition.field);
|
||||||
|
})
|
||||||
.map((condition) => {
|
.map((condition) => {
|
||||||
// 如果明确选择了字段(有 field 且 valid),使用 fieldName 和 fieldValue
|
// 如果明确选择了字段(有 field 且 valid),使用 fieldName 和 fieldValue
|
||||||
if (condition.field && condition.valid) {
|
if (condition.field && condition.valid) {
|
||||||
return {
|
return {
|
||||||
fieldName: condition.field, // 使用 fieldKey
|
fieldName: condition.field, // 使用 fieldKey
|
||||||
fieldValue: condition.value, // 对应的 value
|
fieldValue: condition.value || "", // 对应的 value,可能为空
|
||||||
keyword: "",
|
keyword: "",
|
||||||
queryType: "FUZZY",
|
queryType: "FUZZY",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果有字段名但没有值(如 "车型:"),使用字段名作为 keyword
|
||||||
|
if (condition.fieldLabel && !condition.value) {
|
||||||
|
return {
|
||||||
|
fieldName: "",
|
||||||
|
fieldValue: "",
|
||||||
|
keyword: condition.fieldLabel,
|
||||||
|
queryType: "FUZZY",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 不确定输入的值是 label 还是 value,全部放 keyword
|
// 不确定输入的值是 label 还是 value,全部放 keyword
|
||||||
return {
|
return {
|
||||||
fieldName: "",
|
fieldName: "",
|
||||||
|
|
@ -2087,6 +2273,8 @@ const removeCondition = (index) => {
|
||||||
// 清除所有查询条件和结果
|
// 清除所有查询条件和结果
|
||||||
const clearAll = () => {
|
const clearAll = () => {
|
||||||
currentInput.value = "";
|
currentInput.value = "";
|
||||||
|
lastInputValue.value = ""; // 重置上一次的输入值
|
||||||
|
isConditionMoved.value = false; // 重置条件移动标志
|
||||||
parsedConditions.value = [];
|
parsedConditions.value = [];
|
||||||
preciseConditions.value = [];
|
preciseConditions.value = [];
|
||||||
filteredData.value = [];
|
filteredData.value = [];
|
||||||
|
|
@ -3035,6 +3223,28 @@ function handleConfirm() {
|
||||||
vertical-align: middle;
|
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 {
|
.compare-header-draggable {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue