chore: 版本 0.1.0 更新补齐内置控件集 textarea/select/number/date/tags/between
Node.js Build / build (push) Successful in 6s

This commit is contained in:
2026-08-07 11:11:40 +08:00
parent fd5f7a0752
commit 95acff6a1a
15 changed files with 568 additions and 9 deletions
+11 -6
View File
@@ -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>
+57
View File
@@ -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>
+26
View File
@@ -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>
+26
View File
@@ -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>
+33
View File
@@ -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>
+97
View File
@@ -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>
+26
View File
@@ -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
View File
@@ -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';
}
+9
View File
@@ -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';
+61
View File
@@ -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);