添加产品管理、物模型配置、产品分类界面,完善设备管理的物模型配置界面
This commit is contained in:
+10
-10
@@ -1,49 +1,49 @@
|
|||||||
{
|
{
|
||||||
"hash": "b6c330c5",
|
"hash": "25a37344",
|
||||||
"configHash": "e57f8d4c",
|
"configHash": "ec38f6fa",
|
||||||
"lockfileHash": "74076ca5",
|
"lockfileHash": "74076ca5",
|
||||||
"browserHash": "ed3d8378",
|
"browserHash": "8bfb7e71",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"@element-plus/icons-vue": {
|
"@element-plus/icons-vue": {
|
||||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||||
"file": "@element-plus_icons-vue.js",
|
"file": "@element-plus_icons-vue.js",
|
||||||
"fileHash": "de153aba",
|
"fileHash": "53709748",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "c19fdb3e",
|
"fileHash": "8bc11b32",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"echarts": {
|
"echarts": {
|
||||||
"src": "../../echarts/index.js",
|
"src": "../../echarts/index.js",
|
||||||
"file": "echarts.js",
|
"file": "echarts.js",
|
||||||
"fileHash": "a8d7111b",
|
"fileHash": "bf5b1683",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus": {
|
"element-plus": {
|
||||||
"src": "../../element-plus/es/index.mjs",
|
"src": "../../element-plus/es/index.mjs",
|
||||||
"file": "element-plus.js",
|
"file": "element-plus.js",
|
||||||
"fileHash": "10cb780f",
|
"fileHash": "6f7bcfbf",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"pinia": {
|
"pinia": {
|
||||||
"src": "../../pinia/dist/pinia.js",
|
"src": "../../pinia/dist/pinia.js",
|
||||||
"file": "pinia.js",
|
"file": "pinia.js",
|
||||||
"fileHash": "82d3c424",
|
"fileHash": "c89feaca",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"src": "../../vue-router/dist/vue-router.js",
|
"src": "../../vue-router/dist/vue-router.js",
|
||||||
"file": "vue-router.js",
|
"file": "vue-router.js",
|
||||||
"fileHash": "1b68fa4e",
|
"fileHash": "a9a2c9fa",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||||
"file": "vue.js",
|
"file": "vue.js",
|
||||||
"fileHash": "7e296374",
|
"fileHash": "53d5f754",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// 告警规则列表
|
// 告警规则列表(全局,不含产品/设备归属规则)
|
||||||
export function getAlertRuleList() {
|
export function getAlertRuleList() {
|
||||||
return request.get('/inspection/alert-rule/list')
|
return request.get('/inspection/alert-rule/list')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 某归属对象(产品/设备)下的规则列表;pointCode 可选按点过滤
|
||||||
|
export function getOwnerAlertRules(ownerType, ownerId, pointCode) {
|
||||||
|
return request.get('/inspection/alert-rule/owner', {
|
||||||
|
params: { ownerType, ownerId, pointCode: pointCode || undefined }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 新增告警规则
|
// 新增告警规则
|
||||||
export function addAlertRule(data) {
|
export function addAlertRule(data) {
|
||||||
return request.post('/inspection/alert-rule/add', data)
|
return request.post('/inspection/alert-rule/add', data)
|
||||||
|
|||||||
@@ -45,3 +45,6 @@ export const deleteDevice = (id) => request.delete(`${base}/${id}`)
|
|||||||
|
|
||||||
// 设备日志(分页;结果数组上附带 total)
|
// 设备日志(分页;结果数组上附带 total)
|
||||||
export const getDeviceLogs = (id, params) => request.get(`${base}/${id}/logs`, { params })
|
export const getDeviceLogs = (id, params) => request.get(`${base}/${id}/logs`, { params })
|
||||||
|
|
||||||
|
// 按物模型点下发指令:{ PointId, Value, IsSimulated }
|
||||||
|
export const sendDeviceCommand = (id, data) => request.post(`${base}/${id}/command`, data)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
|
|
||||||
<!-- 查找设备输入框 -->
|
<!-- 查找设备输入框 -->
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
|
<el-select v-model="searchProductId" placeholder="所属产品(全部)" clearable style="width: 200px" @change="onSearch">
|
||||||
|
<el-option v-for="p in productOptions" :key="p.Id" :label="`${p.Model}${p.Name ? ' - ' + p.Name : ''}`" :value="p.Id" />
|
||||||
|
</el-select>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="请输入设备编号 / 名称 / 类型查找设备"
|
placeholder="请输入设备编号 / 名称 / 类型查找设备"
|
||||||
@@ -39,6 +42,12 @@
|
|||||||
<el-table-column prop="Code" label="设备编号" width="130" />
|
<el-table-column prop="Code" label="设备编号" width="130" />
|
||||||
<el-table-column prop="Name" label="设备名称" min-width="140" show-overflow-tooltip />
|
<el-table-column prop="Name" label="设备名称" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column prop="DeviceType" label="设备类型" width="110" />
|
<el-table-column prop="DeviceType" label="设备类型" width="110" />
|
||||||
|
<el-table-column label="所属产品" width="130" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.ProductName" size="small" type="info">{{ row.ProductName }}</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="GatewayCode" label="所属网关" width="130" show-overflow-tooltip />
|
<el-table-column prop="GatewayCode" label="所属网关" width="130" show-overflow-tooltip />
|
||||||
<el-table-column prop="SlaveId" label="从站地址" width="90" align="center" />
|
<el-table-column prop="SlaveId" label="从站地址" width="90" align="center" />
|
||||||
<el-table-column label="协议" width="110">
|
<el-table-column label="协议" width="110">
|
||||||
@@ -54,11 +63,21 @@
|
|||||||
<el-table-column label="最后采集" width="160">
|
<el-table-column label="最后采集" width="160">
|
||||||
<template #default="{ row }">{{ fmtTime(row.LastCollectTime) }}</template>
|
<template #default="{ row }">{{ fmtTime(row.LastCollectTime) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click.stop="openLogs(row)">日志</el-button>
|
<el-button link type="primary" @click.stop="openDetail(row)">详情</el-button>
|
||||||
<el-button link type="primary" @click.stop="openEdit(row)">编辑</el-button>
|
<el-button link type="primary" @click.stop="openEdit(row)">编辑</el-button>
|
||||||
<el-button link type="danger" @click.stop="onDelete(row)">删除</el-button>
|
<el-dropdown trigger="click" @command="(cmd) => onMoreCommand(cmd, row)">
|
||||||
|
<el-button link type="primary" @click.stop>
|
||||||
|
更多<el-icon class="more-arrow"><ArrowDown /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="logs"><el-icon><Document /></el-icon>设备日志</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="delete" divided class="more-danger"><el-icon><Delete /></el-icon>删除设备</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -70,9 +89,15 @@
|
|||||||
:style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px' }"
|
:style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px' }"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
|
<div class="ctx-menu__item" @click="ctxDetail">
|
||||||
|
<el-icon><View /></el-icon><span>设备详情</span>
|
||||||
|
</div>
|
||||||
<div class="ctx-menu__item" @click="ctxEdit">
|
<div class="ctx-menu__item" @click="ctxEdit">
|
||||||
<el-icon><Edit /></el-icon><span>修改设备</span>
|
<el-icon><Edit /></el-icon><span>修改设备</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ctx-menu__item" @click="ctxLogs">
|
||||||
|
<el-icon><Document /></el-icon><span>设备日志</span>
|
||||||
|
</div>
|
||||||
<div class="ctx-menu__item danger" @click="ctxDelete">
|
<div class="ctx-menu__item danger" @click="ctxDelete">
|
||||||
<el-icon><Delete /></el-icon><span>删除设备</span>
|
<el-icon><Delete /></el-icon><span>删除设备</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +131,11 @@
|
|||||||
<el-option v-for="d in deviceTypes" :key="d.value" :label="d.label" :value="d.value" />
|
<el-option v-for="d in deviceTypes" :key="d.value" :label="d.label" :value="d.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="所属产品">
|
||||||
|
<el-select v-model="form.ProductId" placeholder="未分类" clearable style="width: 100%">
|
||||||
|
<el-option v-for="p in productOptions" :key="p.Id" :label="`${p.Model}${p.Name ? ' - ' + p.Name : ''}`" :value="p.Id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="所属网关" prop="GatewayCode">
|
<el-form-item label="所属网关" prop="GatewayCode">
|
||||||
<el-input v-model="form.GatewayCode" placeholder="如 GW-ENV-01(IP:端口或串口标识)" maxlength="64" />
|
<el-input v-model="form.GatewayCode" placeholder="如 GW-ENV-01(IP:端口或串口标识)" maxlength="64" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -133,6 +163,45 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 设备详情抽屉 -->
|
||||||
|
<el-drawer v-model="detailVisible" :title="`设备详情 - ${detailDevice?.Name || ''}`" size="720px">
|
||||||
|
<el-tabs v-if="detailDevice" v-model="detailTab">
|
||||||
|
<el-tab-pane label="基本信息" name="info">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="设备编号">{{ detailDevice.Code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备名称">{{ detailDevice.Name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备类型">{{ detailDevice.DeviceType }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="所属产品">{{ detailDevice.ProductName || '未分类' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="所属网关">{{ detailDevice.GatewayCode }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="从站地址">{{ detailDevice.SlaveId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="通讯协议">{{ protocolLabel(detailDevice.ProtocolType) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="在线状态">
|
||||||
|
<el-tag :type="onlineStatusTag(detailDevice.OnlineStatus)" size="small">{{ onlineStatusLabel(detailDevice.OnlineStatus) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="制造商">{{ detailDevice.Manufacturer || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后采集">{{ fmtTime(detailDevice.LastCollectTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最近错误" :span="2">{{ detailDevice.LastError || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ detailDevice.Remark || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="物模型点" name="points">
|
||||||
|
<ThingPointTable :owner-type="2" :owner-id="detailDevice.Id" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="告警" name="alarm">
|
||||||
|
<ThingAlarmPanel :owner-type="2" :owner-id="detailDevice.Id" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="下发指令" name="command">
|
||||||
|
<PointSendForm ref="pointSendRef" :device-id="detailDevice.Id" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="看板" name="board">
|
||||||
|
<div class="board-body">
|
||||||
|
<div class="board-chart" ref="boardChartRef"></div>
|
||||||
|
<el-empty v-if="!boardLoading && boardEmpty" description="该设备无采集数据(当前仅 BOX-xxx 模拟器设备有曲线)" :image-size="70" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
<!-- 设备日志抽屉 -->
|
<!-- 设备日志抽屉 -->
|
||||||
<el-drawer v-model="logVisible" :title="`设备日志 - ${logDevice?.Name || ''}`" size="520px">
|
<el-drawer v-model="logVisible" :title="`设备日志 - ${logDevice?.Name || ''}`" size="520px">
|
||||||
<div class="log-toolbar">
|
<div class="log-toolbar">
|
||||||
@@ -168,12 +237,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
import {
|
import {
|
||||||
getDeviceList, addDevice, updateDevice, deleteDevice, getDeviceLogs,
|
getDeviceList, addDevice, updateDevice, deleteDevice, getDeviceLogs,
|
||||||
protocolOptions, protocolLabel, onlineStatusLabel, onlineStatusTag
|
protocolOptions, protocolLabel, onlineStatusLabel, onlineStatusTag
|
||||||
} from '@/api/device'
|
} from '@/api/device'
|
||||||
|
import { getProductOptions } from '@/api/product'
|
||||||
|
import { getCurve } from '@/api/dashboard'
|
||||||
|
import ThingPointTable from '@/components/thing/ThingPointTable.vue'
|
||||||
|
import ThingAlarmPanel from '@/components/thing/ThingAlarmPanel.vue'
|
||||||
|
import PointSendForm from '@/components/thing/PointSendForm.vue'
|
||||||
|
|
||||||
// ---------- 列表 ----------
|
// ---------- 列表 ----------
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -182,6 +257,16 @@ const total = ref(0)
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
|
const searchProductId = ref('')
|
||||||
|
const productOptions = ref([])
|
||||||
|
|
||||||
|
async function fetchProductOptions() {
|
||||||
|
try {
|
||||||
|
productOptions.value = (await getProductOptions()) || []
|
||||||
|
} catch (e) {
|
||||||
|
productOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchList() {
|
async function fetchList() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -189,7 +274,8 @@ async function fetchList() {
|
|||||||
list.value = (await getDeviceList({
|
list.value = (await getDeviceList({
|
||||||
pageIndex: page.value,
|
pageIndex: page.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
keyword: keyword.value || undefined
|
keyword: keyword.value || undefined,
|
||||||
|
productId: searchProductId.value || 0
|
||||||
})) || []
|
})) || []
|
||||||
total.value = list.value.total || 0
|
total.value = list.value.total || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -201,7 +287,7 @@ async function fetchList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() { page.value = 1; fetchList() }
|
function onSearch() { page.value = 1; fetchList() }
|
||||||
function onReset() { keyword.value = ''; page.value = 1; fetchList() }
|
function onReset() { keyword.value = ''; searchProductId.value = ''; page.value = 1; fetchList() }
|
||||||
function onSizeChange() { page.value = 1; fetchList() }
|
function onSizeChange() { page.value = 1; fetchList() }
|
||||||
|
|
||||||
// ---------- 新增 / 修改 ----------
|
// ---------- 新增 / 修改 ----------
|
||||||
@@ -215,7 +301,7 @@ const deviceTypes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const defaultForm = () => ({
|
const defaultForm = () => ({
|
||||||
Id: '', Code: '', Name: '', DeviceType: '', GatewayCode: '',
|
Id: '', Code: '', Name: '', DeviceType: '', ProductId: '', GatewayCode: '',
|
||||||
SlaveId: 1, ProtocolType: 1, Manufacturer: '', IsEnabled: true, Remark: ''
|
SlaveId: 1, ProtocolType: 1, Manufacturer: '', IsEnabled: true, Remark: ''
|
||||||
})
|
})
|
||||||
const form = reactive(defaultForm())
|
const form = reactive(defaultForm())
|
||||||
@@ -238,6 +324,7 @@ function openEdit(row) {
|
|||||||
Code: row.Code,
|
Code: row.Code,
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
DeviceType: row.DeviceType,
|
DeviceType: row.DeviceType,
|
||||||
|
ProductId: row.ProductId && row.ProductId !== '0' ? row.ProductId : '',
|
||||||
GatewayCode: row.GatewayCode,
|
GatewayCode: row.GatewayCode,
|
||||||
SlaveId: row.SlaveId,
|
SlaveId: row.SlaveId,
|
||||||
ProtocolType: row.ProtocolType,
|
ProtocolType: row.ProtocolType,
|
||||||
@@ -293,14 +380,105 @@ function onRowContextmenu(row, column, event) {
|
|||||||
function onRowClick() { ctxMenu.visible = false }
|
function onRowClick() { ctxMenu.visible = false }
|
||||||
|
|
||||||
function closeCtxMenu() { ctxMenu.visible = false }
|
function closeCtxMenu() { ctxMenu.visible = false }
|
||||||
|
function ctxDetail() { closeCtxMenu(); openDetail(ctxMenu.row) }
|
||||||
function ctxEdit() { closeCtxMenu(); openEdit(ctxMenu.row) }
|
function ctxEdit() { closeCtxMenu(); openEdit(ctxMenu.row) }
|
||||||
|
function ctxLogs() { closeCtxMenu(); openLogs(ctxMenu.row) }
|
||||||
function ctxDelete() { closeCtxMenu(); onDelete(ctxMenu.row) }
|
function ctxDelete() { closeCtxMenu(); onDelete(ctxMenu.row) }
|
||||||
|
|
||||||
|
// ---------- 操作列「更多」下拉 ----------
|
||||||
|
function onMoreCommand(cmd, row) {
|
||||||
|
if (cmd === 'logs') openLogs(row)
|
||||||
|
else if (cmd === 'delete') onDelete(row)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchList()
|
fetchList()
|
||||||
|
fetchProductOptions()
|
||||||
window.addEventListener('click', closeCtxMenu)
|
window.addEventListener('click', closeCtxMenu)
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => window.removeEventListener('click', closeCtxMenu))
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('click', closeCtxMenu)
|
||||||
|
disposeBoard()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 设备详情 ----------
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const detailTab = ref('info')
|
||||||
|
const detailDevice = ref(null)
|
||||||
|
const pointSendRef = ref(null)
|
||||||
|
|
||||||
|
function openDetail(row) {
|
||||||
|
detailDevice.value = row
|
||||||
|
detailTab.value = 'info'
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切到「下发指令」时重新拉取物模型点(在物模型点 tab 新增/修改后能看到最新数据)
|
||||||
|
watch(detailTab, (tab) => {
|
||||||
|
if (tab === 'board') nextTick(fetchBoard)
|
||||||
|
if (tab === 'command') nextTick(() => pointSendRef.value?.refresh())
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 看板(复用 dashboard/curve 接口) ----------
|
||||||
|
const boardChartRef = ref(null)
|
||||||
|
const boardLoading = ref(false)
|
||||||
|
const boardEmpty = ref(true)
|
||||||
|
let boardChart = null
|
||||||
|
let boardTimer = null
|
||||||
|
|
||||||
|
function disposeBoard() {
|
||||||
|
if (boardTimer) { clearInterval(boardTimer); boardTimer = null }
|
||||||
|
if (boardChart) { boardChart.dispose(); boardChart = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function initBoardChart() {
|
||||||
|
if (!boardChartRef.value || boardChart) return
|
||||||
|
boardChart = echarts.init(boardChartRef.value)
|
||||||
|
boardChart.setOption({
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['温度(℃)', '湿度(%)'] },
|
||||||
|
grid: { left: 50, right: 50, top: 40, bottom: 30 },
|
||||||
|
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
||||||
|
yAxis: [
|
||||||
|
{ type: 'value', name: '℃' },
|
||||||
|
{ type: 'value', name: '%' }
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{ name: '温度(℃)', type: 'line', showSymbol: false, smooth: true, data: [], itemStyle: { color: '#f56c6c' } },
|
||||||
|
{ name: '湿度(%)', type: 'line', showSymbol: false, smooth: true, yAxisIndex: 1, data: [], itemStyle: { color: '#409eff' } }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
window.addEventListener('resize', resizeBoard)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeBoard() { boardChart && boardChart.resize() }
|
||||||
|
|
||||||
|
async function fetchBoard() {
|
||||||
|
if (!detailDevice.value) return
|
||||||
|
boardLoading.value = true
|
||||||
|
try {
|
||||||
|
const rows = (await getCurve(detailDevice.value.Code, 5)) || []
|
||||||
|
boardEmpty.value = rows.length === 0
|
||||||
|
if (rows.length && boardChartRef.value) {
|
||||||
|
initBoardChart()
|
||||||
|
boardChart.setOption({
|
||||||
|
xAxis: { data: rows.map(d => fmtTime(d.CreateTime)) },
|
||||||
|
series: [
|
||||||
|
{ data: rows.map(d => d.DeviceTemperature) },
|
||||||
|
{ data: rows.map(d => d.DeviceHumidity) }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
boardEmpty.value = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
boardEmpty.value = true
|
||||||
|
} finally {
|
||||||
|
boardLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(detailVisible, () => disposeBoard())
|
||||||
|
|
||||||
// ---------- 设备日志 ----------
|
// ---------- 设备日志 ----------
|
||||||
const logVisible = ref(false)
|
const logVisible = ref(false)
|
||||||
@@ -419,4 +597,18 @@ const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
.more-arrow {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 「更多」下拉挂载在 body 下(teleport),scoped 样式覆盖不到,这里用全局样式标红删除项 -->
|
||||||
|
<style>
|
||||||
|
.el-dropdown-menu__item.more-danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.el-dropdown-menu__item.more-danger:hover {
|
||||||
|
color: #f56c6c;
|
||||||
|
background: #fef0f0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,13 +2,125 @@
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>产品分类</span>
|
<div class="card-header">
|
||||||
|
<span>产品分类</span>
|
||||||
|
<el-button type="primary" icon="Plus" @click="openAdd">新增分类</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-empty description="产品分类 - 功能开发中" />
|
|
||||||
|
<el-table :data="categories" v-loading="loading" border stripe>
|
||||||
|
<el-table-column prop="Code" label="分类编码" width="180" />
|
||||||
|
<el-table-column prop="Name" label="分类名称" min-width="180" />
|
||||||
|
<el-table-column prop="Sort" label="排序" width="90" align="center" />
|
||||||
|
<el-table-column prop="Remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="form.Id ? '编辑分类' : '新增分类'" width="460px" destroy-on-close>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||||
|
<el-form-item label="分类编码" prop="Code">
|
||||||
|
<el-input v-model="form.Code" placeholder="如 TBOX / CDC" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类名称" prop="Name">
|
||||||
|
<el-input v-model="form.Name" placeholder="如 温度箱" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序号">
|
||||||
|
<el-input-number v-model="form.Sort" :min="0" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// TODO: 实现产品分类功能
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getProductCategories, addProductCategory, updateProductCategory, deleteProductCategory } from '@/api/product'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const categories = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const defaultForm = () => ({ Id: '', Code: '', Name: '', Sort: 0, Remark: '' })
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
Code: [{ required: true, message: '请输入分类编码', trigger: 'blur' }],
|
||||||
|
Name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
categories.value = (await getProductCategories()) || []
|
||||||
|
} catch (e) {
|
||||||
|
categories.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
Object.assign(form, defaultForm(), row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (form.Id) {
|
||||||
|
await updateProductCategory({ ...form })
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
} else {
|
||||||
|
const payload = { ...form }
|
||||||
|
delete payload.Id
|
||||||
|
await addProductCategory(payload)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch (e) { /* 拦截器已提示 */ } finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDelete(row) {
|
||||||
|
await ElMessageBox.confirm(`确定删除分类「${row.Name || row.Code}」吗?`, '提示', { type: 'warning' })
|
||||||
|
try {
|
||||||
|
await deleteProductCategory(row.Id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
} catch (e) { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchList)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,13 +2,291 @@
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>产品管理</span>
|
<div class="card-header">
|
||||||
|
<span>产品管理({{ total }} 项)</span>
|
||||||
|
<el-button type="primary" icon="Plus" @click="openAdd">新增产品</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-empty description="产品管理 - 功能开发中" />
|
|
||||||
|
<!-- 搜索 + 分类筛选 -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-select v-model="search.categoryId" placeholder="全部分类" clearable style="width: 180px" @change="onSearch">
|
||||||
|
<el-option v-for="c in categories" :key="c.Id" :label="c.Name" :value="c.Id" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="search.keyword"
|
||||||
|
placeholder="请输入型号 / 名称查找产品"
|
||||||
|
clearable
|
||||||
|
style="width: 280px"
|
||||||
|
@keyup.enter="onSearch"
|
||||||
|
@clear="onSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="list" v-loading="loading" border stripe>
|
||||||
|
<el-table-column prop="Model" label="型号" width="140" />
|
||||||
|
<el-table-column prop="Name" label="产品名称" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="分类" width="130">
|
||||||
|
<template #default="{ row }">{{ categoryLabel(row.CategoryId) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="通讯协议" width="110">
|
||||||
|
<template #default="{ row }">{{ protocolLabel(row.ProtocolType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="MessageModel" label="消息模型" width="100" />
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.IsEnabled ? 'success' : 'info'" size="small">{{ row.IsEnabled ? '启用' : '停用' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">{{ fmtTime(row.CreateTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="fetchList"
|
||||||
|
@size-change="onSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑产品 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="form.Id ? '编辑产品' : '新增产品'" width="560px" destroy-on-close>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
|
||||||
|
<el-form-item label="型号" prop="Model">
|
||||||
|
<el-input v-model="form.Model" placeholder="如 GT-100(唯一)" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="产品名称" prop="Name">
|
||||||
|
<el-input v-model="form.Name" placeholder="如 高低温试验箱" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="产品分类">
|
||||||
|
<el-select v-model="form.CategoryId" placeholder="未分类" clearable style="width: 100%">
|
||||||
|
<el-option v-for="c in categories" :key="c.Id" :label="c.Name" :value="c.Id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="通讯协议" prop="ProtocolType">
|
||||||
|
<el-select v-model="form.ProtocolType" style="width: 100%">
|
||||||
|
<el-option v-for="p in protocolOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="消息模型">
|
||||||
|
<el-input v-model="form.MessageModel" placeholder="如 JSON / 透传" maxlength="64" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="制造商">
|
||||||
|
<el-input v-model="form.Manufacturer" placeholder="请输入制造商" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch v-model="form.IsEnabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input v-model="form.Description" type="textarea" :rows="2" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 产品详情抽屉 -->
|
||||||
|
<el-drawer v-model="detailVisible" :title="`产品详情 - ${detailProduct?.Model || ''}`" size="720px">
|
||||||
|
<el-tabs v-if="detailProduct" v-model="activeTab">
|
||||||
|
<el-tab-pane label="基本信息" name="info">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="型号">{{ detailProduct.Model }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产品名称">{{ detailProduct.Name || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="分类">{{ categoryLabel(detailProduct.CategoryId) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="通讯协议">{{ protocolLabel(detailProduct.ProtocolType) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="消息模型">{{ detailProduct.MessageModel || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="制造商">{{ detailProduct.Manufacturer || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">{{ detailProduct.IsEnabled ? '启用' : '停用' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ fmtTime(detailProduct.CreateTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="描述" :span="2">{{ detailProduct.Description || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ detailProduct.Remark || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="物模型点" name="points">
|
||||||
|
<ThingPointTable v-if="detailProduct" :owner-type="1" :owner-id="detailProduct.Id" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="告警" name="alarm">
|
||||||
|
<ThingAlarmPanel v-if="detailProduct" :owner-type="1" :owner-id="detailProduct.Id" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// TODO: 实现产品管理功能
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getProductList, getProductCategories, addProduct, updateProduct, deleteProduct, protocolOptions, protocolLabel } from '@/api/product'
|
||||||
|
import ThingPointTable from '@/components/thing/ThingPointTable.vue'
|
||||||
|
import ThingAlarmPanel from '@/components/thing/ThingAlarmPanel.vue'
|
||||||
|
|
||||||
|
// ---------- 列表 ----------
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const categories = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const search = reactive({ keyword: '', categoryId: '' })
|
||||||
|
const categoryMap = ref({})
|
||||||
|
|
||||||
|
async function fetchCategories() {
|
||||||
|
try {
|
||||||
|
categories.value = (await getProductCategories()) || []
|
||||||
|
const map = {}
|
||||||
|
categories.value.forEach(c => { map[c.Id] = c.Name })
|
||||||
|
categoryMap.value = map
|
||||||
|
} catch (e) {
|
||||||
|
categories.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryLabel = (id) => categoryMap.value[id] || '未分类'
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
list.value = (await getProductList({
|
||||||
|
pageIndex: page.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
keyword: search.keyword || undefined,
|
||||||
|
categoryId: search.categoryId || 0
|
||||||
|
})) || []
|
||||||
|
total.value = list.value.total || 0
|
||||||
|
} catch (e) {
|
||||||
|
list.value = []
|
||||||
|
total.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() { page.value = 1; fetchList() }
|
||||||
|
function onReset() { search.keyword = ''; search.categoryId = ''; page.value = 1; fetchList() }
|
||||||
|
function onSizeChange() { page.value = 1; fetchList() }
|
||||||
|
|
||||||
|
// ---------- 新增 / 编辑 ----------
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
const defaultForm = () => ({
|
||||||
|
Id: '', Model: '', Name: '', CategoryId: '', Manufacturer: '', Description: '',
|
||||||
|
ProtocolType: 1, MessageModel: '', IsEnabled: true, Remark: ''
|
||||||
|
})
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
Model: [{ required: true, message: '请输入型号', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
Object.assign(form, defaultForm(), {
|
||||||
|
Id: row.Id, Model: row.Model, Name: row.Name || '', CategoryId: row.CategoryId || '',
|
||||||
|
Manufacturer: row.Manufacturer || '', Description: row.Description || '',
|
||||||
|
ProtocolType: row.ProtocolType, MessageModel: row.MessageModel || '',
|
||||||
|
IsEnabled: row.IsEnabled, Remark: row.Remark || ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (form.Id) {
|
||||||
|
await updateProduct({ ...form })
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
} else {
|
||||||
|
const payload = { ...form }
|
||||||
|
delete payload.Id
|
||||||
|
await addProduct(payload)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch (e) { /* 拦截器已提示 */ } finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDelete(row) {
|
||||||
|
await ElMessageBox.confirm(`确定删除产品「${row.Model}」吗?`, '提示', { type: 'warning' })
|
||||||
|
try {
|
||||||
|
await deleteProduct(row.Id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
} catch (e) { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 详情 ----------
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const activeTab = ref('info')
|
||||||
|
const detailProduct = ref(null)
|
||||||
|
|
||||||
|
function openDetail(row) {
|
||||||
|
detailProduct.value = row
|
||||||
|
activeTab.value = 'info'
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCategories()
|
||||||
|
fetchList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user