feat(data-tools): 新增入库类型选择功能并优化数据工具界面
- 在补数任务面板中添加入库类型单选按钮组,支持 MySQL 和 InfluxDB - 更新 AddData 接口定义,添加 StorageType 相关类型和选项接口 - 修改补数 API 请求逻辑,根据入库类型动态调整接口路径前缀 - 重构台账设备表单,统一使用装置网络参数作为 MAC 和 NDID 的单一数据源 - 优化台账线路表单,仅当存在 ID 时才设置 lineId 字段,避免空值传递 - 添加入库类型列表获取接口和相关数据处理逻辑 - 更新台账字典代码常量,新增终端型号字典码 - 优化台账树节点添加逻辑,增加前置条件验证和禁用原因提示 - 添加 InfluxDB 配置文件到额外资源目录 - 更新稳定数据分析视图,优化台账树数据结构处理和样式布局 - 完善 API 调试契约检查,确保设备和线路数据映射正确性 - 优化趋势查询性能,禁用全局加载状态提升用户体验
This commit is contained in:
@@ -19,6 +19,16 @@
|
||||
/>
|
||||
|
||||
<el-form ref="formRef" :model="localForm" :rules="formRules" label-width="108px" class="task-form">
|
||||
<div class="form-row form-row-storage">
|
||||
<el-form-item class="form-item-storage-type" label="入库类型" prop="storageType">
|
||||
<el-radio-group v-model="localForm.storageType" :disabled="taskRunning || storageTypeLoading">
|
||||
<el-radio-button v-for="item in storageTypeOptions" :key="item.code" :label="item.code">
|
||||
{{ item.name }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-row form-row-first">
|
||||
<el-form-item class="form-item-line-ids" label="监测点 ID" prop="lineIds">
|
||||
<div class="line-id-input-group">
|
||||
@@ -143,6 +153,8 @@ defineOptions({
|
||||
const props = defineProps<{
|
||||
form: AddData.TaskFormModel
|
||||
preview: AddData.NormalizedPreview | null
|
||||
storageTypeOptions: AddData.StorageTypeOption[]
|
||||
storageTypeLoading: boolean
|
||||
previewLoading: boolean
|
||||
submitLoading: boolean
|
||||
taskRunning: boolean
|
||||
@@ -161,6 +173,7 @@ const guidCount = ref(1)
|
||||
const intervalOptions: AddData.IntervalMinutes[] = [1, 3, 5, 10]
|
||||
const localForm = reactive<AddData.TaskFormModel>({
|
||||
lineMode: 'multiple',
|
||||
storageType: props.form.storageType,
|
||||
lineIds: [...props.form.lineIds],
|
||||
startTime: props.form.startTime,
|
||||
endTime: props.form.endTime,
|
||||
@@ -169,6 +182,7 @@ const localForm = reactive<AddData.TaskFormModel>({
|
||||
|
||||
const syncLocalForm = (form: AddData.TaskFormModel) => {
|
||||
localForm.lineMode = 'multiple'
|
||||
localForm.storageType = form.storageType
|
||||
localForm.lineIds = [...form.lineIds]
|
||||
localForm.startTime = form.startTime
|
||||
localForm.endTime = form.endTime
|
||||
@@ -216,6 +230,7 @@ watch(
|
||||
|
||||
emit('update:form', {
|
||||
lineMode: 'multiple',
|
||||
storageType: value.storageType,
|
||||
lineIds: [...value.lineIds],
|
||||
startTime: value.startTime,
|
||||
endTime: value.endTime,
|
||||
@@ -271,6 +286,7 @@ const handleAppendGuids = () => {
|
||||
}
|
||||
|
||||
const formRules: FormRules<AddData.TaskFormModel> = {
|
||||
storageType: [{ required: true, message: '请选择入库类型', trigger: 'change' }],
|
||||
lineIds: [
|
||||
{
|
||||
validator: (_rule, value: string[], callback) => {
|
||||
@@ -384,6 +400,10 @@ defineExpose({
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.form-row-storage {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.form-row-second {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -394,11 +414,13 @@ defineExpose({
|
||||
}
|
||||
|
||||
.form-item-line-ids,
|
||||
.form-item-storage-type,
|
||||
.form-item-interval {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-item-line-ids :deep(.el-form-item__content),
|
||||
.form-item-storage-type :deep(.el-form-item__content),
|
||||
.form-item-interval :deep(.el-form-item__content) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
ref="taskPanelRef"
|
||||
:form="taskForm"
|
||||
:preview="previewSummary"
|
||||
:storage-type-options="storageTypeOptions"
|
||||
:storage-type-loading="loading.storageType"
|
||||
:preview-loading="loading.preview"
|
||||
:submit-loading="loading.create"
|
||||
:task-running="taskRunning"
|
||||
@@ -36,13 +38,20 @@ import {
|
||||
createAddDataTask,
|
||||
getAddDataPreview,
|
||||
getAddDataTaskStatus,
|
||||
getAddDataStorageTypeList,
|
||||
getAddDataTemplateList
|
||||
} from '@/api/tools/addData'
|
||||
import type { AddData } from '@/api/tools/addData/interface'
|
||||
import AddDataTaskPanel from './components/AddDataTaskPanel.vue'
|
||||
import AddDataTaskStatusCard from './components/AddDataTaskStatusCard.vue'
|
||||
import AddDataTemplateTable from './components/AddDataTemplateTable.vue'
|
||||
import { normalizePreview, normalizeTaskStatus, normalizeTemplateItem, resolveText } from './utils/normalize'
|
||||
import {
|
||||
normalizePreview,
|
||||
normalizeStorageTypeOption,
|
||||
normalizeTaskStatus,
|
||||
normalizeTemplateItem,
|
||||
resolveText
|
||||
} from './utils/normalize'
|
||||
import { buildPayloadSignature, buildTaskPayload as buildTaskRequestPayload } from './utils/taskPayload'
|
||||
|
||||
defineOptions({
|
||||
@@ -53,16 +62,24 @@ type AddDataTaskPanelExpose = {
|
||||
validateTaskForm: () => Promise<boolean>
|
||||
}
|
||||
|
||||
const DEFAULT_STORAGE_TYPE_OPTIONS: AddData.StorageTypeOption[] = [
|
||||
{ code: 'MYSQL', name: 'MySQL' },
|
||||
{ code: 'INFLUXDB', name: 'InfluxDB' }
|
||||
]
|
||||
|
||||
const taskPanelRef = ref<AddDataTaskPanelExpose | null>(null)
|
||||
const activeTab = ref('taskStatus')
|
||||
const storageTypeOptions = ref<AddData.StorageTypeOption[]>([...DEFAULT_STORAGE_TYPE_OPTIONS])
|
||||
const templateRows = ref<AddData.NormalizedTemplateItem[]>([])
|
||||
const previewSummary = ref<AddData.NormalizedPreview | null>(null)
|
||||
const taskStatus = ref<AddData.NormalizedTaskStatus | null>(null)
|
||||
const currentTaskId = ref('')
|
||||
const currentTaskStorageType = ref<AddData.StorageType>('MYSQL')
|
||||
const previewSignature = ref('')
|
||||
const pollTimer = ref<number | null>(null)
|
||||
const pollingBusy = ref(false)
|
||||
const loading = reactive({
|
||||
storageType: false,
|
||||
template: false,
|
||||
preview: false,
|
||||
create: false,
|
||||
@@ -70,6 +87,7 @@ const loading = reactive({
|
||||
})
|
||||
const taskForm = reactive<AddData.TaskFormModel>({
|
||||
lineMode: 'multiple',
|
||||
storageType: 'MYSQL',
|
||||
lineIds: [],
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
@@ -78,6 +96,7 @@ const taskForm = reactive<AddData.TaskFormModel>({
|
||||
|
||||
const handleTaskFormChange = (nextForm: AddData.TaskFormModel) => {
|
||||
taskForm.lineMode = 'multiple'
|
||||
taskForm.storageType = nextForm.storageType
|
||||
taskForm.lineIds = [...nextForm.lineIds]
|
||||
taskForm.startTime = nextForm.startTime
|
||||
taskForm.endTime = nextForm.endTime
|
||||
@@ -94,7 +113,7 @@ const buildTaskPayload = (): AddData.TaskRequestParams => {
|
||||
}
|
||||
|
||||
const buildPreviewDependencySignature = () => {
|
||||
return buildPayloadSignature(buildTaskPayload())
|
||||
return buildPayloadSignature(buildTaskPayload(), taskForm.storageType)
|
||||
}
|
||||
|
||||
const isTerminalStatus = (status?: AddData.TaskStatus) => {
|
||||
@@ -106,6 +125,10 @@ const taskRunning = computed(() => {
|
||||
return Boolean(currentTaskId.value && (status === 'WAITING' || status === 'RUNNING'))
|
||||
})
|
||||
|
||||
const selectedStorageTypeName = computed(() => {
|
||||
return storageTypeOptions.value.find(item => item.code === taskForm.storageType)?.name || taskForm.storageType
|
||||
})
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer.value !== null) {
|
||||
window.clearInterval(pollTimer.value)
|
||||
@@ -113,6 +136,28 @@ const stopPolling = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadStorageTypeOptions = async () => {
|
||||
loading.storageType = true
|
||||
try {
|
||||
const response = await getAddDataStorageTypeList()
|
||||
const rows = Array.isArray(response.data) ? response.data : []
|
||||
const options = rows
|
||||
.map(item => normalizeStorageTypeOption(item))
|
||||
.filter((item): item is AddData.StorageTypeOption => Boolean(item))
|
||||
|
||||
storageTypeOptions.value = options.length ? options : [...DEFAULT_STORAGE_TYPE_OPTIONS]
|
||||
|
||||
if (!storageTypeOptions.value.some(item => item.code === taskForm.storageType)) {
|
||||
taskForm.storageType = storageTypeOptions.value[0]?.code || 'MYSQL'
|
||||
}
|
||||
} catch {
|
||||
// 入库类型接口用于联调校验;接口暂不可用时保留默认选项,避免阻断页面补数流程。
|
||||
storageTypeOptions.value = [...DEFAULT_STORAGE_TYPE_OPTIONS]
|
||||
} finally {
|
||||
loading.storageType = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadTemplateList = async () => {
|
||||
loading.template = true
|
||||
try {
|
||||
@@ -137,7 +182,11 @@ const getValidatedPayload = async () => {
|
||||
return payload
|
||||
}
|
||||
|
||||
const loadTaskStatus = async (taskId = currentTaskId.value, silent = false) => {
|
||||
const loadTaskStatus = async (
|
||||
taskId = currentTaskId.value,
|
||||
silent = false,
|
||||
storageType = currentTaskStorageType.value
|
||||
) => {
|
||||
if (!taskId || pollingBusy.value) return
|
||||
|
||||
pollingBusy.value = true
|
||||
@@ -146,7 +195,7 @@ const loadTaskStatus = async (taskId = currentTaskId.value, silent = false) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getAddDataTaskStatus(taskId)
|
||||
const response = await getAddDataTaskStatus(taskId, storageType)
|
||||
// 状态接口是补数任务唯一的进度来源,统一在这里按正式契约归一化,避免页面层分散兼容字段。
|
||||
const normalizedStatus = normalizeTaskStatus(response.data)
|
||||
taskStatus.value = normalizedStatus
|
||||
@@ -167,15 +216,16 @@ const loadTaskStatus = async (taskId = currentTaskId.value, silent = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
const startPolling = (taskId: string) => {
|
||||
const startPolling = (taskId: string, storageType: AddData.StorageType) => {
|
||||
stopPolling()
|
||||
currentTaskId.value = taskId
|
||||
currentTaskStorageType.value = storageType
|
||||
|
||||
// 创建任务后先立即拉一次状态,再进入固定轮询,避免页面长时间停留在初始态。
|
||||
void loadTaskStatus(taskId).catch(() => null)
|
||||
void loadTaskStatus(taskId, false, storageType).catch(() => null)
|
||||
|
||||
pollTimer.value = window.setInterval(() => {
|
||||
void loadTaskStatus(taskId, true).catch(() => null)
|
||||
void loadTaskStatus(taskId, true, storageType).catch(() => null)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
@@ -188,7 +238,7 @@ const handlePreview = async () => {
|
||||
const response = await getAddDataPreview(payload)
|
||||
// preview 是 create 前的唯一准入检查,必须严格按正式契约读取 totalRowCount 和 tableStats。
|
||||
previewSummary.value = normalizePreview(response.data)
|
||||
previewSignature.value = buildPayloadSignature(payload)
|
||||
previewSignature.value = buildPayloadSignature(payload, taskForm.storageType)
|
||||
ElMessage.success('写入规模预估完成')
|
||||
} finally {
|
||||
loading.preview = false
|
||||
@@ -204,7 +254,7 @@ const handleCreateTask = async () => {
|
||||
const payload = await getValidatedPayload()
|
||||
if (!payload) return
|
||||
|
||||
const currentSignature = buildPayloadSignature(payload)
|
||||
const currentSignature = buildPayloadSignature(payload, taskForm.storageType)
|
||||
if (!previewSummary.value || previewSignature.value !== currentSignature) {
|
||||
ElMessage.warning('参数已变化,请先重新预估写入量')
|
||||
return
|
||||
@@ -212,7 +262,7 @@ const handleCreateTask = async () => {
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`预计写入 ${previewSummary.value.totalRowCount} 条数据,涉及 ${payload.lineIds.length} 个监测点,确认开始补数?`,
|
||||
`预计向 ${selectedStorageTypeName.value} 写入 ${previewSummary.value.totalRowCount} 条数据,涉及 ${payload.lineIds.length} 个监测点,确认开始补数?`,
|
||||
'开始补数',
|
||||
{
|
||||
type: 'warning',
|
||||
@@ -226,7 +276,8 @@ const handleCreateTask = async () => {
|
||||
|
||||
loading.create = true
|
||||
try {
|
||||
const response = await createAddDataTask(payload)
|
||||
const storageType = taskForm.storageType
|
||||
const response = await createAddDataTask(payload, storageType)
|
||||
const taskId = resolveText(response.data?.taskId)
|
||||
|
||||
taskStatus.value = normalizeTaskStatus({
|
||||
@@ -239,7 +290,7 @@ const handleCreateTask = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
startPolling(taskId)
|
||||
startPolling(taskId, storageType)
|
||||
ElMessage.success('补数任务已创建,正在轮询状态')
|
||||
} finally {
|
||||
loading.create = false
|
||||
@@ -255,7 +306,7 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTemplateList()
|
||||
await Promise.allSettled([loadStorageTypeOptions(), loadTemplateList()])
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -71,6 +71,20 @@ export const normalizeTaskStatus = (data?: AddData.TaskStatusResponse | null): A
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeStorageTypeOption = (item: AddData.StorageTypeItem): AddData.StorageTypeOption | null => {
|
||||
const code = resolveText(item.code).toUpperCase() as AddData.StorageType
|
||||
const name = resolveText(item.name, code)
|
||||
|
||||
if (!code) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeTemplateItem = (item: AddData.TemplateItem): AddData.NormalizedTemplateItem => {
|
||||
const decimalScale = resolveText(item.decimalScale)
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ export const buildTaskPayload = (form: AddData.TaskFormModel): AddData.TaskReque
|
||||
}
|
||||
}
|
||||
|
||||
export const buildPayloadSignature = (payload: AddData.TaskRequestParams) => {
|
||||
return JSON.stringify(payload)
|
||||
export const buildPayloadSignature = (payload: AddData.TaskRequestParams, storageType: AddData.StorageType) => {
|
||||
return JSON.stringify({
|
||||
storageType,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<section class="card ledger-form-card">
|
||||
<div class="form-header">
|
||||
<div>
|
||||
<div class="section-title">设备配置</div>
|
||||
<div class="section-title">装置配置</div>
|
||||
</div>
|
||||
<div v-if="!readonly" class="form-actions">
|
||||
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存设备</el-button>
|
||||
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存装置</el-button>
|
||||
<el-button type="danger" plain :icon="Delete" :disabled="!localForm.id" @click="emit('delete')">
|
||||
删除设备
|
||||
删除装置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,9 +23,6 @@
|
||||
<el-form-item label="装置名称" prop="name">
|
||||
<el-input v-model="localForm.name" maxlength="80" clearable placeholder="请输入装置名称" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!isSimpleMode" label="网络设备 ID" prop="ndid">
|
||||
<el-input v-model="localForm.ndid" maxlength="64" clearable placeholder="请输入网络设备 ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="装置网络参数" prop="mac">
|
||||
<el-input v-model="localForm.mac" maxlength="64" clearable placeholder="请输入装置网络参数" />
|
||||
</el-form-item>
|
||||
@@ -115,13 +112,15 @@ const localForm = reactive<AddLedger.EquipmentForm>({
|
||||
})
|
||||
|
||||
const syncLocalForm = (form: AddLedger.EquipmentForm) => {
|
||||
const networkParam = form.mac || form.ndid || ''
|
||||
|
||||
localForm.id = form.id || ''
|
||||
localForm.engineeringId = form.engineeringId || ''
|
||||
localForm.projectId = form.projectId || ''
|
||||
localForm.parentId = form.parentId || ''
|
||||
localForm.name = form.name || ''
|
||||
localForm.ndid = form.ndid || ''
|
||||
localForm.mac = form.mac || ''
|
||||
localForm.ndid = networkParam
|
||||
localForm.mac = networkParam
|
||||
localForm.dev_type = form.dev_type || ''
|
||||
localForm.dev_model = form.dev_model || ''
|
||||
localForm.dev_access_method = form.dev_access_method || ''
|
||||
@@ -147,7 +146,7 @@ watch(
|
||||
return
|
||||
}
|
||||
|
||||
emit('update:form', { ...value })
|
||||
emit('update:form', { ...value, ndid: value.mac || '' })
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -155,7 +154,6 @@ watch(
|
||||
const isSimpleMode = computed(() => props.mode === 'simple')
|
||||
const formRules = computed<FormRules<AddLedger.EquipmentForm>>(() => ({
|
||||
name: [{ required: true, message: '请输入装置名称', trigger: 'blur' }],
|
||||
...(isSimpleMode.value ? {} : { ndid: [{ required: true, message: '请输入网络设备 ID', trigger: 'blur' }] }),
|
||||
mac: [{ required: true, message: '请输入装置网络参数', trigger: 'blur' }],
|
||||
dev_model: [{ required: true, message: '请选择装置型号', trigger: 'change' }]
|
||||
}))
|
||||
|
||||
@@ -15,13 +15,19 @@
|
||||
v-if="ledgerContext.engineering || ledgerContext.projects.length"
|
||||
v-model="projectActiveTab"
|
||||
addable
|
||||
class="ledger-level-tabs"
|
||||
:class="[
|
||||
'ledger-level-tabs',
|
||||
{
|
||||
'is-add-disabled': !props.canAddProject,
|
||||
'project-add-disabled': !props.canAddProject
|
||||
}
|
||||
]"
|
||||
@tab-click="pane => handleContextTabClick(pane, 1)"
|
||||
@tab-add="emit('add-project')"
|
||||
@tab-add="handleAddProject"
|
||||
>
|
||||
<template #add-icon>
|
||||
<el-tooltip content="新增项目" placement="top">
|
||||
<el-icon class="ledger-tab-add-icon">
|
||||
<el-tooltip :content="props.canAddProject ? '新增项目' : props.addProjectDisabledReason" placement="top">
|
||||
<el-icon :class="['ledger-tab-add-icon', { 'is-disabled': !props.canAddProject }]">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -44,13 +50,22 @@
|
||||
v-if="ledgerContext.projects.length || ledgerContext.equipments.length"
|
||||
v-model="equipmentActiveTab"
|
||||
addable
|
||||
class="ledger-level-tabs"
|
||||
:class="[
|
||||
'ledger-level-tabs',
|
||||
{
|
||||
'is-add-disabled': !props.canAddEquipment,
|
||||
'equipment-add-disabled': !props.canAddEquipment
|
||||
}
|
||||
]"
|
||||
@tab-click="pane => handleContextTabClick(pane, 2)"
|
||||
@tab-add="emit('add-equipment')"
|
||||
@tab-add="handleAddEquipment"
|
||||
>
|
||||
<template #add-icon>
|
||||
<el-tooltip content="新增设备" placement="top">
|
||||
<el-icon class="ledger-tab-add-icon">
|
||||
<el-tooltip
|
||||
:content="props.canAddEquipment ? '新增设备' : props.addEquipmentDisabledReason"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon :class="['ledger-tab-add-icon', { 'is-disabled': !props.canAddEquipment }]">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -83,13 +98,19 @@
|
||||
v-if="ledgerContext.equipments.length || ledgerContext.lines.length"
|
||||
v-model="lineActiveTab"
|
||||
addable
|
||||
class="ledger-level-tabs"
|
||||
:class="[
|
||||
'ledger-level-tabs',
|
||||
{
|
||||
'is-add-disabled': !props.canAddLine,
|
||||
'line-add-disabled': !props.canAddLine
|
||||
}
|
||||
]"
|
||||
@tab-click="pane => handleContextTabClick(pane, 3)"
|
||||
@tab-add="emit('add-line')"
|
||||
@tab-add="handleAddLine"
|
||||
>
|
||||
<template #add-icon>
|
||||
<el-tooltip content="新增测点" placement="top">
|
||||
<el-icon class="ledger-tab-add-icon">
|
||||
<el-tooltip :content="props.canAddLine ? '新增测点' : props.addLineDisabledReason" placement="top">
|
||||
<el-icon :class="['ledger-tab-add-icon', { 'is-disabled': !props.canAddLine }]">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -172,6 +193,12 @@ const props = defineProps<{
|
||||
deviceModelOptions: AddLedger.SelectOption[]
|
||||
lineNoOptions: AddLedger.SelectOption<number>[]
|
||||
allLineNoOptions: AddLedger.SelectOption<number>[]
|
||||
canAddProject: boolean
|
||||
canAddEquipment: boolean
|
||||
canAddLine: boolean
|
||||
addProjectDisabledReason: string
|
||||
addEquipmentDisabledReason: string
|
||||
addLineDisabledReason: string
|
||||
mode: 'simple' | 'full'
|
||||
}>()
|
||||
|
||||
@@ -263,6 +290,24 @@ const handleContextTabClick = (pane: TabsPaneContext, level: AddLedger.NodeLevel
|
||||
emit('tab-click', id, level)
|
||||
}
|
||||
|
||||
const handleAddProject = () => {
|
||||
if (!props.canAddProject) return
|
||||
|
||||
emit('add-project')
|
||||
}
|
||||
|
||||
const handleAddEquipment = () => {
|
||||
if (!props.canAddEquipment) return
|
||||
|
||||
emit('add-equipment')
|
||||
}
|
||||
|
||||
const handleAddLine = () => {
|
||||
if (!props.canAddLine) return
|
||||
|
||||
emit('add-line')
|
||||
}
|
||||
|
||||
const validateActiveForm = async (level: AddLedger.NodeLevel | null) => {
|
||||
if (level === 0) return Boolean(await engineeringFormRef.value?.validateForm())
|
||||
if (level === 1) return Boolean(await projectFormRef.value?.validateForm())
|
||||
@@ -320,10 +365,22 @@ defineExpose({
|
||||
border-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
.ledger-level-tabs.is-add-disabled :deep(.el-tabs__new-tab),
|
||||
.ledger-level-tabs.is-add-disabled :deep(.el-tabs__new-tab:hover) {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color);
|
||||
}
|
||||
|
||||
.ledger-tab-add-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ledger-tab-add-icon.is-disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
}
|
||||
|
||||
.ledger-level-tabs :deep(.el-tabs__content) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:rules="formRules"
|
||||
:disabled="readonly"
|
||||
label-width="146px"
|
||||
:class="['ledger-form', isSimpleMode ? 'form-simple' : 'form-three']"
|
||||
:class="['ledger-form', 'ledger-line-form', isSimpleMode ? 'form-simple' : 'form-three']"
|
||||
>
|
||||
<el-form-item label="监测点名" prop="name">
|
||||
<el-input v-model="localForm.name" maxlength="80" clearable placeholder="请输入监测点名" />
|
||||
|
||||
@@ -43,17 +43,24 @@
|
||||
}
|
||||
|
||||
.ledger-form :deep(.el-form-item) {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ledger-form :deep(.el-form-item__label) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.ledger-form :deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
@@ -64,6 +71,11 @@
|
||||
gap: 5px 14px;
|
||||
}
|
||||
|
||||
.ledger-form.ledger-line-form {
|
||||
grid-auto-rows: minmax(32px, auto);
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
.ledger-form.form-three :deep(.el-form-item),
|
||||
.ledger-form.form-simple :deep(.el-form-item) {
|
||||
width: 100%;
|
||||
@@ -82,6 +94,10 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ledger-form :deep(.form-item-wide > .el-form-item__content) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ratio-input-group {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -95,6 +111,11 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ledger-line-form .ratio-input-group :deep(.ratio-field) {
|
||||
min-height: 32px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ratio-input-group :deep(.ratio-field .el-form-item__content) {
|
||||
display: block;
|
||||
line-height: 32px;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/* eslint-env node */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pageDir = path.resolve(currentDir, '..')
|
||||
|
||||
const indexSource = fs.readFileSync(path.join(pageDir, 'index.vue'), 'utf8')
|
||||
const contextPanelSource = fs.readFileSync(path.join(pageDir, 'components', 'LedgerContextPanel.vue'), 'utf8')
|
||||
|
||||
const expectations = [
|
||||
[
|
||||
'index passes add availability props',
|
||||
/:can-add-project="canAddProject"/.test(indexSource) &&
|
||||
/:can-add-equipment="canAddEquipment"/.test(indexSource) &&
|
||||
/:can-add-line="canAddLine"/.test(indexSource) &&
|
||||
/:add-project-disabled-reason="addProjectDisabledReason"/.test(indexSource) &&
|
||||
/:add-equipment-disabled-reason="addEquipmentDisabledReason"/.test(indexSource) &&
|
||||
/:add-line-disabled-reason="addLineDisabledReason"/.test(indexSource)
|
||||
],
|
||||
[
|
||||
'index computes add availability from saved parents',
|
||||
/const resolveEngineeringIdForAddProject = \(\) =>/.test(indexSource) &&
|
||||
/const resolveProjectContextForAddEquipment = \(\) =>/.test(indexSource) &&
|
||||
/const resolveDeviceIdForAddLine = \(\) =>/.test(indexSource) &&
|
||||
/const canAddProject = computed\(\(\) => Boolean\(resolveEngineeringIdForAddProject\(\)\)\)/.test(indexSource) &&
|
||||
/const canAddEquipment = computed\(\(\) => Boolean\(resolveProjectContextForAddEquipment\(\)\.projectId\)\)/.test(
|
||||
indexSource
|
||||
) &&
|
||||
/const canAddLine = computed\(\(\) => Boolean\(resolveDeviceIdForAddLine\(\)\)\)/.test(indexSource)
|
||||
],
|
||||
[
|
||||
'context panel declares availability props',
|
||||
/canAddProject: boolean/.test(contextPanelSource) &&
|
||||
/canAddEquipment: boolean/.test(contextPanelSource) &&
|
||||
/canAddLine: boolean/.test(contextPanelSource) &&
|
||||
/addProjectDisabledReason: string/.test(contextPanelSource) &&
|
||||
/addEquipmentDisabledReason: string/.test(contextPanelSource) &&
|
||||
/addLineDisabledReason: string/.test(contextPanelSource)
|
||||
],
|
||||
[
|
||||
'context panel blocks disabled add events',
|
||||
/const handleAddProject = \(\) => \{[\s\S]*if \(!props\.canAddProject\) return[\s\S]*emit\('add-project'\)/.test(
|
||||
contextPanelSource
|
||||
) &&
|
||||
/const handleAddEquipment = \(\) => \{[\s\S]*if \(!props\.canAddEquipment\) return[\s\S]*emit\('add-equipment'\)/.test(
|
||||
contextPanelSource
|
||||
) &&
|
||||
/const handleAddLine = \(\) => \{[\s\S]*if \(!props\.canAddLine\) return[\s\S]*emit\('add-line'\)/.test(
|
||||
contextPanelSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'context panel marks disabled add controls',
|
||||
/project-add-disabled/.test(contextPanelSource) &&
|
||||
/equipment-add-disabled/.test(contextPanelSource) &&
|
||||
/line-add-disabled/.test(contextPanelSource) &&
|
||||
/ledger-tab-add-icon/.test(contextPanelSource) &&
|
||||
/'is-disabled':\s*!props\.canAddProject/.test(contextPanelSource) &&
|
||||
/'is-disabled':\s*!props\.canAddEquipment/.test(contextPanelSource) &&
|
||||
/'is-disabled':\s*!props\.canAddLine/.test(contextPanelSource) &&
|
||||
/\.ledger-level-tabs\.is-add-disabled/.test(contextPanelSource)
|
||||
]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, matched]) => !matched)
|
||||
|
||||
if (failures.length) {
|
||||
console.error('addLedger add button availability contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('addLedger add button availability contract check passed')
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-env node */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pageDir = path.resolve(currentDir, '..')
|
||||
|
||||
const indexSource = fs.readFileSync(path.join(pageDir, 'index.vue'), 'utf8')
|
||||
|
||||
const expectations = [
|
||||
[
|
||||
'add equipment resolves first device type option as default',
|
||||
/const resolveDefaultDeviceType = \(\) => \{[\s\S]*const defaultDeviceType = deviceTypeOptions\.value\[0\]\?\.value[\s\S]*return defaultDeviceType === undefined \|\| defaultDeviceType === null \? '' : String\(defaultDeviceType\)[\s\S]*\}/.test(
|
||||
indexSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'add equipment applies default device type to draft form',
|
||||
/equipmentForm\.value = \{[\s\S]*\.\.\.createEmptyEquipmentForm\(projectId,\s*engineeringId\),[\s\S]*dev_type:\s*resolveDefaultDeviceType\(\)[\s\S]*\}/.test(
|
||||
indexSource
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, matched]) => !matched)
|
||||
|
||||
if (failures.length) {
|
||||
console.error('addLedger equipment default contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('addLedger equipment default contract check passed')
|
||||
@@ -0,0 +1,56 @@
|
||||
/* eslint-env node */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pageDir = path.resolve(currentDir, '..')
|
||||
|
||||
const equipmentFormSource = fs.readFileSync(path.join(pageDir, 'components', 'EquipmentForm.vue'), 'utf8')
|
||||
const ledgerDataSource = fs.readFileSync(path.join(pageDir, 'utils', 'ledgerData.ts'), 'utf8')
|
||||
|
||||
const expectations = [
|
||||
[
|
||||
'equipment form labels equipment as device unit',
|
||||
/<div class="section-title">装置配置<\/div>/.test(equipmentFormSource) &&
|
||||
/保存装置/.test(equipmentFormSource) &&
|
||||
/删除装置/.test(equipmentFormSource) &&
|
||||
/label="装置名称"[\s\S]*placeholder="请输入装置名称"/.test(equipmentFormSource) &&
|
||||
/message:\s*'请输入装置名称'/.test(equipmentFormSource)
|
||||
],
|
||||
[
|
||||
'equipment form hides independent network device id input',
|
||||
!/label="网络设备 ID"/.test(equipmentFormSource) &&
|
||||
!/placeholder="请输入网络设备 ID"/.test(equipmentFormSource) &&
|
||||
!/ndid:\s*\[\{ required:\s*true/.test(equipmentFormSource)
|
||||
],
|
||||
[
|
||||
'equipment form emits ndid from network param',
|
||||
/const networkParam = form\.mac \|\| form\.ndid \|\| ''/.test(equipmentFormSource) &&
|
||||
/localForm\.ndid = networkParam/.test(equipmentFormSource) &&
|
||||
/localForm\.mac = networkParam/.test(equipmentFormSource) &&
|
||||
/emit\('update:form', \{ \.\.\.value, ndid: value\.mac \|\| '' \}\)/.test(equipmentFormSource)
|
||||
],
|
||||
[
|
||||
'equipment detail normalizes name and network param from compatible backend fields',
|
||||
/const equipmentName = resolveString\(data, 'name', 'dev_name', 'devName', 'deviceName'\) \|\| node\?\.name \|\| ''/.test(
|
||||
ledgerDataSource
|
||||
) &&
|
||||
/const networkParam = resolveString\(data, 'mac', 'ndid', 'unnid'\)/.test(ledgerDataSource) &&
|
||||
/name:\s*equipmentName/.test(ledgerDataSource) &&
|
||||
/ndid:\s*networkParam/.test(ledgerDataSource) &&
|
||||
/mac:\s*networkParam/.test(ledgerDataSource)
|
||||
]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, matched]) => !matched)
|
||||
|
||||
if (failures.length) {
|
||||
console.error('addLedger equipment network param contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('addLedger equipment network param contract check passed')
|
||||
@@ -0,0 +1,71 @@
|
||||
/* eslint-env node */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pageDir = path.resolve(currentDir, '..')
|
||||
|
||||
const formStyleSource = fs.readFileSync(path.join(pageDir, 'components', 'ledgerForm.scss'), 'utf8')
|
||||
const lineFormSource = fs.readFileSync(path.join(pageDir, 'components', 'LineForm.vue'), 'utf8')
|
||||
|
||||
const expectations = [
|
||||
[
|
||||
'form item uses a fixed single-line alignment baseline',
|
||||
/\.ledger-form :deep\(\.el-form-item\) \{[\s\S]*display:\s*flex;[\s\S]*align-items:\s*flex-start;[\s\S]*min-height:\s*32px;[\s\S]*\}/.test(
|
||||
formStyleSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'form label has explicit 32px height',
|
||||
/\.ledger-form :deep\(\.el-form-item__label\) \{[\s\S]*height:\s*32px;[\s\S]*line-height:\s*32px;[\s\S]*\}/.test(
|
||||
formStyleSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'form content centers single-line controls on the 32px baseline',
|
||||
/\.ledger-form :deep\(\.el-form-item__content\) \{[\s\S]*display:\s*flex;[\s\S]*align-items:\s*center;[\s\S]*min-height:\s*32px;[\s\S]*line-height:\s*32px;[\s\S]*\}/.test(
|
||||
formStyleSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'textarea keeps top alignment instead of inheriting centered single-line controls',
|
||||
/\.ledger-form :deep\(\.form-item-wide > \.el-form-item__content\) \{[\s\S]*align-items:\s*flex-start;[\s\S]*\}/.test(
|
||||
formStyleSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'ratio fields keep block layout for nested validation items',
|
||||
/\.ratio-input-group :deep\(\.ratio-field \.el-form-item__content\) \{[\s\S]*display:\s*block;[\s\S]*\}/.test(
|
||||
formStyleSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'line form has a dedicated class for monitor point spacing',
|
||||
/'ledger-line-form'/.test(lineFormSource)
|
||||
],
|
||||
[
|
||||
'line form uses explicit grid row sizing and row gap',
|
||||
/\.ledger-form\.ledger-line-form \{[\s\S]*grid-auto-rows:\s*minmax\(32px,\s*auto\);[\s\S]*row-gap:\s*2px;[\s\S]*\}/.test(
|
||||
formStyleSource
|
||||
)
|
||||
],
|
||||
[
|
||||
'line form ratio field spacing is normalized',
|
||||
/\.ledger-line-form \.ratio-input-group :deep\(\.ratio-field\) \{[\s\S]*min-height:\s*32px;[\s\S]*margin-bottom:\s*0;[\s\S]*\}/.test(
|
||||
formStyleSource
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, matched]) => !matched)
|
||||
|
||||
if (failures.length) {
|
||||
console.error('addLedger form alignment contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('addLedger form alignment contract check passed')
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-env node */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pageDir = path.resolve(currentDir, '..')
|
||||
const srcDir = path.resolve(pageDir, '..', '..', '..')
|
||||
|
||||
const constantsSource = fs.readFileSync(path.join(srcDir, 'constants', 'dictCodes.ts'), 'utf8')
|
||||
const indexSource = fs.readFileSync(path.join(pageDir, 'index.vue'), 'utf8')
|
||||
const equipmentFormSource = fs.readFileSync(path.join(pageDir, 'components', 'EquipmentForm.vue'), 'utf8')
|
||||
|
||||
const expectations = [
|
||||
['dict code defines terminal model', /LEDGER_TERMINAL_MODEL:\s*'Dev_Type'/.test(constantsSource)],
|
||||
[
|
||||
'addLedger uses terminal model dict for equipment model options',
|
||||
/typeof DICT_CODES\.LEDGER_TERMINAL_MODEL/.test(indexSource) &&
|
||||
/resolveDictOptions\(DICT_CODES\.LEDGER_TERMINAL_MODEL,\s*fallbackDeviceModelOptions\)/.test(indexSource) &&
|
||||
/ensureLedgerDictOptionById\(DICT_CODES\.LEDGER_TERMINAL_MODEL,\s*form\.dev_model\s*\|\|\s*''\)/.test(
|
||||
indexSource
|
||||
) &&
|
||||
/loadLedgerDictOptionsByCode\(DICT_CODES\.LEDGER_TERMINAL_MODEL\)/.test(indexSource)
|
||||
],
|
||||
[
|
||||
'equipment form labels terminal dict as device model',
|
||||
/label="装置型号"[\s\S]*placeholder="请选择装置型号"/.test(equipmentFormSource) &&
|
||||
/dev_model:\s*\[\{ required:\s*true,\s*message:\s*'请选择装置型号'/.test(equipmentFormSource)
|
||||
]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, matched]) => !matched)
|
||||
|
||||
if (failures.length) {
|
||||
console.error('addLedger terminal model contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('addLedger terminal model contract check passed')
|
||||
@@ -43,6 +43,12 @@
|
||||
:device-model-options="deviceModelOptions"
|
||||
:line-no-options="lineNoOptions"
|
||||
:all-line-no-options="allLineNoOptions"
|
||||
:can-add-project="canAddProject"
|
||||
:can-add-equipment="canAddEquipment"
|
||||
:can-add-line="canAddLine"
|
||||
:add-project-disabled-reason="addProjectDisabledReason"
|
||||
:add-equipment-disabled-reason="addEquipmentDisabledReason"
|
||||
:add-line-disabled-reason="addLineDisabledReason"
|
||||
:mode="ledgerFormMode"
|
||||
@save-engineering="handleSaveEngineering"
|
||||
@save-project="handleSaveProject"
|
||||
@@ -100,7 +106,6 @@ import {
|
||||
createEmptyLineForm,
|
||||
createEmptyProjectForm,
|
||||
findNodePath,
|
||||
generateGuidText,
|
||||
normalizeEngineeringDetail,
|
||||
normalizeEquipmentDetail,
|
||||
normalizeLineDetail,
|
||||
@@ -127,7 +132,7 @@ type LedgerContextItem<T> = {
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
type LedgerDictCode = typeof DICT_CODES.LEDGER_DEVICE_TYPE | typeof DICT_CODES.LEDGER_DEVICE_MODEL
|
||||
type LedgerDictCode = typeof DICT_CODES.LEDGER_DEVICE_TYPE | typeof DICT_CODES.LEDGER_TERMINAL_MODEL
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const treeData = ref<AddLedger.NormalizedTreeNode[]>([])
|
||||
@@ -171,7 +176,7 @@ const activeTabIds = reactive({
|
||||
})
|
||||
const ledgerDictOptions = reactive<Record<LedgerDictCode, AddLedger.SelectOption[]>>({
|
||||
[DICT_CODES.LEDGER_DEVICE_TYPE]: [],
|
||||
[DICT_CODES.LEDGER_DEVICE_MODEL]: []
|
||||
[DICT_CODES.LEDGER_TERMINAL_MODEL]: []
|
||||
})
|
||||
|
||||
let detailRequestSeq = 0
|
||||
@@ -188,11 +193,19 @@ const fallbackDeviceModelOptions: AddLedger.SelectOption[] = [
|
||||
]
|
||||
|
||||
const deviceTypeOptions = computed(() => resolveDictOptions(DICT_CODES.LEDGER_DEVICE_TYPE, fallbackDeviceTypeOptions))
|
||||
const deviceModelOptions = computed(() => resolveDictOptions(DICT_CODES.LEDGER_DEVICE_MODEL, fallbackDeviceModelOptions))
|
||||
const deviceModelOptions = computed(() =>
|
||||
resolveDictOptions(DICT_CODES.LEDGER_TERMINAL_MODEL, fallbackDeviceModelOptions)
|
||||
)
|
||||
const emptyStateText = computed(() =>
|
||||
treeData.value.length === 0 ? '台账树为空,请先新增一个工程。' : '从左侧台账树选择工程、项目、设备或监测点。'
|
||||
)
|
||||
|
||||
const resolveDefaultDeviceType = () => {
|
||||
const defaultDeviceType = deviceTypeOptions.value[0]?.value
|
||||
|
||||
return defaultDeviceType === undefined || defaultDeviceType === null ? '' : String(defaultDeviceType)
|
||||
}
|
||||
|
||||
const resolveFallbackDictOptions = (code: LedgerDictCode) =>
|
||||
code === DICT_CODES.LEDGER_DEVICE_TYPE ? fallbackDeviceTypeOptions : fallbackDeviceModelOptions
|
||||
|
||||
@@ -238,7 +251,7 @@ const ensureLedgerDictOptionById = async (code: LedgerDictCode, value: string) =
|
||||
const ensureEquipmentDictOptions = async (form: AddLedger.EquipmentForm) => {
|
||||
await Promise.all([
|
||||
ensureLedgerDictOptionById(DICT_CODES.LEDGER_DEVICE_TYPE, form.dev_type || ''),
|
||||
ensureLedgerDictOptionById(DICT_CODES.LEDGER_DEVICE_MODEL, form.dev_model || '')
|
||||
ensureLedgerDictOptionById(DICT_CODES.LEDGER_TERMINAL_MODEL, form.dev_model || '')
|
||||
])
|
||||
}
|
||||
|
||||
@@ -254,7 +267,7 @@ const loadLedgerDictOptionsByCode = (code: LedgerDictCode) => {
|
||||
|
||||
const loadLedgerDictOptions = async () => {
|
||||
loadLedgerDictOptionsByCode(DICT_CODES.LEDGER_DEVICE_TYPE)
|
||||
loadLedgerDictOptionsByCode(DICT_CODES.LEDGER_DEVICE_MODEL)
|
||||
loadLedgerDictOptionsByCode(DICT_CODES.LEDGER_TERMINAL_MODEL)
|
||||
}
|
||||
|
||||
const getCurrentPath = () => {
|
||||
@@ -268,7 +281,52 @@ const resolveContext = () => {
|
||||
return resolveContextFromPath(path)
|
||||
}
|
||||
|
||||
const resolveEngineeringIdForAddProject = () => {
|
||||
const context = resolveContext()
|
||||
const engineeringIdFromContext = resolveSavedContextItemId(ledgerContext.engineering)
|
||||
|
||||
return activeLevel.value === 0
|
||||
? engineeringForm.value.id || selectedNode.value?.id || ''
|
||||
: context.engineeringId || engineeringIdFromContext
|
||||
}
|
||||
|
||||
const resolveProjectContextForAddEquipment = () => {
|
||||
const context = resolveContext()
|
||||
const activeProjectItem = ledgerContext.projects.find(item => item.id === activeTabIds.project)
|
||||
const projectIdFromTab = resolveSavedContextItemId(activeProjectItem)
|
||||
const projectId =
|
||||
activeLevel.value === 1
|
||||
? projectForm.value.id || selectedNode.value?.id || ''
|
||||
: context.projectId || projectIdFromTab
|
||||
const engineeringId =
|
||||
projectForm.value.engineeringId ||
|
||||
context.engineeringId ||
|
||||
activeProjectItem?.form.engineeringId ||
|
||||
resolveSavedContextItemId(ledgerContext.engineering)
|
||||
|
||||
return {
|
||||
projectId,
|
||||
engineeringId
|
||||
}
|
||||
}
|
||||
|
||||
const resolveDeviceIdForAddLine = () => {
|
||||
const context = resolveContext()
|
||||
const activeEquipmentItem = ledgerContext.equipments.find(item => item.id === activeTabIds.equipment)
|
||||
const deviceIdFromTab = resolveSavedContextItemId(activeEquipmentItem)
|
||||
|
||||
return activeLevel.value === 2
|
||||
? equipmentForm.value.id || selectedNode.value?.id || ''
|
||||
: context.deviceId || deviceIdFromTab
|
||||
}
|
||||
|
||||
const allLineNoOptions = computed(() => buildLineNoOptions(Array.from({ length: 20 }, (_item, index) => index + 1)))
|
||||
const canAddProject = computed(() => Boolean(resolveEngineeringIdForAddProject()))
|
||||
const canAddEquipment = computed(() => Boolean(resolveProjectContextForAddEquipment().projectId))
|
||||
const canAddLine = computed(() => Boolean(resolveDeviceIdForAddLine()))
|
||||
const addProjectDisabledReason = computed(() => (canAddProject.value ? '' : '请先选择或保存父级工程'))
|
||||
const addEquipmentDisabledReason = computed(() => (canAddEquipment.value ? '' : '请先选择或保存父级项目'))
|
||||
const addLineDisabledReason = computed(() => (canAddLine.value ? '' : '请先选择或保存父级设备'))
|
||||
|
||||
const loadAvailableLineNoOptions = async (deviceId: string, lineId = '', currentLineNo?: number) => {
|
||||
if (!deviceId) {
|
||||
@@ -637,15 +695,10 @@ const handleAddEngineering = () => {
|
||||
}
|
||||
|
||||
const handleAddProject = () => {
|
||||
const context = resolveContext()
|
||||
const engineeringIdFromContext = resolveSavedContextItemId(ledgerContext.engineering)
|
||||
const engineeringId =
|
||||
activeLevel.value === 0
|
||||
? engineeringForm.value.id || selectedNode.value?.id || ''
|
||||
: context.engineeringId || engineeringIdFromContext
|
||||
const engineeringId = resolveEngineeringIdForAddProject()
|
||||
|
||||
if (!engineeringId) {
|
||||
ElMessage.warning('请先选择或保存父级工程')
|
||||
ElMessage.warning(addProjectDisabledReason.value)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -662,27 +715,19 @@ const handleAddProject = () => {
|
||||
}
|
||||
|
||||
const handleAddEquipment = () => {
|
||||
const context = resolveContext()
|
||||
const activeProjectItem = ledgerContext.projects.find(item => item.id === activeTabIds.project)
|
||||
const projectIdFromTab = resolveSavedContextItemId(activeProjectItem)
|
||||
const projectId =
|
||||
activeLevel.value === 1
|
||||
? projectForm.value.id || selectedNode.value?.id || ''
|
||||
: context.projectId || projectIdFromTab
|
||||
const engineeringId =
|
||||
projectForm.value.engineeringId ||
|
||||
context.engineeringId ||
|
||||
activeProjectItem?.form.engineeringId ||
|
||||
resolveSavedContextItemId(ledgerContext.engineering)
|
||||
const { projectId, engineeringId } = resolveProjectContextForAddEquipment()
|
||||
|
||||
if (!projectId) {
|
||||
ElMessage.warning('请先选择或保存父级项目')
|
||||
ElMessage.warning(addEquipmentDisabledReason.value)
|
||||
return
|
||||
}
|
||||
|
||||
selectedNode.value = null
|
||||
activeLevel.value = 2
|
||||
equipmentForm.value = createEmptyEquipmentForm(projectId, engineeringId)
|
||||
equipmentForm.value = {
|
||||
...createEmptyEquipmentForm(projectId, engineeringId),
|
||||
dev_type: resolveDefaultDeviceType()
|
||||
}
|
||||
ledgerContext.equipments = [
|
||||
...ledgerContext.equipments.filter(item => !item.draft),
|
||||
createDraftContextItem(draftIds.equipment, '新增设备', 2, equipmentForm.value)
|
||||
@@ -692,16 +737,10 @@ const handleAddEquipment = () => {
|
||||
}
|
||||
|
||||
const handleAddLine = async () => {
|
||||
const context = resolveContext()
|
||||
const activeEquipmentItem = ledgerContext.equipments.find(item => item.id === activeTabIds.equipment)
|
||||
const deviceIdFromTab = resolveSavedContextItemId(activeEquipmentItem)
|
||||
const deviceId =
|
||||
activeLevel.value === 2
|
||||
? equipmentForm.value.id || selectedNode.value?.id || ''
|
||||
: context.deviceId || deviceIdFromTab
|
||||
const deviceId = resolveDeviceIdForAddLine()
|
||||
|
||||
if (!deviceId) {
|
||||
ElMessage.warning('请先选择或保存父级设备')
|
||||
ElMessage.warning(addLineDisabledReason.value)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -797,11 +836,6 @@ const handleSaveLine = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!lineForm.value.line_id) {
|
||||
// 新增测点必须带 32 位 line_id,后端仍负责最终唯一性校验。
|
||||
lineForm.value.line_id = generateGuidText()
|
||||
}
|
||||
|
||||
loading.saving = true
|
||||
try {
|
||||
const response = await saveAddLedgerLine(lineForm.value)
|
||||
|
||||
@@ -52,7 +52,7 @@ export function createEmptyEquipmentForm(parentProjectId = '', parentEngineering
|
||||
export function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
||||
return {
|
||||
id: '',
|
||||
line_id: generateGuidText(),
|
||||
line_id: '',
|
||||
deviceId: parentDeviceId,
|
||||
parentId: parentDeviceId,
|
||||
name: '',
|
||||
@@ -184,15 +184,17 @@ export const normalizeEquipmentDetail = (
|
||||
context: LedgerContextIds = { engineeringId: '', projectId: '', deviceId: '' }
|
||||
): AddLedger.EquipmentForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
const equipmentName = resolveString(data, 'name', 'dev_name', 'devName', 'deviceName') || node?.name || ''
|
||||
const networkParam = resolveString(data, 'mac', 'ndid', 'unnid')
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id', 'equipmentId') || node?.id || '',
|
||||
engineeringId: resolveString(data, 'engineeringId', 'associated_engineering') || context.engineeringId,
|
||||
projectId: resolveString(data, 'projectId', 'associated_project') || context.projectId,
|
||||
parentId: context.projectId,
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
ndid: resolveString(data, 'ndid'),
|
||||
mac: resolveString(data, 'mac'),
|
||||
name: equipmentName,
|
||||
ndid: networkParam,
|
||||
mac: networkParam,
|
||||
dev_type: resolveString(data, 'dev_type', 'devType'),
|
||||
dev_model: resolveString(data, 'dev_model', 'devModel'),
|
||||
dev_access_method: resolveString(data, 'dev_access_method', 'devAccessMethod'),
|
||||
|
||||
Reference in New Issue
Block a user