chore: 版本 0.1.0 更新 between-ui/in-ui 可配项与 query-component 完整支持
Node.js Build / build (push) Successful in 6s
Node.js Build / build (push) Successful in 6s
This commit is contained in:
+57
-6
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, watch } from 'vue';
|
import { computed, markRaw, reactive, toRaw, watch } from 'vue';
|
||||||
import { getConfig } from './config.js';
|
import { getConfig } from './config.js';
|
||||||
import {
|
import {
|
||||||
applyFilters,
|
applyFilters,
|
||||||
@@ -16,7 +16,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['change']);
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
const initialState = createFieldState(props.schema);
|
|
||||||
const state = reactive(createFieldState(props.schema));
|
const state = reactive(createFieldState(props.schema));
|
||||||
|
|
||||||
const rowGroups = computed(() => groupRows(props.schema));
|
const rowGroups = computed(() => groupRows(props.schema));
|
||||||
@@ -31,10 +30,36 @@ const colStyle = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析控件:
|
||||||
|
* - kind === 'query':读 query-component(未配置时由模板走默认 select)
|
||||||
|
* - kind === 'value':按当前 query 优先 between-ui(BETWEEN)/ in-ui(IN|NOT_IN),
|
||||||
|
* 未配置时默认 between=双 input、in=tags;其它 query 走 value-component
|
||||||
|
*/
|
||||||
|
function resolveRaw(fieldDef, kind) {
|
||||||
|
if (kind === 'query') {
|
||||||
|
return { key: 'query-component', raw: fieldDef['query-component'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = state[fieldDef.field]?.query;
|
||||||
|
if (query === 'BETWEEN') {
|
||||||
|
return {
|
||||||
|
key: 'between-ui',
|
||||||
|
raw: fieldDef['between-ui'] ?? 'between',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (query === 'IN' || query === 'NOT_IN') {
|
||||||
|
return {
|
||||||
|
key: 'in-ui',
|
||||||
|
raw: fieldDef['in-ui'] ?? 'tags',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { key: 'value-component', raw: fieldDef['value-component'] };
|
||||||
|
}
|
||||||
|
|
||||||
function resolveComponent(fieldDef, kind) {
|
function resolveComponent(fieldDef, kind) {
|
||||||
const key = kind === 'query' ? 'query-component' : 'value-component';
|
const { key, raw } = resolveRaw(fieldDef, kind);
|
||||||
const raw = fieldDef[key] ?? fieldDef['value-component'];
|
if (raw && typeof raw === 'object') return markRaw(toRaw(raw));
|
||||||
if (raw && typeof raw === 'object') return raw;
|
|
||||||
const name = typeof raw === 'string' ? raw : 'input';
|
const name = typeof raw === 'string' ? raw : 'input';
|
||||||
const mapped = config.value.componentMap[name];
|
const mapped = config.value.componentMap[name];
|
||||||
if (mapped) return mapped;
|
if (mapped) return mapped;
|
||||||
@@ -42,6 +67,16 @@ function resolveComponent(fieldDef, kind) {
|
|||||||
return UnknownPlaceholder;
|
return UnknownPlaceholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveComponentName(fieldDef, kind) {
|
||||||
|
const { raw } = resolveRaw(fieldDef, kind);
|
||||||
|
if (raw && typeof raw === 'object') return 'custom';
|
||||||
|
return typeof raw === 'string' ? raw : 'input';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasQueryComponent(fieldDef) {
|
||||||
|
return fieldDef['query-component'] != null;
|
||||||
|
}
|
||||||
|
|
||||||
function isNullQuery(fieldDef) {
|
function isNullQuery(fieldDef) {
|
||||||
const q = state[fieldDef.field]?.query;
|
const q = state[fieldDef.field]?.query;
|
||||||
return q === 'IS_NULL' || q === 'IS_NOT_NULL';
|
return q === 'IS_NULL' || q === 'IS_NOT_NULL';
|
||||||
@@ -110,6 +145,7 @@ defineExpose({ getFilters, setFilters, loadFilters, reset });
|
|||||||
<label class="tf-label">{{ fieldDef['field-name'] || fieldDef.field }}</label>
|
<label class="tf-label">{{ fieldDef['field-name'] || fieldDef.field }}</label>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
|
v-if="!hasQueryComponent(fieldDef)"
|
||||||
class="tf-control"
|
class="tf-control"
|
||||||
v-model="state[fieldDef.field].query"
|
v-model="state[fieldDef.field].query"
|
||||||
:disabled="state[fieldDef.field]?.disabled"
|
:disabled="state[fieldDef.field]?.disabled"
|
||||||
@@ -123,12 +159,27 @@ defineExpose({ getFilters, setFilters, loadFilters, reset });
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<component
|
||||||
|
v-else
|
||||||
|
:is="resolveComponent(fieldDef, 'query')"
|
||||||
|
v-model="state[fieldDef.field].query"
|
||||||
|
:disabled="state[fieldDef.field]?.disabled"
|
||||||
|
:read-only="state[fieldDef.field]?.readOnly"
|
||||||
|
:name="resolveComponentName(fieldDef, 'query')"
|
||||||
|
:placeholder="fieldDef['field-name']"
|
||||||
|
:field="fieldDef.field"
|
||||||
|
:field-name="fieldDef['field-name']"
|
||||||
|
:query-option="resolveQueryOption(fieldDef)"
|
||||||
|
:schema="fieldDef"
|
||||||
|
:options="fieldDef.options || []"
|
||||||
|
/>
|
||||||
|
|
||||||
<component
|
<component
|
||||||
:is="resolveComponent(fieldDef, 'value')"
|
:is="resolveComponent(fieldDef, 'value')"
|
||||||
v-model="state[fieldDef.field].value"
|
v-model="state[fieldDef.field].value"
|
||||||
:disabled="state[fieldDef.field]?.disabled || isNullQuery(fieldDef)"
|
:disabled="state[fieldDef.field]?.disabled || isNullQuery(fieldDef)"
|
||||||
:read-only="state[fieldDef.field]?.readOnly"
|
:read-only="state[fieldDef.field]?.readOnly"
|
||||||
:name="typeof fieldDef['value-component'] === 'string' ? fieldDef['value-component'] : 'custom'"
|
:name="resolveComponentName(fieldDef, 'value')"
|
||||||
:placeholder="fieldDef['field-name']"
|
:placeholder="fieldDef['field-name']"
|
||||||
:field="fieldDef.field"
|
:field="fieldDef.field"
|
||||||
:field-name="fieldDef['field-name']"
|
:field-name="fieldDef['field-name']"
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* @property {'AND'|'OR'} [logic]
|
* @property {'AND'|'OR'} [logic]
|
||||||
* @property {string|import('vue').Component} [value-component]
|
* @property {string|import('vue').Component} [value-component]
|
||||||
* @property {string|import('vue').Component} [query-component]
|
* @property {string|import('vue').Component} [query-component]
|
||||||
|
* @property {string|import('vue').Component} [between-ui]
|
||||||
|
* @property {string|import('vue').Component} [in-ui]
|
||||||
* @property {string} field
|
* @property {string} field
|
||||||
* @property {string} [field-name]
|
* @property {string} [field-name]
|
||||||
* @property {Array<{value: string, label?: string}>} [query-option]
|
* @property {Array<{value: string, label?: string}>} [query-option]
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const schema = [
|
|||||||
'value-component': 'between',
|
'value-component': 'between',
|
||||||
field: 'created',
|
field: 'created',
|
||||||
'field-name': '创建时间',
|
'field-name': '创建时间',
|
||||||
'between-ui': 'dual-input',
|
'between-ui': 'between',
|
||||||
'query-option': [{ value: 'BETWEEN', label: '↔', 'desc-cn': '区间' }],
|
'query-option': [{ value: 'BETWEEN', label: '↔', 'desc-cn': '区间' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+242
-1
@@ -1,6 +1,11 @@
|
|||||||
import { afterEach, describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
import { defineComponent, h } from 'vue';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import TransForm from '../src/TransForm.vue';
|
import TransForm from '../src/TransForm.vue';
|
||||||
|
import HeadlessBetween from '../src/components/HeadlessBetween.vue';
|
||||||
|
import HeadlessDate from '../src/components/HeadlessDate.vue';
|
||||||
|
import HeadlessSelect from '../src/components/HeadlessSelect.vue';
|
||||||
|
import HeadlessTags from '../src/components/HeadlessTags.vue';
|
||||||
import { resetConfig, setConfig } from '../src/config.js';
|
import { resetConfig, setConfig } from '../src/config.js';
|
||||||
|
|
||||||
// 每个用例后恢复全局配置,避免 setConfig 污染其它用例
|
// 每个用例后恢复全局配置,避免 setConfig 污染其它用例
|
||||||
@@ -55,6 +60,242 @@ describe('TransForm 操作符下拉(默认 query-option)', () => {
|
|||||||
schema: [{ row: 12, field: 'custom', 'query-component': 'CustomQuery' }],
|
schema: [{ row: 12, field: 'custom', 'query-component': 'CustomQuery' }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('select').findAll('option')).toHaveLength(0);
|
// 配置了 query-component 后不再渲染默认 select,未知组件走占位
|
||||||
|
expect(wrapper.find('select').exists()).toBe(false);
|
||||||
|
expect(wrapper.find('.tf-unknown').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('between-ui / in-ui(DESIGN Q32/Q33)', () => {
|
||||||
|
it('未配置 between-ui 且 query=BETWEEN 时默认渲染双 input(HeadlessBetween)', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'age',
|
||||||
|
'field-name': '年龄',
|
||||||
|
'value-component': 'number',
|
||||||
|
'default-query': 'BETWEEN',
|
||||||
|
'query-option': [{ value: 'BETWEEN', label: 'BETWEEN' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.findComponent(HeadlessBetween).exists()).toBe(true);
|
||||||
|
expect(wrapper.findAll('.tf-between-input')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('未配置 in-ui 且 query=IN 时默认渲染 tags(HeadlessTags)', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'tags',
|
||||||
|
'field-name': '标签',
|
||||||
|
'value-component': 'input',
|
||||||
|
'default-query': 'IN',
|
||||||
|
'query-option': [{ value: 'IN', label: 'IN' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.findComponent(HeadlessTags).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('between-ui 指定 date 时渲染 HeadlessDate', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'age',
|
||||||
|
'field-name': '年龄',
|
||||||
|
'value-component': 'between',
|
||||||
|
'between-ui': 'date',
|
||||||
|
'default-query': 'BETWEEN',
|
||||||
|
'query-option': [{ value: 'BETWEEN', label: 'BETWEEN' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.findComponent(HeadlessDate).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(HeadlessBetween).exists()).toBe(false);
|
||||||
|
expect(wrapper.find('input[type="date"]').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('in-ui 指定 select 时渲染 HeadlessSelect', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'status',
|
||||||
|
'field-name': '状态',
|
||||||
|
'value-component': 'tags',
|
||||||
|
'in-ui': 'select',
|
||||||
|
'default-query': 'IN',
|
||||||
|
options: [
|
||||||
|
{ value: 'a', label: 'A' },
|
||||||
|
{ value: 'b', label: 'B' },
|
||||||
|
],
|
||||||
|
'query-option': [{ value: 'IN', label: 'IN' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.findComponent(HeadlessSelect).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(HeadlessTags).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('非 BETWEEN/IN 时仍走 value-component,忽略 between-ui/in-ui', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'age',
|
||||||
|
'value-component': 'number',
|
||||||
|
'between-ui': 'date',
|
||||||
|
'in-ui': 'select',
|
||||||
|
'default-query': 'EQ',
|
||||||
|
'query-option': [
|
||||||
|
{ value: 'EQ', label: '=' },
|
||||||
|
{ value: 'BETWEEN', label: 'BETWEEN' },
|
||||||
|
{ value: 'IN', label: 'IN' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.find('input[type="number"]').exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(HeadlessDate).exists()).toBe(false);
|
||||||
|
expect(wrapper.findComponent(HeadlessSelect).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('query-component(DESIGN Q17/Q18)', () => {
|
||||||
|
it('无 query-component 时保持默认操作符下拉', () => {
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'name',
|
||||||
|
'field-name': '名称',
|
||||||
|
'query-option': [
|
||||||
|
{ value: 'EQ', label: '=' },
|
||||||
|
{ value: 'LIKE', label: 'LIKE' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const select = wrapper.find('select');
|
||||||
|
expect(select.exists()).toBe(true);
|
||||||
|
expect(select.findAll('option')).toHaveLength(2);
|
||||||
|
expect(wrapper.vm.getFilters()[0].query).toBe('EQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('配置 query-component 时渲染自定义组件且 v-model 绑定 query', async () => {
|
||||||
|
const CustomQuery = defineComponent({
|
||||||
|
name: 'CustomQuery',
|
||||||
|
props: {
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
field: { type: String, default: '' },
|
||||||
|
fieldName: { type: String, default: '' },
|
||||||
|
queryOption: { type: Array, default: () => [] },
|
||||||
|
schema: { type: Object, default: () => ({}) },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
readOnly: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
return () =>
|
||||||
|
h('button', {
|
||||||
|
class: 'custom-query-btn',
|
||||||
|
type: 'button',
|
||||||
|
disabled: props.disabled,
|
||||||
|
onClick: () => emit('update:modelValue', 'LIKE'),
|
||||||
|
}, `query:${props.modelValue}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'name',
|
||||||
|
'field-name': '名称',
|
||||||
|
'value-component': 'input',
|
||||||
|
'query-component': CustomQuery,
|
||||||
|
'default-query': 'EQ',
|
||||||
|
'query-option': [
|
||||||
|
{ value: 'EQ', label: '=' },
|
||||||
|
{ value: 'LIKE', label: 'LIKE' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('select').exists()).toBe(false);
|
||||||
|
const btn = wrapper.find('.custom-query-btn');
|
||||||
|
expect(btn.exists()).toBe(true);
|
||||||
|
expect(btn.text()).toBe('query:EQ');
|
||||||
|
expect(btn.attributes('disabled')).toBeUndefined();
|
||||||
|
|
||||||
|
await btn.trigger('click');
|
||||||
|
expect(wrapper.vm.getFilters()[0].query).toBe('LIKE');
|
||||||
|
expect(wrapper.find('.custom-query-btn').text()).toBe('query:LIKE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('query-component 字符串名可通过 componentMap 注册', async () => {
|
||||||
|
const CustomQuery = defineComponent({
|
||||||
|
name: 'MappedQuery',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
return () =>
|
||||||
|
h('select', {
|
||||||
|
class: 'mapped-query',
|
||||||
|
value: props.modelValue,
|
||||||
|
onChange: (e) => emit('update:modelValue', e.target.value),
|
||||||
|
}, [
|
||||||
|
h('option', { value: 'EQ' }, '='),
|
||||||
|
h('option', { value: 'GT' }, '>'),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setConfig({ componentMap: { MappedQuery: CustomQuery } });
|
||||||
|
|
||||||
|
const wrapper = mount(TransForm, {
|
||||||
|
props: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
row: 12,
|
||||||
|
field: 'age',
|
||||||
|
'query-component': 'MappedQuery',
|
||||||
|
'default-query': 'EQ',
|
||||||
|
'query-option': [
|
||||||
|
{ value: 'EQ', label: '=' },
|
||||||
|
{ value: 'GT', label: '>' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapped = wrapper.find('.mapped-query');
|
||||||
|
expect(mapped.exists()).toBe(true);
|
||||||
|
await mapped.setValue('GT');
|
||||||
|
expect(wrapper.vm.getFilters()[0].query).toBe('GT');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user