You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/formula/formulaConfig/index.vue

850 lines
28 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<ContentWrap>
<el-form
class="-mb-15px recipe-config-filter-form"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="auto"
>
<el-form-item :label="t('RecipeManagement.RecipeConfig.searchRecipeCodeLabel')" prop="recipeCode">
<el-input
v-model="queryParams.recipeCode"
:placeholder="t('RecipeManagement.RecipeConfig.searchRecipeCodePlaceholder')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.searchNameLabel')" prop="name">
<el-input
v-model="queryParams.name"
:placeholder="t('RecipeManagement.RecipeConfig.searchNamePlaceholder')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.searchProductNameLabel')" prop="productName">
<el-input
v-model="queryParams.productName"
:placeholder="t('RecipeManagement.RecipeConfig.searchProductNamePlaceholder')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
{{ t('RecipeManagement.RecipeConfig.searchButtonText') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('RecipeManagement.RecipeConfig.resetButtonText') }}
</el-button>
<el-button type="primary" plain @click="openDialog('create')">
<Icon icon="ep:plus" class="mr-5px" />
{{ t('RecipeManagement.RecipeConfig.createButtonText') }}
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading">
<Icon icon="ep:download" class="mr-5px" />
{{ t('RecipeManagement.RecipeConfig.exportButtonText') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
highlight-current-row
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<!-- <el-table-column type="selection" width="55" reserve-selection /> -->
<el-table-column
:label="t('RecipeManagement.RecipeConfig.tableRecipeCodeColumn')"
align="center"
prop="recipeCode"
sortable />
<el-table-column
:label="t('RecipeManagement.RecipeConfig.tableNameColumn')"
align="center"
prop="name"
sortable />
<el-table-column
:label="t('RecipeManagement.RecipeConfig.tableRecipeTypeColumn')"
align="center"
prop="recipeType"
sortable>
<template #default="scope">
{{ getRecipeTypeLabel(scope.row.recipeType) }}
</template>
</el-table-column>
<el-table-column
:label="t('RecipeManagement.RecipeConfig.tableProductNameColumn')"
align="center"
prop="productName"
sortable />
<el-table-column
:label="t('RecipeManagement.RecipeConfig.tableMachineNameColumn')"
align="center"
prop="machineName"
sortable />
<el-table-column
:label="t('RecipeManagement.RecipeConfig.tableRecipeDescColumn')"
align="center"
prop="recipeDesc"
/>
<el-table-column
:label="t('RecipeManagement.RecipeConfig.tableOperateColumn')"
align="center"
width="240px"
fixed="right"
>
<template #default="scope">
<el-button link type="primary" @click.stop="openDetail(scope.row)">
{{ t('RecipeManagement.RecipeConfig.tableConfigAction') }}
</el-button>
<el-button link type="primary" @click.stop="openDialog('update', scope.row)">
{{ t('RecipeManagement.RecipeConfig.tableEditAction') }}
</el-button>
<el-button link type="danger" @click.stop="handleDelete(scope.row)">
{{ t('RecipeManagement.RecipeConfig.tableDeleteAction') }}
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="handlePagination" />
</ContentWrap>
<RecipeDetailList
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"
class="recipe-config-dialog-form"
label-width="auto"
v-loading="dialogLoading"
>
<el-form-item :label="t('RecipeManagement.RecipeConfig.dialogRecipeCodeLabel')" prop="recipeCode">
<el-input
v-model="dialogForm.recipeCode"
:placeholder="t('RecipeManagement.RecipeConfig.dialogRecipeCodePlaceholder')"
clearable
:disabled="dialogMode == 'update'"
/>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.dialogNameLabel')" prop="name">
<el-input
v-model="dialogForm.name"
:placeholder="t('RecipeManagement.RecipeConfig.dialogNamePlaceholder')"
clearable
/>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.dialogRecipeTypeLabel')" prop="recipeTypeId">
<el-select
v-model="dialogForm.recipeTypeId"
:placeholder="t('RecipeManagement.RecipeConfig.dialogRecipeTypePlaceholder')"
clearable
filterable
class="!w-full"
:loading="recipeTypeLoading"
>
<el-option v-for="item in recipeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.dialogProductCategoryLabel')" prop="productCategoryId">
<el-select
v-model="dialogForm.productCategoryId"
:placeholder="t('RecipeManagement.RecipeConfig.dialogProductCategoryPlaceholder')"
clearable
filterable
class="!w-full"
:loading="categoryLoading"
@change="handleCategoryChange"
>
<el-option v-for="item in categoryOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.dialogProductNameLabel')" prop="productId">
<el-select
v-model="dialogForm.productId"
:placeholder="t('RecipeManagement.RecipeConfig.dialogProductNamePlaceholder')"
clearable
filterable
class="!w-full"
:loading="productLoading"
:disabled="!dialogForm.productCategoryId"
>
<el-option v-for="item in productOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.dialogMachineNameLabel')" prop="machineId">
<el-select
v-model="dialogForm.machineId"
:placeholder="t('RecipeManagement.RecipeConfig.dialogMachineNamePlaceholder')"
clearable
filterable
class="!w-full"
:loading="deviceLoading"
:disabled="dialogMode == 'update'"
>
<el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('RecipeManagement.RecipeConfig.dialogRecipeDescLabel')" prop="recipeDesc">
<el-input
v-model="dialogForm.recipeDesc"
:placeholder="t('RecipeManagement.RecipeConfig.dialogRecipeDescPlaceholder')"
clearable
type="textarea"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
{{ t('RecipeManagement.RecipeConfig.dialogCancelButton') }}
</el-button>
<el-button type="primary" @click="submitDialog" :disabled="dialogLoading">
{{ t('RecipeManagement.RecipeConfig.dialogSaveButton') }}
</el-button>
</template>
</Dialog>
<Dialog :title="t('RecipeManagement.RecipeConfig.configDialogTitle')" v-model="configVisible" width="920px">
<div v-loading="configLoading">
<div class="formula-config-picklist">
<div class="formula-config-panel">
<div class="formula-config-panel__header">
<div>{{ t('RecipeManagement.RecipeConfig.configSourceTitle') }}</div>
<div class="formula-config-panel__meta">{{ filteredSourceItems.length }}</div>
</div>
<div class="formula-config-panel__filter">
<el-input
v-model="sourceKeyword"
:placeholder="t('RecipeManagement.RecipeConfig.configFilterPlaceholder')"
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">
{{ t('RecipeManagement.RecipeConfig.configMoveToTargetButton') }}
</el-button>
<el-button :disabled="!targetCheckedKeys.length" @click="removeFromTarget">
{{ t('RecipeManagement.RecipeConfig.configMoveToSourceButton') }}
</el-button>
</div>
<div class="formula-config-panel">
<div class="formula-config-panel__header">
<div>{{ t('RecipeManagement.RecipeConfig.configTargetTitle') }}</div>
<div class="formula-config-panel__meta">{{ filteredTargetItems.length }}</div>
</div>
<div class="formula-config-panel__filter">
<el-input
v-model="targetKeyword"
:placeholder="t('RecipeManagement.RecipeConfig.configFilterPlaceholder')"
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">
{{ t('RecipeManagement.RecipeConfig.configCancelButton') }}
</el-button>
<el-button type="primary" @click="submitConfig" :disabled="configLoading">
{{ t('RecipeManagement.RecipeConfig.configSaveButton') }}
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import download from '@/utils/download'
import { ProductApi } from '@/api/erp/product/product'
import { ProductCategoryApi } from '@/api/erp/product/category'
import { DeviceApi } from '@/api/iot/device'
import { RecipeApi } from '@/api/iot/recipe'
import { RecipeConfigApi, RecipeConfigVO } from '@/api/iot/recipeConfig'
import RecipeDetailList from './components/RecipeDetailList.vue'
type SelectOption<T = any> = { label: string; value: T }
defineOptions({ name: 'FormulaConfig' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const queryFormRef = ref()
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeCode: '',
name: '',
productName: ''
})
const exportLoading = ref(false)
const selectedIds = ref<number[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? []
}
const list = ref<RecipeConfigVO[]>([])
const total = ref(0)
const buildExportParams = () => {
const recipeCode = queryParams.recipeCode?.trim()
const name = queryParams.name?.trim()
const productName = queryParams.productName?.trim()
return {
recipeCode: recipeCode ? recipeCode : undefined,
name: name ? name : undefined,
productName: productName ? productName : undefined
}
}
const buildQueryParams = () => {
const recipeCode = queryParams.recipeCode?.trim()
const name = queryParams.name?.trim()
const productName = queryParams.productName?.trim()
return {
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
recipeCode: recipeCode ? recipeCode : undefined,
name: name ? name : undefined,
productName: productName ? productName : undefined
}
}
const getList = async () => {
loading.value = true
try {
const data = await RecipeConfigApi.getRecipeConfigPage(buildQueryParams())
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields?.()
handleQuery()
}
const handlePagination = () => {
getList()
}
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const exportParams: any = {
...buildExportParams()
}
if (selectedIds.value.length) {
exportParams.ids = selectedIds.value.join(',')
}
const data = await RecipeConfigApi.exportRecipeConfig(exportParams)
download.excel(data, t('RecipeManagement.RecipeConfig.exportFileName'))
} catch {
} finally {
exportLoading.value = false
}
}
const handleDelete = async (row: RecipeConfigVO) => {
try {
await message.delConfirm()
await RecipeConfigApi.deleteRecipeConfig(row.id)
message.success(t('common.delSuccess'))
await getList()
} catch { }
}
const productLoading = ref(false)
const productOptions = ref<SelectOption<string>[]>([])
const categoryLoading = ref(false)
const categoryOptions = ref<SelectOption<string>[]>([])
const deviceLoading = ref(false)
const deviceOptions = ref<SelectOption<string>[]>([])
const recipeTypeLoading = ref(false)
const recipeTypeOptions = ref<SelectOption<string>[]>([])
const getRecipeTypeLabel = (value: unknown) => {
if (value === undefined || value === null || value === '') return ''
const matched = recipeTypeOptions.value.find(
(item) => item.value === value || String(item.value) === String(value) || item.label === value
)
return matched?.label ?? String(value)
}
const getCategoryOptions = async () => {
categoryLoading.value = true
try {
const data = await ProductCategoryApi.getProductCategorySimpleList()
categoryOptions.value = (data ?? []).map((item: any) => ({ label: item.name, value: item.id }))
} finally {
categoryLoading.value = false
}
}
const getProductOptions = async (categoryId: string | number | undefined) => {
if (categoryId === undefined || categoryId === null || categoryId === '') {
productOptions.value = []
return
}
productLoading.value = true
try {
const data = await ProductApi.getProductSimpleList({ categoryId })
productOptions.value = (data ?? []).map((item: any) => ({ label: item.name, value: item.id }))
} finally {
productLoading.value = false
}
}
const getDeviceOptions = async () => {
deviceLoading.value = true
try {
const data = await DeviceApi.getDeviceList()
deviceOptions.value = (data ?? []).map((item: any) => ({ label: item.deviceName, value: item.id }))
} finally {
deviceLoading.value = false
}
}
const getRecipeTypeOptions = async () => {
recipeTypeLoading.value = true
try {
const data = await RecipeApi.getRecipePage({})
recipeTypeOptions.value = (data?.list ?? []).map((item: any) => ({ label: item.name, value: item.id }))
} finally {
recipeTypeLoading.value = false
}
}
const ensureOptionsLoaded = async () => {
if (!categoryOptions.value.length) await getCategoryOptions()
if (!deviceOptions.value.length) await getDeviceOptions()
if (!recipeTypeOptions.value.length) await getRecipeTypeOptions()
}
type DialogMode = 'create' | 'update'
const dialogVisible = ref(false)
const dialogMode = ref<DialogMode>('create')
const dialogTitle = computed(() =>
dialogMode.value === 'create'
? t('RecipeManagement.RecipeConfig.dialogCreateTitle')
: t('RecipeManagement.RecipeConfig.dialogEditTitle')
)
const dialogFormRef = ref()
const dialogLoading = ref(false)
const dialogForm = reactive({
id: undefined as number | undefined,
recipeCode: '',
name: '',
recipeTypeId: '' as string | number | undefined,
productCategoryId: '' as string | number | undefined,
productId: '' as string | number | undefined,
machineId: '' as string | number | undefined,
recipeDesc: ''
})
const dialogRules = reactive({
recipeCode: [
{
required: true,
message: t('RecipeManagement.RecipeConfig.validatorRecipeCodeRequired'),
trigger: 'blur'
}
],
name: [
{
required: true,
message: t('RecipeManagement.RecipeConfig.validatorNameRequired'),
trigger: 'blur'
}
]
})
const openDialog = async (mode: DialogMode, row?: RecipeConfigVO) => {
await ensureOptionsLoaded()
dialogMode.value = mode
dialogVisible.value = true
nextTick(() => {
dialogFormRef.value?.clearValidate?.()
})
if (mode === 'create') {
dialogForm.id = undefined
dialogForm.recipeCode = ''
dialogForm.name = ''
dialogForm.recipeTypeId = ''
dialogForm.productCategoryId = ''
dialogForm.productId = ''
dialogForm.machineId = ''
dialogForm.recipeDesc = ''
productOptions.value = []
return
}
dialogForm.id = row?.id
dialogForm.recipeCode = row?.recipeCode ?? ''
dialogForm.name = row?.name ?? row?.recipeName ?? ''
dialogForm.recipeTypeId =
(row as any)?.recipeTypeId ??
(row?.recipeType === undefined || row?.recipeType === null ? '' : (row as any).recipeType)
dialogForm.productCategoryId = (row as any)?.productCategoryId ?? (row as any)?.categoryId ?? ''
const findProductValueByName = (name: string | undefined) => {
if (!name) return ''
const matched = productOptions.value.find((o) => o.label === name || String(o.value) === String((row as any)?.productId))
return matched?.value ?? ''
}
const findDeviceValueByName = (name: string | undefined) => {
if (!name) return ''
const matched = deviceOptions.value.find((o) => o.label === name || String(o.value) === String((row as any)?.machineId ?? (row as any)?.deviceId))
return matched?.value ?? ''
}
await getProductOptions(dialogForm.productCategoryId)
dialogForm.productId = (row as any)?.productId ?? findProductValueByName(row?.productName)
dialogForm.machineId = (row as any)?.machineId ?? (row as any)?.deviceId ?? findDeviceValueByName(row?.machineName ?? row?.deviceName)
dialogForm.recipeDesc = row?.recipeDesc ?? row?.remark ?? ''
}
const handleCategoryChange = async (value: string | number | undefined) => {
dialogForm.productId = ''
await getProductOptions(value)
}
const submitDialog = async () => {
if (!dialogFormRef.value) return
await dialogFormRef.value.validate()
dialogLoading.value = true
try {
const data = {
id: dialogForm.id,
recipeCode: dialogForm.recipeCode,
name: dialogForm.name,
recipeTypeId: dialogForm.recipeTypeId,
productCategoryId: dialogForm.productCategoryId,
productId: dialogForm.productId,
machineId: dialogForm.machineId,
recipeDesc: dialogForm.recipeDesc
}
if (dialogMode.value === 'create') {
await RecipeConfigApi.createRecipeConfig(data)
message.success(t('common.createSuccess'))
} else {
await RecipeConfigApi.updateRecipeConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
await getList()
} finally {
dialogLoading.value = false
}
}
const detailRef = ref()
const detailVisible = ref(false)
const detailMeta = reactive({
id: undefined as number | undefined,
recipeCode: '',
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.id) return
openConfigDialog(detailMeta.id)
}
const handleRowClick = async (row: RecipeConfigVO, column: any) => {
if (column?.type === 'selection') return
if (column?.label === t('RecipeManagement.RecipeConfig.tableOperateColumn')) return
await openDetail(row)
}
const closeDetail = () => {
detailVisible.value = false
detailMeta.id = undefined
detailMeta.recipeCode = ''
detailMeta.name = ''
}
type TransferItem = { key: number; label: string; disabled?: boolean }
const configVisible = ref(false)
const configLoading = ref(false)
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 t('RecipeManagement.RecipeConfig.configDeviceLabelFallback')
})
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.getDeviceAttributeList(deviceId)
const list = (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[]
configCandidates.value = deviceItems
}
const openConfigDialog = async (recipeId: number | string) => {
configVisible.value = true
configRecipeId.value = recipeId
configSelectedKeys.value = []
configCandidates.value = []
configSelectedItems.value = []
sourceKeyword.value = ''
targetKeyword.value = ''
sourceCheckedKeys.value = []
targetCheckedKeys.value = []
configLoading.value = true
try {
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) => id != null && id !== '')
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
}
}
const submitConfig = async () => {
if (!configRecipeId.value) return
configLoading.value = true
try {
await RecipeConfigApi.updateRecipeDeviceAttribute({ recipeId: configRecipeId.value, ids: configSelectedKeys.value })
message.success(t('common.updateSuccess'))
configVisible.value = false
if (detailVisible.value && String(detailMeta.id) === String(configRecipeId.value)) {
await detailRef.value?.refresh?.()
}
} finally {
configLoading.value = false
}
}
onMounted(() => {
getList()
ensureOptionsLoaded()
})
</script>
<style scoped>
.recipe-config-filter-form :deep(.el-form-item__label) {
min-width: 68px;
}
.recipe-config-dialog-form :deep(.el-form-item__label) {
min-width: 100px;
}
.formula-config-picklist {
display: flex;
width: 100%;
}
.formula-config-panel {
width: calc((100% - 96px) / 2);
overflow: hidden;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
.formula-config-panel__header {
display: flex;
padding: 10px 12px;
font-weight: 600;
border-bottom: 1px solid var(--el-border-color);
justify-content: space-between;
}
.formula-config-panel__meta {
font-weight: 400;
color: var(--el-text-color-secondary);
}
.formula-config-panel__filter {
padding: 10px 12px;
border-bottom: 1px solid var(--el-border-color);
}
.formula-config-panel__table {
width: 100%;
}
.formula-config-actions {
display: flex;
width: 96px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
}
:deep(.formula-config-row--disabled) {
opacity: 0.55;
}
</style>