feat:添加配方配置列表

main
黄伟杰 4 weeks ago
parent 07ded7adf1
commit f67e5136b5

@ -65,6 +65,15 @@ export interface HistoryRecordParams {
collectionEndTime?: string
}
export interface DeviceContactModelVO {
id: number
attributeCode?: string
attributeName?: string
attributeType?: string
dataType?: string
dataUnit?: string
}
// 物联设备 API
export const DeviceApi = {
// 查询物联设备分页
@ -127,6 +136,10 @@ export const DeviceApi = {
getDeviceAttributePage: async (params) => {
return await request.get({ url: `/iot/device/device-attribute/page`, params })
},
getDeviceContactModelPage: async () => {
return await request.get({ url: `/iot/device-contact-model/page` })
},
// 新增设备属性
createDeviceAttribute: async (data) => {
return await request.post({ url: `/iot/device-contact-model/create`, data })

@ -1,13 +1,21 @@
import request from '@/config/axios'
export interface RecipeConfigVO {
id: number
recipeCode: string
recipeName: string
recipeName?: string
name?: string
recipeType?: string | number
productId?: number
productName?: string
machineName?: string
deviceId?: number
deviceName?: string
recipeDesc?: string
remark?: string
isEnable?: string | number
dataUnit?: string
createTime?: string
}
export interface RecipePointDetailVO {
@ -31,7 +39,6 @@ export interface RecipePointCandidateVO {
type PageResult<T> = { list: T[]; total: number }
const STORAGE_KEYS = {
recipeList: 'mock:recipeConfig:recipes',
pointCandidates: 'mock:recipeConfig:pointCandidates',
recipePointRelation: 'mock:recipeConfig:recipePointRelation'
} as const
@ -54,23 +61,6 @@ const saveToStorage = (key: string, value: any) => {
window.localStorage.setItem(key, JSON.stringify(value))
}
const buildMockRecipes = (): RecipeConfigVO[] => {
return Array.from({ length: 18 }).map((_, idx) => {
const no = String(idx + 1).padStart(3, '0')
return {
id: idx + 1,
recipeCode: `RCP-${no}`,
recipeName: `配方-${no}`,
recipeType: idx % 2 === 0 ? '标准' : '自定义',
productId: undefined,
productName: idx % 3 === 0 ? '产品A' : idx % 3 === 1 ? '产品B' : '产品C',
deviceId: undefined,
deviceName: idx % 2 === 0 ? '设备1' : '设备2',
remark: idx % 4 === 0 ? '示例数据' : ''
}
})
}
const buildMockPointCandidates = (): RecipePointCandidateVO[] => {
const pointTypes = ['模拟量', '开关量', '计算量']
const dataTypes = ['int', 'float', 'string', 'bool']
@ -88,9 +78,6 @@ const buildMockPointCandidates = (): RecipePointCandidateVO[] => {
}
const ensureMockSeeded = () => {
const recipeList = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, [])
if (!recipeList.length) saveToStorage(STORAGE_KEYS.recipeList, buildMockRecipes())
const pointCandidates = loadFromStorage<RecipePointCandidateVO[]>(STORAGE_KEYS.pointCandidates, [])
if (!pointCandidates.length) saveToStorage(STORAGE_KEYS.pointCandidates, buildMockPointCandidates())
@ -119,103 +106,53 @@ const contains = (value: string | undefined, keyword: string | undefined) => {
export const RecipeConfigApi = {
getRecipeConfigPage: async (params: any) => {
ensureMockSeeded()
await sleep(120)
const recipeCode = params?.recipeCode
const recipeName = params?.recipeName
const productName = params?.productName
const pageNo = params?.pageNo
const pageSize = params?.pageSize
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const filtered = list.filter((item) => {
return (
contains(item.recipeCode, recipeCode) &&
contains(item.recipeName, recipeName) &&
contains(item.productName, productName)
)
})
return paginate(filtered, pageNo, pageSize)
const finalParams = {
...(params ?? {}),
name: params?.name ?? params?.recipeName
}
return await request.get({ url: `/iot/recipe/page`, params: finalParams })
},
createRecipeConfig: async (data: Partial<RecipeConfigVO>) => {
ensureMockSeeded()
await sleep(120)
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const maxId = list.reduce((acc, cur) => Math.max(acc, cur.id), 0)
const nextId = maxId + 1
const newItem: RecipeConfigVO = {
id: nextId,
recipeCode: data.recipeCode ?? '',
recipeName: data.recipeName ?? '',
const finalData = {
id: data.id,
name: data.name ?? data.recipeName,
recipeCode: data.recipeCode,
recipeType: data.recipeType,
productId: data.productId,
productName: data.productName,
deviceId: data.deviceId,
deviceName: data.deviceName,
remark: data.remark
machineName: data.machineName ?? data.deviceName,
recipeDesc: data.recipeDesc ?? data.remark,
isEnable: data.isEnable,
dataUnit: data.dataUnit
}
saveToStorage(STORAGE_KEYS.recipeList, [newItem, ...list])
return nextId
return await request.post({ url: `/iot/recipe/create`, data: finalData })
},
updateRecipeConfig: async (data: Partial<RecipeConfigVO>) => {
ensureMockSeeded()
await sleep(120)
if (!data.id) return true
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const next = list.map((item) => {
if (item.id !== data.id) return item
return {
...item,
recipeCode: data.recipeCode ?? item.recipeCode,
recipeName: data.recipeName ?? item.recipeName,
recipeType: data.recipeType ?? item.recipeType,
productId: data.productId ?? item.productId,
productName: data.productName ?? item.productName,
deviceId: data.deviceId ?? item.deviceId,
deviceName: data.deviceName ?? item.deviceName,
remark: data.remark ?? item.remark
}
})
saveToStorage(STORAGE_KEYS.recipeList, next)
return true
const finalData = {
id: data.id,
name: data.name ?? data.recipeName,
recipeCode: data.recipeCode,
recipeType: data.recipeType,
productName: data.productName,
machineName: data.machineName ?? data.deviceName,
recipeDesc: data.recipeDesc ?? data.remark,
isEnable: data.isEnable,
dataUnit: data.dataUnit
}
return await request.put({ url: `/iot/recipe/update`, data: finalData })
},
deleteRecipeConfig: async (id: number) => {
ensureMockSeeded()
await sleep(120)
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
saveToStorage(
STORAGE_KEYS.recipeList,
list.filter((item) => item.id !== id)
)
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})
delete relation[String(id)]
saveToStorage(STORAGE_KEYS.recipePointRelation, relation)
return true
return await request.delete({ url: `/iot/recipe/delete?id=` + id })
},
exportRecipeConfig: async (params: any) => {
ensureMockSeeded()
await sleep(120)
const ids = String(params?.ids ?? '')
.split(',')
.map((v) => Number(v))
.filter((v) => !Number.isNaN(v))
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const exportList = ids.length ? list.filter((item) => ids.includes(item.id)) : list
const header = ['配方编码', '配方名称', '配方类型', '关联产品', '关联设备', '备注']
const rows = exportList.map((item) => [
item.recipeCode,
item.recipeName,
item.recipeType ?? '',
item.productName ?? '',
item.deviceName ?? '',
item.remark ?? ''
])
const csv = [header, ...rows].map((r) => r.map((v) => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n')
return new Blob([csv], { type: 'text/csv;charset=utf-8' })
const finalParams = {
...(params ?? {}),
name: params?.name ?? params?.recipeName
}
return await request.download({ url: `/iot/recipe/export-excel`, params: finalParams })
},
getPointCandidatePage: async (params: any): Promise<PageResult<RecipePointCandidateVO>> => {

@ -6,9 +6,9 @@
v-model="queryParams.recipeCode" placeholder="请输入配方编码" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="配方名称" prop="recipeName">
<el-form-item label="配方名称" prop="name">
<el-input
v-model="queryParams.recipeName" placeholder="请输入配方名称" clearable @keyup.enter="handleQuery"
v-model="queryParams.name" placeholder="请输入配方名称" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
@ -37,15 +37,15 @@ ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-to
highlight-current-row @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column type="selection" width="55" reserve-selection />
<el-table-column label="配方编码" align="center" prop="recipeCode" />
<el-table-column label="配方名称" align="center" prop="recipeName" />
<el-table-column label="配方名称" align="center" prop="name" />
<el-table-column label="配方类型" align="center" prop="recipeType">
<template #default="scope">
{{ getRecipeTypeLabel(scope.row.recipeType) }}
</template>
</el-table-column>
<el-table-column label="关联产品" align="center" prop="productName" />
<el-table-column label="关联设备" align="center" prop="deviceName" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="关联设备" align="center" prop="machineName" />
<el-table-column label="配方描述" align="center" prop="recipeDesc" />
<el-table-column label="操作" align="center" width="240px" fixed="right">
<template #default="scope">
<el-button link type="primary" @click.stop="openConfigDialog(scope.row)">配置</el-button>
@ -64,7 +64,7 @@ ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-to
<template #extra>
<el-button link type="info" @click="closeDetail"></el-button>
</template>
<el-tab-pane :label="`详情:${detailMeta.recipeCode} - ${detailMeta.recipeName}`" name="detail">
<el-tab-pane :label="`详情:${detailMeta.recipeCode} - ${detailMeta.name}`" name="detail">
<el-table
v-loading="detailLoading" :data="detailList" :stripe="true" :show-overflow-tooltip="true"
row-key="id">
@ -90,8 +90,8 @@ v-loading="detailLoading" :data="detailList" :stripe="true" :show-overflow-toolt
<el-form-item label="配方编码" prop="recipeCode">
<el-input v-model="dialogForm.recipeCode" placeholder="请输入配方编码" clearable />
</el-form-item>
<el-form-item label="配方名称" prop="recipeName">
<el-input v-model="dialogForm.recipeName" placeholder="请输入配方名称" clearable />
<el-form-item label="配方名称" prop="name">
<el-input v-model="dialogForm.name" placeholder="请输入配方名称" clearable />
</el-form-item>
<el-form-item label="配方类型" prop="recipeType">
<el-select
@ -100,22 +100,22 @@ v-model="dialogForm.recipeType" placeholder="请选择配方类型" clearable fi
<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="关联产品" prop="productId">
<el-form-item label="关联产品" prop="productName">
<el-select
v-model="dialogForm.productId" placeholder="请选择关联产品" clearable filterable class="!w-full"
v-model="dialogForm.productName" placeholder="请选择关联产品" clearable filterable class="!w-full"
:loading="productLoading">
<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="关联设备" prop="deviceId">
<el-form-item label="关联设备" prop="machineName">
<el-select
v-model="dialogForm.deviceId" placeholder="请选择关联设备" clearable filterable class="!w-full"
v-model="dialogForm.machineName" placeholder="请选择关联设备" clearable filterable class="!w-full"
:loading="deviceLoading">
<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="备注" prop="remark">
<el-input v-model="dialogForm.remark" placeholder="请输入备注" clearable type="textarea" />
<el-form-item label="配方描述" prop="recipeDesc">
<el-input v-model="dialogForm.recipeDesc" placeholder="请输入配方描述" clearable type="textarea" />
</el-form-item>
</el-form>
<template #footer>
@ -144,7 +144,7 @@ import { DeviceApi } from '@/api/iot/device'
import { RecipeApi } from '@/api/iot/recipe'
import { RecipeConfigApi, RecipeConfigVO, RecipePointDetailVO } from '@/api/iot/recipeConfig'
type SelectOption = { label: string; value: number }
type SelectOption<T = any> = { label: string; value: T }
defineOptions({ name: 'FormulaConfig' })
@ -156,7 +156,7 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeCode: '',
recipeName: '',
name: '',
productName: ''
})
@ -171,13 +171,13 @@ const total = ref(0)
const buildQueryParams = () => {
const recipeCode = queryParams.recipeCode?.trim()
const recipeName = queryParams.recipeName?.trim()
const name = queryParams.name?.trim()
const productName = queryParams.productName?.trim()
return {
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
recipeCode: recipeCode ? recipeCode : undefined,
recipeName: recipeName ? recipeName : undefined,
name: name ? name : undefined,
productName: productName ? productName : undefined
}
}
@ -228,25 +228,11 @@ const handleDelete = async (row: RecipeConfigVO) => {
}
const productLoading = ref(false)
const productOptions = ref<SelectOption[]>([])
const productOptions = ref<SelectOption<string>[]>([])
const deviceLoading = ref(false)
const deviceOptions = ref<SelectOption[]>([])
const deviceOptions = ref<SelectOption<string>[]>([])
const recipeTypeLoading = ref(false)
const recipeTypeOptions = ref<SelectOption[]>([])
const productLabelMap = computed<Record<number, string>>(() => {
return productOptions.value.reduce((acc, cur) => {
acc[cur.value] = cur.label
return acc
}, {} as Record<number, string>)
})
const deviceLabelMap = computed<Record<number, string>>(() => {
return deviceOptions.value.reduce((acc, cur) => {
acc[cur.value] = cur.label
return acc
}, {} as Record<number, string>)
})
const recipeTypeOptions = ref<SelectOption<number>[]>([])
const recipeTypeLabelMap = computed<Record<number, string>>(() => {
return recipeTypeOptions.value.reduce((acc, cur) => {
@ -265,7 +251,7 @@ const getProductOptions = async () => {
productLoading.value = true
try {
const data = await ProductApi.getProductSimpleList()
productOptions.value = (data ?? []).map((item: any) => ({ label: item.name, value: item.id }))
productOptions.value = (data ?? []).map((item: any) => ({ label: item.name, value: item.name }))
} finally {
productLoading.value = false
}
@ -275,7 +261,7 @@ const getDeviceOptions = async () => {
deviceLoading.value = true
try {
const data = await DeviceApi.getDeviceList()
deviceOptions.value = (data ?? []).map((item: any) => ({ label: item.deviceName, value: item.id }))
deviceOptions.value = (data ?? []).map((item: any) => ({ label: item.deviceName, value: item.deviceName }))
} finally {
deviceLoading.value = false
}
@ -309,16 +295,16 @@ const dialogLoading = ref(false)
const dialogForm = reactive({
id: undefined as number | undefined,
recipeCode: '',
recipeName: '',
name: '',
recipeType: undefined as number | undefined,
productId: undefined as number | undefined,
deviceId: undefined as number | undefined,
remark: ''
productName: '' as string | undefined,
machineName: '' as string | undefined,
recipeDesc: ''
})
const dialogRules = reactive({
recipeCode: [{ required: true, message: '配方编码不能为空', trigger: 'blur' }],
recipeName: [{ required: true, message: '配方名称不能为空', trigger: 'blur' }]
name: [{ required: true, message: '配方名称不能为空', trigger: 'blur' }]
})
const openDialog = async (mode: DialogMode, row?: RecipeConfigVO) => {
@ -332,21 +318,21 @@ const openDialog = async (mode: DialogMode, row?: RecipeConfigVO) => {
if (mode === 'create') {
dialogForm.id = undefined
dialogForm.recipeCode = ''
dialogForm.recipeName = ''
dialogForm.name = ''
dialogForm.recipeType = undefined
dialogForm.productId = undefined
dialogForm.deviceId = undefined
dialogForm.remark = ''
dialogForm.productName = ''
dialogForm.machineName = ''
dialogForm.recipeDesc = ''
return
}
dialogForm.id = row?.id
dialogForm.recipeCode = row?.recipeCode ?? ''
dialogForm.recipeName = row?.recipeName ?? ''
dialogForm.name = row?.name ?? row?.recipeName ?? ''
dialogForm.recipeType = typeof row?.recipeType === 'number' ? row.recipeType : Number(row?.recipeType) || undefined
dialogForm.productId = row?.productId
dialogForm.deviceId = row?.deviceId
dialogForm.remark = row?.remark ?? ''
dialogForm.productName = row?.productName ?? ''
dialogForm.machineName = row?.machineName ?? row?.deviceName ?? ''
dialogForm.recipeDesc = row?.recipeDesc ?? row?.remark ?? ''
}
const submitDialog = async () => {
@ -357,13 +343,11 @@ const submitDialog = async () => {
const data = {
id: dialogForm.id,
recipeCode: dialogForm.recipeCode,
recipeName: dialogForm.recipeName,
name: dialogForm.name,
recipeType: dialogForm.recipeType,
productId: dialogForm.productId,
productName: dialogForm.productId ? productLabelMap.value[dialogForm.productId] : undefined,
deviceId: dialogForm.deviceId,
deviceName: dialogForm.deviceId ? deviceLabelMap.value[dialogForm.deviceId] : undefined,
remark: dialogForm.remark
productName: dialogForm.productName,
machineName: dialogForm.machineName,
recipeDesc: dialogForm.recipeDesc
}
if (dialogMode.value === 'create') {
await RecipeConfigApi.createRecipeConfig(data)
@ -385,14 +369,14 @@ const detailLoading = ref(false)
const detailList = ref<RecipePointDetailVO[]>([])
const detailTotal = ref(0)
const detailMeta = reactive({
recipeId: undefined as number | undefined,
recipeId: undefined as string | undefined,
recipeCode: '',
recipeName: ''
name: ''
})
const detailQueryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeId: undefined as number | undefined
recipeId: undefined as string | undefined
})
const getDetailList = async () => {
@ -404,7 +388,13 @@ const getDetailList = async () => {
pageSize: detailQueryParams.pageSize,
recipeId: detailQueryParams.recipeId
})
detailList.value = data.list
detailList.value = (data.list ?? []).map((item: any) => {
return {
...item,
pointName: item.pointName ?? item.attributeName,
pointType: item.pointType ?? item.attributeType
}
})
detailTotal.value = data.total
} finally {
detailLoading.value = false
@ -412,10 +402,10 @@ const getDetailList = async () => {
}
const openDetail = async (row: RecipeConfigVO) => {
detailMeta.recipeId = row.id
detailMeta.recipeId = row.recipeCode
detailMeta.recipeCode = row.recipeCode
detailMeta.recipeName = row.recipeName
detailQueryParams.recipeId = row.id
detailMeta.name = row.name ?? row.recipeName ?? ''
detailQueryParams.recipeId = row.recipeCode
detailQueryParams.pageNo = 1
detailVisible.value = true
detailActiveTab.value = 'detail'
@ -433,7 +423,7 @@ const closeDetail = () => {
detailActiveTab.value = 'detail'
detailMeta.recipeId = undefined
detailMeta.recipeCode = ''
detailMeta.recipeName = ''
detailMeta.name = ''
detailQueryParams.recipeId = undefined
detailQueryParams.pageNo = 1
detailQueryParams.pageSize = 10
@ -449,25 +439,29 @@ type TransferItem = { key: number; label: string; disabled?: boolean }
const configVisible = ref(false)
const configLoading = ref(false)
const configRecipeId = ref<number | undefined>(undefined)
const configRecipeId = ref<string | undefined>(undefined)
const configCandidates = ref<TransferItem[]>([])
const configSelectedKeys = ref<number[]>([])
const openConfigDialog = async (row: RecipeConfigVO) => {
configVisible.value = true
configRecipeId.value = row.id
configRecipeId.value = row.recipeCode
configSelectedKeys.value = []
configCandidates.value = []
configLoading.value = true
try {
const [candidateRes, selectedRes] = await Promise.all([
RecipeConfigApi.getPointCandidatePage({ pageNo: 1, pageSize: 1000 }),
RecipeConfigApi.getRecipePointDetailPage({ pageNo: 1, pageSize: 1000, recipeId: row.id })
DeviceApi.getDeviceContactModelPage(),
RecipeConfigApi.getRecipePointDetailPage({ pageNo: 1, pageSize: 1000, recipeId: row.recipeCode })
])
configCandidates.value = (candidateRes.list ?? []).map((item: any) => ({
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.pointName}${item.pointType ? '' + item.pointType : ''}${item.dataType ? '/' + item.dataType : ''}${item.dataUnit ? ' ' + item.dataUnit : ''}${item.pointType ? '' : ''}`
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 ?? [])

Loading…
Cancel
Save