feat:配方配置使用mock数据源

main
黄伟杰 4 weeks ago
parent 2e9fd85c92
commit a22d95d134

@ -0,0 +1,280 @@
export interface RecipeConfigVO {
id: number
recipeCode: string
recipeName: string
recipeType?: string
productId?: number
productName?: string
deviceId?: number
deviceName?: string
remark?: string
}
export interface RecipePointDetailVO {
id: number
recipeId: number
attributeId?: number
pointName: string
pointType: string
dataType: string
dataUnit: string
}
export interface RecipePointCandidateVO {
id: number
pointName: string
pointType: string
dataType: string
dataUnit: string
}
type PageResult<T> = { list: T[]; total: number }
const STORAGE_KEYS = {
recipeList: 'mock:recipeConfig:recipes',
pointCandidates: 'mock:recipeConfig:pointCandidates',
recipePointRelation: 'mock:recipeConfig:recipePointRelation'
} as const
const safeParseJson = <T>(value: string | null): T | undefined => {
if (!value) return undefined
try {
return JSON.parse(value) as T
} catch {
return undefined
}
}
const loadFromStorage = <T>(key: string, fallback: T): T => {
const parsed = safeParseJson<T>(window.localStorage.getItem(key))
return parsed ?? fallback
}
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']
const units = ['kg', 'm³', '℃', 'A', 'V', '']
return Array.from({ length: 60 }).map((_, idx) => {
const no = String(idx + 1).padStart(3, '0')
return {
id: idx + 1,
pointName: `点位-${no}`,
pointType: pointTypes[idx % pointTypes.length],
dataType: dataTypes[idx % dataTypes.length],
dataUnit: units[idx % units.length]
}
})
}
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())
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})
if (!Object.keys(relation).length) saveToStorage(STORAGE_KEYS.recipePointRelation, {})
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const paginate = <T>(items: T[], pageNo: number, pageSize: number): PageResult<T> => {
const safePageNo = Math.max(1, Number(pageNo) || 1)
const safePageSize = Math.max(1, Number(pageSize) || 10)
const start = (safePageNo - 1) * safePageSize
return {
total: items.length,
list: items.slice(start, start + safePageSize)
}
}
const contains = (value: string | undefined, keyword: string | undefined) => {
const v = (value ?? '').toLowerCase()
const k = (keyword ?? '').trim().toLowerCase()
if (!k) return true
return v.includes(k)
}
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)
},
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 ?? '',
recipeType: data.recipeType,
productId: data.productId,
productName: data.productName,
deviceId: data.deviceId,
deviceName: data.deviceName,
remark: data.remark
}
saveToStorage(STORAGE_KEYS.recipeList, [newItem, ...list])
return nextId
},
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
},
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
},
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' })
},
getPointCandidatePage: async (params: any): Promise<PageResult<RecipePointCandidateVO>> => {
ensureMockSeeded()
await sleep(80)
const keyword = params?.keyword
const pageNo = params?.pageNo
const pageSize = params?.pageSize
const list = loadFromStorage<RecipePointCandidateVO[]>(
STORAGE_KEYS.pointCandidates,
buildMockPointCandidates()
)
const filtered = keyword
? list.filter((item) => contains(item.pointName, keyword))
: list
return paginate(filtered, pageNo, pageSize)
},
getRecipePointDetailPage: async (params: any) => {
ensureMockSeeded()
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[] }) => {
ensureMockSeeded()
await sleep(120)
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})
relation[String(data.recipeId)] = Array.from(new Set(data.attributeIds ?? [])).filter((v) => {
return typeof v === 'number' && !Number.isNaN(v)
})
saveToStorage(STORAGE_KEYS.recipePointRelation, relation)
return true
}
}

@ -0,0 +1,521 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="配方编码" prop="recipeCode">
<el-input
v-model="queryParams.recipeCode"
placeholder="请输入配方编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="配方名称" prop="recipeName">
<el-input
v-model="queryParams.recipeName"
placeholder="请输入配方名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="queryParams.productName"
placeholder="请输入产品名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 查询</el-button>
<el-button type="primary" plain @click="openDialog('create')">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading">
<Icon icon="ep:download" class="mr-5px" /> 导出
</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"
@selection-change="handleSelectionChange"
>
<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="recipeType" />
<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" width="240px" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="openConfigDialog(scope.row)"></el-button>
<el-button link type="info" @click="openDetail(scope.row)"></el-button>
<el-button link type="warning" @click="openDialog('update', scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="handlePagination"
/>
</ContentWrap>
<ContentWrap v-if="detailVisible">
<div class="flex items-center justify-between mb-12px">
<div class="text-14px">
详情{{ detailMeta.recipeCode }} - {{ detailMeta.recipeName }}
</div>
<el-button link type="info" @click="closeDetail"></el-button>
</div>
<el-table
v-loading="detailLoading"
:data="detailList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<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"
/>
</ContentWrap>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
<el-form
ref="dialogFormRef"
:model="dialogForm"
:rules="dialogRules"
label-width="100px"
v-loading="dialogLoading"
>
<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>
<el-form-item label="配方类型" prop="recipeType">
<el-input v-model="dialogForm.recipeType" placeholder="请输入配方类型" clearable />
</el-form-item>
<el-form-item label="关联产品" prop="productId">
<el-select
v-model="dialogForm.productId"
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-select
v-model="dialogForm.deviceId"
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>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitDialog" :disabled="dialogLoading"> </el-button>
</template>
</Dialog>
<Dialog title="配置" v-model="configVisible" width="920px">
<div v-loading="configLoading">
<el-transfer
v-model="configSelectedKeys"
:data="configCandidates"
filterable
:titles="['候选点位', '已选点位']"
/>
</div>
<template #footer>
<el-button @click="configVisible = false"> </el-button>
<el-button type="primary" @click="submitConfig" :disabled="configLoading"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import download from '@/utils/download'
import { ProductApi } from '@/api/erp/product/product'
import { DeviceApi } from '@/api/iot/device'
import { RecipeConfigApi, RecipeConfigVO, RecipePointDetailVO } from '@/api/iot/recipeConfig'
type SelectOption = { label: string; value: number }
defineOptions({ name: 'FormulaConfig' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const tableRef = ref()
const queryFormRef = ref()
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeCode: '',
recipeName: '',
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 buildQueryParams = () => {
const recipeCode = queryParams.recipeCode?.trim()
const recipeName = queryParams.recipeName?.trim()
const productName = queryParams.productName?.trim()
return {
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
recipeCode: recipeCode ? recipeCode : undefined,
recipeName: recipeName ? recipeName : 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 handlePagination = () => {
getList()
}
const handleExport = async () => {
if (!selectedIds.value.length) {
message.error('请选择需要导出的数据')
return
}
try {
await message.exportConfirm()
exportLoading.value = true
const data = await RecipeConfigApi.exportRecipeConfig({ ids: selectedIds.value.join(',') })
download.excel(data, '配方配置.xls')
} 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[]>([])
const deviceLoading = ref(false)
const deviceOptions = 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 getProductOptions = async () => {
productLoading.value = true
try {
const data = await ProductApi.getProductSimpleList()
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 ensureOptionsLoaded = async () => {
if (!productOptions.value.length) await getProductOptions()
if (!deviceOptions.value.length) await getDeviceOptions()
}
type DialogMode = 'create' | 'update'
const dialogVisible = ref(false)
const dialogMode = ref<DialogMode>('create')
const dialogTitle = computed(() => (dialogMode.value === 'create' ? '新增配方配置' : '编辑配方配置'))
const dialogFormRef = ref()
const dialogLoading = ref(false)
const dialogForm = reactive({
id: undefined as number | undefined,
recipeCode: '',
recipeName: '',
recipeType: '',
productId: undefined as number | undefined,
deviceId: undefined as number | undefined,
remark: ''
})
const dialogRules = reactive({
recipeCode: [{ required: true, message: '配方编码不能为空', trigger: 'blur' }],
recipeName: [{ required: true, message: '配方名称不能为空', 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.recipeName = ''
dialogForm.recipeType = ''
dialogForm.productId = undefined
dialogForm.deviceId = undefined
dialogForm.remark = ''
return
}
dialogForm.id = row?.id
dialogForm.recipeCode = row?.recipeCode ?? ''
dialogForm.recipeName = row?.recipeName ?? ''
dialogForm.recipeType = row?.recipeType ?? ''
dialogForm.productId = row?.productId
dialogForm.deviceId = row?.deviceId
dialogForm.remark = row?.remark ?? ''
}
const submitDialog = async () => {
if (!dialogFormRef.value) return
await dialogFormRef.value.validate()
dialogLoading.value = true
try {
const data = {
id: dialogForm.id,
recipeCode: dialogForm.recipeCode,
recipeName: dialogForm.recipeName,
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
}
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 detailVisible = ref(false)
const detailLoading = ref(false)
const detailList = ref<RecipePointDetailVO[]>([])
const detailTotal = ref(0)
const detailMeta = reactive({
recipeId: undefined as number | undefined,
recipeCode: '',
recipeName: ''
})
const detailQueryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeId: undefined as number | 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
detailTotal.value = data.total
} finally {
detailLoading.value = false
}
}
const openDetail = async (row: RecipeConfigVO) => {
detailMeta.recipeId = row.id
detailMeta.recipeCode = row.recipeCode
detailMeta.recipeName = row.recipeName
detailQueryParams.recipeId = row.id
detailQueryParams.pageNo = 1
detailVisible.value = true
await getDetailList()
}
const closeDetail = () => {
detailVisible.value = false
detailMeta.recipeId = undefined
detailMeta.recipeCode = ''
detailMeta.recipeName = ''
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 }
const configVisible = ref(false)
const configLoading = ref(false)
const configRecipeId = ref<number | undefined>(undefined)
const configCandidates = ref<TransferItem[]>([])
const configSelectedKeys = ref<number[]>([])
const openConfigDialog = async (row: RecipeConfigVO) => {
configVisible.value = true
configRecipeId.value = row.id
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 })
])
configCandidates.value = (candidateRes.list ?? []).map((item: any) => ({
key: item.id,
label: `${item.pointName}${item.pointType ? '' + item.pointType : ''}${item.dataType ? '/' + item.dataType : ''}${item.dataUnit ? ' ' + item.dataUnit : ''}${item.pointType ? '' : ''}`
}))
configSelectedKeys.value = (selectedRes.list ?? [])
.map((item: any) => (item.attributeId !== undefined ? item.attributeId : item.id))
.filter((id: any) => typeof id === 'number')
} finally {
configLoading.value = false
}
}
const submitConfig = async () => {
if (!configRecipeId.value) return
configLoading.value = true
try {
await RecipeConfigApi.saveRecipePointConfig({
recipeId: configRecipeId.value,
attributeIds: configSelectedKeys.value
})
message.success(t('common.updateSuccess'))
configVisible.value = false
if (detailVisible.value && detailQueryParams.recipeId === configRecipeId.value) {
await getDetailList()
}
} finally {
configLoading.value = false
}
}
onMounted(() => {
getList()
ensureOptionsLoaded()
})
</script>
<style scoped></style>
Loading…
Cancel
Save