diff --git a/io.sc.platform.core.frontend/src/platform/components/form/FormField.ts b/io.sc.platform.core.frontend/src/platform/components/form/FormField.ts index bc40ab3e..7f7fd63a 100644 --- a/io.sc.platform.core.frontend/src/platform/components/form/FormField.ts +++ b/io.sc.platform.core.frontend/src/platform/components/form/FormField.ts @@ -91,3 +91,26 @@ export abstract class FormFieldMethods { return false; } } + +/** + * 获取组件默认值 + * @param field 组件配置对象 + * @returns + */ +export const getDefaultValue = (field) => { + if (!Tools.isUndefinedOrNull(field.defaultValue)) { + return field.defaultValue; + } else if (field.type === 'w-checkbox') { + return false; + } else if (field.type === 'w-checkbox-group') { + return []; + } else if ( + (field.type === 'w-select' || field.type === 'w-user-select' || field.type === 'w-org-select' || field.type === 'w-grid-select') && + field.multiple + ) { + return []; + } else if (field.type === 'w-code-mirror' || field.type === 'w-date') { + return ''; + } + return undefined; +}; diff --git a/io.sc.platform.core.frontend/src/platform/components/form/WForm.vue b/io.sc.platform.core.frontend/src/platform/components/form/WForm.vue index 1a841a90..1f90d8bc 100644 --- a/io.sc.platform.core.frontend/src/platform/components/form/WForm.vue +++ b/io.sc.platform.core.frontend/src/platform/components/form/WForm.vue @@ -49,6 +49,7 @@ import { ref, reactive, watch, computed, toRaw, useAttrs, getCurrentInstance } f import { useQuasar } from 'quasar'; import { VueTools, Tools } from '@/platform'; import { PageStatusEnum } from '@/platform/components/utils'; +import { getDefaultValue } from './FormField.ts'; const $q = useQuasar(); const attrs = useAttrs(); @@ -85,30 +86,6 @@ let fields_ = ref([...props.fields]); // 不同屏幕尺寸下 colsNum 为 0 时一行显示的字段个数 const screenCols = { xs: 1, sm: 2, md: 3, lg: 4, xl: 6 }; -const defaultValueHandler = (field) => { - if (!Tools.isUndefinedOrNull(field.defaultValue)) { - return field.defaultValue; - } else if (field.type === 'w-checkbox') { - return false; - } else if (field.type === 'w-checkbox-group') { - return []; - } else if (field.type === 'w-option-group') { - if (!field.optionType || field.optionType === 'radio') { - return undefined; - } else { - return []; - } - } else if ( - (field.type === 'w-select' || field.type === 'w-user-select' || field.type === 'w-org-select' || field.type === 'w-grid-select') && - field.multiple - ) { - return []; - } else if (field.type === 'w-code-mirror' || field.type === 'w-date') { - return ''; - } - return undefined; -}; - watch( () => props.fields, (newVal, oldVal) => { @@ -116,7 +93,7 @@ watch( fields_ = ref([...props.fields]); for (const field of fields_.value as any) { if (field.name) { - formModel[field.name] = defaultValueHandler(field); + formModel[field.name] = getDefaultValue(field); formFields[field.name] = field; } } @@ -145,7 +122,7 @@ const fieldsComputed = computed(() => { for (const field of fields_.value as any) { if (field.name) { - formModel[field.name] = defaultValueHandler(field); + formModel[field.name] = getDefaultValue(field); formFields[field.name] = field; } } @@ -232,7 +209,7 @@ const getData = () => { const setData = (data) => { if (Tools.isEmpty(data)) { for (const field of fields_.value as any) { - formData[field.name] = defaultValueHandler(field); + formData[field.name] = getDefaultValue(field); } } else { for (const field of fields_.value as any) { @@ -247,7 +224,7 @@ const setData = (data) => { */ const reset = () => { Object.keys(formData).forEach((key) => { - formData[key] = defaultValueHandler(formFields[key]); + formData[key] = getDefaultValue(formFields[key]); }); }; const formValidate = async () => { diff --git a/io.sc.platform.core.frontend/src/platform/components/grid/WGrid.vue b/io.sc.platform.core.frontend/src/platform/components/grid/WGrid.vue index 5794f340..f2cbaf75 100644 --- a/io.sc.platform.core.frontend/src/platform/components/grid/WGrid.vue +++ b/io.sc.platform.core.frontend/src/platform/components/grid/WGrid.vue @@ -1740,4 +1740,3 @@ VueTools.expose2Instance(instance); -./ts/grid.ts diff --git a/io.sc.platform.core.frontend/src/platform/components/index.ts b/io.sc.platform.core.frontend/src/platform/components/index.ts index 10267fb1..446cdc8d 100644 --- a/io.sc.platform.core.frontend/src/platform/components/index.ts +++ b/io.sc.platform.core.frontend/src/platform/components/index.ts @@ -30,6 +30,7 @@ import WFile from './file/WFile.vue'; import WLabel from './label/WLabel.vue'; import WRadio from './radio/WRadio.vue'; import WTextEditor from './text-editor/WTextEditor.vue'; +import WQueryBuilder from './query-builder/WQueryBuilder.vue'; import WGrid from './grid/WGrid.vue'; @@ -93,6 +94,7 @@ export default { app.component('WLabel', WLabel); app.component('WRadio', WRadio); app.component('WTextEditor', WTextEditor); + app.component('WQueryBuilder', WQueryBuilder); app.component('WGrid', WGrid); diff --git a/io.sc.platform.core.frontend/src/platform/components/query-builder/Bracket.vue b/io.sc.platform.core.frontend/src/platform/components/query-builder/Bracket.vue new file mode 100644 index 00000000..ec38427c --- /dev/null +++ b/io.sc.platform.core.frontend/src/platform/components/query-builder/Bracket.vue @@ -0,0 +1,18 @@ + + + diff --git a/io.sc.platform.core.frontend/src/platform/components/query-builder/WQueryBuilder.vue b/io.sc.platform.core.frontend/src/platform/components/query-builder/WQueryBuilder.vue new file mode 100644 index 00000000..04cbd8fe --- /dev/null +++ b/io.sc.platform.core.frontend/src/platform/components/query-builder/WQueryBuilder.vue @@ -0,0 +1,420 @@ + + + + + diff --git a/io.sc.platform.core.frontend/src/platform/components/query-builder/criteria.ts b/io.sc.platform.core.frontend/src/platform/components/query-builder/criteria.ts new file mode 100644 index 00000000..cab63d2c --- /dev/null +++ b/io.sc.platform.core.frontend/src/platform/components/query-builder/criteria.ts @@ -0,0 +1,378 @@ +import { Parser } from 'node-sql-parser'; +import { Tools } from '@/platform'; + +/** + * criteria 模式 + */ +export const criteriaMode = { + and: { + name: 'and', + option: { label: '并且', value: 'and' }, + }, + or: { + name: 'or', + option: { label: '或者', value: 'or' }, + }, + not: { + name: 'not', + option: { label: '不满足', value: 'not' }, + }, +}; + +/** + * criteria 匹配操作 + */ +export const criteriaOperator = { + equals: { + name: 'equals', + option: { label: '等于', value: 'equals' }, + sqlTemplate: ` #{fieldName} = #{value} `, + }, + notEquals: { + name: 'notEquals', + option: { label: '不等于', value: 'notEquals' }, + sqlTemplate: ` #{fieldName} <> #{value} `, + }, + contains: { + name: 'contains', + option: { label: '包含', value: 'contains' }, + sqlTemplate: ` #{fieldName} LIKE '%#{value}%' `, + }, + notContains: { + name: 'notContains', + option: { label: '不包含', value: 'notContains' }, + sqlTemplate: ` #{fieldName} NOT LIKE '%#{value}%' `, + }, + greaterThan: { + name: 'greaterThan', + option: { label: '大于', value: 'greaterThan' }, + sqlTemplate: ` #{fieldName} > #{value} `, + }, + greaterOrEqual: { + name: 'greaterOrEqual', + option: { label: '大于等于', value: 'greaterOrEqual' }, + sqlTemplate: ` #{fieldName} >= #{value} `, + }, + lessThan: { + name: 'lessThan', + option: { label: '小于', value: 'lessThan' }, + sqlTemplate: ` #{fieldName} < #{value} `, + }, + lessOrEqual: { + name: 'lessOrEqual', + option: { label: '小于等于', value: 'lessOrEqual' }, + sqlTemplate: ` #{fieldName} <= #{value} `, + }, + inSet: { + name: 'inSet', + option: { label: '在...之内', value: 'inSet' }, + sqlTemplate: ' #{fieldName} IN #{value} ', + }, + notInSet: { + name: 'notInSet', + option: { label: '不在...之内', value: 'notInSet' }, + sqlTemplate: ' #{fieldName} NOT IN #{value} ', + }, + startWith: { + name: 'startWith', + option: { label: '以...开始', value: 'startWith' }, + sqlTemplate: ` #{fieldName} LIKE '#{value}%' `, + }, + notStartWith: { + name: 'notStartWith', + option: { label: '不以...开始', value: 'notStartWith' }, + sqlTemplate: ` #{fieldName} NOT LIKE '#{value}%' `, + }, + endWith: { + name: 'endWith', + option: { label: '以...结束', value: 'endWith' }, + sqlTemplate: ` #{fieldName} LIKE '%#{value}' `, + }, + notEndWith: { + name: 'notEndWith', + option: { label: '不以...结束', value: 'notEndWith' }, + sqlTemplate: ` #{fieldName} NOT LIKE '%#{value}' `, + }, + between: { + name: 'between', + option: { label: '在...之间', value: 'between' }, + sqlTemplate: ` #{fieldName} BETWEEN #{start} AND #{end} `, + }, + notBetween: { + name: 'notBetween', + option: { label: '不在...之间', value: 'notBetween' }, + sqlTemplate: ` #{fieldName} NOT BETWEEN #{start} AND #{end} `, + }, + isBlank: { + name: 'isBlank', + option: { label: '为空', value: 'isBlank' }, + sqlTemplate: ` #{fieldName} = '' `, + }, + notBlank: { + name: 'notBlank', + option: { label: '不为空', value: 'notBlank' }, + sqlTemplate: ` #{fieldName} <> '' `, + }, + isNull: { + name: 'isNull', + option: { label: '为 null', value: 'isNull' }, + sqlTemplate: ` #{fieldName} IS NULL `, + }, + notNull: { + name: 'notNull', + option: { label: '不为 null', value: 'notNull' }, + sqlTemplate: ` #{fieldName} IS NOT NULL `, + }, +}; + +export class CriteriaModeFactory { + /** + * 构建下拉框、checkbox多选按钮等组件所需的 options + * @param names 默认为空数组,将所有支持的模式都构建进去,否则根据传入的名字数组进行构建。 + */ + public static buildOptions(names: Array = []) { + const options = []; + if (names && names.length > 0) { + names.forEach((name) => { + if (criteriaMode[name]) { + options.push(criteriaMode[name]['option']); + } + }); + } else { + Object.keys(criteriaMode).forEach((operator) => { + options.push(criteriaMode[operator]['option']); + }); + } + return options; + } +} + +export class CriteriaOperatorFactory { + /** + * 构建下拉框、checkbox多选按钮等组件所需的 options + * @param names 默认为空数组,将所有匹配操作都构建进去,否则根据传入的名字数组进行构建。 + * @param exclude 默认为空数组,否则根据传入的名字数组进行排除 + */ + public static buildOptions(names: Array = [], exclude: Array = []) { + const options = []; + if (names && names.length > 0) { + names.forEach((name) => { + if (criteriaOperator[name]) { + options.push(criteriaOperator[name]['option']); + } + }); + } else { + Object.keys(criteriaOperator).forEach((operator) => { + if (exclude && !exclude.includes(operator)) { + options.push(criteriaOperator[operator]['option']); + } + }); + } + return options; + } +} + +export class CriteriaUtil { + /** + * 将SQL语句转换为 criteria 对象 + * @param sql + * @returns + */ + public static sqlToCriteria = (sql) => { + const parser = new Parser(); + const ast = parser.astify(sql); + const parserResult = astParserToCriteria(ast['where']); + const criteriaResult = { + operator: ast['where']['operator'].toLowerCase(), + criteria: parserResult, + }; + return criteriaResult; + }; + + /** + * 将 criteria 对象转换为 SQL 语句 + * @param criteria + */ + public static criteriaToSql = (criteria) => { + let sql = ''; + criteria.criteria.forEach((item, index) => { + if (Tools.hasOwnProperty(item, 'fieldName')) { + if (index === criteria.criteria.length - 1) { + sql += replaceSql(criteriaOperator[item['operator']]['sqlTemplate'], item); + } else { + sql += + replaceSql(criteriaOperator[item['operator']]['sqlTemplate'], item) + + (criteria.operator === criteriaMode.not.name ? criteriaMode.and.name : criteria.operator); + } + } else if (Tools.hasOwnProperty(item, 'criteria')) { + if (index === criteria.criteria.length - 1) { + sql += ' ( ' + CriteriaUtil.criteriaToSql(item) + ' ) '; + } else { + sql += ' ( ' + CriteriaUtil.criteriaToSql(item) + ' ) ' + (criteria.operator === criteriaMode.not.name ? criteriaMode.and.name : criteria.operator); + } + } + }); + if (criteria.operator === criteriaMode.not.name) { + sql = `not (` + sql + `)`; + } + return sql; + }; +} + +/** + * SQL解析语法树中的操作转换为 criteria 中的操作 + * @param astOperator + * @param astValue + */ +const astOperatorToCriteriaOperator = (astOperator, astValue) => { + if (astOperator === '=') { + return criteriaOperator.equals.name; + } else if (astOperator === '<>') { + return criteriaOperator.notEquals.name; + } else if (astOperator === 'LIKE' && astValue.startsWith('%') && astValue.endsWith('%')) { + return criteriaOperator.contains.name; + } else if (astOperator === 'NOT LIKE' && astValue.startsWith('%') && astValue.endsWith('%')) { + return criteriaOperator.notContains.name; + } else if (astOperator === '>') { + return criteriaOperator.greaterThan.name; + } else if (astOperator === '>=') { + return criteriaOperator.greaterOrEqual.name; + } else if (astOperator === '<') { + return criteriaOperator.lessThan.name; + } else if (astOperator === '<=') { + return criteriaOperator.lessOrEqual.name; + } else if (astOperator === 'IN') { + return criteriaOperator.inSet.name; + } else if (astOperator === 'NOT IN') { + return criteriaOperator.notInSet.name; + } else if (astOperator === 'LIKE' && astValue.endsWith('%')) { + return criteriaOperator.startWith.name; + } else if (astOperator === 'NOT LIKE' && astValue.endsWith('%')) { + return criteriaOperator.notStartWith.name; + } else if (astOperator === 'LIKE' && astValue.startsWith('%')) { + return criteriaOperator.endWith.name; + } else if (astOperator === 'NOT LIKE' && astValue.startsWith('%')) { + return criteriaOperator.notEndWith.name; + } else if (astOperator === 'BETWEEN') { + return criteriaOperator.between.name; + } else if (astOperator === 'NOT BETWEEN') { + return criteriaOperator.notBetween.name; + } + return astOperator; +}; + +/** + * SQL解析语法树中的值转换为 criteria 中的值 + * @param astOperator 操作类型 + * @param astValue 值 + */ +const astValueToCriteriaValue = (astOperator, astValue) => { + if ((astOperator === 'LIKE' || astOperator === 'NOT LIKE') && typeof astValue === 'string' && astValue.indexOf('%') > -1) { + return astValue.replace(/%/g, ''); + } else if ((astOperator === 'IN' || astOperator === 'NOT IN') && Array.isArray(astValue)) { + const result = []; + astValue.forEach((value) => { + result.push(value['value']); + }); + return result; + } + return astValue; +}; + +// AST语法树中需要往下循环的类型 +const astForEachType = { + binary_expr: 'binary_expr', + function: 'function', +}; + +/** + * 将SQL解析语法树结构解析转换为 criteria 对象 + * @param astObj + * @returns + */ +const astParserToCriteria = (astObj) => { + const criterias = []; + let flag = true; + if (astObj['parentheses'] && astObj['type'] === astForEachType.binary_expr) { + // 有括号且类型为 binary_expr 为一组表达式子规则 + const childCriterias = []; + if (astObj['left'] && astForEachType[astObj['left']['type']]) { + flag = false; + childCriterias.push(...astParserToCriteria(astObj['left'])); + } + if (astObj['right'] && astForEachType[astObj['right']['type']]) { + flag = false; + childCriterias.push(...astParserToCriteria(astObj['right'])); + } + criterias.push({ operator: astObj['operator'].toLowerCase(), criteria: childCriterias }); + } else if (astObj['parentheses'] && astObj['type'] === astForEachType.function) { + // 有括号且类型为 function 为一组子规则 + if (astObj['name']['name'][0]['value'] === criteriaMode.not.name) { + flag = false; + const childCriterias = []; + childCriterias.push(...astParserToCriteria(astObj['args']['value'][0])); + criterias.push({ operator: criteriaMode.not.name, criteria: childCriterias }); + } + } else { + if (astObj && astObj['left'] && astForEachType[astObj['left']['type']]) { + flag = false; + const leftCriterias = astParserToCriteria(astObj['left']); + criterias.push(...leftCriterias); + } + if (astObj && astObj['right'] && astForEachType[astObj['right']['type']]) { + flag = false; + const rightCriterias = astParserToCriteria(astObj['right']); + criterias.push(...rightCriterias); + } + } + if (flag) { + if (astObj['operator'] === 'BETWEEN' || astObj['operator'] === 'NOT BETWEEN') { + const criteria = { + fieldName: astObj['left']['column'], + operator: astOperatorToCriteriaOperator(astObj['operator'], astObj['right']['value']), + start: astObj['right']['value'][0]['value'], + end: astObj['right']['value'][1]['value'], + }; + criterias.push(criteria); + } else { + const criteria = { + fieldName: astObj['left']['column'], + operator: astOperatorToCriteriaOperator(astObj['operator'], astObj['right']['value']), + value: astValueToCriteriaValue(astObj['operator'], astObj['right']['value']), + }; + criterias.push(criteria); + } + } + return criterias; +}; + +const replaceSql = (str, data) => { + const regex = /#{(\w+)}/g; + return str.replace(regex, (match, fieldName) => { + if ((data['operator'] === criteriaOperator.inSet.name || data['operator'] === criteriaOperator.notInSet.name) && fieldName === 'value') { + let replaceValue = '(' + data[fieldName] + ')' || ''; + if (Array.isArray(data[fieldName]) && data[fieldName].length > 0 && typeof data[fieldName][0] === 'string') { + replaceValue = '('; + data[fieldName].forEach((item) => { + replaceValue += `'` + item + `',`; + }); + replaceValue = replaceValue.substring(0, replaceValue.length - 1) + ')'; + } + return replaceValue; + } else if ( + data['operator'] === criteriaOperator.contains.name || + data['operator'] === criteriaOperator.notContains.name || + data['operator'] === criteriaOperator.startWith.name || + data['operator'] === criteriaOperator.notStartWith.name || + data['operator'] === criteriaOperator.endWith.name || + data['operator'] === criteriaOperator.notEndWith.name + ) { + return data[fieldName] || ''; + } else { + if (fieldName !== 'fieldName' && typeof data[fieldName] === 'string') { + return `'` + data[fieldName] + `'` || ''; + } + return data[fieldName] || ''; + } + }); +}; diff --git a/io.sc.platform.core.frontend/src/platform/components/query-builder/css/comm.css b/io.sc.platform.core.frontend/src/platform/components/query-builder/css/comm.css new file mode 100644 index 00000000..c21b3e98 --- /dev/null +++ b/io.sc.platform.core.frontend/src/platform/components/query-builder/css/comm.css @@ -0,0 +1,22 @@ +.bracketBorders { + border-width: 1px 0px 1px 1px; + border-top-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-color: rgb(208, 208, 208); + border-bottom-color: rgb(208, 208, 208); + border-left-color: rgb(208, 208, 208); + border-image: initial; + border-right-style: initial; + border-right-color: initial; +} +.box { + width: 100%; + white-space: nowrap; + overflow: hidden; + overflow: auto; +} +.queryBuilderFlex { + display: flex; + flex-wrap: nowrap; +} \ No newline at end of file diff --git a/io.sc.platform.core.frontend/src/platform/components/select/WOrgSelect.vue b/io.sc.platform.core.frontend/src/platform/components/select/WOrgSelect.vue index 6dcbb453..1294590b 100644 --- a/io.sc.platform.core.frontend/src/platform/components/select/WOrgSelect.vue +++ b/io.sc.platform.core.frontend/src/platform/components/select/WOrgSelect.vue @@ -63,7 +63,7 @@