chore: 初始化 trans-form 项目脚手架
Node.js Build / build (push) Has been skipped

- 基于 grill-me Q1-Q61 决策
- Vitest 测试与 CI workflow
- middleware: parseFilters 核心实现
- component: Vue3 TransForm 基础组件 + Storybook

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 00:47:58 +00:00
commit ccd16bf956
14 changed files with 2126 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
export { DEFAULT_OPERATOR_MAP, mapOperators } from './operatorMap.js';
export { splitMultiValue } from './splitMultiValue.js';
export { toTree } from './toTree.js';
export { parseFilters } from './parseFilters.js';
+25
View File
@@ -0,0 +1,25 @@
/** @type {Record<string, string>} */
export const DEFAULT_OPERATOR_MAP = {
EQ: '=',
NE: '<>',
GT: '>',
GTE: '>=',
LT: '<',
LTE: '<=',
LIKE: 'LIKE',
IN: 'IN',
NOT_IN: 'NOT IN',
BETWEEN: 'BETWEEN',
IS_NULL: 'IS NULL',
IS_NOT_NULL: 'IS NOT NULL',
};
/**
* @param {string} query
* @param {Record<string, string>} [operatorMap]
*/
export function mapOperators(query, operatorMap = DEFAULT_OPERATOR_MAP) {
const q = String(query).toUpperCase();
const merged = { ...DEFAULT_OPERATOR_MAP, ...operatorMap };
return merged[q] ?? q;
}
+103
View File
@@ -0,0 +1,103 @@
import { mapOperators } from './operatorMap.js';
import { splitMultiValue } from './splitMultiValue.js';
import { toTree } from './toTree.js';
const DEFAULT_SKIP_FLAGS = ['disabled', 'hidden'];
/**
* @param {object} item
* @param {string[]} skipFlags
*/
function shouldSkipByFlags(item, skipFlags) {
return skipFlags.some((flag) => item[flag] === true);
}
/**
* @param {Array<{field?: string, value?: *, query?: string, logic?: string, empty?: boolean, disabled?: boolean, hidden?: boolean}>} conditions
* @param {{
* allowedFields: string[],
* mode?: 'pass-through'|'flat'|'tree',
* unknownField?: 'error'|'skip',
* strict?: boolean,
* operatorMap?: Record<string, string>,
* skipFlags?: string[],
* skipEmpty?: boolean,
* }} options
*/
export function parseFilters(conditions, options) {
const {
allowedFields,
mode = 'pass-through',
unknownField = 'error',
strict = false,
operatorMap = {},
skipFlags = DEFAULT_SKIP_FLAGS,
skipEmpty = false,
} = options;
if (!Array.isArray(allowedFields) || allowedFields.length === 0) {
throw new Error('parseFilters: allowedFields is required');
}
if (!Array.isArray(conditions)) {
if (strict) throw new Error('parseFilters: conditions must be an array');
return mode === 'tree' ? [] : [];
}
const allowed = new Set(allowedFields.map(String));
const errors = [];
const out = [];
for (let i = 0; i < conditions.length; i++) {
const c = conditions[i];
if (!c || typeof c !== 'object') {
if (strict) errors.push({ index: i, reason: 'invalid item' });
continue;
}
if (shouldSkipByFlags(c, skipFlags)) continue;
if (skipEmpty && c.empty === true) continue;
if (c.field == null || c.query == null) {
if (strict) errors.push({ index: i, reason: 'missing field or query' });
continue;
}
const field = String(c.field);
if (!allowed.has(field)) {
if (unknownField === 'error') {
throw new Error(`parseFilters: unknown field "${field}"`);
}
continue;
}
const query = String(c.query).toUpperCase();
const item = {
field,
query,
op: mapOperators(query, operatorMap),
value: c.value ?? null,
};
const parsed = splitMultiValue(query, c.value);
if (parsed != null) item.values = parsed;
if (c.empty === true) item.empty = true;
if (c.disabled === true) item.disabled = true;
if (c.hidden === true) item.hidden = true;
if (c.readOnly === true) item.readOnly = true;
if (c.logic != null) item.logic = c.logic;
out.push(item);
}
if (strict && errors.length) {
throw new Error(`parseFilters: ${errors.length} validation error(s)`);
}
if (mode === 'flat') {
return out.map(({ logic, ...rest }) => rest);
}
if (mode === 'tree') {
return toTree(out);
}
return out;
}
+18
View File
@@ -0,0 +1,18 @@
/**
* @param {string} query
* @param {string|null|undefined} value
* @returns {string[]|[string,string]|null}
*/
export function splitMultiValue(query, value) {
if (value == null || value === '') return null;
const s = String(value);
const q = String(query).toUpperCase();
if (q === 'IN' || q === 'NOT_IN') {
return s.split(',').map((v) => v.trim()).filter(Boolean);
}
if (q === 'BETWEEN') {
const parts = s.split(',').map((v) => v.trim());
return parts.length >= 2 ? [parts[0], parts[1]] : null;
}
return null;
}
+15
View File
@@ -0,0 +1,15 @@
/**
* @param {Array<object>} flat
* @returns {Array<object>}
*/
export function toTree(flat) {
if (!flat.length) return [];
if (flat.length === 1) return [flat[0]];
let acc = flat[0];
for (let i = 1; i < flat.length; i++) {
const logic = flat[i].logic || 'AND';
acc = { logic, children: [acc, flat[i]] };
}
return [acc];
}