feat: 统一列表模糊搜索行为
- 统一管理端和用户中心搜索参数及多字段包含匹配 - 修复聊天搜索竞态并优化部门重名路径展示 - 补充部门展开、模型空白词和聊天查询回归测试
This commit is contained in:
@@ -79,9 +79,7 @@ watchListRouteState(route, agentListRouteSchema, (state) => {
|
||||
pageSize: state.pageSize,
|
||||
queryParams: {
|
||||
categoryId: state.categoryId || undefined,
|
||||
description: state.keyword || undefined,
|
||||
isQueryOr: true,
|
||||
name: state.keyword || undefined,
|
||||
keyword: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -187,9 +185,7 @@ function handleSearch(keyword: string) {
|
||||
searchKeyword.value = keyword;
|
||||
pageDataRef.value?.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
isQueryOr: true,
|
||||
name: keyword || undefined,
|
||||
description: keyword || undefined,
|
||||
keyword: keyword || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,9 +264,7 @@ function changeCategory(category: any) {
|
||||
selectedCategoryId.value = categoryId;
|
||||
pageDataRef.value?.setQuery({
|
||||
categoryId: categoryId || undefined,
|
||||
isQueryOr: true,
|
||||
name: searchKeyword.value || undefined,
|
||||
description: searchKeyword.value || undefined,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -290,9 +284,7 @@ async function loadCategories() {
|
||||
selectedCategoryId.value = '';
|
||||
initialCategoryChangePending = false;
|
||||
pageDataRef.value?.setQuery?.({
|
||||
isQueryOr: true,
|
||||
name: searchKeyword.value || undefined,
|
||||
description: searchKeyword.value || undefined,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -452,9 +444,7 @@ async function handleDeleteAction(row: AgentInfo) {
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
isQueryOr: true,
|
||||
name: initialListState.keyword || undefined,
|
||||
description: initialListState.keyword || undefined,
|
||||
keyword: initialListState.keyword || undefined,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
|
||||
@@ -100,8 +100,7 @@ watchListRouteState(route, botListRouteSchema, (state) => {
|
||||
pageSize: state.pageSize,
|
||||
queryParams: {
|
||||
categoryId: state.categoryId || undefined,
|
||||
isQueryOr: true,
|
||||
title: state.keyword || undefined,
|
||||
keyword: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -310,8 +309,7 @@ const handleSearch = (params: string) => {
|
||||
searchKeyword.value = params;
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
title: params || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: params || undefined,
|
||||
});
|
||||
};
|
||||
const handleButtonClick = () => {
|
||||
@@ -395,8 +393,7 @@ function changeCategory(category: any) {
|
||||
selectedCategoryId.value = categoryId;
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: categoryId || undefined,
|
||||
title: searchKeyword.value || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
function showControlDialog(item: any) {
|
||||
@@ -471,8 +468,7 @@ const getSideList = async () => {
|
||||
selectedCategoryId.value = '';
|
||||
initialCategoryChangePending = false;
|
||||
pageDataRef.value?.setQuery?.({
|
||||
title: searchKeyword.value || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -538,8 +534,7 @@ function handlePageStateChange(state: {
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
title: initialListState.keyword || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: initialListState.keyword || undefined,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
|
||||
@@ -101,6 +101,7 @@ const loadingAssistants = ref(false);
|
||||
const loadingKnowledgeOptions = ref(false);
|
||||
const sending = ref(false);
|
||||
const sessionSearchKeyword = ref('');
|
||||
const appliedSessionSearchKeyword = ref('');
|
||||
const currentAssistantId = ref<string>();
|
||||
const currentAssistantDetail = ref<any>();
|
||||
const currentSession = ref<any>();
|
||||
@@ -122,6 +123,7 @@ const sessionContextMenuVisible = ref(false);
|
||||
const sessionContextMenuTarget = ref<null | SessionItem>(null);
|
||||
const sessionContextMenuX = ref(0);
|
||||
const sessionContextMenuY = ref(0);
|
||||
let latestSessionRequestId = 0;
|
||||
|
||||
const variantSwitchController = createChatVariantSwitchController<
|
||||
ChatTimeHistoryRecord,
|
||||
@@ -242,17 +244,7 @@ const extraKnowledgeOptionList = computed(() => {
|
||||
return knowledgeOptions.value.filter((item) => !excluded.has(String(item.value)));
|
||||
});
|
||||
const filteredSessions = computed(() => {
|
||||
const keyword = sessionSearchKeyword.value.trim().toLowerCase();
|
||||
if (!keyword) {
|
||||
return sessions.value;
|
||||
}
|
||||
return sessions.value.filter((session) => {
|
||||
return [
|
||||
resolveSessionLabel(session),
|
||||
session.lastMessagePreview,
|
||||
session.assistantName,
|
||||
].some((value) => String(value || '').toLowerCase().includes(keyword));
|
||||
});
|
||||
return sessions.value;
|
||||
});
|
||||
const groupedSessions = computed(() => {
|
||||
const today: SessionItem[] = [];
|
||||
@@ -432,14 +424,21 @@ async function fetchSessions(options: { append?: boolean } = {}) {
|
||||
loadingMoreSessions.value = true;
|
||||
} else {
|
||||
loadingSessions.value = true;
|
||||
loadingMoreSessions.value = false;
|
||||
}
|
||||
const requestId = ++latestSessionRequestId;
|
||||
const nextPageNumber = append ? sessionPage.value.pageNumber + 1 : 1;
|
||||
const requestKeyword = appliedSessionSearchKeyword.value;
|
||||
const [, res] = await tryit(api.get)('/api/v1/chatWorkspace/sessions', {
|
||||
params: {
|
||||
keyword: requestKeyword || undefined,
|
||||
pageNumber: nextPageNumber,
|
||||
pageSize: sessionPage.value.pageSize,
|
||||
},
|
||||
});
|
||||
if (requestId !== latestSessionRequestId) {
|
||||
return;
|
||||
}
|
||||
if (append) {
|
||||
loadingMoreSessions.value = false;
|
||||
} else {
|
||||
@@ -458,6 +457,13 @@ async function fetchSessions(options: { append?: boolean } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSessionSearch() {
|
||||
const keyword = sessionSearchKeyword.value.trim();
|
||||
sessionSearchKeyword.value = keyword;
|
||||
appliedSessionSearchKeyword.value = keyword;
|
||||
void fetchSessions();
|
||||
}
|
||||
|
||||
function mergeFreshSessionRecords(records: SessionItem[]) {
|
||||
const freshIds = new Set(records.map((item) => String(item.sessionId)));
|
||||
const optimisticIds = new Set(optimisticSessionIds.value);
|
||||
@@ -1581,6 +1587,8 @@ async function deleteSession(targetSession?: SessionItem) {
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
placeholder="搜索会话"
|
||||
@clear="handleSessionSearch"
|
||||
@keyup.enter="handleSessionSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1661,7 +1669,7 @@ async function deleteSession(targetSession?: SessionItem) {
|
||||
class="chat-workspace__empty"
|
||||
/>
|
||||
<ElEmpty
|
||||
v-else-if="sessionSearchKeyword"
|
||||
v-else-if="appliedSessionSearchKeyword"
|
||||
description="未找到匹配会话"
|
||||
class="chat-workspace__empty"
|
||||
/>
|
||||
|
||||
@@ -405,9 +405,11 @@ function closeDetail() {
|
||||
<ElInput
|
||||
v-if="isSuperAdmin"
|
||||
v-model="query.userAccount"
|
||||
clearable
|
||||
class="chat-history-page__filter-control is-input"
|
||||
placeholder="搜索聊天用户"
|
||||
placeholder="搜索用户账号"
|
||||
:prefix-icon="Search"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -91,8 +91,7 @@ watchListRouteState(route, documentCollectionListRouteSchema, (state) => {
|
||||
pageSize: state.pageSize,
|
||||
queryParams: {
|
||||
categoryId: state.categoryId || undefined,
|
||||
isQueryOr: true,
|
||||
title: state.keyword || undefined,
|
||||
keyword: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -590,8 +589,7 @@ const handleSearch = (params: any) => {
|
||||
searchKeyword.value = String(params || '');
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
title: searchKeyword.value || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
};
|
||||
const reloadKnowledgeList = () => {
|
||||
@@ -631,8 +629,7 @@ const getCategoryList = async () => {
|
||||
selectedCategoryId.value = '';
|
||||
initialCategoryChangePending = false;
|
||||
pageDataRef.value?.setQuery?.({
|
||||
title: searchKeyword.value || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -763,8 +760,7 @@ function changeCategory(category: any) {
|
||||
selectedCategoryId.value = categoryId;
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: categoryId || undefined,
|
||||
title: searchKeyword.value || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
function handlePageStateChange(state: {
|
||||
@@ -822,8 +818,7 @@ function handlePageStateChange(state: {
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
title: initialListState.keyword || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: initialListState.keyword || undefined,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
|
||||
@@ -100,7 +100,7 @@ defineExpose({
|
||||
},
|
||||
search(searchText: string) {
|
||||
pageDataRef.value?.setQuery?.({
|
||||
title: searchText,
|
||||
keyword: searchText.trim() || undefined,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ const treeData = computed(() => [
|
||||
const refreshList = () => {
|
||||
const query: Record<string, any> = {};
|
||||
if (searchKeyword.value.trim()) {
|
||||
query.question = searchKeyword.value.trim();
|
||||
query.keyword = searchKeyword.value.trim();
|
||||
}
|
||||
if (selectedCategoryId.value !== 'all') {
|
||||
query.categoryId = selectedCategoryId.value;
|
||||
|
||||
@@ -117,7 +117,7 @@ const headerButtons = [
|
||||
},
|
||||
];
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ packageName: params, isQueryOr: true });
|
||||
pageDataRef.value.setQuery({ keyword: params || undefined });
|
||||
};
|
||||
const handleHeaderButtonClick = (button: any) => {
|
||||
if (button.key === 'create') {
|
||||
|
||||
@@ -6,26 +6,23 @@ import {
|
||||
} from './plugin-query';
|
||||
|
||||
describe('plugin query params', () => {
|
||||
it('uses the persisted name field and an explicit like operator', () => {
|
||||
it('uses the unified keyword for plugin name and description search', () => {
|
||||
expect(buildPluginPageQueryParams('7', ' 1213 ')).toEqual({
|
||||
category: '7',
|
||||
name: '1213',
|
||||
name__op: 'like',
|
||||
keyword: '1213',
|
||||
});
|
||||
});
|
||||
|
||||
it('retains category while clearing the keyword', () => {
|
||||
expect(buildPluginPageQueryParams('7', ' ')).toEqual({
|
||||
category: '7',
|
||||
name: undefined,
|
||||
name__op: undefined,
|
||||
keyword: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses contains matching for plugin tool names', () => {
|
||||
it('uses the unified keyword for plugin tool name and description search', () => {
|
||||
expect(buildPluginToolPageQueryParams(' HTTP ')).toEqual({
|
||||
name: 'HTTP',
|
||||
name__op: 'like',
|
||||
keyword: 'HTTP',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,17 @@ function normalizeKeyword(keyword?: string): string {
|
||||
}
|
||||
|
||||
function buildPluginPageQueryParams(categoryId: string, keyword?: string) {
|
||||
const name = normalizeKeyword(keyword);
|
||||
const normalizedKeyword = normalizeKeyword(keyword);
|
||||
return {
|
||||
category: categoryId || '0',
|
||||
name: name || undefined,
|
||||
name__op: name ? 'like' : undefined,
|
||||
keyword: normalizedKeyword || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPluginToolPageQueryParams(keyword?: string) {
|
||||
const name = normalizeKeyword(keyword);
|
||||
const normalizedKeyword = normalizeKeyword(keyword);
|
||||
return {
|
||||
name: name || undefined,
|
||||
name__op: name ? 'like' : undefined,
|
||||
keyword: normalizedKeyword || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const saveDialog = ref();
|
||||
const previewDialog = ref();
|
||||
const selectedCategoryId = ref('');
|
||||
const formInline = ref({
|
||||
resourceName: '',
|
||||
resourceType: '',
|
||||
@@ -68,13 +69,19 @@ function initDict() {
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
keyword: formInline.value.resourceName.trim() || undefined,
|
||||
resourceType: formInline.value.resourceType || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
});
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
@@ -177,7 +184,12 @@ const sideFormRules = computed(() => {
|
||||
const sideSaveLoading = ref(false);
|
||||
|
||||
function changeCategory(category: any) {
|
||||
pageDataRef.value.setQuery({ categoryId: category.id });
|
||||
selectedCategoryId.value = String(category.id || '');
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
keyword: formInline.value.resourceName.trim() || undefined,
|
||||
resourceType: formInline.value.resourceType || undefined,
|
||||
});
|
||||
}
|
||||
function showControlDialog(item: any) {
|
||||
sideFormRef.value?.resetFields();
|
||||
|
||||
@@ -310,7 +310,7 @@ const currentPageSize = ref(initialListState.pageSize);
|
||||
const suppressInitialCategoryChange = ref(Boolean(initialListState.categoryId));
|
||||
const initialQueryParams = {
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
title: initialListState.keyword || undefined,
|
||||
keyword: initialListState.keyword || undefined,
|
||||
};
|
||||
watchListRouteState(route, workflowListRouteSchema, (state) => {
|
||||
if (String(state.categoryId) !== String(selectedCategoryId.value)) {
|
||||
@@ -325,7 +325,7 @@ watchListRouteState(route, workflowListRouteSchema, (state) => {
|
||||
pageSize: state.pageSize,
|
||||
queryParams: {
|
||||
categoryId: state.categoryId || undefined,
|
||||
title: state.keyword || undefined,
|
||||
keyword: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -401,7 +401,7 @@ async function updateVisibilityScope(
|
||||
function applyFilters() {
|
||||
pageDataRef.value?.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
title: searchKeyword.value || undefined,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
function currentListState(): WorkflowListRouteState {
|
||||
|
||||
@@ -66,7 +66,7 @@ watchListRouteState($route, workflowExecListRouteSchema, (state) => {
|
||||
const restored = pageDataRef.value?.restoreState?.({
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
queryParams: { execKey: state.execKey || undefined },
|
||||
queryParams: { keyword: state.execKey || undefined },
|
||||
});
|
||||
if (workflowChanged && !restored) pageDataRef.value?.reload?.();
|
||||
});
|
||||
@@ -79,7 +79,7 @@ function search(formEl: FormInstance | undefined) {
|
||||
if (valid) {
|
||||
appliedExecKey.value = formInline.value.execKey;
|
||||
pageDataRef.value.setQuery({
|
||||
execKey: appliedExecKey.value || undefined,
|
||||
keyword: appliedExecKey.value || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -222,7 +222,7 @@ function getTagType(row: any) {
|
||||
:page-size="initialListState.pageSize"
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
execKey: initialListState.execKey || undefined,
|
||||
keyword: initialListState.execKey || undefined,
|
||||
}"
|
||||
:extra-query-params="{
|
||||
workflowId: $route.query.workflowId,
|
||||
|
||||
@@ -37,7 +37,9 @@ function initDict() {
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
pageDataRef.value.setQuery({
|
||||
keyword: formInline.value.nodeName.trim() || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const headerButtons = [
|
||||
];
|
||||
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ apiKey: params, isQueryOr: true });
|
||||
pageDataRef.value.setQuery({ keyword: params || undefined });
|
||||
};
|
||||
const headerButtonClick = (action: any) => {
|
||||
if (action.key === 'addApiKey') {
|
||||
|
||||
@@ -35,7 +35,9 @@ const formInline = ref({
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
pageDataRef.value.setQuery({
|
||||
keyword: formInline.value.title.trim() || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/* eslint-disable vue/one-component-per-file -- Inline stubs keep this interaction test isolated. */
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import SysDeptList from './SysDeptList.vue';
|
||||
|
||||
const apiMocks = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
const tableMocks = vi.hoisted(() => ({
|
||||
clearSelection: vi.fn(),
|
||||
toggleRowExpansion: vi.fn(),
|
||||
}));
|
||||
const dictStoreMocks = vi.hoisted(() => ({
|
||||
fetchDictionary: vi.fn(),
|
||||
getDictLabel: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('#/api/request', () => ({ api: apiMocks }));
|
||||
vi.mock('#/locales', () => ({ $t: (key: string) => key }));
|
||||
vi.mock('#/store', () => ({ useDictStore: () => dictStoreMocks }));
|
||||
vi.mock('./SysDeptModal.vue', () => ({
|
||||
default: defineComponent({
|
||||
name: 'SysDeptModal',
|
||||
setup: () => () => h('div'),
|
||||
}),
|
||||
}));
|
||||
vi.mock('#/components/dict/DictSelect.vue', () => ({
|
||||
default: defineComponent({
|
||||
name: 'DictSelect',
|
||||
setup: () => () => h('div'),
|
||||
}),
|
||||
}));
|
||||
vi.mock('element-plus', () => ({
|
||||
ElButton: defineComponent({
|
||||
name: 'ElButton',
|
||||
setup(_, { attrs, slots }) {
|
||||
return () => h('button', attrs, slots.default?.());
|
||||
},
|
||||
}),
|
||||
ElForm: defineComponent({
|
||||
name: 'ElForm',
|
||||
setup(_, { expose, slots }) {
|
||||
expose({
|
||||
resetFields: vi.fn(),
|
||||
validate: (callback: (valid: boolean) => void) => callback(true),
|
||||
});
|
||||
return () => h('form', slots.default?.());
|
||||
},
|
||||
}),
|
||||
ElFormItem: defineComponent({
|
||||
name: 'ElFormItem',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
}),
|
||||
ElIcon: defineComponent({
|
||||
name: 'ElIcon',
|
||||
setup(_, { slots }) {
|
||||
return () => h('span', slots.default?.());
|
||||
},
|
||||
}),
|
||||
ElInput: defineComponent({
|
||||
name: 'ElInput',
|
||||
inheritAttrs: false,
|
||||
props: { modelValue: { default: '', type: String } },
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () =>
|
||||
h('input', {
|
||||
...attrs,
|
||||
value: props.modelValue,
|
||||
onInput: (event: Event) =>
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value),
|
||||
});
|
||||
},
|
||||
}),
|
||||
ElMessage: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
ElMessageBox: { confirm: vi.fn() },
|
||||
ElTable: defineComponent({
|
||||
name: 'ElTable',
|
||||
setup(_, { expose, slots }) {
|
||||
expose(tableMocks);
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
}),
|
||||
ElTableColumn: defineComponent({
|
||||
name: 'ElTableColumn',
|
||||
setup: () => () => h('div'),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('sysDeptList search expansion', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
apiMocks.get.mockResolvedValue({ data: [] });
|
||||
});
|
||||
|
||||
it('expands every matched department path after keyword search', async () => {
|
||||
const matchedTree = [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [{ id: 'matched', deptName: '研发一部' }],
|
||||
id: 'branch',
|
||||
deptName: '技术部',
|
||||
},
|
||||
],
|
||||
id: 'root',
|
||||
deptName: '总行',
|
||||
},
|
||||
];
|
||||
const rootDepartment = matchedTree[0];
|
||||
const branchDepartment = rootDepartment?.children[0];
|
||||
if (!rootDepartment || !branchDepartment) {
|
||||
throw new Error('部门搜索测试数据缺少祖先节点');
|
||||
}
|
||||
apiMocks.get
|
||||
.mockResolvedValueOnce({ data: [] })
|
||||
.mockResolvedValueOnce({ data: matchedTree });
|
||||
const wrapper = mount(SysDeptList, {
|
||||
global: {
|
||||
directives: {
|
||||
access: {},
|
||||
loading: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
tableMocks.toggleRowExpansion.mockClear();
|
||||
|
||||
const input = wrapper.get('input');
|
||||
await input.setValue(' 研发 ');
|
||||
await input.trigger('keyup', { key: 'Enter' });
|
||||
await flushPromises();
|
||||
|
||||
expect(apiMocks.get).toHaveBeenLastCalledWith('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
keyword: '研发',
|
||||
status: undefined,
|
||||
},
|
||||
});
|
||||
expect(tableMocks.toggleRowExpansion).toHaveBeenCalledTimes(2);
|
||||
expect(tableMocks.toggleRowExpansion).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
rootDepartment,
|
||||
true,
|
||||
);
|
||||
expect(tableMocks.toggleRowExpansion).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
branchDepartment,
|
||||
true,
|
||||
);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
@@ -23,6 +23,12 @@ import { useDictStore } from '#/store';
|
||||
|
||||
import SysDeptModal from './SysDeptModal.vue';
|
||||
|
||||
interface DepartmentTreeNode {
|
||||
children?: DepartmentTreeNode[];
|
||||
id: number | string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void getTree();
|
||||
initDict();
|
||||
@@ -30,7 +36,7 @@ onMounted(() => {
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||
const treeData = ref([]);
|
||||
const treeData = ref<DepartmentTreeNode[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const batchActionType = ref<'' | 'delete' | 'disable' | 'enable'>('');
|
||||
@@ -83,6 +89,16 @@ function clearSelection() {
|
||||
tableRef.value?.clearSelection();
|
||||
}
|
||||
|
||||
function setTreeExpanded(rows: DepartmentTreeNode[], expanded: boolean) {
|
||||
for (const row of rows) {
|
||||
if (!row.children?.length) {
|
||||
continue;
|
||||
}
|
||||
tableRef.value?.toggleRowExpansion(row, expanded);
|
||||
setTreeExpanded(row.children, expanded);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return selectedRows.value
|
||||
.map((row) => row?.id)
|
||||
@@ -235,14 +251,18 @@ function removeSelected() {
|
||||
async function getTree() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const keyword = formInline.value.deptName.trim();
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
...formInline.value,
|
||||
keyword: keyword || undefined,
|
||||
status: formInline.value.status || undefined,
|
||||
},
|
||||
});
|
||||
treeData.value = res.data || [];
|
||||
clearSelection();
|
||||
await nextTick();
|
||||
setTreeExpanded(treeData.value, keyword.length > 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -258,7 +278,10 @@ async function getTree() {
|
||||
<ElInput
|
||||
class="search-input"
|
||||
v-model="formInline.deptName"
|
||||
:placeholder="$t('sysDept.deptName')"
|
||||
clearable
|
||||
:placeholder="$t('sysDept.searchPlaceholder')"
|
||||
@clear="search(formRef)"
|
||||
@keyup.enter="search(formRef)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="status" class="!mr-3">
|
||||
|
||||
@@ -108,7 +108,7 @@ watchListRouteState(route, sysFeedbackListRouteSchema, (state) => {
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
queryParams: {
|
||||
feedbackContent: state.feedbackContent || undefined,
|
||||
keyword: state.feedbackContent || undefined,
|
||||
feedbackType: state.feedbackType || undefined,
|
||||
status: state.status || undefined,
|
||||
},
|
||||
@@ -119,7 +119,11 @@ function search(formEl?: FormInstance) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
appliedFormData.value = { ...formData.value };
|
||||
pageDataRef.value.setQuery(appliedFormData.value);
|
||||
pageDataRef.value.setQuery({
|
||||
feedbackType: appliedFormData.value.feedbackType || undefined,
|
||||
keyword: appliedFormData.value.feedbackContent.trim() || undefined,
|
||||
status: appliedFormData.value.status || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -242,7 +246,7 @@ function openFeedbackDetail(row: any) {
|
||||
:page-size="initialListState.pageSize"
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
feedbackContent: initialListState.feedbackContent || undefined,
|
||||
keyword: initialListState.feedbackContent || undefined,
|
||||
feedbackType: initialListState.feedbackType || undefined,
|
||||
status: initialListState.status || undefined,
|
||||
}"
|
||||
|
||||
@@ -61,8 +61,7 @@ watchListRouteState(route, sysJobListRouteSchema, (state) => {
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
queryParams: {
|
||||
isQueryOr: true,
|
||||
jobName: state.keyword || undefined,
|
||||
keyword: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -88,8 +87,7 @@ function initDict() {
|
||||
const handleSearch = (params: string) => {
|
||||
searchKeyword.value = params;
|
||||
pageDataRef.value.setQuery({
|
||||
jobName: params || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: params || undefined,
|
||||
});
|
||||
};
|
||||
function reloadCurrentList() {
|
||||
@@ -230,8 +228,7 @@ function handlePageStateChange(state: {
|
||||
:page-size="initialListState.pageSize"
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
jobName: initialListState.keyword || undefined,
|
||||
isQueryOr: true,
|
||||
keyword: initialListState.keyword || undefined,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
|
||||
@@ -37,7 +37,7 @@ function buildTimeQuery(range: string[]) {
|
||||
|
||||
function applyQuery() {
|
||||
pageDataRef.value?.setQuery({
|
||||
actionName: actionName.value || undefined,
|
||||
keyword: actionName.value || undefined,
|
||||
...buildTimeQuery(timeRange.value),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,8 +47,7 @@ function initDict() {
|
||||
}
|
||||
|
||||
const handleSearch = (params: string) => {
|
||||
// 后端支持 positionName 模糊查询
|
||||
pageDataRef.value.setQuery({ positionName: params });
|
||||
pageDataRef.value.setQuery({ keyword: params || undefined });
|
||||
};
|
||||
|
||||
function reset(formEl?: FormInstance) {
|
||||
|
||||
@@ -49,7 +49,7 @@ function isAdminRole(data: any) {
|
||||
return data?.roleKey === 'super_admin';
|
||||
}
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ roleName: params, isQueryOr: true });
|
||||
pageDataRef.value.setQuery({ keyword: params || undefined });
|
||||
};
|
||||
function reset(formEl?: FormInstance) {
|
||||
formEl?.resetFields();
|
||||
|
||||
Reference in New Issue
Block a user