|
|
|
|
@ -21,6 +21,9 @@ v-model="queryParams.productName" placeholder="请输入产品名称" clearable
|
|
|
|
|
<el-button @click="handleQuery">
|
|
|
|
|
<Icon icon="ep:search" class="mr-5px" /> 查询
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button @click="resetQuery">
|
|
|
|
|
<Icon icon="ep:refresh" class="mr-5px" /> 重置
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button type="primary" plain @click="openDialog('create')">
|
|
|
|
|
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
|
|
|
|
</el-button>
|
|
|
|
|
@ -49,7 +52,7 @@ ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-to
|
|
|
|
|
<el-table-column label="操作" align="center" width="240px" fixed="right">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-button link type="primary" @click.stop="openDetail(scope.row)">配置</el-button>
|
|
|
|
|
<el-button link type="warning" @click.stop="openDialog('update', scope.row)">编辑</el-button>
|
|
|
|
|
<el-button link type="primary" @click.stop="openDialog('update', scope.row)">编辑</el-button>
|
|
|
|
|
<el-button link type="danger" @click.stop="handleDelete(scope.row)">删除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
@ -60,15 +63,9 @@ ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-to
|
|
|
|
|
</ContentWrap>
|
|
|
|
|
|
|
|
|
|
<RecipeDetailList
|
|
|
|
|
ref="detailRef"
|
|
|
|
|
:visible="detailVisible"
|
|
|
|
|
:recipe-id="detailMeta.recipeCode"
|
|
|
|
|
:manual-recipe-id="detailMeta.id"
|
|
|
|
|
:recipe-code="detailMeta.recipeCode"
|
|
|
|
|
:name="detailMeta.name"
|
|
|
|
|
@config="handleDetailConfig"
|
|
|
|
|
@close="closeDetail"
|
|
|
|
|
/>
|
|
|
|
|
ref="detailRef" :visible="detailVisible"
|
|
|
|
|
:recipe-id="detailMeta.id ? String(detailMeta.id) : undefined" :manual-recipe-id="detailMeta.id"
|
|
|
|
|
:recipe-code="detailMeta.recipeCode" :name="detailMeta.name" @config="handleDetailConfig" @close="closeDetail" />
|
|
|
|
|
|
|
|
|
|
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
|
|
|
|
|
<el-form ref="dialogFormRef" :model="dialogForm" :rules="dialogRules" label-width="100px" v-loading="dialogLoading">
|
|
|
|
|
@ -111,9 +108,59 @@ v-model="dialogForm.machineName" placeholder="请选择关联设备" clearable f
|
|
|
|
|
|
|
|
|
|
<Dialog title="配置" v-model="configVisible" width="920px">
|
|
|
|
|
<div v-loading="configLoading">
|
|
|
|
|
<el-transfer
|
|
|
|
|
class="formula-config-transfer" v-model="configSelectedKeys" :data="configCandidates" filterable
|
|
|
|
|
:titles="['来源', '目标']" />
|
|
|
|
|
<div class="formula-config-picklist">
|
|
|
|
|
<div class="formula-config-panel">
|
|
|
|
|
<div class="formula-config-panel__header">
|
|
|
|
|
<div>来源</div>
|
|
|
|
|
<div class="formula-config-panel__meta">{{ filteredSourceItems.length }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="formula-config-panel__filter">
|
|
|
|
|
<el-input v-model="sourceKeyword" placeholder="筛选" clearable />
|
|
|
|
|
</div>
|
|
|
|
|
<el-table
|
|
|
|
|
class="formula-config-panel__table"
|
|
|
|
|
:data="filteredSourceItems"
|
|
|
|
|
height="440"
|
|
|
|
|
:show-header="false"
|
|
|
|
|
row-key="key"
|
|
|
|
|
@selection-change="handleSourceSelectionChange"
|
|
|
|
|
:row-class-name="getSourceRowClass"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column type="selection" width="44" :selectable="isSourceSelectable" />
|
|
|
|
|
<el-table-column prop="label" />
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="formula-config-actions">
|
|
|
|
|
<el-button type="primary" :disabled="!sourceCheckedKeys.length" @click="addToTarget">
|
|
|
|
|
>>
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button :disabled="!targetCheckedKeys.length" @click="removeFromTarget">
|
|
|
|
|
<<
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="formula-config-panel">
|
|
|
|
|
<div class="formula-config-panel__header">
|
|
|
|
|
<div>目标</div>
|
|
|
|
|
<div class="formula-config-panel__meta">{{ filteredTargetItems.length }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="formula-config-panel__filter">
|
|
|
|
|
<el-input v-model="targetKeyword" placeholder="筛选" clearable />
|
|
|
|
|
</div>
|
|
|
|
|
<el-table
|
|
|
|
|
class="formula-config-panel__table"
|
|
|
|
|
:data="filteredTargetItems"
|
|
|
|
|
height="440"
|
|
|
|
|
:show-header="false"
|
|
|
|
|
row-key="key"
|
|
|
|
|
@selection-change="handleTargetSelectionChange"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column type="selection" width="44" />
|
|
|
|
|
<el-table-column prop="label" />
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="configVisible = false">取 消</el-button>
|
|
|
|
|
@ -138,6 +185,7 @@ const message = useMessage()
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const queryFormRef = ref()
|
|
|
|
|
const queryParams = reactive({
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
@ -184,6 +232,11 @@ const handleQuery = () => {
|
|
|
|
|
getList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resetQuery = () => {
|
|
|
|
|
queryFormRef.value?.resetFields?.()
|
|
|
|
|
handleQuery()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handlePagination = () => {
|
|
|
|
|
getList()
|
|
|
|
|
}
|
|
|
|
|
@ -342,19 +395,23 @@ const detailVisible = ref(false)
|
|
|
|
|
const detailMeta = reactive({
|
|
|
|
|
id: undefined as number | undefined,
|
|
|
|
|
recipeCode: '',
|
|
|
|
|
name: ''
|
|
|
|
|
name: '',
|
|
|
|
|
deviceId: undefined as number | undefined,
|
|
|
|
|
machineName: ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const openDetail = async (row: RecipeConfigVO) => {
|
|
|
|
|
detailMeta.id = row.id
|
|
|
|
|
detailMeta.recipeCode = row.recipeCode
|
|
|
|
|
detailMeta.name = row.name ?? row.recipeName ?? ''
|
|
|
|
|
detailMeta.deviceId = row.deviceId
|
|
|
|
|
detailMeta.machineName = row.machineName ?? row.deviceName ?? ''
|
|
|
|
|
detailVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDetailConfig = () => {
|
|
|
|
|
if (!detailMeta.recipeCode) return
|
|
|
|
|
openConfigDialog(detailMeta.recipeCode)
|
|
|
|
|
if (!detailMeta.id) return
|
|
|
|
|
openConfigDialog(detailMeta.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleRowClick = async (row: RecipeConfigVO, column: any) => {
|
|
|
|
|
@ -374,34 +431,136 @@ type TransferItem = { key: number; label: string; disabled?: boolean }
|
|
|
|
|
|
|
|
|
|
const configVisible = ref(false)
|
|
|
|
|
const configLoading = ref(false)
|
|
|
|
|
const configRecipeId = ref<string | undefined>(undefined)
|
|
|
|
|
const configRecipeId = ref<string | number | undefined>(undefined)
|
|
|
|
|
const configCandidates = ref<TransferItem[]>([])
|
|
|
|
|
const configSelectedKeys = ref<number[]>([])
|
|
|
|
|
const configSelectedItems = ref<TransferItem[]>([])
|
|
|
|
|
|
|
|
|
|
const sourceKeyword = ref('')
|
|
|
|
|
const targetKeyword = ref('')
|
|
|
|
|
const sourceCheckedKeys = ref<number[]>([])
|
|
|
|
|
const targetCheckedKeys = ref<number[]>([])
|
|
|
|
|
|
|
|
|
|
const configDeviceLabel = computed(() => {
|
|
|
|
|
const name = detailMeta.machineName?.trim()
|
|
|
|
|
if (name) return name
|
|
|
|
|
if (detailMeta.deviceId !== undefined && detailMeta.deviceId !== null) return String(detailMeta.deviceId)
|
|
|
|
|
return '-'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const normalizeKey = (value: any): number | undefined => {
|
|
|
|
|
if (value === null || value === undefined || value === '') return undefined
|
|
|
|
|
const n = Number(value)
|
|
|
|
|
if (Number.isNaN(n)) return undefined
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedKeySet = computed(() => {
|
|
|
|
|
return new Set<number>(configSelectedKeys.value ?? [])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const contains = (text: string, keyword: string) => {
|
|
|
|
|
const k = (keyword ?? '').trim().toLowerCase()
|
|
|
|
|
if (!k) return true
|
|
|
|
|
return (text ?? '').toLowerCase().includes(k)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filteredSourceItems = computed(() => {
|
|
|
|
|
return (configCandidates.value ?? []).filter((it) => contains(it.label, sourceKeyword.value))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const filteredTargetItems = computed(() => {
|
|
|
|
|
const set = selectedKeySet.value
|
|
|
|
|
return (configCandidates.value ?? [])
|
|
|
|
|
.filter((it) => set.has(it.key))
|
|
|
|
|
.filter((it) => contains(it.label, targetKeyword.value))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const isSourceSelectable = (row: TransferItem) => {
|
|
|
|
|
return !selectedKeySet.value.has(row.key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getSourceRowClass = ({ row }: { row: TransferItem }) => {
|
|
|
|
|
return selectedKeySet.value.has(row.key) ? 'formula-config-row--disabled' : ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSourceSelectionChange = (rows: TransferItem[]) => {
|
|
|
|
|
sourceCheckedKeys.value = (rows ?? []).map((r) => r.key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleTargetSelectionChange = (rows: TransferItem[]) => {
|
|
|
|
|
targetCheckedKeys.value = (rows ?? []).map((r) => r.key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const addToTarget = () => {
|
|
|
|
|
const next = new Set<number>(configSelectedKeys.value ?? [])
|
|
|
|
|
for (const key of sourceCheckedKeys.value ?? []) next.add(key)
|
|
|
|
|
configSelectedKeys.value = Array.from(next.values())
|
|
|
|
|
sourceCheckedKeys.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const removeFromTarget = () => {
|
|
|
|
|
const toRemove = new Set<number>(targetCheckedKeys.value ?? [])
|
|
|
|
|
configSelectedKeys.value = (configSelectedKeys.value ?? []).filter((k) => !toRemove.has(k))
|
|
|
|
|
targetCheckedKeys.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const buildTransferLabel = (item: any) => {
|
|
|
|
|
return `${item.attributeName ?? item.pointName ?? ''}${item.attributeType || item.pointType ? '(' + (item.attributeType ?? item.pointType) : ''}${item.dataType ? '/' + item.dataType : ''}${item.dataUnit ? ' ' + item.dataUnit : ''}${item.attributeType || item.pointType ? ')' : ''}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadConfigCandidatesByDevice = async (deviceId: number) => {
|
|
|
|
|
const data = await DeviceApi.getDeviceAttributePage({ pageNo: 1, pageSize: 100, deviceId })
|
|
|
|
|
const list = (data?.list ?? data?.data ?? []) as any[]
|
|
|
|
|
const deviceItems = (list ?? [])
|
|
|
|
|
.map((item: any) => {
|
|
|
|
|
const key = normalizeKey(item?.attributeId ?? item?.id)
|
|
|
|
|
if (key === undefined) return undefined
|
|
|
|
|
return {
|
|
|
|
|
key,
|
|
|
|
|
label: buildTransferLabel(item)
|
|
|
|
|
} as TransferItem
|
|
|
|
|
})
|
|
|
|
|
.filter((it: any) => it && typeof it.key === 'number') as TransferItem[]
|
|
|
|
|
const merged = new Map<number, TransferItem>()
|
|
|
|
|
for (const it of deviceItems) merged.set(it.key, it)
|
|
|
|
|
for (const it of configSelectedItems.value) {
|
|
|
|
|
if (!merged.has(it.key)) merged.set(it.key, it)
|
|
|
|
|
}
|
|
|
|
|
configCandidates.value = Array.from(merged.values())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const openConfigDialog = async (recipeCode: string) => {
|
|
|
|
|
const openConfigDialog = async (recipeId: number | string) => {
|
|
|
|
|
configVisible.value = true
|
|
|
|
|
configRecipeId.value = recipeCode
|
|
|
|
|
configRecipeId.value = recipeId
|
|
|
|
|
configSelectedKeys.value = []
|
|
|
|
|
configCandidates.value = []
|
|
|
|
|
configSelectedItems.value = []
|
|
|
|
|
sourceKeyword.value = ''
|
|
|
|
|
targetKeyword.value = ''
|
|
|
|
|
sourceCheckedKeys.value = []
|
|
|
|
|
targetCheckedKeys.value = []
|
|
|
|
|
configLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const [candidateRes, selectedRes] = await Promise.all([
|
|
|
|
|
DeviceApi.getDeviceContactModelPage(),
|
|
|
|
|
RecipeConfigApi.getRecipePointDetailPage({ pageNo: 1, pageSize: 100, recipeId: recipeCode })
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const candidateList = Array.isArray(candidateRes)
|
|
|
|
|
? candidateRes
|
|
|
|
|
: ((candidateRes as any)?.list ?? (candidateRes as any)?.data ?? [])
|
|
|
|
|
|
|
|
|
|
configCandidates.value = (candidateList ?? []).map((item: any) => ({
|
|
|
|
|
key: item.id,
|
|
|
|
|
label: `${item.attributeName ?? item.pointName ?? ''}${item.attributeType || item.pointType ? '(' + (item.attributeType ?? item.pointType) : ''}${item.dataType ? '/' + item.dataType : ''}${item.dataUnit ? ' ' + item.dataUnit : ''}${item.attributeType || item.pointType ? ')' : ''}`
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
configSelectedKeys.value = (selectedRes.list ?? [])
|
|
|
|
|
const selectedRes = await RecipeConfigApi.getRecipePointDetailPage({ recipeId, pageNo: 1, pageSize: 100 })
|
|
|
|
|
const selectedList = Array.isArray(selectedRes) ? selectedRes : selectedRes?.list ?? selectedRes?.data ?? []
|
|
|
|
|
const selectedKeys = (selectedList ?? [])
|
|
|
|
|
.map((item: any) => (item.attributeId !== undefined ? item.attributeId : item.id))
|
|
|
|
|
.map((id: any) => normalizeKey(id))
|
|
|
|
|
.filter((id: any) => typeof id === 'number')
|
|
|
|
|
configSelectedKeys.value = selectedKeys
|
|
|
|
|
configSelectedItems.value = (selectedList ?? [])
|
|
|
|
|
.map((item: any) => ({
|
|
|
|
|
key: normalizeKey(item.attributeId !== undefined ? item.attributeId : item.id),
|
|
|
|
|
label: buildTransferLabel(item)
|
|
|
|
|
}))
|
|
|
|
|
.filter((it: any) => typeof it.key === 'number')
|
|
|
|
|
const deviceId = detailMeta.deviceId
|
|
|
|
|
if (deviceId !== undefined && deviceId !== null) {
|
|
|
|
|
await loadConfigCandidatesByDevice(deviceId)
|
|
|
|
|
} else {
|
|
|
|
|
configCandidates.value = configSelectedItems.value
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
configLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
@ -411,13 +570,10 @@ const submitConfig = async () => {
|
|
|
|
|
if (!configRecipeId.value) return
|
|
|
|
|
configLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
await RecipeConfigApi.saveRecipePointConfig({
|
|
|
|
|
recipeId: configRecipeId.value,
|
|
|
|
|
attributeIds: configSelectedKeys.value
|
|
|
|
|
})
|
|
|
|
|
await RecipeConfigApi.updateRecipeDeviceAttribute({ recipeId: configRecipeId.value, ids: configSelectedKeys.value })
|
|
|
|
|
message.success(t('common.updateSuccess'))
|
|
|
|
|
configVisible.value = false
|
|
|
|
|
if (detailVisible.value && detailMeta.recipeCode === configRecipeId.value) {
|
|
|
|
|
if (detailVisible.value && String(detailMeta.id) === String(configRecipeId.value)) {
|
|
|
|
|
await detailRef.value?.refresh?.()
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
@ -432,20 +588,51 @@ onMounted(() => {
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
:deep(.formula-config-transfer.el-transfer) {
|
|
|
|
|
--el-transfer-panel-body-height: 440px;
|
|
|
|
|
|
|
|
|
|
.formula-config-picklist {
|
|
|
|
|
display: flex;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.formula-config-transfer .el-transfer-panel) {
|
|
|
|
|
.formula-config-panel {
|
|
|
|
|
width: calc((100% - 96px) / 2);
|
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: var(--el-bg-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-transfer__buttons) {
|
|
|
|
|
.formula-config-panel__header {
|
|
|
|
|
display: flex;
|
|
|
|
|
text-align: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border-bottom: 1px solid var(--el-border-color);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.formula-config-panel__meta {
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.formula-config-panel__filter {
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border-bottom: 1px solid var(--el-border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.formula-config-panel__table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.formula-config-actions {
|
|
|
|
|
width: 96px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.formula-config-row--disabled) {
|
|
|
|
|
opacity: 0.55;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|