This commit is contained in:
+3
-2
@@ -6,6 +6,7 @@ import {
|
|||||||
buildFilters,
|
buildFilters,
|
||||||
createFieldState,
|
createFieldState,
|
||||||
groupRows,
|
groupRows,
|
||||||
|
resolveQueryOption,
|
||||||
} from './composables/useFilters.js';
|
} from './composables/useFilters.js';
|
||||||
import UnknownPlaceholder from './components/UnknownPlaceholder.vue';
|
import UnknownPlaceholder from './components/UnknownPlaceholder.vue';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -114,7 +115,7 @@ defineExpose({ getFilters, setFilters, loadFilters, reset });
|
|||||||
:disabled="state[fieldDef.field]?.disabled"
|
:disabled="state[fieldDef.field]?.disabled"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="opt in fieldDef['query-option'] || []"
|
v-for="opt in resolveQueryOption(fieldDef)"
|
||||||
:key="opt.value"
|
:key="opt.value"
|
||||||
:value="opt.value"
|
:value="opt.value"
|
||||||
>
|
>
|
||||||
@@ -131,7 +132,7 @@ defineExpose({ getFilters, setFilters, loadFilters, reset });
|
|||||||
:placeholder="fieldDef['field-name']"
|
:placeholder="fieldDef['field-name']"
|
||||||
:field="fieldDef.field"
|
:field="fieldDef.field"
|
||||||
:field-name="fieldDef['field-name']"
|
:field-name="fieldDef['field-name']"
|
||||||
:query-option="fieldDef['query-option'] || []"
|
:query-option="resolveQueryOption(fieldDef)"
|
||||||
:schema="fieldDef"
|
:schema="fieldDef"
|
||||||
:options="fieldDef.options || []"
|
:options="fieldDef.options || []"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,22 @@
|
|||||||
* @property {string} [query]
|
* @property {string} [query]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getDefaultQueryOption } from '../config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析字段的查询操作符列表:
|
||||||
|
* - schema 显式配置了 query-option 时,优先使用字段自己的;
|
||||||
|
* - 否则若配置了自定义 query-component(操作符由组件自己管理),返回空数组、不套默认;
|
||||||
|
* - 否则返回默认操作符列表(getDefaultQueryOption 深拷贝,调用方可安全修改)。
|
||||||
|
* @param {SchemaField} [fieldDef]
|
||||||
|
* @returns {Array<{value: string, label?: string}>}
|
||||||
|
*/
|
||||||
|
export function resolveQueryOption(fieldDef) {
|
||||||
|
if (fieldDef?.['query-option']) return fieldDef['query-option'];
|
||||||
|
if (fieldDef?.['query-component']) return [];
|
||||||
|
return getDefaultQueryOption();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SchemaField[]} schema
|
* @param {SchemaField[]} schema
|
||||||
*/
|
*/
|
||||||
@@ -26,7 +42,7 @@ export function createFieldState(schema) {
|
|||||||
const defaultQuery =
|
const defaultQuery =
|
||||||
item['default-query'] ??
|
item['default-query'] ??
|
||||||
item.query ??
|
item.query ??
|
||||||
item['query-option']?.[0]?.value ??
|
resolveQueryOption(item)[0]?.value ??
|
||||||
'EQ';
|
'EQ';
|
||||||
state[item.field] = {
|
state[item.field] = {
|
||||||
value: item.value ?? '',
|
value: item.value ?? '',
|
||||||
@@ -60,7 +76,7 @@ export function buildFilters(schema, state) {
|
|||||||
const row = {
|
const row = {
|
||||||
field: key,
|
field: key,
|
||||||
value: empty ? null : value,
|
value: empty ? null : value,
|
||||||
query: current.query ?? fieldDef['query-option']?.[0]?.value ?? 'EQ',
|
query: current.query ?? resolveQueryOption(fieldDef)[0]?.value ?? 'EQ',
|
||||||
hidden: current.hidden === true,
|
hidden: current.hidden === true,
|
||||||
disabled: current.disabled === true,
|
disabled: current.disabled === true,
|
||||||
readOnly: current.readOnly === true,
|
readOnly: current.readOnly === true,
|
||||||
|
|||||||
+36
-1
@@ -20,13 +20,35 @@ export const BUILTIN_COMPONENT_MAP = {
|
|||||||
between: HeadlessBetween,
|
between: HeadlessBetween,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @typedef {{ componentMap?: Record<string, import('vue').Component>, colRatio?: { name?: number, query?: number, value?: number }, changeEmit?: 'immediate'|'debounce'|'manual' }} TransFormConfig */
|
/**
|
||||||
|
* 默认查询操作符全集(对应 DESIGN.md Q12/Q61 操作符码表)。
|
||||||
|
* 注意:这是共享常量,请通过 getDefaultQueryOption() 获取深拷贝后再修改,
|
||||||
|
* 不要直接改动本常量,以免污染全局配置。
|
||||||
|
*/
|
||||||
|
export const DEFAULT_QUERY_OPTION = [
|
||||||
|
{ value: 'EQ', label: '=' },
|
||||||
|
{ value: 'NE', label: '≠' },
|
||||||
|
{ value: 'GT', label: '>' },
|
||||||
|
{ value: 'GTE', label: '≥' },
|
||||||
|
{ value: 'LT', label: '<' },
|
||||||
|
{ value: 'LTE', label: '≤' },
|
||||||
|
{ value: 'LIKE', label: 'LIKE' },
|
||||||
|
{ value: 'IN', label: 'IN' },
|
||||||
|
{ value: 'NOT_IN', label: 'NOT IN' },
|
||||||
|
{ value: 'BETWEEN', label: 'BETWEEN' },
|
||||||
|
{ value: 'IS_NULL', label: 'IS NULL' },
|
||||||
|
{ value: 'IS_NOT_NULL', label: 'IS NOT NULL' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @typedef {{ componentMap?: Record<string, import('vue').Component>, colRatio?: { name?: number, query?: number, value?: number }, changeEmit?: 'immediate'|'debounce'|'manual', queryOption?: Array<{value: string, label?: string}> }} TransFormConfig */
|
||||||
|
|
||||||
/** @type {TransFormConfig & { componentMap: Record<string, import('vue').Component> }} */
|
/** @type {TransFormConfig & { componentMap: Record<string, import('vue').Component> }} */
|
||||||
const globalConfig = {
|
const globalConfig = {
|
||||||
componentMap: { ...BUILTIN_COMPONENT_MAP },
|
componentMap: { ...BUILTIN_COMPONENT_MAP },
|
||||||
colRatio: { name: 3, query: 2, value: 7 },
|
colRatio: { name: 3, query: 2, value: 7 },
|
||||||
changeEmit: 'immediate',
|
changeEmit: 'immediate',
|
||||||
|
// 默认操作符列表(深拷贝一份,避免外部修改 DEFAULT_QUERY_OPTION 常量污染配置)
|
||||||
|
queryOption: structuredClone(DEFAULT_QUERY_OPTION),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,6 +65,18 @@ export function setConfig(patch) {
|
|||||||
globalConfig.colRatio = { ...globalConfig.colRatio, ...patch.colRatio };
|
globalConfig.colRatio = { ...globalConfig.colRatio, ...patch.colRatio };
|
||||||
}
|
}
|
||||||
if (patch.changeEmit) globalConfig.changeEmit = patch.changeEmit;
|
if (patch.changeEmit) globalConfig.changeEmit = patch.changeEmit;
|
||||||
|
// queryOption:整体替换整个数组(深拷贝存储,防止外部后续修改污染配置)
|
||||||
|
if (patch.queryOption) {
|
||||||
|
globalConfig.queryOption = structuredClone(patch.queryOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认查询操作符列表(始终返回深拷贝,调用方可安全修改,不影响全局配置)。
|
||||||
|
* @returns {Array<{value: string, label?: string}>}
|
||||||
|
*/
|
||||||
|
export function getDefaultQueryOption() {
|
||||||
|
return structuredClone(globalConfig.queryOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
@@ -53,4 +87,5 @@ export function resetConfig() {
|
|||||||
globalConfig.componentMap = { ...BUILTIN_COMPONENT_MAP };
|
globalConfig.componentMap = { ...BUILTIN_COMPONENT_MAP };
|
||||||
globalConfig.colRatio = { name: 3, query: 2, value: 7 };
|
globalConfig.colRatio = { name: 3, query: 2, value: 7 };
|
||||||
globalConfig.changeEmit = 'immediate';
|
globalConfig.changeEmit = 'immediate';
|
||||||
|
globalConfig.queryOption = structuredClone(DEFAULT_QUERY_OPTION);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import './styles/trans-form.css';
|
|||||||
|
|
||||||
export { default as TransFormPlugin, TransForm, setConfig, getConfig } from './plugin.js';
|
export { default as TransFormPlugin, TransForm, setConfig, getConfig } from './plugin.js';
|
||||||
|
|
||||||
|
export { DEFAULT_QUERY_OPTION, getDefaultQueryOption, resetConfig } from './config.js';
|
||||||
|
export { resolveQueryOption } from './composables/useFilters.js';
|
||||||
|
|
||||||
export { default as HeadlessInput } from './components/HeadlessInput.vue';
|
export { default as HeadlessInput } from './components/HeadlessInput.vue';
|
||||||
export { default as HeadlessTextarea } from './components/HeadlessTextarea.vue';
|
export { default as HeadlessTextarea } from './components/HeadlessTextarea.vue';
|
||||||
export { default as HeadlessSelect } from './components/HeadlessSelect.vue';
|
export { default as HeadlessSelect } from './components/HeadlessSelect.vue';
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import TransForm from '../src/TransForm.vue';
|
||||||
|
import { resetConfig, setConfig } from '../src/config.js';
|
||||||
|
|
||||||
|
// 每个用例后恢复全局配置,避免 setConfig 污染其它用例
|
||||||
|
afterEach(() => {
|
||||||
|
resetConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TransForm 操作符下拉(默认 query-option)', () => {
|
||||||
|
it('无 query-option 的字段渲染默认操作符列表(12 项,第一项 EQ)', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: { schema: [{ row: 12, field: 'name', 'field-name': '名称' }] },
|
||||||
|
});
|
||||||
|
const opts = wrapper.find('select').findAll('option');
|
||||||
|
expect(opts).toHaveLength(12);
|
||||||
|
expect(opts[0].text()).toBe('=');
|
||||||
|
expect(opts[0].attributes('value')).toBe('EQ');
|
||||||
|
expect(opts[11].text()).toBe('IS NOT NULL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('有 query-option 的字段仍渲染 schema 自己的选项(不被默认覆盖)', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'status',
|
||||||
|
'field-name': '状态',
|
||||||
|
'query-option': [{ value: 'NE', label: '≠' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const opts = wrapper.find('select').findAll('option');
|
||||||
|
expect(opts).toHaveLength(1);
|
||||||
|
expect(opts[0].text()).toBe('≠');
|
||||||
|
expect(opts[0].attributes('value')).toBe('NE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setConfig 自定义 queryOption 后,无 query-option 字段的下拉随之更新', () => {
|
||||||
|
setConfig({ queryOption: [{ value: 'GT', label: '>' }] });
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: { schema: [{ row: 12, field: 'age', 'field-name': '年龄' }] },
|
||||||
|
});
|
||||||
|
const opts = wrapper.find('select').findAll('option');
|
||||||
|
expect(opts).toHaveLength(1);
|
||||||
|
expect(opts[0].text()).toBe('>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('有 query-component 但无 query-option 的字段不套默认下拉(操作符由组件自己管理)', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [{ row: 12, field: 'custom', 'query-component': 'CustomQuery' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.find('select').findAll('option')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
+92
-1
@@ -1,11 +1,18 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
import { getDefaultQueryOption, resetConfig, setConfig } from '../src/config.js';
|
||||||
import {
|
import {
|
||||||
applyFilters,
|
applyFilters,
|
||||||
buildFilters,
|
buildFilters,
|
||||||
createFieldState,
|
createFieldState,
|
||||||
groupRows,
|
groupRows,
|
||||||
|
resolveQueryOption,
|
||||||
} from '../src/composables/useFilters.js';
|
} from '../src/composables/useFilters.js';
|
||||||
|
|
||||||
|
// 每个用例后恢复全局配置,避免 setConfig 污染其它用例
|
||||||
|
afterEach(() => {
|
||||||
|
resetConfig();
|
||||||
|
});
|
||||||
|
|
||||||
const schema = [
|
const schema = [
|
||||||
{
|
{
|
||||||
row: 6,
|
row: 6,
|
||||||
@@ -68,3 +75,87 @@ describe('groupRows', () => {
|
|||||||
expect(groups[1]).toHaveLength(1);
|
expect(groups[1]).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resolveQueryOption(默认 query-option 解析)', () => {
|
||||||
|
it('无 query-option 且无 query-component 时返回默认操作符列表', () => {
|
||||||
|
const opts = resolveQueryOption({ field: 'name' });
|
||||||
|
expect(opts).toHaveLength(12);
|
||||||
|
expect(opts[0]).toEqual({ value: 'EQ', label: '=' });
|
||||||
|
expect(opts.map((o) => o.value)).toEqual([
|
||||||
|
'EQ', 'NE', 'GT', 'GTE', 'LT', 'LTE',
|
||||||
|
'LIKE', 'IN', 'NOT_IN', 'BETWEEN', 'IS_NULL', 'IS_NOT_NULL',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('有 query-option 的字段仍用 schema 自己的(不被默认覆盖)', () => {
|
||||||
|
const fieldDef = { field: 'name', 'query-option': [{ value: 'NE', label: '≠' }] };
|
||||||
|
expect(resolveQueryOption(fieldDef)).toEqual([{ value: 'NE', label: '≠' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('有 query-component 但无 query-option 时返回空数组(组件自己管理)', () => {
|
||||||
|
const fieldDef = { field: 'name', 'query-component': 'CustomQuery' };
|
||||||
|
expect(resolveQueryOption(fieldDef)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getDefaultQueryOption() 返回深拷贝:修改返回值不影响后续调用', () => {
|
||||||
|
const first = getDefaultQueryOption();
|
||||||
|
first.push({ value: 'X', label: 'x' });
|
||||||
|
first[0].label = '被改坏了';
|
||||||
|
first.splice(1, 1);
|
||||||
|
|
||||||
|
const second = getDefaultQueryOption();
|
||||||
|
expect(second).toHaveLength(12);
|
||||||
|
expect(second[0]).toEqual({ value: 'EQ', label: '=' });
|
||||||
|
expect(second[1]).toEqual({ value: 'NE', label: '≠' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setConfig({ queryOption }) 后 getDefaultQueryOption() 返回新列表', () => {
|
||||||
|
setConfig({
|
||||||
|
queryOption: [
|
||||||
|
{ value: 'EQ', label: '=' },
|
||||||
|
{ value: 'GT', label: '>' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(getDefaultQueryOption()).toEqual([
|
||||||
|
{ value: 'EQ', label: '=' },
|
||||||
|
{ value: 'GT', label: '>' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resetConfig() 恢复默认操作符列表', () => {
|
||||||
|
setConfig({ queryOption: [{ value: 'ONLY_EQ', label: '=' }] });
|
||||||
|
resetConfig();
|
||||||
|
expect(getDefaultQueryOption()).toHaveLength(12);
|
||||||
|
expect(getDefaultQueryOption()[0]).toEqual({ value: 'EQ', label: '=' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('默认 query fallback(无 query-option 时取默认列表第一项)', () => {
|
||||||
|
it('createFieldState:无 query-option 且无 query-component 默认取默认列表第一项(EQ)', () => {
|
||||||
|
const state = createFieldState([{ field: 'name' }]);
|
||||||
|
expect(state.name.query).toBe('EQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('buildFilters:无 query-option 且无 query-component 默认取默认列表第一项(EQ)', () => {
|
||||||
|
const state = createFieldState([{ field: 'name' }]);
|
||||||
|
const filters = buildFilters([{ field: 'name' }], state);
|
||||||
|
expect(filters[0].query).toBe('EQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setConfig 自定义 queryOption 后,默认 query 取新列表第一项', () => {
|
||||||
|
setConfig({ queryOption: [{ value: 'GT', label: '>' }] });
|
||||||
|
const state = createFieldState([{ field: 'age' }]);
|
||||||
|
expect(state.age.query).toBe('GT');
|
||||||
|
expect(buildFilters([{ field: 'age' }], state)[0].query).toBe('GT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('有 query-component 但无 query-option 时,默认 query 仍为 EQ', () => {
|
||||||
|
const state = createFieldState([{ field: 'custom', 'query-component': 'CustomQuery' }]);
|
||||||
|
expect(state.custom.query).toBe('EQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('显式 default-query 优先级最高,覆盖默认列表第一项', () => {
|
||||||
|
const state = createFieldState([{ field: 'name', 'default-query': 'LIKE' }]);
|
||||||
|
expect(state.name.query).toBe('LIKE');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user