chore: 版本 0.1.0 更新补齐内置控件集 textarea/select/number/date/tags/between
Node.js Build / build (push) Successful in 6s
Node.js Build / build (push) Successful in 6s
This commit is contained in:
+11
-6
@@ -7,9 +7,7 @@ import {
|
||||
createFieldState,
|
||||
groupRows,
|
||||
} from './composables/useFilters.js';
|
||||
import HeadlessInput from './components/HeadlessInput.vue';
|
||||
import UnknownPlaceholder from './components/UnknownPlaceholder.vue';
|
||||
|
||||
const props = defineProps({
|
||||
schema: { type: Array, default: () => [] },
|
||||
showReset: { type: Boolean, default: true },
|
||||
@@ -39,13 +37,15 @@ function resolveComponent(fieldDef, kind) {
|
||||
const name = typeof raw === 'string' ? raw : 'input';
|
||||
const mapped = config.value.componentMap[name];
|
||||
if (mapped) return mapped;
|
||||
if (name === 'input' || name === 'textarea' || name === 'number' || name === 'date') {
|
||||
return HeadlessInput;
|
||||
}
|
||||
console.warn(`[TransForm] unknown ${key}: "${name}"`);
|
||||
return UnknownPlaceholder;
|
||||
}
|
||||
|
||||
function isNullQuery(fieldDef) {
|
||||
const q = state[fieldDef.field]?.query;
|
||||
return q === 'IS_NULL' || q === 'IS_NOT_NULL';
|
||||
}
|
||||
|
||||
function emitChange() {
|
||||
emit('change', getFilters());
|
||||
}
|
||||
@@ -125,10 +125,15 @@ defineExpose({ getFilters, setFilters, loadFilters, reset });
|
||||
<component
|
||||
:is="resolveComponent(fieldDef, 'value')"
|
||||
v-model="state[fieldDef.field].value"
|
||||
:disabled="state[fieldDef.field]?.disabled"
|
||||
:disabled="state[fieldDef.field]?.disabled || isNullQuery(fieldDef)"
|
||||
:read-only="state[fieldDef.field]?.readOnly"
|
||||
:name="typeof fieldDef['value-component'] === 'string' ? fieldDef['value-component'] : 'custom'"
|
||||
:placeholder="fieldDef['field-name']"
|
||||
:field="fieldDef.field"
|
||||
:field-name="fieldDef['field-name']"
|
||||
:query-option="fieldDef['query-option'] || []"
|
||||
:schema="fieldDef"
|
||||
:options="fieldDef.options || []"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const start = computed({
|
||||
get() {
|
||||
return String(props.modelValue ?? '').split(',')[0] ?? '';
|
||||
},
|
||||
set(value) {
|
||||
const endVal = String(props.modelValue ?? '').split(',')[1] ?? '';
|
||||
emit('update:modelValue', `${value ?? ''},${endVal}`);
|
||||
},
|
||||
});
|
||||
|
||||
const end = computed({
|
||||
get() {
|
||||
return String(props.modelValue ?? '').split(',')[1] ?? '';
|
||||
},
|
||||
set(value) {
|
||||
const startVal = String(props.modelValue ?? '').split(',')[0] ?? '';
|
||||
emit('update:modelValue', `${startVal},${value ?? ''}`);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
class="tf-control tf-between-input"
|
||||
type="text"
|
||||
v-model="start"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<input
|
||||
class="tf-control tf-between-input"
|
||||
type="text"
|
||||
v-model="end"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
<template>
|
||||
<input
|
||||
class="tf-control tf-date"
|
||||
type="date"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target).value)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
<template>
|
||||
<input
|
||||
class="tf-control tf-number"
|
||||
type="number"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target).value)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
|
||||
const list = computed(() => (props.options?.length ? props.options : props.queryOption));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select
|
||||
class="tf-control tf-select"
|
||||
:value="modelValue"
|
||||
:disabled="disabled || readOnly"
|
||||
@change="$emit('update:modelValue', ($event.target).value)"
|
||||
>
|
||||
<option v-if="placeholder" value="" disabled>{{ placeholder }}</option>
|
||||
<option v-for="opt in list" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const draft = ref('');
|
||||
|
||||
const tags = computed(() => {
|
||||
if (props.modelValue == null || props.modelValue === '') return [];
|
||||
return String(props.modelValue)
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.length > 0);
|
||||
});
|
||||
|
||||
function emitTags(next) {
|
||||
emit('update:modelValue', next.join(','));
|
||||
}
|
||||
|
||||
function commitDraft() {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
const parts = draft.value
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length > 0);
|
||||
draft.value = '';
|
||||
if (!parts.length) return;
|
||||
emitTags([...tags.value, ...parts]);
|
||||
}
|
||||
|
||||
function removeTag(index) {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
const next = [...tags.value];
|
||||
next.splice(index, 1);
|
||||
emitTags(next);
|
||||
}
|
||||
|
||||
function onKeydown(event) {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
if (event.key === 'Enter' || event.key === ',') {
|
||||
event.preventDefault();
|
||||
commitDraft();
|
||||
}
|
||||
}
|
||||
|
||||
function onInput(event) {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
const value = event.target.value;
|
||||
draft.value = value;
|
||||
if (value.includes(',')) {
|
||||
commitDraft();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tf-control tf-tags" :class="{ 'is-disabled': disabled }">
|
||||
<span
|
||||
v-for="(tag, index) in tags"
|
||||
:key="`${tag}-${index}`"
|
||||
class="tf-tag"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
type="button"
|
||||
class="tf-tag-x"
|
||||
:disabled="disabled || readOnly"
|
||||
@click.stop="removeTag(index)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
class="tf-tag-input"
|
||||
type="text"
|
||||
:value="draft"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="onInput"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
class="tf-control tf-textarea"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target).value)"
|
||||
></textarea>
|
||||
</template>
|
||||
+24
-2
@@ -1,8 +1,30 @@
|
||||
import HeadlessInput from './components/HeadlessInput.vue';
|
||||
import HeadlessTextarea from './components/HeadlessTextarea.vue';
|
||||
import HeadlessSelect from './components/HeadlessSelect.vue';
|
||||
import HeadlessNumber from './components/HeadlessNumber.vue';
|
||||
import HeadlessDate from './components/HeadlessDate.vue';
|
||||
import HeadlessTags from './components/HeadlessTags.vue';
|
||||
import HeadlessBetween from './components/HeadlessBetween.vue';
|
||||
|
||||
/**
|
||||
* 内置控件映射(Q5/Q58):字符串 value-component 先查这里,
|
||||
* 用户通过 setConfig({ componentMap }) 可覆盖或新增。
|
||||
*/
|
||||
export const BUILTIN_COMPONENT_MAP = {
|
||||
input: HeadlessInput,
|
||||
textarea: HeadlessTextarea,
|
||||
select: HeadlessSelect,
|
||||
number: HeadlessNumber,
|
||||
date: HeadlessDate,
|
||||
tags: HeadlessTags,
|
||||
between: HeadlessBetween,
|
||||
};
|
||||
|
||||
/** @typedef {{ componentMap?: Record<string, import('vue').Component>, colRatio?: { name?: number, query?: number, value?: number }, changeEmit?: 'immediate'|'debounce'|'manual' }} TransFormConfig */
|
||||
|
||||
/** @type {TransFormConfig & { componentMap: Record<string, import('vue').Component> }} */
|
||||
const globalConfig = {
|
||||
componentMap: {},
|
||||
componentMap: { ...BUILTIN_COMPONENT_MAP },
|
||||
colRatio: { name: 3, query: 2, value: 7 },
|
||||
changeEmit: 'immediate',
|
||||
};
|
||||
@@ -28,7 +50,7 @@ export function getConfig() {
|
||||
}
|
||||
|
||||
export function resetConfig() {
|
||||
globalConfig.componentMap = {};
|
||||
globalConfig.componentMap = { ...BUILTIN_COMPONENT_MAP };
|
||||
globalConfig.colRatio = { name: 3, query: 2, value: 7 };
|
||||
globalConfig.changeEmit = 'immediate';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import './styles/trans-form.css';
|
||||
|
||||
export { default as TransFormPlugin, TransForm, setConfig, getConfig } from './plugin.js';
|
||||
|
||||
export { default as HeadlessInput } from './components/HeadlessInput.vue';
|
||||
export { default as HeadlessTextarea } from './components/HeadlessTextarea.vue';
|
||||
export { default as HeadlessSelect } from './components/HeadlessSelect.vue';
|
||||
export { default as HeadlessNumber } from './components/HeadlessNumber.vue';
|
||||
export { default as HeadlessDate } from './components/HeadlessDate.vue';
|
||||
export { default as HeadlessTags } from './components/HeadlessTags.vue';
|
||||
export { default as HeadlessBetween } from './components/HeadlessBetween.vue';
|
||||
export { default as UnknownPlaceholder } from './components/UnknownPlaceholder.vue';
|
||||
|
||||
@@ -63,6 +63,67 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ---- tags(IN/NOT_IN 多值标签) ---- */
|
||||
.tf-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 32px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tf-tags.is-disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.tf-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #eef1f6;
|
||||
border: 1px solid var(--tf-border);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tf-tag-x {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--tf-label-color);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.tf-tag-x:hover {
|
||||
color: #cf222e;
|
||||
}
|
||||
|
||||
.tf-tag-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
font-size: 13px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* ---- between(区间双输入) ---- */
|
||||
.tf-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--tf-gap);
|
||||
}
|
||||
|
||||
.tf-between-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tf-actions {
|
||||
display: flex;
|
||||
gap: var(--tf-gap);
|
||||
|
||||
Reference in New Issue
Block a user