Files
zhgh_zjvtit/target/classes/static/components/plugins/conditionGroup/index.vue
T
2026-08-26 14:25:17 +08:00

594 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
module.exports = {
name: "index",
props: {
group: {
type: Object,
required: true
},
field_options: {
type: Array,
required: true
},
operator_options: {
type: Array,
default: () => [
{ label: "等于", value: "=" },
{ label: "不等于", value: "!=" },
{ label: "大于", value: ">" },
{ label: "小于", value: "<" },
{ label: "大于等于", value: ">=" },
{ label: "小于等于", value: "<=" },
{ label: "包含", value: "LIKE" },
{ label: "为空", value: "IS NULL" },
{ label: "不为空", value: "IS NOT NULL" },
{ label: "在列表中", value: "IN" },
{ label: "不在列表中", value: "NOT IN" },
{ label: "在范围内", value: "BETWEEN" }
]
},
can_remove: {
type: Boolean,
default: false
}
},
data() {
return {
fieldTypes: {}, // 存储字段类型信息
errors: [] // 存储验证错误
}
},
created() {
// 初始化字段类型信息
this.initFieldTypes()
},
methods: {
initFieldTypes() {
// 从field_options中提取字段类型信息
this.field_options.forEach((field) => {
if (field.type) {
this.fieldTypes[field.value] = field.type
} else {
// 默认为字符串类型
this.fieldTypes[field.value] = "string"
}
})
},
getFieldType(fieldName) {
return this.fieldTypes[fieldName] || "string"
},
addCondition() {
if (!this.group.conditions) {
this.$set(this.group, "conditions", [])
}
this.group.conditions.push({
field: "",
operator: "=",
value: "",
valid: false
})
this.validateAndEmitChange()
},
removeCondition(index) {
this.group.conditions.splice(index, 1)
this.validateAndEmitChange()
},
addGroup() {
if (!this.group.groups) {
this.$set(this.group, "groups", [])
}
this.group.groups.push({
logic: "AND",
conditions: [],
groups: []
})
this.validateAndEmitChange()
},
removeGroup(index) {
this.group.groups.splice(index, 1)
this.validateAndEmitChange()
},
isNullOperator(operator) {
return operator === "IS NULL" || operator === "IS NOT NULL"
},
isInOperator(operator) {
return operator === "IN" || operator === "NOT IN"
},
isBetweenOperator(operator) {
return operator === "BETWEEN"
},
validateCondition(condition) {
// 检查条件是否有效
if (!condition.field || !condition.operator) {
return false
}
// 对于IS NULL和IS NOT NULL操作符,不需要值
if (this.isNullOperator(condition.operator)) {
return true
}
// 对于其他操作符,需要值
return condition.value !== undefined && condition.value !== ""
},
validateGroup(group) {
let isValid = true
// 验证条件
if (group.conditions && group.conditions.length > 0) {
group.conditions.forEach((condition) => {
condition.valid = this.validateCondition(condition)
isValid = isValid && condition.valid
})
}
// 验证嵌套组
if (group.groups && group.groups.length > 0) {
group.groups.forEach((nestedGroup) => {
isValid = isValid && this.validateGroup(nestedGroup)
})
}
// 一个组至少需要一个条件或嵌套组才有效
if ((group.conditions && group.conditions.length > 0) || (group.groups && group.groups.length > 0)) {
return isValid
} else {
return false
}
},
formatValue(value, type) {
// 根据字段类型格式化值
if (value === null || value === undefined || value === "") {
return "NULL"
}
switch (type.toLowerCase()) {
case "number":
case "int":
case "integer":
case "float":
case "double":
case "decimal":
return value
case "date":
case "datetime":
case "time":
case "string":
default:
// 转义单引号
const escaped = String(value).replace(/'/g, "''")
return `'${escaped}'`
}
},
buildSql(group = this.group) {
let sql = []
// 处理条件
if (group.conditions && group.conditions.length > 0) {
group.conditions.forEach((condition) => {
if (condition.field && condition.operator) {
const fieldType = this.getFieldType(condition.field)
if (this.isNullOperator(condition.operator)) {
sql.push(`${condition.field} ${condition.operator}`)
} else if (this.isInOperator(condition.operator) && condition.value) {
// 处理IN操作符 - 假设值是以逗号分隔的列表
const values = condition.value
.split(",")
.map((v) => v.trim())
.filter((v) => v !== "")
.map((v) => this.formatValue(v, fieldType))
.join(", ")
if (values) {
sql.push(`${condition.field} ${condition.operator} (${values})`)
}
} else if (this.isBetweenOperator(condition.operator) && condition.value) {
// 处理BETWEEN操作符 - 假设值是以逗号分隔的两个值
const parts = condition.value.split(",").map((v) => v.trim())
if (parts.length === 2) {
const from = this.formatValue(parts[0], fieldType)
const to = this.formatValue(parts[1], fieldType)
sql.push(`${condition.field} BETWEEN ${from} AND ${to}`)
}
} else if (condition.value !== undefined && condition.value !== "") {
// 其他操作符
const formattedValue = this.formatValue(condition.value, fieldType)
sql.push(`${condition.field} ${condition.operator} ${formattedValue}`)
}
}
})
}
// 处理嵌套组
if (group.groups && group.groups.length > 0) {
group.groups.forEach((nestedGroup) => {
const nestedSql = this.buildSql(nestedGroup)
if (nestedSql) {
sql.push(`(${nestedSql})`)
}
})
}
// 用逻辑运算符连接所有条件
return sql.length > 0 ? sql.join(` ${group.logic} `) : ""
},
validateAndEmitChange() {
// 验证所有条件
const isValid = this.validateGroup(this.group)
// 发出变更事件
this.$emit("change", {
sql: this.buildSql(),
valid: isValid
})
},
getOperatorsByFieldType(fieldType) {
// 根据字段类型过滤可用的操作符
if (!fieldType) return this.operator_options
const type = fieldType.toLowerCase()
return this.operator_options.filter((op) => {
// 数值类型
if (['number', 'int', 'integer', 'float', 'double', 'decimal'].includes(type)) {
// 数值类型不应该使用LIKE操作符
return op.value !== 'LIKE'
}
// 日期类型
else if (['date', 'datetime', 'time'].includes(type)) {
// 日期类型不应该使用LIKE和IN操作符
return !['LIKE', 'IN', 'NOT IN'].includes(op.value)
}
// 字符串类型,支持所有操作符
return true
})
}
},
watch: {
group: {
handler: "validateAndEmitChange",
deep: true
},
field_options: {
handler: "initFieldTypes",
immediate: true
}
}
}
</script>
<template>
<div class="condition-builder">
<!-- 主逻辑选择和组操作 -->
<div class="condition-group-header">
<div class="logic-selector">
<span class="logic-label">匹配方式</span>
<el-radio-group v-model="group.logic" size="small" @change="validateAndEmitChange">
<el-radio-button label="AND">满足所有条件</el-radio-button>
<el-radio-button label="OR">满足任意条件</el-radio-button>
</el-radio-group>
</div>
<el-button v-if="can_remove" type="danger" size="mini" icon="el-icon-delete" @click="$emit('remove')" class="remove-group-btn">
删除组
</el-button>
</div>
<!-- 条件列表 -->
<div class="conditions-container">
<div
v-for="(condition, index) in group.conditions"
:key="'c-' + index"
class="condition-row"
:class="{ 'invalid-condition': condition.valid === false }"
>
<div class="condition-index">{{ index + 1 }}</div>
<el-select
v-model="condition.field"
filterable
placeholder="请选择字段"
size="small"
class="field-select"
@change="validateAndEmitChange"
>
<el-option v-for="field in field_options" :key="field.value" :label="field.label" :value="field.value">
<span>{{ field.label }}</span>
<span class="field-type-hint" v-if="field.type">({{ field.type }})</span>
</el-option>
</el-select>
<el-select v-model="condition.operator" placeholder="操作符" size="small" class="operator-select" @change="validateAndEmitChange">
<el-option
v-for="op in getOperatorsByFieldType(getFieldType(condition.field))"
:key="op.value"
:label="op.label"
:value="op.value"
></el-option>
</el-select>
<!-- 根据操作符类型和字段类型显示不同的输入控件 -->
<template v-if="!isNullOperator(condition.operator)">
<!-- 对于IN操作符显示标签输入框 -->
<el-input
v-if="isInOperator(condition.operator)"
v-model="condition.value"
placeholder="多个值用逗号分隔"
size="small"
class="value-input"
@change="validateAndEmitChange"
></el-input>
<!-- 对于BETWEEN操作符显示两个输入框 -->
<div v-else-if="isBetweenOperator(condition.operator)" class="between-inputs">
<el-input
v-model="condition.value"
placeholder="起始值,结束值"
size="small"
class="value-input"
@change="validateAndEmitChange"
></el-input>
</div>
<!-- 对于其他操作符根据字段类型显示不同的输入控件 -->
<template v-else>
<!-- 数字类型 -->
<el-input
v-if="['number', 'int', 'integer', 'float', 'double', 'decimal'].includes(getFieldType(condition.field))"
v-model.number="condition.value"
placeholder="请输入数值"
size="small"
class="value-input"
type="number"
@change="validateAndEmitChange"
></el-input>
<!-- 日期类型 -->
<el-date-picker
v-else-if="getFieldType(condition.field) === 'date'"
v-model="condition.value"
type="date"
placeholder="选择日期"
size="small"
class="value-input"
value-format="yyyy-MM-dd"
@change="validateAndEmitChange"
></el-date-picker>
<!-- 日期时间类型 -->
<el-date-picker
v-else-if="getFieldType(condition.field) === 'datetime'"
v-model="condition.value"
type="datetime"
placeholder="选择日期时间"
size="small"
class="value-input"
value-format="yyyy-MM-dd HH:mm:ss"
@change="validateAndEmitChange"
></el-date-picker>
<!-- 时间类型 -->
<el-time-picker
v-else-if="getFieldType(condition.field) === 'time'"
v-model="condition.value"
placeholder="选择时间"
size="small"
class="value-input"
value-format="HH:mm:ss"
@change="validateAndEmitChange"
></el-time-picker>
<!-- 默认为字符串类型 -->
<el-input
v-else
v-model="condition.value"
placeholder="请输入值"
size="small"
class="value-input"
@change="validateAndEmitChange"
></el-input>
</template>
</template>
<!-- 空值占位使布局保持一致 -->
<div v-else class="value-placeholder"></div>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeCondition(index)" class="remove-btn"></el-button>
</div>
<!-- 空条件提示 -->
<div class="empty-condition-hint" v-if="!group.conditions || group.conditions.length === 0">
<i class="el-icon-info"></i>
请添加筛选条件
</div>
</div>
<!-- 嵌套条件组 -->
<div v-for="(nestedGroup, index) in group.groups" :key="'g-' + index" class="nested-group">
<div class="nested-group-header">
<span class="nested-group-title">子条件组 {{ index + 1 }}</span>
</div>
<condition-group
:group="nestedGroup"
:field_options="field_options"
:operator_options="operator_options"
:can_remove="true"
@remove="removeGroup(index)"
@change="validateAndEmitChange"
></condition-group>
</div>
<!-- 操作按钮区 -->
<div class="condition-actions">
<el-button type="primary" size="small" icon="el-icon-plus" @click="addCondition">添加条件</el-button>
<el-button type="success" size="small" icon="el-icon-folder-add" @click="addGroup">添加条件组</el-button>
</div>
<!-- SQL预览 -->
<div class="sql-preview" v-if="buildSql()">
<div class="sql-preview-header">
<div class="sql-preview-title">条件预览:</div>
</div>
<el-input type="textarea" :value="buildSql()" readonly :rows="2" class="sql-preview-content"></el-input>
</div>
</div>
</template>
<style scoped>
.condition-builder {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
margin-bottom: 15px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.condition-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.logic-label {
margin-right: 10px;
color: #606266;
}
.conditions-container {
padding: 5px;
margin-bottom: 15px;
}
.condition-row {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 8px;
background-color: #f9f9f9;
border-radius: 4px;
transition: all 0.3s;
}
.condition-row:hover {
background-color: #f0f7ff;
}
.invalid-condition {
border: 1px dashed #f56c6c;
}
.condition-index {
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #409eff;
color: white;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
.field-select {
width: 150px;
margin-right: 10px;
}
.operator-select {
width: 120px;
margin-right: 10px;
}
.value-input {
width: 200px;
margin-right: 10px;
}
.between-inputs {
display: flex;
align-items: center;
width: 200px;
margin-right: 10px;
}
.value-placeholder {
width: 200px;
margin-right: 10px;
}
.field-type-hint {
color: #909399;
margin-left: 5px;
font-size: 12px;
}
.nested-group {
margin: 10px 0;
padding: 10px 0 10px 20px;
border-left: 2px solid #409eff;
background-color: #f9fafc;
border-radius: 0 4px 4px 0;
}
.nested-group-header {
margin-bottom: 10px;
}
.nested-group-title {
font-weight: bold;
color: #409eff;
}
.condition-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #ebeef5;
}
.remove-btn {
margin-left: auto;
}
.remove-group-btn {
margin-left: 10px;
}
.sql-preview {
margin-top: 20px;
padding: 15px;
background-color: #f8f8f8;
border-radius: 4px;
border-left: 3px solid #409eff;
}
.sql-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.sql-preview-title {
font-size: 14px;
font-weight: bold;
color: #303133;
}
.sql-preview-content {
font-family: "Courier New", Courier, monospace;
background-color: #f8f8f8;
}
.empty-condition-hint {
padding: 15px;
text-align: center;
color: #909399;
background-color: #f5f7fa;
border-radius: 4px;
margin: 10px 0;
}
</style>