wangshaoping 5 months ago
parent
commit
edd437bc35
  1. 3
      io.sc.platform.core.frontend/package.json
  2. 23
      io.sc.platform.core.frontend/src/platform/components/form/FormField.ts
  3. 33
      io.sc.platform.core.frontend/src/platform/components/form/WForm.vue
  4. 1
      io.sc.platform.core.frontend/src/platform/components/grid/WGrid.vue
  5. 2
      io.sc.platform.core.frontend/src/platform/components/index.ts
  6. 18
      io.sc.platform.core.frontend/src/platform/components/query-builder/Bracket.vue
  7. 420
      io.sc.platform.core.frontend/src/platform/components/query-builder/WQueryBuilder.vue
  8. 378
      io.sc.platform.core.frontend/src/platform/components/query-builder/criteria.ts
  9. 22
      io.sc.platform.core.frontend/src/platform/components/query-builder/css/comm.css
  10. 6
      io.sc.platform.core.frontend/src/platform/components/select/WOrgSelect.vue
  11. 6
      io.sc.platform.core.frontend/src/platform/components/select/WUserSelect.vue
  12. 152
      io.sc.platform.core.frontend/src/views/likm/Form.vue
  13. 23
      io.sc.platform.core.frontend/src/views/likm/Grid.vue

3
io.sc.platform.core.frontend/package.json

@ -140,6 +140,7 @@
"vue-dompurify-html": "5.1.0", "vue-dompurify-html": "5.1.0",
"vue-i18n": "10.0.0", "vue-i18n": "10.0.0",
"vue-router": "4.4.3", "vue-router": "4.4.3",
"xml-formatter": "3.6.3" "xml-formatter": "3.6.3",
"node-sql-parser": "5.3.2"
} }
} }

23
io.sc.platform.core.frontend/src/platform/components/form/FormField.ts

@ -91,3 +91,26 @@ export abstract class FormFieldMethods {
return false; 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;
};

33
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 { useQuasar } from 'quasar';
import { VueTools, Tools } from '@/platform'; import { VueTools, Tools } from '@/platform';
import { PageStatusEnum } from '@/platform/components/utils'; import { PageStatusEnum } from '@/platform/components/utils';
import { getDefaultValue } from './FormField.ts';
const $q = useQuasar(); const $q = useQuasar();
const attrs = useAttrs(); const attrs = useAttrs();
@ -85,30 +86,6 @@ let fields_ = ref([...props.fields]);
// colsNum 0 // colsNum 0
const screenCols = { xs: 1, sm: 2, md: 3, lg: 4, xl: 6 }; 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( watch(
() => props.fields, () => props.fields,
(newVal, oldVal) => { (newVal, oldVal) => {
@ -116,7 +93,7 @@ watch(
fields_ = ref([...props.fields]); fields_ = ref([...props.fields]);
for (const field of fields_.value as any) { for (const field of fields_.value as any) {
if (field.name) { if (field.name) {
formModel[field.name] = defaultValueHandler(field); formModel[field.name] = getDefaultValue(field);
formFields[field.name] = field; formFields[field.name] = field;
} }
} }
@ -145,7 +122,7 @@ const fieldsComputed = computed(() => {
for (const field of fields_.value as any) { for (const field of fields_.value as any) {
if (field.name) { if (field.name) {
formModel[field.name] = defaultValueHandler(field); formModel[field.name] = getDefaultValue(field);
formFields[field.name] = field; formFields[field.name] = field;
} }
} }
@ -232,7 +209,7 @@ const getData = () => {
const setData = (data) => { const setData = (data) => {
if (Tools.isEmpty(data)) { if (Tools.isEmpty(data)) {
for (const field of fields_.value as any) { for (const field of fields_.value as any) {
formData[field.name] = defaultValueHandler(field); formData[field.name] = getDefaultValue(field);
} }
} else { } else {
for (const field of fields_.value as any) { for (const field of fields_.value as any) {
@ -247,7 +224,7 @@ const setData = (data) => {
*/ */
const reset = () => { const reset = () => {
Object.keys(formData).forEach((key) => { Object.keys(formData).forEach((key) => {
formData[key] = defaultValueHandler(formFields[key]); formData[key] = getDefaultValue(formFields[key]);
}); });
}; };
const formValidate = async () => { const formValidate = async () => {

1
io.sc.platform.core.frontend/src/platform/components/grid/WGrid.vue

@ -1740,4 +1740,3 @@ VueTools.expose2Instance(instance);
<style lang="css"> <style lang="css">
@import './css/grid.css'; @import './css/grid.css';
</style> </style>
./ts/grid.ts

2
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 WLabel from './label/WLabel.vue';
import WRadio from './radio/WRadio.vue'; import WRadio from './radio/WRadio.vue';
import WTextEditor from './text-editor/WTextEditor.vue'; import WTextEditor from './text-editor/WTextEditor.vue';
import WQueryBuilder from './query-builder/WQueryBuilder.vue';
import WGrid from './grid/WGrid.vue'; import WGrid from './grid/WGrid.vue';
@ -93,6 +94,7 @@ export default {
app.component('WLabel', WLabel); app.component('WLabel', WLabel);
app.component('WRadio', WRadio); app.component('WRadio', WRadio);
app.component('WTextEditor', WTextEditor); app.component('WTextEditor', WTextEditor);
app.component('WQueryBuilder', WQueryBuilder);
app.component('WGrid', WGrid); app.component('WGrid', WGrid);

18
io.sc.platform.core.frontend/src/platform/components/query-builder/Bracket.vue

@ -0,0 +1,18 @@
<template>
<div class="bracketBorders w-[10px]"></div>
</template>
<style lang="css">
.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;
}
</style>

420
io.sc.platform.core.frontend/src/platform/components/query-builder/WQueryBuilder.vue

@ -0,0 +1,420 @@
<template>
<div v-show="fieldMethodsClass.getShow(props, modelValue)" class="box">
<div class="queryBuilderFlex gap-x-[5px]">
<div style="min-width: 100px" class="content-center">
<w-select
v-model="modelValueObject['operator']"
label="模式"
:options="CriteriaModeFactory.buildOptions()"
@update-value="
(args) => {
if (props.mode === 'criteria') {
modelValue = {
operator: args.value,
criteria: modelValue['criteria'],
};
} else {
modelValueObject['operator'] = args.value;
modelValueObject['criteria'] = modelValueObject['criteria'];
}
}
"
></w-select>
</div>
<Bracket></Bracket>
<div class="grid pt-1.5 pb-1.5 gap-y-[10px]">
<template v-for="(condition, index) in modelValueObject['criteria']" :key="index">
<div v-if="Tools.hasOwnProperty(condition, 'fieldName')" class="queryBuilderFlex gap-x-[5px]">
<div class="content-center">
<q-btn v-if="modelValueObject.criteria.length > 1" round unelevated push color="red" icon="remove_circle" size="8px" @click="removeClick(index)"
><q-tooltip>删除</q-tooltip></q-btn
>
<q-btn v-else round unelevated push color="grey" :disable="true" icon="remove_circle" size="8px"><q-tooltip>删除</q-tooltip></q-btn>
</div>
<w-select v-model="modelValueObject['criteria'][index]['fieldName']" :options="props.options" @update-value="fieldUpdateValue"></w-select>
<w-select
v-model="modelValueObject['criteria'][index]['operator']"
:options="getUseOperator(modelValueObject['criteria'][index]['fieldName'])"
@update-value="
(args) => {
operatorUpdateValue(args, modelValueObject['criteria'][index]['fieldName']);
}
"
></w-select>
<template
v-if="
modelValueObject['criteria'][index]['operator'] === criteriaOperator.between.name ||
modelValueObject['criteria'][index]['operator'] === criteriaOperator.notBetween.name
"
>
<component
:is="getUseField(modelValueObject['criteria'][index]['fieldName'])?.type"
v-model="modelValueObject['criteria'][index]['start']"
v-bind="getUseField(modelValueObject['criteria'][index]['fieldName'])"
@update:model-value="fieldValueUpdate"
></component>
<div class="content-center"></div>
<component
:is="getUseField(modelValueObject['criteria'][index]['fieldName'])?.type"
v-model="modelValueObject['criteria'][index]['end']"
v-bind="getUseField(modelValueObject['criteria'][index]['fieldName'])"
@update:model-value="fieldValueUpdate"
></component>
</template>
<template
v-else-if="
modelValueObject['criteria'][index]['operator'] !== criteriaOperator.isBlank.name &&
modelValueObject['criteria'][index]['operator'] !== criteriaOperator.notBlank.name &&
modelValueObject['criteria'][index]['operator'] !== criteriaOperator.isNull.name &&
modelValueObject['criteria'][index]['operator'] !== criteriaOperator.notNull.name
"
>
<component
:is="getUseField(modelValueObject['criteria'][index]['fieldName'])?.type"
v-model="modelValueObject['criteria'][index]['value']"
v-bind="getUseField(modelValueObject['criteria'][index]['fieldName'])"
@update:model-value="fieldValueUpdate"
></component>
</template>
</div>
<div v-else-if="!Tools.hasOwnProperty(condition, 'fieldName')" class="queryBuilderFlex gap-x-[5px]">
<div class="content-center">
<q-btn
v-if="modelValueObject['criteria'].length > 1"
round
unelevated
push
color="red"
icon="remove_circle"
size="8px"
@click="removeClick(index)"
><q-tooltip>删除</q-tooltip></q-btn
>
<q-btn v-else round unelevated push color="grey" :disable="true" icon="remove_circle" size="8px"><q-tooltip>删除</q-tooltip></q-btn>
</div>
<w-query-builder
v-model="modelValueObject['criteria'][index]"
:options="props.options"
@update-parent-model-value="
() => {
/**
* 事件说明
* 无论最外层使用的 sql 还是 criteria 模式嵌套组件绑定值一定是 criteria 对象值
* 只有 sql 模式下最顶层的模型值需要随着用户的选择被更改所以要进行判断
* 当模式为 sql 时当前组件一定是顶层组件更新其模型显示值
* 当模式为 criteria 时当前组件可能是嵌套组件也可能是顶层组件直接使用 emit 往上抛事件即可
*/
if (props.mode !== 'criteria') {
modelValue = CriteriaUtil.criteriaToSql(modelValueObject);
} else {
emit('updateParentModelValue');
fieldMethodsClass.updateValue(modelValue);
}
}
"
></w-query-builder>
</div>
</template>
<div class="queryBuilderFlex gap-x-[5px]">
<div class="content-center">
<q-btn round unelevated push color="green" icon="add_circle" size="8px" @click="addClick"><q-tooltip>新增</q-tooltip></q-btn>
</div>
<div class="content-center">
<q-btn round unelevated push color="orange" icon="add_box" size="8px" @click="addGroupClick"><q-tooltip>新增组</q-tooltip></q-btn>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onBeforeMount, reactive, watch, toRaw } from 'vue';
import { Tools } from '@/platform';
import { FormFieldMethods } from '../form/FormField';
import { FormFieldProps, getDefaultValue } from '@/platform/components/form/FormField.ts';
import Bracket from './Bracket.vue';
import { criteriaMode, criteriaOperator, CriteriaModeFactory, CriteriaOperatorFactory, CriteriaUtil } from './criteria';
const modelValue = defineModel<object | string>();
const modelValueObject = reactive({});
interface FieldProps extends FormFieldProps {
options: Array<object>;
/**
* 组件模式
* criteria默认模式组件绑定值为 criteria 查询对象
* sql组件绑定值为 sql 条件字符串
*/
mode?: string;
}
const props = withDefaults(defineProps<FieldProps>(), {
showIf: true,
options: () => [],
mode: 'criteria',
});
const emit = defineEmits<{
//
(e: 'updateParentModelValue'): void;
}>();
class FieldMethods extends FormFieldMethods {
updateValue = (value_) => {
if (props['onUpdateValue']) {
props['onUpdateValue']({
value: value_,
form: props['form'],
});
}
};
validate = () => {
return true;
};
setValue = (value) => {
modelValue.value = value;
};
getValue = () => {
return modelValue.value;
};
//
clearValue = () => {
modelValue.value =
props.mode === 'criteria'
? {
operator: criteriaMode.and.name,
criteria: [{ fieldName: '', operator: criteriaOperator.equals.name, value: undefined }],
}
: '';
};
}
const fieldMethodsClass = new FieldMethods();
// options undefined
const componentDefaultValue = {};
if (props.options && props.options.length > 0) {
props.options.forEach((item) => {
componentDefaultValue[item['value']] = getDefaultValue(item['useComponent']);
});
}
// 使
const getUseField = (field) => {
const option = props.options.find((item) => item['value'] === field);
if (!Tools.isEmpty(option)) {
return option['useComponent'];
}
return { type: 'w-text' };
};
// operator
const getUseOperator = (field) => {
if (!Tools.isEmpty(field)) {
const option = props.options.find((item) => item['value'] === field);
if (!Tools.isEmpty(option)) {
if (option['operator']) {
return CriteriaOperatorFactory.buildOptions(option['operator']);
} else if (option['excludeOperator']) {
return CriteriaOperatorFactory.buildOptions([], option['excludeOperator']);
}
}
}
return CriteriaOperatorFactory.buildOptions();
};
//
const fieldUpdateValue = (args) => {
if (props.mode === 'criteria') {
modelValue.value.criteria.forEach((item) => {
if (item['fieldName'] === args['value']) {
if (item['operator'] === criteriaOperator.between.name || item['operator'] === criteriaOperator.notBetween.name) {
item['start'] = componentDefaultValue[item['fieldName']];
item['end'] = componentDefaultValue[item['fieldName']];
item['value'] = undefined;
} else {
item['start'] = undefined;
item['end'] = undefined;
item['value'] = componentDefaultValue[item['fieldName']];
}
}
});
modelValue.value = {
operator: modelValue.value.operator,
criteria: modelValue.value.criteria,
};
emit('updateParentModelValue');
} else {
modelValueObject['criteria'].forEach((item) => {
if (item['fieldName'] === args['value']) {
if (item['operator'] === criteriaOperator.between.name || item['operator'] === criteriaOperator.notBetween.name) {
item['start'] = componentDefaultValue[item['fieldName']];
item['end'] = componentDefaultValue[item['fieldName']];
item['value'] = undefined;
} else {
item['start'] = undefined;
item['end'] = undefined;
item['value'] = componentDefaultValue[item['fieldName']];
}
}
});
modelValue.value = CriteriaUtil.criteriaToSql(modelValueObject);
}
};
//
const operatorUpdateValue = (args, fieldName) => {
if (props.mode === 'criteria') {
modelValue.value.criteria.forEach((item) => {
if (item['fieldName'] === fieldName) {
if (args['value'] === criteriaOperator.between.name || args['value'] === criteriaOperator.notBetween.name) {
item['start'] = componentDefaultValue[fieldName];
item['end'] = componentDefaultValue[fieldName];
item['value'] = undefined;
} else {
item['start'] = undefined;
item['end'] = undefined;
item['value'] = componentDefaultValue[fieldName];
}
}
});
modelValue.value = {
operator: modelValue.value.operator,
criteria: modelValue.value.criteria,
};
emit('updateParentModelValue');
} else {
modelValueObject['criteria'].forEach((item) => {
if (item['fieldName'] === fieldName) {
if (args['value'] === criteriaOperator.between.name || args['value'] === criteriaOperator.notBetween.name) {
item['start'] = componentDefaultValue[fieldName];
item['end'] = componentDefaultValue[fieldName];
item['value'] = undefined;
} else {
item['start'] = undefined;
item['end'] = undefined;
item['value'] = componentDefaultValue[fieldName];
}
}
});
modelValue.value = CriteriaUtil.criteriaToSql(modelValueObject);
}
};
//
const fieldValueUpdate = (value) => {
if (props.mode !== 'criteria') {
modelValue.value = CriteriaUtil.criteriaToSql(modelValueObject);
} else {
emit('updateParentModelValue');
fieldMethodsClass.updateValue(modelValue.value);
}
};
const addClick = () => {
if (props.mode === 'criteria') {
modelValue.value.criteria.push({ fieldName: '', operator: criteriaOperator.equals.name, value: undefined });
modelValue.value = {
operator: modelValue.value.operator,
criteria: modelValue.value.criteria,
};
emit('updateParentModelValue');
} else {
modelValueObject['criteria'].push({ fieldName: '', operator: criteriaOperator.equals.name, value: undefined });
modelValue.value = CriteriaUtil.criteriaToSql(modelValueObject);
}
};
const removeClick = (index) => {
if (props.mode === 'criteria') {
modelValue.value.criteria.splice(index, 1);
modelValue.value = {
operator: modelValue.value.operator,
criteria: modelValue.value.criteria,
};
emit('updateParentModelValue');
} else {
modelValueObject['criteria'].splice(index, 1);
modelValue.value = CriteriaUtil.criteriaToSql(modelValueObject);
}
};
const addGroupClick = () => {
if (props.mode === 'criteria') {
modelValue.value.criteria.push({
operator: criteriaMode.and.name,
criteria: [{ fieldName: '', operator: criteriaOperator.equals.name, value: undefined }],
});
modelValue.value = {
operator: modelValue.value.operator,
criteria: modelValue.value.criteria,
};
emit('updateParentModelValue');
} else {
modelValueObject['criteria'].push({
operator: criteriaMode.and.name,
criteria: [{ fieldName: '', operator: criteriaOperator.equals.name, value: undefined }],
});
modelValue.value = CriteriaUtil.criteriaToSql(modelValueObject);
}
};
watch(
() => modelValue.value,
(newVal, oldVal) => {
if (props.mode === 'criteria') {
if (!Tools.isEmpty(newVal)) {
modelValueObject['operator'] = newVal['operator'];
modelValueObject['criteria'] = newVal['criteria'];
} else {
modelValueObject['operator'] = criteriaMode.and.name;
modelValueObject['criteria'] = [{ fieldName: '', operator: criteriaOperator.equals.name, value: undefined }];
}
} else {
if (Tools.isEmpty(newVal)) {
modelValueObject['operator'] = criteriaMode.and.name;
modelValueObject['criteria'] = [{ fieldName: '', operator: criteriaOperator.equals.name, value: undefined }];
}
}
if (newVal !== oldVal) {
fieldMethodsClass.updateValue(newVal);
}
},
);
const getSqlValue = () => {
return props.mode === 'criteria' ? CriteriaUtil.criteriaToSql(modelValue.value) : modelValue.value;
};
const getCriteriaValue = () => {
return props.mode === 'criteria' ? modelValue.value : toRaw(modelValueObject);
};
onBeforeMount(() => {
if (Tools.isEmpty(modelValue.value)) {
modelValue.value =
props.mode === 'criteria'
? {
operator: criteriaMode.and.name,
criteria: [{ fieldName: '', operator: criteriaOperator.equals.name, value: undefined }],
}
: '';
modelValueObject['operator'] = criteriaMode.and.name;
modelValueObject['criteria'] = [{ fieldName: '', operator: criteriaOperator.equals.name, value: undefined }];
} else {
// modelValue modelValueObject
if (props.mode === 'criteria') {
modelValueObject['operator'] = modelValue.value['operator'];
modelValueObject['criteria'] = modelValue.value['criteria'];
} else {
const sqlPrefix = 'select * from t where ';
const criteria = CriteriaUtil.sqlToCriteria(sqlPrefix + modelValue.value);
modelValueObject['operator'] = criteria['operator'];
modelValueObject['criteria'] = criteria['criteria'];
}
}
});
defineExpose({
validate: fieldMethodsClass.validate,
setValue: fieldMethodsClass.setValue,
getValue: fieldMethodsClass.getValue,
clearValue: fieldMethodsClass.clearValue,
getSqlValue,
getCriteriaValue,
});
</script>
<style lang="css">
@import './css/comm.css';
</style>

378
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<string> = []) {
const options = <any>[];
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<string> = [], exclude: Array<string> = []) {
const options = <any>[];
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 = <any>[];
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 = <any>[];
let flag = true;
if (astObj['parentheses'] && astObj['type'] === astForEachType.binary_expr) {
// 有括号且类型为 binary_expr 为一组表达式子规则
const childCriterias = <any>[];
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 = <any>[];
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] || '';
}
});
};

22
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;
}

6
io.sc.platform.core.frontend/src/platform/components/select/WOrgSelect.vue

@ -63,7 +63,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, useAttrs, toRaw, watch } from 'vue'; import { ref, computed, useAttrs, toRaw, watch, onMounted } from 'vue';
import { Tools, axios, Environment, Formater } from '@/platform'; import { Tools, axios, Environment, Formater } from '@/platform';
import { FormFieldProps } from '@/platform/components/form/FormField.ts'; import { FormFieldProps } from '@/platform/components/form/FormField.ts';
import { FormFieldMethods } from '../form/FormField'; import { FormFieldMethods } from '../form/FormField';
@ -264,6 +264,10 @@ const setObjectValueByValue = async (value) => {
} }
}; };
onMounted(() => {
setObjectValueByValue(modelValue.value);
});
defineExpose({ defineExpose({
validate: fieldMethodsClass.validate, validate: fieldMethodsClass.validate,
setValue: fieldMethodsClass.setValue, setValue: fieldMethodsClass.setValue,

6
io.sc.platform.core.frontend/src/platform/components/select/WUserSelect.vue

@ -84,7 +84,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, useAttrs, toRaw, watch } from 'vue'; import { ref, computed, useAttrs, toRaw, watch, onMounted } from 'vue';
import { Tools, axios, Environment, Formater } from '@/platform'; import { Tools, axios, Environment, Formater } from '@/platform';
import { FormFieldProps } from '@/platform/components/form/FormField.ts'; import { FormFieldProps } from '@/platform/components/form/FormField.ts';
import { FormFieldMethods } from '../form/FormField'; import { FormFieldMethods } from '../form/FormField';
@ -309,6 +309,10 @@ const setObjectValueByValue = async (value) => {
} }
}; };
onMounted(() => {
setObjectValueByValue(modelValue.value);
});
defineExpose({ defineExpose({
validate: fieldMethodsClass.validate, validate: fieldMethodsClass.validate,
setValue: fieldMethodsClass.setValue, setValue: fieldMethodsClass.setValue,

152
io.sc.platform.core.frontend/src/views/likm/Form.vue

@ -6,11 +6,131 @@
<br /> <br />
<br /> <br />
<div class="flex justify-center"> <div class="flex justify-center">
<div class="w-[800px]"> <div v-if="mode === 'criteria'" class="w-[800px]">
<w-text-editor label="富文本组件" :required-if="true"></w-text-editor> <w-query-builder
<br /> ref="queryBuilderRef"
v-model="objectModelValue"
mode="criteria"
:options="[
{
label: '姓名',
value: 'name',
operator: ['equals', 'contains'],
useComponent: {
type: 'w-text',
requiredIf: true,
},
},
{
label: '年龄',
value: 'age',
excludeOperator: ['contains', 'notContains'],
useComponent: {
type: 'w-number',
},
},
{
label: '性别',
value: 'sex',
useComponent: {
type: 'w-radio',
options: [
{ label: '男', value: '1' },
{ label: '女', value: '2' },
],
},
},
{
label: '学历',
value: 'xl',
useComponent: {
type: 'w-checkbox-group',
options: [
{ label: '博士', value: '1' },
{ label: '硕士', value: '2' },
{ label: '本科及以下', value: '3' },
],
},
},
{
label: '出生日期',
value: 'birthday',
useComponent: {
type: 'w-date',
},
},
]"
></w-query-builder>
</div>
<div v-else-if="mode === 'sql'" class="w-[800px]">
<w-query-builder
ref="queryBuilderRef"
v-model="string2ModelValue"
mode="sql"
:options="[
{
label: '姓名',
value: 'name',
operator: ['equals', 'contains'],
useComponent: {
type: 'w-text',
requiredIf: true,
},
},
{
label: '年龄',
value: 'age',
excludeOperator: ['contains', 'notContains'],
useComponent: {
type: 'w-number',
},
},
{
label: '性别',
value: 'sex',
useComponent: {
type: 'w-radio',
options: [
{ label: '男', value: '1' },
{ label: '女', value: '2' },
],
},
},
{
label: '学历',
value: 'xl',
useComponent: {
type: 'w-checkbox-group',
options: [
{ label: '博士', value: '1' },
{ label: '硕士', value: '2' },
{ label: '本科及以下', value: '3' },
],
},
},
{
label: '出生日期',
value: 'birthday',
useComponent: {
type: 'w-date',
},
},
]"
></w-query-builder>
</div> </div>
<!-- <div class="pl-[20px]">自定义表格选择值: {{ arr1ModelValue }} <br /><br /><br /></div> --> </div>
<br /><br />
<div class="pl-[20px]">模型值: {{ mode === 'criteria' ? objectModelValue : string2ModelValue }} <br /><br /></div>
<br /><br />
<div class="pl-[20px]">
<q-btn
label="切换模式"
@click="
() => {
mode = mode === 'criteria' ? 'sql' : 'criteria';
}
"
></q-btn>
</div> </div>
<br /> <br />
<br /> <br />
@ -254,7 +374,7 @@
:cols-x-gap="8" :cols-x-gap="8"
@update-value=" @update-value="
(args) => { (args) => {
console.info('form.updateValue=====', args); // console.info('form.updateValue=====', args);
} }
" "
> >
@ -264,18 +384,34 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, reactive, computed } from 'vue';
import { Environment, Formater } from '@/platform'; import { Environment, Formater } from '@/platform';
const mode = ref('criteria');
const booleanModelValue = ref(false); const booleanModelValue = ref(false);
const string1ModelValue = ref(''); const string1ModelValue = ref('');
const string2ModelValue = ref(''); const string2ModelValue = ref(`name = '1' and age <> 22 and ( sex = '2' or xl NOT IN ('1','2') or ( not ( birthday = '2024-09-25' and name LIKE '%222%' ) ) )`);
const numberModelValue = ref(1); const numberModelValue = ref(1);
const objectModelValue = ref(undefined);
const arr1ModelValue = ref([]); const arr1ModelValue = ref([]);
const arr2ModelValue = ref([]); const arr2ModelValue = ref([]);
const arr3ModelValue = ref([]); const arr3ModelValue = ref([]);
const objectModelValue = ref({
operator: 'and',
criteria: [
{ fieldName: 'name', operator: 'contains', value: '张三' },
{ fieldName: 'age', operator: 'notEquals', value: 23 },
{
operator: 'or',
criteria: [
{ fieldName: 'sex', operator: 'equals', value: '2' },
{ fieldName: 'xl', operator: 'inSet', value: ['1', '2'] },
],
},
{ fieldName: 'birthday', operator: 'equals', value: '2024-09-20' },
],
});
const queryBuilderRef = ref();
const formRef = ref(); const formRef = ref();
const submit = async () => { const submit = async () => {

23
io.sc.platform.core.frontend/src/views/likm/Grid.vue

@ -4,7 +4,28 @@
ref="gridRef" ref="gridRef"
title="示例列表" title="示例列表"
:data-url="Environment.apiContextPath('/api/system/application')" :data-url="Environment.apiContextPath('/api/system/application')"
:toolbar-actions="['add', 'edit', 'query']" db-click-operation="testAdd"
:toolbar-actions="[
'add',
{
extend: 'add',
name: 'testAdd',
click: (args) => {
console.info('testAdd======', args);
},
},
[
'remove',
{
extend: 'edit',
name: 'testEdit',
click: (args) => {
console.info('testEdit======', args);
},
},
],
'query',
]"
:query-form-fields="[{ name: 'code', label: '编码', type: 'w-text' }]" :query-form-fields="[{ name: 'code', label: '编码', type: 'w-text' }]"
:columns="[ :columns="[
{ name: 'code', label: '编码', type: 'w-text' }, { name: 'code', label: '编码', type: 'w-text' },

Loading…
Cancel
Save