重构
This commit is contained in:
@ -2,9 +2,7 @@
|
||||
<el-dialog :visible.sync="dialogTableVisible" :close-on-press-escape="false" :close-on-click-modal="false" :show-close="false" destroy-on-close lock-scroll append-to-body width="400px" class="ems-dialog" :title="mode === 'add'?'新增配置':`编辑配置` " >
|
||||
<el-form v-loading="loading>0" ref="addTempForm" :model="formData" :rules="rules" size="medium" label-width="140px">
|
||||
<el-form-item label="站点" prop="siteId">
|
||||
<el-select v-model="formData.siteId" :disabled="mode === 'edit'" placeholder="请选择站点" :loading="searchLoading" loading-text="正在加载数据">
|
||||
<el-option v-for="(item,index) in siteList" :key="index+'zdxeSelect'" :label="item.siteName" :value="item.siteId" ></el-option>
|
||||
</el-select>
|
||||
<el-input v-model="formData.siteId" placeholder="请先在顶部选择站点" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="消息等级" prop="qos">
|
||||
<el-select v-model="formData.qos" placeholder="请选择消息等级">
|
||||
@ -30,13 +28,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import {editMqtt,addMqtt,getMqttDetail} from "@/api/ems/site";
|
||||
import {getAllSites} from '@/api/ems/zddt'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading:0,
|
||||
siteList:[],
|
||||
searchLoading:false,
|
||||
dialogTableVisible:false,
|
||||
mode:'',
|
||||
formData: {
|
||||
@ -48,7 +43,7 @@ export default {
|
||||
},
|
||||
rules: {
|
||||
siteId:[
|
||||
{ required: true, message: '请选择站点', trigger: 'blur'},
|
||||
{ required: true, message: '请先在顶部选择站点', trigger: 'blur'},
|
||||
],
|
||||
qos:[
|
||||
{ required: true, message: '请选择消息等级', trigger: 'blur'},
|
||||
@ -63,23 +58,20 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDialog(id){
|
||||
getRouteSiteId() {
|
||||
const siteId = this.$route?.query?.siteId
|
||||
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
|
||||
},
|
||||
showDialog(id, siteId = ''){
|
||||
this.dialogTableVisible = true
|
||||
this.getZdList()
|
||||
if(id){
|
||||
this.mode = 'edit'
|
||||
this.formData.id = id
|
||||
this.getDetail(id)
|
||||
}else{
|
||||
this.mode = 'add'
|
||||
this.formData.siteId = siteId || this.getRouteSiteId()
|
||||
}
|
||||
},
|
||||
//获取站点列表
|
||||
getZdList(){
|
||||
this.searchLoading=true
|
||||
getAllSites().then(response => {
|
||||
this.siteList = response?.data || []
|
||||
}).finally(() => {this.searchLoading=false})
|
||||
},
|
||||
getDetail(id){
|
||||
getMqttDetail(id).then(response => {
|
||||
@ -132,6 +124,8 @@ export default {
|
||||
// 清空所有数据
|
||||
this.formData= {
|
||||
id:'',//设备唯一标识
|
||||
siteId:'',
|
||||
qos:'',
|
||||
mqttTopic:'',
|
||||
topicName:''
|
||||
}
|
||||
|
||||
@ -160,7 +160,7 @@ export default {
|
||||
}).finally(() => {this.loading=false})
|
||||
},
|
||||
addPowerConfig(id=''){
|
||||
this.$refs.addMqtt.showDialog(id);
|
||||
this.$refs.addMqtt.showDialog(id, this.form.siteId);
|
||||
},
|
||||
deleteMqtt(row){
|
||||
this.$confirm(`确认要删除该配置吗?`, {
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="点位ID">
|
||||
<el-input v-model.trim="queryParams.pointId" placeholder="请输入点位ID" clearable style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据键">
|
||||
<el-input v-model="queryParams.dataKey" placeholder="请输入数据键" clearable style="width: 180px" />
|
||||
</el-form-item>
|
||||
@ -115,6 +118,18 @@
|
||||
<el-form ref="pointForm" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="点位ID" prop="pointId">
|
||||
<el-input v-model.trim="form.pointId" placeholder="请输入点位ID(系统唯一)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="点位名称" prop="pointName">
|
||||
<el-input v-model.trim="form.pointName" placeholder="请输入点位名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col v-if="form.id" :span="12">
|
||||
<el-form-item label="站点" prop="siteId">
|
||||
<el-input v-model="form.siteId" disabled />
|
||||
</el-form-item>
|
||||
@ -129,19 +144,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="点位ID" prop="pointId">
|
||||
<el-input v-model.trim="form.pointId" placeholder="请输入点位ID(系统唯一)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="点位名称" prop="pointName">
|
||||
<el-input v-model.trim="form.pointName" placeholder="请输入点位名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12" v-if="form.pointType === 'data' || form.pointType === 'calc'">
|
||||
<el-col :span="12" v-if="form.pointType === 'data'">
|
||||
<el-form-item label="设备类型" prop="deviceCategory">
|
||||
<el-select v-model="form.deviceCategory" placeholder="请选择设备类型" @change="handleFormCategoryChange">
|
||||
<el-option
|
||||
@ -199,6 +202,9 @@
|
||||
:rows="3"
|
||||
placeholder="例如:voltageA * currentA + powerLoss"
|
||||
/>
|
||||
<div class="calc-expression-tips">
|
||||
示例:A + B * 2;(A + B) / C;voltageA * currentA + powerLoss
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" v-if="form.pointType === 'calc'">
|
||||
@ -356,6 +362,7 @@ export default {
|
||||
siteId: '',
|
||||
deviceCategory: '',
|
||||
deviceId: '',
|
||||
pointId: '',
|
||||
dataKey: '',
|
||||
pointDesc: '',
|
||||
pointType: 'data'
|
||||
@ -370,6 +377,8 @@ export default {
|
||||
siteId: '',
|
||||
deviceId: '',
|
||||
pointId: '',
|
||||
pointName: '',
|
||||
dataKey: '',
|
||||
pointType: 'data',
|
||||
rangeType: 'custom',
|
||||
startTime: '',
|
||||
@ -439,6 +448,15 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatPointCurveName(point = {}) {
|
||||
const pointId = String(point.pointId || '').trim()
|
||||
const pointName = String(point.pointName || '').trim()
|
||||
const dataKey = String(point.dataKey || '').trim()
|
||||
const idPart = pointId || '-'
|
||||
const namePart = pointName || '-'
|
||||
const keyPart = dataKey || '-'
|
||||
return `${idPart}-${namePart}(${keyPart})`
|
||||
},
|
||||
formatDeviceInfo(row) {
|
||||
if (!row) {
|
||||
return '-'
|
||||
@ -505,14 +523,14 @@ export default {
|
||||
return
|
||||
}
|
||||
const points = this.tableData
|
||||
.filter(item => item.pointType !== 'calc' && item.siteId && item.deviceId && item.dataKey)
|
||||
.filter(item => item.pointType === 'data' && item.siteId && item.deviceId && item.dataKey)
|
||||
.map(item => ({
|
||||
siteId: item.siteId,
|
||||
deviceId: item.deviceId,
|
||||
dataKey: item.dataKey
|
||||
}))
|
||||
if (!points.length) {
|
||||
const calcRows = this.tableData.filter(item => item.pointType === 'calc')
|
||||
const calcRows = this.tableData.filter(item => item.pointType !== 'data')
|
||||
if (!calcRows.length) {
|
||||
this.tableData = this.applyCalcLatestValues(this.tableData)
|
||||
return
|
||||
@ -544,13 +562,13 @@ export default {
|
||||
})
|
||||
const mergedRows = this.applyCalcLatestValues([...depRowsWithLatest, ...this.tableData])
|
||||
const calcLatestMap = mergedRows
|
||||
.filter(item => item.pointType === 'calc')
|
||||
.filter(item => item.pointType !== 'data')
|
||||
.reduce((acc, item) => {
|
||||
acc[this.getCalcRowKey(item)] = item.latestValue
|
||||
return acc
|
||||
}, {})
|
||||
this.tableData = this.tableData.map(row => {
|
||||
if (row.pointType !== 'calc') return row
|
||||
if (row.pointType === 'data') return row
|
||||
const nextLatest = calcLatestMap[this.getCalcRowKey(row)]
|
||||
return {
|
||||
...row,
|
||||
@ -584,12 +602,83 @@ export default {
|
||||
getCalcRowKey(row) {
|
||||
return `${row.id || ''}__${row.siteId || ''}__${row.pointId || ''}`
|
||||
},
|
||||
extractExpressionTokens(expression) {
|
||||
tokenizeCalcExpression(expression) {
|
||||
const expr = String(expression || '').trim()
|
||||
if (!expr) return []
|
||||
if (!/^[0-9A-Za-z_+\-*/().\s]+$/.test(expr)) return []
|
||||
const matched = expr.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) || []
|
||||
return Array.from(new Set(matched.map(item => String(item || '').trim()).filter(Boolean)))
|
||||
if (!expr) {
|
||||
return []
|
||||
}
|
||||
if (!/^[0-9A-Za-z_+\-*/().\s]+$/.test(expr)) {
|
||||
throw new Error('计算表达式仅支持四则运算和括号')
|
||||
}
|
||||
const tokens = []
|
||||
let index = 0
|
||||
while (index < expr.length) {
|
||||
const ch = expr[index]
|
||||
if (/\s/.test(ch)) {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
if (/[0-9.]/.test(ch)) {
|
||||
const start = index
|
||||
let hasDot = ch === '.'
|
||||
index += 1
|
||||
while (index < expr.length) {
|
||||
const next = expr[index]
|
||||
if (/[0-9]/.test(next)) {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
if (next === '.' && !hasDot) {
|
||||
hasDot = true
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
const numText = expr.slice(start, index)
|
||||
if (!Number.isFinite(Number(numText))) {
|
||||
throw new Error(`数值格式错误: ${numText}`)
|
||||
}
|
||||
tokens.push({ type: 'number', value: Number(numText) })
|
||||
continue
|
||||
}
|
||||
if (/[A-Za-z_]/.test(ch)) {
|
||||
const start = index
|
||||
index += 1
|
||||
while (index < expr.length && /[A-Za-z0-9_]/.test(expr[index])) {
|
||||
index += 1
|
||||
}
|
||||
const word = expr.slice(start, index)
|
||||
tokens.push({ type: 'identifier', value: word })
|
||||
continue
|
||||
}
|
||||
if (ch === '(' || ch === ')') {
|
||||
tokens.push({ type: ch, value: ch })
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
if (['+', '-', '*', '/'].includes(ch)) {
|
||||
tokens.push({ type: 'operator', value: ch })
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
throw new Error(`表达式包含非法字符: ${ch}`)
|
||||
}
|
||||
tokens.push({ type: 'eof', value: '<eof>' })
|
||||
return tokens
|
||||
},
|
||||
extractExpressionTokens(expression) {
|
||||
const reserved = new Set(['IF'])
|
||||
try {
|
||||
const tokens = this.tokenizeCalcExpression(expression)
|
||||
const identifiers = tokens
|
||||
.filter(token => token.type === 'identifier')
|
||||
.map(token => String(token.value || '').trim())
|
||||
.filter(token => token && !reserved.has(token.toUpperCase()))
|
||||
return Array.from(new Set(identifiers))
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
loadCalcDependencyRows(calcRows = []) {
|
||||
if (!calcRows.length) {
|
||||
@ -656,7 +745,7 @@ export default {
|
||||
applyCalcLatestValues(rows = []) {
|
||||
const dataPointMap = {}
|
||||
rows.forEach(row => {
|
||||
if (row.pointType === 'calc') return
|
||||
if (row.pointType !== 'data') return
|
||||
if (row.latestValue === '-' || row.latestValue === '' || row.latestValue === null || row.latestValue === undefined) {
|
||||
return
|
||||
}
|
||||
@ -687,7 +776,7 @@ export default {
|
||||
})
|
||||
})
|
||||
return rows.map(row => {
|
||||
if (row.pointType !== 'calc') return row
|
||||
if (row.pointType === 'data') return row
|
||||
return {
|
||||
...row,
|
||||
latestValue: this.evaluateCalcExpression(row.calcExpression, row.siteId, row.deviceId, dataPointMap)
|
||||
@ -697,37 +786,260 @@ export default {
|
||||
evaluateCalcExpression(expression, siteId, deviceId, dataPointMap) {
|
||||
const expr = String(expression || '').trim()
|
||||
if (!expr) return '-'
|
||||
if (!/^[0-9A-Za-z_+\-*/().\s]+$/.test(expr)) return '-'
|
||||
const tokenPattern = /\b[A-Za-z_][A-Za-z0-9_]*\b/g
|
||||
const missingKeys = []
|
||||
const resolvedExpr = expr.replace(tokenPattern, token => {
|
||||
const candidates = [token, token.toUpperCase(), token.toLowerCase()]
|
||||
let value
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
const candidate = candidates[i]
|
||||
const deviceKey = `${siteId || ''}__${deviceId || ''}__${candidate}`
|
||||
const siteKey = `${siteId || ''}__${candidate}`
|
||||
value = dataPointMap[deviceKey] !== undefined ? dataPointMap[deviceKey] : dataPointMap[siteKey]
|
||||
if (value !== undefined && value !== null && value !== '' && value !== '-') {
|
||||
break
|
||||
try {
|
||||
const tokens = this.tokenizeCalcExpression(expr)
|
||||
let current = 0
|
||||
|
||||
const peek = () => tokens[current] || { type: 'eof', value: '<eof>' }
|
||||
const matchType = type => {
|
||||
if (peek().type !== type) return false
|
||||
current += 1
|
||||
return true
|
||||
}
|
||||
const matchOperator = op => {
|
||||
const token = peek()
|
||||
if (token.type !== 'operator' || token.value !== op) return false
|
||||
current += 1
|
||||
return true
|
||||
}
|
||||
const expectType = (type, message) => {
|
||||
if (!matchType(type)) {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
if (value === undefined || value === null || value === '' || value === '-') {
|
||||
missingKeys.push(token)
|
||||
return 'NaN'
|
||||
const resolveIdentifierValue = name => {
|
||||
const candidates = [name, name.toUpperCase(), name.toLowerCase()]
|
||||
for (let i = 0; i < candidates.length; i += 1) {
|
||||
const candidate = candidates[i]
|
||||
const deviceKey = `${siteId || ''}__${deviceId || ''}__${candidate}`
|
||||
const siteKey = `${siteId || ''}__${candidate}`
|
||||
const value = dataPointMap[deviceKey] !== undefined ? dataPointMap[deviceKey] : dataPointMap[siteKey]
|
||||
if (value === undefined || value === null || value === '' || value === '-') {
|
||||
continue
|
||||
}
|
||||
const numberValue = Number(value)
|
||||
if (Number.isFinite(numberValue)) {
|
||||
return numberValue
|
||||
}
|
||||
}
|
||||
throw new Error(`缺少变量: ${name}`)
|
||||
}
|
||||
const numberValue = Number(value)
|
||||
if (Number.isNaN(numberValue)) {
|
||||
missingKeys.push(token)
|
||||
return 'NaN'
|
||||
const toBoolean = value => Number(value) !== 0
|
||||
|
||||
const evaluateNode = node => {
|
||||
if (!node || !node.type) {
|
||||
throw new Error('表达式节点无效')
|
||||
}
|
||||
if (node.type === 'number') return node.value
|
||||
if (node.type === 'variable') return resolveIdentifierValue(node.name)
|
||||
if (node.type === 'unary') {
|
||||
const value = evaluateNode(node.node)
|
||||
if (node.operator === '+') return value
|
||||
if (node.operator === '-') return -value
|
||||
if (node.operator === '!') return toBoolean(value) ? 0 : 1
|
||||
throw new Error(`不支持的一元操作符: ${node.operator}`)
|
||||
}
|
||||
if (node.type === 'binary') {
|
||||
if (node.operator === '&&') {
|
||||
const left = evaluateNode(node.left)
|
||||
if (!toBoolean(left)) return 0
|
||||
return toBoolean(evaluateNode(node.right)) ? 1 : 0
|
||||
}
|
||||
if (node.operator === '||') {
|
||||
const left = evaluateNode(node.left)
|
||||
if (toBoolean(left)) return 1
|
||||
return toBoolean(evaluateNode(node.right)) ? 1 : 0
|
||||
}
|
||||
const left = evaluateNode(node.left)
|
||||
const right = evaluateNode(node.right)
|
||||
if (node.operator === '+') return left + right
|
||||
if (node.operator === '-') return left - right
|
||||
if (node.operator === '*') return left * right
|
||||
if (node.operator === '/') {
|
||||
if (right === 0) {
|
||||
throw new Error('除数不能为0')
|
||||
}
|
||||
return left / right
|
||||
}
|
||||
if (node.operator === '>=') return left >= right ? 1 : 0
|
||||
if (node.operator === '<=') return left <= right ? 1 : 0
|
||||
if (node.operator === '>') return left > right ? 1 : 0
|
||||
if (node.operator === '<') return left < right ? 1 : 0
|
||||
if (node.operator === '==') return left === right ? 1 : 0
|
||||
if (node.operator === '!=') return left !== right ? 1 : 0
|
||||
throw new Error(`不支持的操作符: ${node.operator}`)
|
||||
}
|
||||
if (node.type === 'ternary') {
|
||||
return toBoolean(evaluateNode(node.condition))
|
||||
? evaluateNode(node.trueNode)
|
||||
: evaluateNode(node.falseNode)
|
||||
}
|
||||
throw new Error(`不支持的节点类型: ${node.type}`)
|
||||
}
|
||||
return String(numberValue)
|
||||
})
|
||||
if (missingKeys.length > 0) {
|
||||
return '-'
|
||||
}
|
||||
try {
|
||||
const result = Function(`"use strict"; return (${resolvedExpr});`)()
|
||||
|
||||
const parseExpression = () => parseTernary()
|
||||
const parseTernary = () => {
|
||||
const conditionNode = parseOr()
|
||||
if (matchType('?')) {
|
||||
const trueNode = parseTernary()
|
||||
expectType(':', '三元表达式缺少 :')
|
||||
const falseNode = parseTernary()
|
||||
return {
|
||||
type: 'ternary',
|
||||
condition: conditionNode,
|
||||
trueNode,
|
||||
falseNode
|
||||
}
|
||||
}
|
||||
return conditionNode
|
||||
}
|
||||
const parseOr = () => {
|
||||
let left = parseAnd()
|
||||
while (matchOperator('||')) {
|
||||
left = {
|
||||
type: 'binary',
|
||||
operator: '||',
|
||||
left,
|
||||
right: parseAnd()
|
||||
}
|
||||
}
|
||||
return left
|
||||
}
|
||||
const parseAnd = () => {
|
||||
let left = parseEquality()
|
||||
while (matchOperator('&&')) {
|
||||
left = {
|
||||
type: 'binary',
|
||||
operator: '&&',
|
||||
left,
|
||||
right: parseEquality()
|
||||
}
|
||||
}
|
||||
return left
|
||||
}
|
||||
const parseEquality = () => {
|
||||
let left = parseComparison()
|
||||
while (true) {
|
||||
if (matchOperator('==')) {
|
||||
left = {
|
||||
type: 'binary',
|
||||
operator: '==',
|
||||
left,
|
||||
right: parseComparison()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (matchOperator('!=')) {
|
||||
left = {
|
||||
type: 'binary',
|
||||
operator: '!=',
|
||||
left,
|
||||
right: parseComparison()
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return left
|
||||
}
|
||||
const parseComparison = () => {
|
||||
let left = parseAddSub()
|
||||
while (true) {
|
||||
if (matchOperator('>=')) {
|
||||
left = { type: 'binary', operator: '>=', left, right: parseAddSub() }
|
||||
continue
|
||||
}
|
||||
if (matchOperator('<=')) {
|
||||
left = { type: 'binary', operator: '<=', left, right: parseAddSub() }
|
||||
continue
|
||||
}
|
||||
if (matchOperator('>')) {
|
||||
left = { type: 'binary', operator: '>', left, right: parseAddSub() }
|
||||
continue
|
||||
}
|
||||
if (matchOperator('<')) {
|
||||
left = { type: 'binary', operator: '<', left, right: parseAddSub() }
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return left
|
||||
}
|
||||
const parseAddSub = () => {
|
||||
let left = parseMulDiv()
|
||||
while (true) {
|
||||
if (matchOperator('+')) {
|
||||
left = { type: 'binary', operator: '+', left, right: parseMulDiv() }
|
||||
continue
|
||||
}
|
||||
if (matchOperator('-')) {
|
||||
left = { type: 'binary', operator: '-', left, right: parseMulDiv() }
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return left
|
||||
}
|
||||
const parseMulDiv = () => {
|
||||
let left = parseUnary()
|
||||
while (true) {
|
||||
if (matchOperator('*')) {
|
||||
left = { type: 'binary', operator: '*', left, right: parseUnary() }
|
||||
continue
|
||||
}
|
||||
if (matchOperator('/')) {
|
||||
left = { type: 'binary', operator: '/', left, right: parseUnary() }
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return left
|
||||
}
|
||||
const parseUnary = () => {
|
||||
if (matchOperator('+')) return { type: 'unary', operator: '+', node: parseUnary() }
|
||||
if (matchOperator('-')) return { type: 'unary', operator: '-', node: parseUnary() }
|
||||
if (matchOperator('!')) return { type: 'unary', operator: '!', node: parseUnary() }
|
||||
return parsePrimary()
|
||||
}
|
||||
const parsePrimary = () => {
|
||||
const token = peek()
|
||||
if (matchType('number')) {
|
||||
return { type: 'number', value: token.value }
|
||||
}
|
||||
if (matchType('identifier')) {
|
||||
const identifier = String(token.value || '')
|
||||
if (matchType('(')) {
|
||||
if (identifier.toUpperCase() !== 'IF') {
|
||||
throw new Error(`不支持的函数: ${identifier}`)
|
||||
}
|
||||
const condition = parseExpression()
|
||||
expectType(',', 'IF函数缺少第1个逗号')
|
||||
const trueValue = parseExpression()
|
||||
expectType(',', 'IF函数缺少第2个逗号')
|
||||
const falseValue = parseExpression()
|
||||
expectType(')', 'IF函数缺少右括号')
|
||||
return {
|
||||
type: 'ternary',
|
||||
condition,
|
||||
trueNode: trueValue,
|
||||
falseNode: falseValue
|
||||
}
|
||||
}
|
||||
return { type: 'variable', name: identifier }
|
||||
}
|
||||
if (matchType('(')) {
|
||||
const node = parseExpression()
|
||||
expectType(')', '括号不匹配')
|
||||
return node
|
||||
}
|
||||
throw new Error(`表达式语法错误: ${token.value}`)
|
||||
}
|
||||
|
||||
const rootNode = parseExpression()
|
||||
if (peek().type !== 'eof') {
|
||||
throw new Error('表达式尾部有多余内容')
|
||||
}
|
||||
const result = evaluateNode(rootNode)
|
||||
if (!Number.isFinite(result)) {
|
||||
return '-'
|
||||
}
|
||||
@ -748,6 +1060,7 @@ export default {
|
||||
siteId: currentSiteId,
|
||||
deviceCategory: '',
|
||||
deviceId: '',
|
||||
pointId: '',
|
||||
dataKey: '',
|
||||
pointDesc: '',
|
||||
pointType: this.activePointTab
|
||||
@ -820,7 +1133,7 @@ export default {
|
||||
id: null,
|
||||
siteId: querySiteId,
|
||||
pointType: this.activePointTab,
|
||||
deviceCategory: this.queryParams.deviceCategory || '',
|
||||
deviceCategory: this.activePointTab === 'data' ? (this.queryParams.deviceCategory || '') : '',
|
||||
deviceId: '',
|
||||
registerAddress: '',
|
||||
pointId: '',
|
||||
@ -848,8 +1161,9 @@ export default {
|
||||
}
|
||||
this.resetForm()
|
||||
this.dialogVisible = true
|
||||
this.getFormDeviceList()
|
||||
if (this.form.pointType === 'calc') {
|
||||
if (this.form.pointType === 'data') {
|
||||
this.getFormDeviceList()
|
||||
} else {
|
||||
this.loadCalcDataPointOptions()
|
||||
}
|
||||
},
|
||||
@ -889,6 +1203,10 @@ export default {
|
||||
handleFormPointTypeChange() {
|
||||
if (this.form.pointType !== 'calc') {
|
||||
this.form.calcExpression = ''
|
||||
} else {
|
||||
this.form.deviceCategory = ''
|
||||
this.form.deviceId = ''
|
||||
this.formDeviceList = []
|
||||
}
|
||||
this.loadCalcDataPointOptions()
|
||||
},
|
||||
@ -929,11 +1247,14 @@ export default {
|
||||
this.selectedCalcDataKey = ''
|
||||
},
|
||||
openCurveDialog(row) {
|
||||
this.curveDialogTitle = `曲线 - ${row.pointDesc || row.dataKey || ''}`
|
||||
const curveName = this.formatPointCurveName(row)
|
||||
this.curveDialogTitle = `曲线 - ${curveName}`
|
||||
this.curveQuery = {
|
||||
siteId: row.siteId || '',
|
||||
deviceId: row.deviceId || '',
|
||||
pointId: row.pointId || '',
|
||||
pointName: row.pointName || '',
|
||||
dataKey: row.dataKey || '',
|
||||
pointType: row.pointType || 'data',
|
||||
rangeType: 'custom',
|
||||
startTime: '',
|
||||
@ -1080,7 +1401,7 @@ export default {
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: this.curveQuery.pointId || '点位值',
|
||||
name: this.formatPointCurveName(this.curveQuery),
|
||||
type: 'line',
|
||||
data: yData,
|
||||
connectNulls: true,
|
||||
@ -1149,8 +1470,10 @@ export default {
|
||||
this.$message.warning('请输入计算表达式')
|
||||
return
|
||||
}
|
||||
if (!/^[0-9A-Za-z_+\-*/().\s]+$/.test(calcExpression)) {
|
||||
this.$message.warning('计算表达式格式不正确')
|
||||
try {
|
||||
this.tokenizeCalcExpression(calcExpression)
|
||||
} catch (e) {
|
||||
this.$message.warning(e?.message || '计算表达式格式不正确')
|
||||
return
|
||||
}
|
||||
this.form.registerAddress = this.form.registerAddress || ''
|
||||
@ -1162,12 +1485,14 @@ export default {
|
||||
this.form.pointType = pointType
|
||||
|
||||
if (pointType === 'calc') {
|
||||
this.form.deviceCategory = ''
|
||||
this.form.deviceId = ''
|
||||
const request = this.form.id ? updatePointMatch : addPointMatch
|
||||
const calcData = {
|
||||
id: this.form.id,
|
||||
siteId: this.form.siteId,
|
||||
pointType: 'calc',
|
||||
deviceCategory: this.form.deviceCategory,
|
||||
deviceCategory: '',
|
||||
deviceId: '',
|
||||
registerAddress: '',
|
||||
pointId: this.form.pointId,
|
||||
@ -1301,6 +1626,13 @@ export default {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.calc-expression-tips {
|
||||
margin-top: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.point-config-layout {
|
||||
flex-direction: column;
|
||||
|
||||
@ -5,9 +5,7 @@
|
||||
<div class="items-container">
|
||||
<div class="item-title">站点:</div>
|
||||
<div class="item-content">
|
||||
<el-select v-model="siteId" :disabled="mode === 'edit'" placeholder="请选择站点" :loading="searchLoading" loading-text="正在加载数据">
|
||||
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList" :key="index+'zdxeSelect'"></el-option>
|
||||
</el-select>
|
||||
<el-input v-model="siteId" placeholder="请先在顶部选择站点" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="items-container">
|
||||
@ -103,15 +101,12 @@
|
||||
</template>
|
||||
<script>
|
||||
import {addPriceConfig,editPriceConfig,detailPriceConfig} from '@/api/ems/powerTariff'
|
||||
import {getAllSites} from '@/api/ems/zddt'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
mode:'',
|
||||
id:'',
|
||||
searchLoading:false,
|
||||
siteId:'',
|
||||
siteList:[],
|
||||
powerDate:'',//时间
|
||||
//尖-peak,峰-high,平-flat,谷=valley
|
||||
priceTypeOptions:[{
|
||||
@ -137,6 +132,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRouteSiteId() {
|
||||
const siteId = this.$route?.query?.siteId
|
||||
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
|
||||
},
|
||||
addRow(){
|
||||
this.hoursOptions.push({
|
||||
startTime:'',
|
||||
@ -147,15 +146,7 @@ export default {
|
||||
deleteRow(index){
|
||||
this.hoursOptions.splice(index,1)
|
||||
},
|
||||
//获取站点列表
|
||||
getZdList(){
|
||||
this.searchLoading=true
|
||||
getAllSites().then(response => {
|
||||
this.siteList = response?.data || []
|
||||
}).finally(() => {this.searchLoading=false})
|
||||
},
|
||||
showDialog(id){
|
||||
this.getZdList()
|
||||
showDialog(id, siteId = ''){
|
||||
this.id = id
|
||||
if(id) {
|
||||
this.mode='edit'
|
||||
@ -172,11 +163,12 @@ export default {
|
||||
}).finally(()=>this.loading = false)
|
||||
}else {
|
||||
this.mode='add'
|
||||
this.siteId = siteId || this.getRouteSiteId()
|
||||
}
|
||||
this.dialogTableVisible=true
|
||||
},
|
||||
saveDialog() {
|
||||
if(this.siteId === '') return this.$message.error('请选择站点')
|
||||
if(this.siteId === '') return this.$message.error('请先在顶部选择站点')
|
||||
if(this.powerDate === '') return this.$message.error('请选择时间')
|
||||
let priceArr=[]
|
||||
this.priceTypeOptions.forEach(item=>{
|
||||
@ -244,8 +236,6 @@ export default {
|
||||
this.mode=''
|
||||
this.id=''
|
||||
this.siteId=''
|
||||
this.siteList=[]
|
||||
this.searchLoading=false
|
||||
this.powerDate=''
|
||||
this.hoursOptions=[]
|
||||
this.priceTypeOptions.forEach(item=>{
|
||||
@ -303,4 +293,4 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -129,7 +129,7 @@ export default {
|
||||
}).finally(() => {this.loading=false})
|
||||
},
|
||||
addPowerConfig(id=''){
|
||||
this.$refs.addPowerTariff.showDialog(id);
|
||||
this.$refs.addPowerTariff.showDialog(id, this.siteId);
|
||||
},
|
||||
deletePowerConfig(row){
|
||||
this.$confirm(`确认要删除${row.month}月的电价配置吗?`, {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,109 +1,92 @@
|
||||
<template>
|
||||
<div
|
||||
class="ems-dashboard-editor-container"
|
||||
style="background-color: #ffffff"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form :inline="true" class="select-container">
|
||||
<el-form-item label="故障名称">
|
||||
<el-input
|
||||
<div class="protect-plan-page" v-loading="loading">
|
||||
<el-card class="query-card" shadow="never">
|
||||
<div class="query-head">
|
||||
<div class="query-title">设备保护方案</div>
|
||||
<el-button type="primary" @click="addPlan" native-type="button">新增方案</el-button>
|
||||
</div>
|
||||
<el-form :inline="true" class="query-form" @submit.native.prevent>
|
||||
<el-form-item label="故障名称">
|
||||
<el-input
|
||||
v-model="form.faultName"
|
||||
clearable
|
||||
placeholder="请输入故障名称"
|
||||
style="width: 150px"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
style="width: 220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
|
||||
<el-button @click="onReset" native-type="button">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" @click="addPlan" native-type="button"
|
||||
>新增方案</el-button
|
||||
>
|
||||
<el-table
|
||||
class="common-table"
|
||||
:data="tableData"
|
||||
stripe
|
||||
max-height="600px"
|
||||
style="width: 100%; margin-top: 25px"
|
||||
>
|
||||
<el-table-column prop="siteId" label="站点" width="100"> </el-table-column>
|
||||
<el-table-column prop="faultName" label="设备保护名称" width="100"> </el-table-column>
|
||||
<el-table-column prop="faultLevel" label="故障等级" width="100">
|
||||
<template slot-scope="scope">等级{{scope.row.faultLevel}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isAlert" label="是否告警" width="100">
|
||||
<template slot-scope="scope">{{scope.row.isAlert === 1 ? '是' : '否'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="处理方案描述" width="200" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column prop="protectionSettings" label="保护前提" show-overflow-tooltip width="400">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="handleProtectionSettings(scope.row.protectionSettings)"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="faultDelaySeconds" label="保护前提延时(s)" width="120">
|
||||
</el-table-column>
|
||||
<el-table-column prop="protectionPlan" label="保护方案" show-overflow-tooltip width="200">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="handleProtectionPlan(scope.row.protectionPlan)"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="releaseDelaySeconds" label="保护方案延时(s)" width="120">
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click="editDevice(scope.row)" type="warning" size="mini">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" @click="deleteDevice(scope.row)" size="mini">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-show="tableData.length > 0"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pageNum"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 30, 40]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="totalSize"
|
||||
style="margin-top: 15px; text-align: center"
|
||||
>
|
||||
</el-pagination>
|
||||
<add-plan
|
||||
ref="addPlan"
|
||||
@update="getData"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table class="common-table" :data="tableData" stripe max-height="620px">
|
||||
<el-table-column prop="faultName" label="设备保护名称" min-width="140" />
|
||||
<el-table-column prop="faultLevel" label="故障等级" width="100">
|
||||
<template slot-scope="scope">等级{{ scope.row.faultLevel }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isAlert" label="是否告警" width="100">
|
||||
<template slot-scope="scope">{{ scope.row.isAlert === 1 ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="处理方案描述" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="protectionSettings" label="保护前提" min-width="360" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div class="rich-lines" v-html="handleProtectionSettings(scope.row.protectionSettings)"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="faultDelaySeconds" label="前提延时(s)" width="110" />
|
||||
<el-table-column prop="protectionPlan" label="保护方案" min-width="260" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div class="rich-lines" v-html="handleProtectionPlan(scope.row.protectionPlan)"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="releaseDelaySeconds" label="方案延时(s)" width="110" />
|
||||
<el-table-column fixed="right" label="操作" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click="editDevice(scope.row)" type="warning" size="mini">编辑</el-button>
|
||||
<el-button type="danger" @click="deleteDevice(scope.row)" size="mini">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-show="tableData.length > 0"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pageNum"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 30, 40]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="totalSize"
|
||||
class="pager"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<add-plan ref="addPlan" @update="getData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
protectPlanList,
|
||||
deleteProtectPlan,
|
||||
} from "@/api/ems/site";
|
||||
import getQuerySiteId from '@/mixins/ems/getQuerySiteId'
|
||||
import { protectPlanList, deleteProtectPlan } from "@/api/ems/site";
|
||||
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
|
||||
import AddPlan from "./AddPlan.vue";
|
||||
|
||||
export default {
|
||||
name: "SBBH",
|
||||
components: { AddPlan },
|
||||
mixins: [getQuerySiteId],
|
||||
data() {
|
||||
return {
|
||||
form:{
|
||||
faultName:''
|
||||
form: {
|
||||
faultName: "",
|
||||
},
|
||||
loading: false,
|
||||
tableData: [],
|
||||
pageSize: 10, //分页栏当前每个数据总数
|
||||
pageNum: 1, //分页栏当前页数
|
||||
totalSize: 0, //table表格数据总数
|
||||
pageSize: 10,
|
||||
pageNum: 1,
|
||||
totalSize: 0,
|
||||
dialogTableVisible: false,
|
||||
};
|
||||
},
|
||||
@ -112,39 +95,52 @@ export default {
|
||||
this.pageNum = 1;
|
||||
this.getData();
|
||||
},
|
||||
handleProtectionSettings(data){
|
||||
if(!data || !JSON.parse(data)) return
|
||||
const arr = JSON.parse(data),
|
||||
str= arr.map((item,index)=>{
|
||||
const {categoryName='',deviceId='',point='',faultOperator='',faultValue='',releaseOperator='',releaseValue='',relationNext=''} = item
|
||||
return `<div>${index+1}、 <span>${categoryName ? categoryName + '-' : ''}${deviceId ? deviceId + '-' : ''}${ point || ''}</span> <span>故障:${faultOperator || ''}${ faultValue || ''}</span> <span>释放:${releaseOperator || ''}${releaseValue || ''}</span> ${arr[index+1] ? '<span>关系:'+(relationNext || '')+'</span>' : ''}</div>`
|
||||
})
|
||||
return str.join('')
|
||||
handleProtectionSettings(data) {
|
||||
if (!data || !JSON.parse(data)) return;
|
||||
const arr = JSON.parse(data);
|
||||
const str = arr.map((item, index) => {
|
||||
const {
|
||||
categoryName = "",
|
||||
deviceId = "",
|
||||
point = "",
|
||||
faultOperator = "",
|
||||
faultValue = "",
|
||||
releaseOperator = "",
|
||||
releaseValue = "",
|
||||
relationNext = "",
|
||||
} = item;
|
||||
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
|
||||
deviceId ? deviceId + "-" : ""
|
||||
}${point || ""}</span> <span>故障:${faultOperator || ""}${faultValue || ""}</span> <span>释放:${
|
||||
releaseOperator || ""
|
||||
}${releaseValue || ""}</span> ${
|
||||
arr[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
|
||||
}</div>`;
|
||||
});
|
||||
return str.join("");
|
||||
},
|
||||
handleProtectionPlan(data){
|
||||
if(!data || !JSON.parse(data)) return
|
||||
const arr = JSON.parse(data),
|
||||
str= arr.map((item,index)=>{
|
||||
const {categoryName='',deviceId='',point='',value=''} = item
|
||||
return `<div>${index+1}、 <span>${categoryName ? categoryName + '-' : ''}${deviceId ? deviceId + '-' : ''}${ point || ''}</span> <span>故障:=${ value || ''}</span> </div>`
|
||||
})
|
||||
return str.join('')
|
||||
handleProtectionPlan(data) {
|
||||
if (!data || !JSON.parse(data)) return;
|
||||
const arr = JSON.parse(data);
|
||||
const str = arr.map((item, index) => {
|
||||
const { categoryName = "", deviceId = "", point = "", value = "" } = item;
|
||||
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
|
||||
deviceId ? deviceId + "-" : ""
|
||||
}${point || ""}</span> <span>故障:=${value || ""}</span> </div>`;
|
||||
});
|
||||
return str.join("");
|
||||
},
|
||||
// 新增方案 展示弹窗
|
||||
addPlan() {
|
||||
if (!this.siteId) {
|
||||
this.$message.warning('请先在顶部选择站点')
|
||||
return
|
||||
this.$message.warning("请先在顶部选择站点");
|
||||
return;
|
||||
}
|
||||
this.$refs.addPlan.open('', this.siteId)
|
||||
this.$refs.addPlan.open("", this.siteId);
|
||||
},
|
||||
// 编辑设备
|
||||
editDevice(row) {
|
||||
this.$refs.addPlan.open(row.id, this.siteId || row.siteId)
|
||||
this.$refs.addPlan.open(row.id, this.siteId);
|
||||
},
|
||||
//删除设备
|
||||
deleteDevice(row) {
|
||||
console.log('删除')
|
||||
this.$confirm(`确认要设备保护${row.faultName}吗?`, {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@ -167,19 +163,14 @@ export default {
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
//只有在废弃成功的情况下会走到这里
|
||||
this.$message({
|
||||
type: "success",
|
||||
message: "删除成功!",
|
||||
});
|
||||
this.getData();
|
||||
//调用接口 更新表格数据
|
||||
})
|
||||
.catch(() => {
|
||||
//取消关机
|
||||
});
|
||||
.catch(() => {});
|
||||
},
|
||||
// 分页
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.$nextTick(() => {
|
||||
@ -192,29 +183,27 @@ export default {
|
||||
this.getData();
|
||||
});
|
||||
},
|
||||
// 搜索
|
||||
onSearch() {
|
||||
this.pageNum = 1; //每次搜索从1开始搜索
|
||||
this.pageNum = 1;
|
||||
this.getData();
|
||||
},
|
||||
// 重置
|
||||
onReset() {
|
||||
this.form={
|
||||
this.form = {
|
||||
faultName: "",
|
||||
}
|
||||
this.pageNum = 1; //每次搜索从1开始搜索
|
||||
};
|
||||
this.pageNum = 1;
|
||||
this.getData();
|
||||
},
|
||||
// 获取数据
|
||||
getData() {
|
||||
if (!this.siteId) {
|
||||
this.tableData = []
|
||||
this.totalSize = 0
|
||||
return
|
||||
this.tableData = [];
|
||||
this.totalSize = 0;
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
const { pageNum, pageSize } = this,{faultName=''}=this.form;
|
||||
protectPlanList({ siteId: this.siteId, faultName,pageNum, pageSize })
|
||||
const { pageNum, pageSize } = this;
|
||||
const { faultName = "" } = this.form;
|
||||
protectPlanList({ siteId: this.siteId, faultName, pageNum, pageSize })
|
||||
.then((response) => {
|
||||
this.tableData = response?.rows || [];
|
||||
this.totalSize = response?.total || 0;
|
||||
@ -227,4 +216,46 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.protect-plan-page {
|
||||
padding: 16px;
|
||||
background: linear-gradient(180deg, #f5f8ff 0%, #f7f9fc 100%);
|
||||
|
||||
.query-card,
|
||||
.table-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e7edf7;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.query-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.query-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1d2a3a;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.rich-lines {
|
||||
color: #3e4b5a;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pager {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,10 +7,12 @@
|
||||
<el-form v-loading="loading>0" ref="addTempForm" inline :model="formData" :rules="rules" size="medium"
|
||||
label-width="120px" class="device-form base-form">
|
||||
<el-form-item label="站点" prop="siteId">
|
||||
<el-select v-model="formData.siteId" placeholder="请选择" :style="{width: '100%'}" @change="changeType">
|
||||
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList"
|
||||
:key="index+'siteOptions'"></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="formData.siteId"
|
||||
placeholder="请先在顶部选择站点"
|
||||
disabled
|
||||
:style="{width: '100%'}"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备id" prop="deviceId">
|
||||
<el-input v-model="formData.deviceId" placeholder="请输入" maxlength="60" clearable :style="{width: '100%'}">
|
||||
@ -25,12 +27,6 @@
|
||||
:style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作状态" prop="communicationStatus">
|
||||
<el-select v-model="formData.communicationStatus" placeholder="请选择" :style="{width: '100%'}">
|
||||
<el-option :label="value" :value="key" v-for="(value,key) in communicationStatusOptions"
|
||||
:key="key+'communicationStatusOptions'"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备类型" prop="deviceType">
|
||||
<el-select v-model="formData.deviceType" placeholder="请选择" :style="{width: '100%'}">
|
||||
<el-option :label="value" :value="key" v-for="(value,key) in deviceTypeOptions"
|
||||
@ -46,7 +42,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="上级设备" prop="parentId" v-if="dccDeviceCategoryList.includes(formData.deviceCategory)">
|
||||
<el-select v-model="formData.parentId"
|
||||
:placeholder="parentDeviceList.length === 0 && !formData.siteId ? '请先选择站点' : '请选择'"
|
||||
:placeholder="parentDeviceList.length === 0 && !formData.siteId ? '请先在顶部选择站点' : '请选择'"
|
||||
:style="{width: '100%'}">
|
||||
<el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in parentDeviceList"
|
||||
:key="index+'parentDeviceList'"></el-option>
|
||||
@ -86,50 +82,43 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- pcs配置-->
|
||||
<el-form v-if="isPcs" ref="pcsSettingForm" inline :model="pcsSetting" size="medium"
|
||||
label-width="120px" class="device-form pcs-form" :rules="pcsSettingRules">
|
||||
<div style="font-size: 14px;padding: 10px 0 20px;font-weight: 600;">PCS配置</div>
|
||||
<el-form-item label="开关机地址" prop="pointAddress">
|
||||
<el-input v-model="pcsSetting.pointAddress" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="功率地址" prop="powerAddress">
|
||||
<el-input v-model="pcsSetting.powerAddress" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="开机指令" prop="startCommand">
|
||||
<el-input v-model="pcsSetting.startCommand" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="开机目标功率" prop="startPower">
|
||||
<el-input v-model="pcsSetting.startPower" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关机指令" prop="stopCommand">
|
||||
<el-input v-model="pcsSetting.stopCommand" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关机目标功率" prop="stopPower">
|
||||
<el-input v-model="pcsSetting.stopPower" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="倍率" prop="powerMultiplier">
|
||||
<el-input v-model="pcsSetting.powerMultiplier" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="电池簇数" prop="clusterNum">
|
||||
<el-input v-model="pcsSetting.clusterNum" placeholder="请输入" clearable :style="{width: '100%'}">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<br>
|
||||
<template v-for="index in parseInt(pcsSetting.clusterNum) || 0">
|
||||
<el-form-item :label="'电池簇'+(index)+'地址'"
|
||||
prop="clusterPointAddress">
|
||||
<el-input v-model="pcsSetting.clusterPointAddress[index-1]" placeholder="请输入" clearable
|
||||
:style="{width: '100%'}">
|
||||
</el-input>
|
||||
<el-form v-if="isPcs" ref="pcsSettingForm" :model="pcsSetting" size="medium"
|
||||
label-position="top" class="pcs-form" :rules="pcsSettingRules">
|
||||
<div class="pcs-form__title">PCS配置</div>
|
||||
<div class="pcs-form__grid">
|
||||
<el-form-item label="开关机地址" prop="pointAddress" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.pointAddress" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="功率地址" prop="powerAddress" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.powerAddress" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开机指令" prop="startCommand" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.startCommand" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="关机指令" prop="stopCommand" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.stopCommand" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开机目标功率" prop="startPower" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.startPower" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="关机目标功率" prop="stopPower" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.stopPower" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="倍率" prop="powerMultiplier" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.powerMultiplier" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电池簇数" prop="clusterNum" class="pcs-form__item">
|
||||
<el-input v-model="pcsSetting.clusterNum" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-if="(parseInt(pcsSetting.clusterNum) || 0) > 0" class="pcs-form__cluster">
|
||||
<div class="pcs-form__cluster-title">电池簇地址</div>
|
||||
<template v-for="index in parseInt(pcsSetting.clusterNum) || 0">
|
||||
<el-form-item :key="'clusterAddress' + index" :label="'电池簇' + index + '地址'" prop="clusterPointAddress">
|
||||
<el-input v-model="pcsSetting.clusterPointAddress[index - 1]" placeholder="请输入" clearable :style="{width: '100%'}" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
@ -141,7 +130,6 @@
|
||||
</template>
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
import {getAllSites} from '@/api/ems/zddt'
|
||||
import {validText} from '@/utils/validate'
|
||||
import {addDevice, getDeviceDetailInfo, getParentDeviceId, updateDevice} from "@/api/ems/site";
|
||||
import {getAllDeviceCategory} from '@/api/ems/search'
|
||||
@ -191,7 +179,6 @@ export default {
|
||||
dccDeviceCategoryList: ['CLUSTER', 'BATTERY'],//需要展示上级设备的设备类型
|
||||
dialogTableVisible: false,
|
||||
parentDeviceList: [],//上级设备列表 从接口获取数据
|
||||
siteList: [],//站点列表 从接口获取数据
|
||||
deviceCategoryList: [],//设备类别列表 从接口获取数据
|
||||
formData: {
|
||||
id: '',//设备唯一标识
|
||||
@ -199,7 +186,6 @@ export default {
|
||||
deviceId: '',//设备id
|
||||
deviceName: '',//设备名称
|
||||
description: '',//设备描述
|
||||
communicationStatus: '',//工作状态
|
||||
deviceType: '',//设备类型
|
||||
deviceCategory: '',//设备类别
|
||||
parentId: '',//上级设备id
|
||||
@ -226,7 +212,7 @@ export default {
|
||||
},
|
||||
rules: {
|
||||
siteId: [
|
||||
{required: true, message: '请选择站点', trigger: ['blur', 'change']}
|
||||
{required: true, message: '请先在顶部选择站点', trigger: ['blur', 'change']}
|
||||
],
|
||||
deviceId: [
|
||||
{required: true, message: '请输入设备id', trigger: 'blur'},
|
||||
@ -240,9 +226,6 @@ export default {
|
||||
{required: true, message: '请输入设备描述', trigger: 'blur'},
|
||||
{validator: validateText, trigger: 'blur'}
|
||||
],
|
||||
communicationStatus: [
|
||||
{required: true, message: '请选择工作状态', trigger: ['blur', 'change']}
|
||||
],
|
||||
deviceType: [
|
||||
{required: true, message: '请选择设备类型', trigger: ['blur', 'change']}
|
||||
],
|
||||
@ -312,7 +295,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
communicationStatusOptions: state => state?.ems?.communicationStatusOptions || {},
|
||||
deviceTypeOptions: state => state?.ems?.deviceTypeOptions || {}
|
||||
}),
|
||||
isPcs() {
|
||||
@ -324,12 +306,22 @@ export default {
|
||||
handler(newVal) {
|
||||
//打开弹窗
|
||||
if (newVal) {
|
||||
this.getZdList()
|
||||
if (this.mode === 'add') {
|
||||
this.syncSiteFromRoute(true)
|
||||
}
|
||||
this.getDeviceCategoryList()
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
'$route.query.siteId': {
|
||||
handler() {
|
||||
if (!this.dialogTableVisible || this.mode !== 'add') {
|
||||
return
|
||||
}
|
||||
this.syncSiteFromRoute(true)
|
||||
}
|
||||
},
|
||||
id: {
|
||||
handler(newVal) {
|
||||
if ((newVal || newVal === 0) && this.mode !== 'add') {
|
||||
@ -355,20 +347,24 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
syncSiteFromRoute(force = false) {
|
||||
const routeSiteId = this.$route?.query?.siteId
|
||||
const normalizedSiteId = routeSiteId === undefined || routeSiteId === null ? '' : String(routeSiteId).trim()
|
||||
if (!normalizedSiteId) {
|
||||
if (force) {
|
||||
this.formData.siteId = ''
|
||||
}
|
||||
return
|
||||
}
|
||||
if (force || !this.formData.siteId) {
|
||||
this.formData.siteId = normalizedSiteId
|
||||
}
|
||||
},
|
||||
changeType() {
|
||||
if (this.dccDeviceCategoryList.includes(this.formData.deviceCategory)) {
|
||||
this.getParentDeviceList()
|
||||
}
|
||||
},
|
||||
//获取站点列表
|
||||
getZdList() {
|
||||
this.loading += 1
|
||||
getAllSites().then(response => {
|
||||
this.siteList = response?.data || []
|
||||
}).finally(() => {
|
||||
this.loading -= 1
|
||||
})
|
||||
},
|
||||
// 获取设备类别
|
||||
getDeviceCategoryList() {
|
||||
this.loading += 1
|
||||
@ -381,7 +377,8 @@ export default {
|
||||
//获取上级id列表
|
||||
getParentDeviceList(init = false) {
|
||||
if (!this.formData.siteId) {
|
||||
return console.log('请先选择站点')
|
||||
this.$message.warning('请先在顶部选择站点')
|
||||
return
|
||||
}
|
||||
!init && (this.formData.parentId = '')
|
||||
this.loading = this.loading + 1
|
||||
@ -399,7 +396,6 @@ export default {
|
||||
deviceId = '',//设备id
|
||||
deviceName = '',//设备名称
|
||||
description = '',//设备描述
|
||||
communicationStatus = '',//工作状态
|
||||
deviceType = '',//设备类型
|
||||
deviceCategory = '',//设备类别
|
||||
parentId = '',//上级设备id
|
||||
@ -429,7 +425,6 @@ export default {
|
||||
deviceId,
|
||||
deviceName,
|
||||
description,
|
||||
communicationStatus,
|
||||
deviceType,
|
||||
deviceCategory,
|
||||
parentId,
|
||||
@ -503,7 +498,6 @@ export default {
|
||||
deviceId: '',//设备id
|
||||
deviceName: '',//设备名称
|
||||
description: '',//设备描述
|
||||
communicationStatus: '',//工作状态
|
||||
deviceType: '',//设备类型
|
||||
deviceCategory: '',//设备类别
|
||||
parentId: '',//上级设备id
|
||||
@ -516,6 +510,7 @@ export default {
|
||||
parity: '',//校验位
|
||||
slaveId: '',//从站地址
|
||||
}
|
||||
this.parentDeviceList = []
|
||||
this.pcsSetting = {
|
||||
deviceSettingId: '',
|
||||
powerAddress: '',//功率地址
|
||||
@ -538,24 +533,17 @@ export default {
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.form-layout {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-layout.has-pcs {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.base-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-layout.has-pcs .base-form,
|
||||
.form-layout.has-pcs .pcs-form {
|
||||
width: calc(50% - 8px);
|
||||
}
|
||||
|
||||
.device-form {
|
||||
::v-deep .el-form-item--medium .el-form-item__content {
|
||||
width: 260px;
|
||||
@ -567,7 +555,92 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.form-layout.has-pcs .device-form .el-form-item {
|
||||
width: 100%;
|
||||
.pcs-form {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -360px;
|
||||
width: 340px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
padding: 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.pcs-form__title {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.pcs-form__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 10px;
|
||||
}
|
||||
|
||||
.pcs-form__item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pcs-form__cluster {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.pcs-form__cluster-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.pcs-form {
|
||||
::v-deep .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__label {
|
||||
line-height: 20px;
|
||||
padding-bottom: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__content {
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
::v-deep .el-input__inner {
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__error {
|
||||
position: static;
|
||||
line-height: 1.2;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label:before {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.ems-dialog {
|
||||
::v-deep .el-dialog,
|
||||
::v-deep .el-dialog__body {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -110,6 +110,11 @@
|
||||
sortable="custom"
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="selectable" label="操作" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="selectPoint(scope.row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-show="tableData.length > 0"
|
||||
@ -147,6 +152,7 @@ export default {
|
||||
this.pageNum = 1;
|
||||
this.totalSize = 0;
|
||||
this.dataType = '';
|
||||
this.selectable = false;
|
||||
this.form = {
|
||||
sortMethod: "desc", //升序不传或者asc、降序desc)
|
||||
sortData: this.defaultSort.prop,
|
||||
@ -191,9 +197,14 @@ export default {
|
||||
pageSize: 10, //分页栏当前每个数据总数
|
||||
pageNum: 1, //分页栏当前页数
|
||||
totalSize: 0, //table表格数据总数
|
||||
selectable: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectPoint(row) {
|
||||
this.$emit('select-point', row || {})
|
||||
this.show = false
|
||||
},
|
||||
showChart({pointName}) {
|
||||
if (pointName) {
|
||||
const {deviceCategory, deviceId} = this;
|
||||
@ -235,12 +246,13 @@ export default {
|
||||
this.getData()
|
||||
});
|
||||
},
|
||||
showTable({deviceCategory, siteId, deviceId, parentId = ""}, dataType) {
|
||||
showTable({deviceCategory, siteId, deviceId, parentId = ""}, dataType, options = {}) {
|
||||
this.dataType = dataType;
|
||||
this.deviceCategory = deviceCategory;
|
||||
this.siteId = siteId;
|
||||
this.deviceId = deviceId;
|
||||
this.parentId = deviceCategory === "BATTERY" ? parentId : ""; //只有单体电池需要这个值
|
||||
this.selectable = !!options.selectable
|
||||
this.show = true;
|
||||
this.getData()
|
||||
},
|
||||
|
||||
@ -27,10 +27,6 @@
|
||||
prop="siteId"
|
||||
label="站点ID">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="siteName"
|
||||
label="站点名称">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="deviceId"
|
||||
label="设备ID"
|
||||
@ -111,6 +107,17 @@ export default {
|
||||
}
|
||||
this.siteId = normalizedSiteId
|
||||
this.onSearch()
|
||||
},
|
||||
'$route.query.siteName'(newSiteName) {
|
||||
const normalizedSiteName = this.getSelectedSiteName(newSiteName)
|
||||
if (normalizedSiteName === this.selectedSiteName) {
|
||||
return
|
||||
}
|
||||
this.selectedSiteName = normalizedSiteName
|
||||
this.tableData = (this.tableData || []).map(item => ({
|
||||
...item,
|
||||
siteName: normalizedSiteName
|
||||
}))
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -119,6 +126,7 @@ export default {
|
||||
mode: '',//新增、编辑设备
|
||||
editDeviceId: '',//编辑设备id
|
||||
siteId: '',
|
||||
selectedSiteName: '',
|
||||
deviceCategory: '',//搜索栏设备类型
|
||||
deviceCategoryList: [],//设备类别
|
||||
tableData: [],
|
||||
@ -153,6 +161,17 @@ export default {
|
||||
hasValidSiteId(siteId) {
|
||||
return !!(siteId !== undefined && siteId !== null && String(siteId).trim())
|
||||
},
|
||||
getSelectedSiteName(routeSiteName) {
|
||||
const name = routeSiteName === undefined || routeSiteName === null ? '' : String(routeSiteName).trim()
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
const matchedSite = (this.$store.getters.zdList || []).find(item => item.siteId === this.siteId)
|
||||
if (matchedSite && matchedSite.siteName) {
|
||||
return matchedSite.siteName
|
||||
}
|
||||
return this.siteId || ''
|
||||
},
|
||||
// 获取设备类别
|
||||
getDeviceCategoryList() {
|
||||
getAllDeviceCategory().then(response => {
|
||||
@ -259,7 +278,12 @@ export default {
|
||||
this.loading = true
|
||||
const {siteId, deviceCategory, pageNum, pageSize} = this
|
||||
getDeviceInfoList({siteId, deviceCategory, pageNum, pageSize}).then(response => {
|
||||
this.tableData = response?.rows || [];
|
||||
const selectedSiteName = this.getSelectedSiteName(this.$route.query.siteName)
|
||||
this.selectedSiteName = selectedSiteName
|
||||
this.tableData = (response?.rows || []).map(item => ({
|
||||
...item,
|
||||
siteName: selectedSiteName
|
||||
}));
|
||||
this.totalSize = response?.total || 0
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
@ -268,6 +292,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.siteId = this.hasValidSiteId(this.$route.query.siteId) ? String(this.$route.query.siteId).trim() : ''
|
||||
this.selectedSiteName = this.getSelectedSiteName(this.$route.query.siteName)
|
||||
this.pageNum = 1//每次搜索从1开始搜索
|
||||
this.getDeviceCategoryList()
|
||||
this.getData()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user