feat:配方配置-对接接口

main
黄伟杰 2 months ago
parent a51f19db91
commit c49e2fed52

@ -20,12 +20,14 @@ export interface RecipeConfigVO {
export interface RecipePointDetailVO { export interface RecipePointDetailVO {
id: number id: number
recipeId: number recipeId: string | number
attributeId?: number attributeId?: number
pointName: string pointName?: string
pointType: string pointType?: string
dataType: string dataType?: string
dataUnit: string dataUnit?: string
attributeName?: string
attributeType?: string
} }
export interface RecipePointCandidateVO { export interface RecipePointCandidateVO {
@ -172,39 +174,10 @@ export const RecipeConfigApi = {
}, },
getRecipePointDetailPage: async (params: any) => { getRecipePointDetailPage: async (params: any) => {
ensureMockSeeded() return await request.get({ url: `/iot/recipe-device-attribute/page`, params })
await sleep(120)
const recipeId = Number(params?.recipeId)
const pageNo = params?.pageNo
const pageSize = params?.pageSize
if (!recipeId) return { list: [], total: 0 }
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})
const selectedIds = relation[String(recipeId)] ?? []
const candidates = loadFromStorage<RecipePointCandidateVO[]>(
STORAGE_KEYS.pointCandidates,
buildMockPointCandidates()
)
const map = candidates.reduce((acc, cur) => {
acc[cur.id] = cur
return acc
}, {} as Record<number, RecipePointCandidateVO>)
const detailAll: RecipePointDetailVO[] = selectedIds
.map((id) => map[id])
.filter(Boolean)
.map((item) => ({
id: item.id,
recipeId,
attributeId: item.id,
pointName: item.pointName,
pointType: item.pointType,
dataType: item.dataType,
dataUnit: item.dataUnit
}))
return paginate(detailAll, pageNo, pageSize)
}, },
saveRecipePointConfig: async (data: { recipeId: number; attributeIds: number[] }) => { saveRecipePointConfig: async (data: { recipeId: string | number; attributeIds: number[] }) => {
ensureMockSeeded() ensureMockSeeded()
await sleep(120) await sleep(120)
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {}) const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})

@ -0,0 +1,31 @@
import request from '@/config/axios'
export interface RecipePointVO {
id: number
recipeId: string | number
name?: string
max?: string | number
min?: string | number
dataType?: string
dataUnit?: string
remark?: string
createTime?: string
}
export const RecipePointApi = {
getRecipePointPage: async (params: any) => {
return await request.get({ url: `/iot/recipe-point/page`, params })
},
createRecipePoint: async (params: Partial<RecipePointVO>) => {
return await request.post({ url: `/iot/recipe-point/create`, data: params })
},
updateRecipePoint: async (params: Partial<RecipePointVO>) => {
return await request.put({ url: `/iot/recipe-point/update`, data: params })
},
deleteRecipePoint: async (id: number) => {
return await request.delete({ url: `/iot/recipe-point/delete`, params: { id } })
}
}

@ -0,0 +1,332 @@
<template>
<ContentWrap v-if="visible">
<el-tabs v-model="activeTab" class="mb-12px">
<template #extra>
<el-button link type="info" @click="handleClose"></el-button>
</template>
<el-tab-pane :label="`详情:${recipeCode} - ${name}`" name="detail">
<div class="mb-12px text-left">
<el-button type="primary" @click="handleConfig"></el-button>
</div>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id">
<el-table-column label="序号" align="center" width="80">
<template #default="scope">
{{ (queryParams.pageNo - 1) * queryParams.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="点位名称" align="center" prop="pointName" />
<el-table-column label="点位类型" align="center" prop="pointType" />
<el-table-column label="数据类型" align="center" prop="dataType" />
<el-table-column label="单位" align="center" prop="dataUnit" />
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="handlePagination"
/>
</el-tab-pane>
<el-tab-pane label="手动录入参数" name="manual">
<div class="mb-12px flex justify-start">
<el-button type="primary" @click="openManualDialog('create')"></el-button>
</div>
<el-table
v-loading="manualLoading"
:data="manualList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="上限" align="center" prop="max" />
<el-table-column label="下限" align="center" prop="min" />
<el-table-column label="单位" align="center" prop="dataUnit" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" width="150px" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="openManualDialog('update', scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleManualDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="manualTotal"
v-model:page="manualQueryParams.pageNo"
v-model:limit="manualQueryParams.pageSize"
@pagination="handleManualPagination"
/>
</el-tab-pane>
</el-tabs>
<Dialog
:title="manualDialogMode === 'create' ? '新增参数' : '编辑参数'"
v-model="manualDialogVisible"
width="640px"
>
<el-form
ref="manualFormRef"
:model="manualForm"
:rules="manualRules"
label-width="90px"
v-loading="manualDialogLoading"
>
<el-form-item label="名称" prop="name">
<el-input v-model="manualForm.name" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item label="上限" prop="max">
<el-input v-model="manualForm.max" placeholder="请输入上限" clearable />
</el-form-item>
<el-form-item label="下限" prop="min">
<el-input v-model="manualForm.min" placeholder="请输入下限" clearable />
</el-form-item>
<el-form-item label="数据类型" prop="dataType">
<el-select v-model="manualForm.dataType" placeholder="请选择数据类型" clearable filterable class="!w-full">
<el-option
v-for="dict in dataTypeDictOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="单位" prop="dataUnit">
<el-select v-model="manualForm.dataUnit" placeholder="请选择单位" clearable filterable class="!w-full">
<el-option
v-for="dict in unitDictOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="manualForm.remark" placeholder="请输入备注" clearable type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="manualDialogVisible = false"> </el-button>
<el-button type="primary" :disabled="manualDialogLoading" @click="submitManualDialog"> </el-button>
</template>
</Dialog>
</ContentWrap>
</template>
<script setup lang="ts">
import { RecipeConfigApi, RecipePointDetailVO } from '@/api/iot/recipeConfig'
import { RecipePointApi, RecipePointVO } from '@/api/iot/recipePoint'
import { getStrDictOptions } from '@/utils/dict'
defineOptions({ name: 'RecipeDetailList' })
const message = useMessage()
const { t } = useI18n()
const props = defineProps<{
visible: boolean
recipeId?: string
manualRecipeId?: string | number
recipeCode: string
name: string
}>()
const emit = defineEmits<{
(e: 'close'): void
(e: 'config'): void
}>()
const activeTab = ref('detail')
const loading = ref(false)
const list = ref<RecipePointDetailVO[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
const manualLoading = ref(false)
const manualList = ref<RecipePointVO[]>([])
const manualTotal = ref(0)
const manualQueryParams = reactive({
pageNo: 1,
pageSize: 10
})
type ManualDialogMode = 'create' | 'update'
const manualDialogVisible = ref(false)
const manualDialogMode = ref<ManualDialogMode>('create')
const manualDialogLoading = ref(false)
const manualFormRef = ref()
const manualForm = reactive({
id: undefined as number | undefined,
name: '',
max: '' as string | undefined,
min: '' as string | undefined,
dataType: '' as string | undefined,
dataUnit: '' as string | undefined,
remark: '' as string | undefined
})
const manualRules = reactive({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
max: [{ required: true, message: '上限不能为空', trigger: 'blur' }],
min: [{ required: true, message: '下限不能为空', trigger: 'blur' }],
dataType: [{ required: true, message: '数据类型不能为空', trigger: 'change' }],
dataUnit: [{ required: true, message: '单位不能为空', trigger: 'change' }]
})
const dataTypeDictOptions = computed(() => getStrDictOptions('iot_device_data_type'))
const unitDictOptions = computed(() => getStrDictOptions('iot_device_attribute_unit'))
const normalizeDetail = (item: any): RecipePointDetailVO => {
return {
...item,
pointName: item.pointName ?? item.attributeName,
pointType: item.pointType ?? item.attributeType
}
}
const getList = async () => {
if (!props.recipeId) return
loading.value = true
try {
const pageSize = Math.min(100, Math.max(1, Number(queryParams.pageSize) || 10))
const data = await RecipeConfigApi.getRecipePointDetailPage({
pageNo: queryParams.pageNo,
pageSize,
recipeId: props.recipeId
})
list.value = (data.list ?? []).map(normalizeDetail)
total.value = data.total
} finally {
loading.value = false
}
}
const handlePagination = () => {
getList()
}
const getManualList = async () => {
if (!props.manualRecipeId) return
manualLoading.value = true
try {
const pageSize = Math.min(100, Math.max(1, Number(manualQueryParams.pageSize) || 10))
const data = await RecipePointApi.getRecipePointPage({
pageNo: manualQueryParams.pageNo,
pageSize,
recipeId: props.manualRecipeId
})
manualList.value = data.list ?? []
manualTotal.value = data.total
} finally {
manualLoading.value = false
}
}
const handleManualPagination = () => {
getManualList()
}
const openManualDialog = (mode: ManualDialogMode, row?: RecipePointVO) => {
manualDialogMode.value = mode
manualDialogVisible.value = true
nextTick(() => {
manualFormRef.value?.clearValidate?.()
})
if (mode === 'create') {
manualForm.id = undefined
manualForm.name = ''
manualForm.max = ''
manualForm.min = ''
manualForm.dataType = ''
manualForm.dataUnit = ''
manualForm.remark = ''
return
}
manualForm.id = row?.id
manualForm.name = row?.name ?? ''
manualForm.max = row?.max === undefined || row?.max === null ? '' : String(row.max)
manualForm.min = row?.min === undefined || row?.min === null ? '' : String(row.min)
manualForm.dataType = row?.dataType === undefined || row?.dataType === null ? '' : String(row.dataType)
manualForm.dataUnit = row?.dataUnit ?? ''
manualForm.remark = row?.remark ?? ''
}
const submitManualDialog = async () => {
if (!props.manualRecipeId) return
await manualFormRef.value?.validate?.()
manualDialogLoading.value = true
try {
const params = {
id: manualForm.id,
recipeId: props.manualRecipeId,
name: manualForm.name,
max: manualForm.max,
min: manualForm.min,
dataType: manualForm.dataType,
dataUnit: manualForm.dataUnit,
remark: manualForm.remark
}
if (manualDialogMode.value === 'create') {
await RecipePointApi.createRecipePoint(params)
message.success(t('common.createSuccess'))
} else {
await RecipePointApi.updateRecipePoint(params)
message.success(t('common.updateSuccess'))
}
manualDialogVisible.value = false
await getManualList()
} finally {
manualDialogLoading.value = false
}
}
const handleManualDelete = async (row: RecipePointVO) => {
try {
await message.delConfirm()
await RecipePointApi.deleteRecipePoint(row.id)
message.success(t('common.delSuccess'))
await getManualList()
} catch {}
}
const handleClose = () => {
emit('close')
}
const handleConfig = () => {
emit('config')
}
watch(
() => [props.visible, props.recipeId],
([visible]) => {
if (!visible) return
queryParams.pageNo = 1
manualQueryParams.pageNo = 1
activeTab.value = 'detail'
getList()
},
{ immediate: true }
)
watch(
() => activeTab.value,
(tab) => {
if (!props.visible) return
if (tab === 'manual') {
getManualList()
}
}
)
const refresh = async () => {
if (activeTab.value === 'manual') {
await getManualList()
return
}
await getList()
}
defineExpose({ refresh })
</script>

@ -48,7 +48,7 @@ ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-to
<el-table-column label="配方描述" align="center" prop="recipeDesc" /> <el-table-column label="配方描述" align="center" prop="recipeDesc" />
<el-table-column label="操作" align="center" width="240px" fixed="right"> <el-table-column label="操作" align="center" width="240px" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" @click.stop="openConfigDialog(scope.row)">配置</el-button> <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="warning" @click.stop="openDialog('update', scope.row)">编辑</el-button>
<el-button link type="danger" @click.stop="handleDelete(scope.row)">删除</el-button> <el-button link type="danger" @click.stop="handleDelete(scope.row)">删除</el-button>
</template> </template>
@ -59,31 +59,16 @@ ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-to
@pagination="handlePagination" /> @pagination="handlePagination" />
</ContentWrap> </ContentWrap>
<ContentWrap v-if="detailVisible"> <RecipeDetailList
<el-tabs v-model="detailActiveTab" class="mb-12px"> ref="detailRef"
<template #extra> :visible="detailVisible"
<el-button link type="info" @click="closeDetail"></el-button> :recipe-id="detailMeta.recipeCode"
</template> :manual-recipe-id="detailMeta.id"
<el-tab-pane :label="`详情:${detailMeta.recipeCode} - ${detailMeta.name}`" name="detail"> :recipe-code="detailMeta.recipeCode"
<el-table :name="detailMeta.name"
v-loading="detailLoading" :data="detailList" :stripe="true" :show-overflow-tooltip="true" @config="handleDetailConfig"
row-key="id"> @close="closeDetail"
<el-table-column label="序号" align="center" width="80"> />
<template #default="scope">
{{ (detailQueryParams.pageNo - 1) * detailQueryParams.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="点位名称" align="center" prop="pointName" />
<el-table-column label="点位类型" align="center" prop="pointType" />
<el-table-column label="数据类型" align="center" prop="dataType" />
<el-table-column label="单位" align="center" prop="dataUnit" />
</el-table>
<Pagination
:total="detailTotal" v-model:page="detailQueryParams.pageNo"
v-model:limit="detailQueryParams.pageSize" @pagination="handleDetailPagination" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px"> <Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
<el-form ref="dialogFormRef" :model="dialogForm" :rules="dialogRules" label-width="100px" v-loading="dialogLoading"> <el-form ref="dialogFormRef" :model="dialogForm" :rules="dialogRules" label-width="100px" v-loading="dialogLoading">
@ -142,7 +127,8 @@ import download from '@/utils/download'
import { ProductApi } from '@/api/erp/product/product' import { ProductApi } from '@/api/erp/product/product'
import { DeviceApi } from '@/api/iot/device' import { DeviceApi } from '@/api/iot/device'
import { RecipeApi } from '@/api/iot/recipe' import { RecipeApi } from '@/api/iot/recipe'
import { RecipeConfigApi, RecipeConfigVO, RecipePointDetailVO } from '@/api/iot/recipeConfig' import { RecipeConfigApi, RecipeConfigVO } from '@/api/iot/recipeConfig'
import RecipeDetailList from './components/RecipeDetailList.vue'
type SelectOption<T = any> = { label: string; value: T } type SelectOption<T = any> = { label: string; value: T }
@ -261,7 +247,7 @@ const getDeviceOptions = async () => {
const getRecipeTypeOptions = async () => { const getRecipeTypeOptions = async () => {
recipeTypeLoading.value = true recipeTypeLoading.value = true
try { try {
const data = await RecipeApi.getRecipePage() const data = await RecipeApi.getRecipePage({})
recipeTypeOptions.value = (data?.list ?? []).map((item: any) => ({ label: item.name, value: item.name })) recipeTypeOptions.value = (data?.list ?? []).map((item: any) => ({ label: item.name, value: item.name }))
} finally { } finally {
recipeTypeLoading.value = false recipeTypeLoading.value = false
@ -351,53 +337,24 @@ const submitDialog = async () => {
} }
} }
const detailRef = ref()
const detailVisible = ref(false) const detailVisible = ref(false)
const detailActiveTab = ref('detail')
const detailLoading = ref(false)
const detailList = ref<RecipePointDetailVO[]>([])
const detailTotal = ref(0)
const detailMeta = reactive({ const detailMeta = reactive({
recipeId: undefined as string | undefined, id: undefined as number | undefined,
recipeCode: '', recipeCode: '',
name: '' name: ''
}) })
const detailQueryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeId: undefined as string | undefined
})
const getDetailList = async () => {
if (!detailQueryParams.recipeId) return
detailLoading.value = true
try {
const data = await RecipeConfigApi.getRecipePointDetailPage({
pageNo: detailQueryParams.pageNo,
pageSize: detailQueryParams.pageSize,
recipeId: detailQueryParams.recipeId
})
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
}
}
const openDetail = async (row: RecipeConfigVO) => { const openDetail = async (row: RecipeConfigVO) => {
detailMeta.recipeId = row.recipeCode detailMeta.id = row.id
detailMeta.recipeCode = row.recipeCode detailMeta.recipeCode = row.recipeCode
detailMeta.name = row.name ?? row.recipeName ?? '' detailMeta.name = row.name ?? row.recipeName ?? ''
detailQueryParams.recipeId = row.recipeCode
detailQueryParams.pageNo = 1
detailVisible.value = true detailVisible.value = true
detailActiveTab.value = 'detail' }
await getDetailList()
const handleDetailConfig = () => {
if (!detailMeta.recipeCode) return
openConfigDialog(detailMeta.recipeCode)
} }
const handleRowClick = async (row: RecipeConfigVO, column: any) => { const handleRowClick = async (row: RecipeConfigVO, column: any) => {
@ -408,19 +365,9 @@ const handleRowClick = async (row: RecipeConfigVO, column: any) => {
const closeDetail = () => { const closeDetail = () => {
detailVisible.value = false detailVisible.value = false
detailActiveTab.value = 'detail' detailMeta.id = undefined
detailMeta.recipeId = undefined
detailMeta.recipeCode = '' detailMeta.recipeCode = ''
detailMeta.name = '' detailMeta.name = ''
detailQueryParams.recipeId = undefined
detailQueryParams.pageNo = 1
detailQueryParams.pageSize = 10
detailList.value = []
detailTotal.value = 0
}
const handleDetailPagination = () => {
getDetailList()
} }
type TransferItem = { key: number; label: string; disabled?: boolean } type TransferItem = { key: number; label: string; disabled?: boolean }
@ -431,16 +378,16 @@ const configRecipeId = ref<string | undefined>(undefined)
const configCandidates = ref<TransferItem[]>([]) const configCandidates = ref<TransferItem[]>([])
const configSelectedKeys = ref<number[]>([]) const configSelectedKeys = ref<number[]>([])
const openConfigDialog = async (row: RecipeConfigVO) => { const openConfigDialog = async (recipeCode: string) => {
configVisible.value = true configVisible.value = true
configRecipeId.value = row.recipeCode configRecipeId.value = recipeCode
configSelectedKeys.value = [] configSelectedKeys.value = []
configCandidates.value = [] configCandidates.value = []
configLoading.value = true configLoading.value = true
try { try {
const [candidateRes, selectedRes] = await Promise.all([ const [candidateRes, selectedRes] = await Promise.all([
DeviceApi.getDeviceContactModelPage(), DeviceApi.getDeviceContactModelPage(),
RecipeConfigApi.getRecipePointDetailPage({ pageNo: 1, pageSize: 1000, recipeId: row.recipeCode }) RecipeConfigApi.getRecipePointDetailPage({ pageNo: 1, pageSize: 100, recipeId: recipeCode })
]) ])
const candidateList = Array.isArray(candidateRes) const candidateList = Array.isArray(candidateRes)
@ -470,8 +417,8 @@ const submitConfig = async () => {
}) })
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
configVisible.value = false configVisible.value = false
if (detailVisible.value && detailQueryParams.recipeId === configRecipeId.value) { if (detailVisible.value && detailMeta.recipeCode === configRecipeId.value) {
await getDetailList() await detailRef.value?.refresh?.()
} }
} finally { } finally {
configLoading.value = false configLoading.value = false

Loading…
Cancel
Save