|
|
<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>
|