添加设备管理界面
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+11
-11
@@ -1,49 +1,49 @@
|
|||||||
{
|
{
|
||||||
"hash": "a6f0f194",
|
"hash": "25a37344",
|
||||||
"configHash": "147f2811",
|
"configHash": "ec38f6fa",
|
||||||
"lockfileHash": "77198382",
|
"lockfileHash": "74076ca5",
|
||||||
"browserHash": "ee71f75c",
|
"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": "9553abbf",
|
"fileHash": "53709748",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "3e018dd2",
|
"fileHash": "8bc11b32",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"echarts": {
|
"echarts": {
|
||||||
"src": "../../echarts/index.js",
|
"src": "../../echarts/index.js",
|
||||||
"file": "echarts.js",
|
"file": "echarts.js",
|
||||||
"fileHash": "275a5510",
|
"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": "cc57a904",
|
"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": "0a5304b8",
|
"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": "0ef5b6fe",
|
"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": "52e89e85",
|
"fileHash": "53d5f754",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 后端路由前缀(IotDeviceController: [Route("api/config/device")])
|
||||||
|
const base = '/config/device'
|
||||||
|
|
||||||
|
// ============ 枚举常量(与后端 Model.Entity.Config 枚举一一对应) ============
|
||||||
|
|
||||||
|
// 通讯协议(IotDeviceProtocolEnum)
|
||||||
|
export const PROTOCOL_TYPES = {
|
||||||
|
1: 'Modbus TCP',
|
||||||
|
2: 'Modbus RTU',
|
||||||
|
3: 'S7',
|
||||||
|
4: 'TCP',
|
||||||
|
5: '串口'
|
||||||
|
}
|
||||||
|
export const protocolOptions = Object.entries(PROTOCOL_TYPES).map(([value, label]) => ({ value: Number(value), label }))
|
||||||
|
export const protocolLabel = (v) => PROTOCOL_TYPES[v] || '-'
|
||||||
|
|
||||||
|
// 在线状态(IotDeviceOnlineStatusEnum)
|
||||||
|
export const ONLINE_STATUS = {
|
||||||
|
0: { label: '离线', tag: 'info' },
|
||||||
|
1: { label: '在线', tag: 'success' },
|
||||||
|
2: { label: '连接中', tag: 'warning' },
|
||||||
|
3: { label: '通讯异常', tag: 'danger' }
|
||||||
|
}
|
||||||
|
export const onlineStatusLabel = (s) => ONLINE_STATUS[s]?.label || '离线'
|
||||||
|
export const onlineStatusTag = (s) => ONLINE_STATUS[s]?.tag || 'info'
|
||||||
|
|
||||||
|
// ============ 设备接口 ============
|
||||||
|
|
||||||
|
// 设备列表(服务端分页;结果数组上附带 total 总记录数,来自响应头 X-Total-Count)
|
||||||
|
export const getDeviceList = (params) => request.get(`${base}/list`, { params })
|
||||||
|
|
||||||
|
// 设备详情
|
||||||
|
export const getDeviceDetail = (id) => request.get(`${base}/${id}`)
|
||||||
|
|
||||||
|
// 新增设备(后端校验编号唯一性,并在数据库建记录)
|
||||||
|
export const addDevice = (data) => request.post(base, data)
|
||||||
|
|
||||||
|
// 修改设备
|
||||||
|
export const updateDevice = (data) => request.put(base, data)
|
||||||
|
|
||||||
|
// 删除设备(软删除)
|
||||||
|
export const deleteDevice = (id) => request.delete(`${base}/${id}`)
|
||||||
|
|
||||||
|
// 设备日志(分页;结果数组上附带 total)
|
||||||
|
export const getDeviceLogs = (id, params) => request.get(`${base}/${id}/logs`, { params })
|
||||||
@@ -2,13 +2,421 @@
|
|||||||
<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-input
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="请输入设备编号 / 名称 / 类型查找设备"
|
||||||
|
clearable
|
||||||
|
style="width: 320px"
|
||||||
|
@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"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
row-key="Id"
|
||||||
|
@row-contextmenu="onRowContextmenu"
|
||||||
|
@row-click="onRowClick"
|
||||||
|
>
|
||||||
|
<el-table-column prop="Code" label="设备编号" width="130" />
|
||||||
|
<el-table-column prop="Name" label="设备名称" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="DeviceType" label="设备类型" width="110" />
|
||||||
|
<el-table-column prop="GatewayCode" label="所属网关" width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="SlaveId" label="从站地址" width="90" align="center" />
|
||||||
|
<el-table-column label="协议" width="110">
|
||||||
|
<template #default="{ row }">{{ protocolLabel(row.ProtocolType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="在线状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="onlineStatusTag(row.OnlineStatus)" size="small">
|
||||||
|
{{ onlineStatusLabel(row.OnlineStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最后采集" width="160">
|
||||||
|
<template #default="{ row }">{{ fmtTime(row.LastCollectTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click.stop="openLogs(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>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<div
|
||||||
|
v-show="ctxMenu.visible"
|
||||||
|
class="ctx-menu"
|
||||||
|
:style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="ctx-menu__item" @click="ctxEdit">
|
||||||
|
<el-icon><Edit /></el-icon><span>修改设备</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctx-menu__item danger" @click="ctxDelete">
|
||||||
|
<el-icon><Delete /></el-icon><span>删除设备</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<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="100px">
|
||||||
|
<el-form-item label="设备编号" prop="Code">
|
||||||
|
<el-input v-model="form.Code" placeholder="请输入唯一设备编号" maxlength="64" />
|
||||||
|
</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="设备类型" prop="DeviceType">
|
||||||
|
<el-select v-model="form.DeviceType" placeholder="请选择设备类型" style="width: 100%">
|
||||||
|
<el-option v-for="d in deviceTypes" :key="d.value" :label="d.label" :value="d.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属网关" prop="GatewayCode">
|
||||||
|
<el-input v-model="form.GatewayCode" placeholder="如 GW-ENV-01(IP:端口或串口标识)" maxlength="64" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="从站地址" prop="SlaveId">
|
||||||
|
<el-input-number v-model="form.SlaveId" :min="1" :max="247" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<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-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.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="logVisible" :title="`设备日志 - ${logDevice?.Name || ''}`" size="520px">
|
||||||
|
<div class="log-toolbar">
|
||||||
|
<el-button link type="primary" size="small" @click="fetchLogs(true)">刷新</el-button>
|
||||||
|
<span class="log-count">共 {{ logTotal }} 条</span>
|
||||||
|
</div>
|
||||||
|
<el-timeline v-loading="logLoading" class="log-timeline">
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="log in logList"
|
||||||
|
:key="log.Id"
|
||||||
|
:timestamp="fmtTime(log.LogTime)"
|
||||||
|
:type="logTagType(log.Level)"
|
||||||
|
>
|
||||||
|
<el-tag :type="logTagType(log.Level)" size="small">{{ log.Level }}</el-tag>
|
||||||
|
<span class="log-type">{{ log.LogType }}</span>
|
||||||
|
<div class="log-message">{{ log.Message }}</div>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
<el-empty v-if="!logLoading && logList.length === 0" description="暂无日志" />
|
||||||
|
<div class="log-pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="logPage"
|
||||||
|
v-model:page-size="logPageSize"
|
||||||
|
:total="logTotal"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@current-change="fetchLogs"
|
||||||
|
@size-change="onLogSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// TODO: 实现设备管理功能
|
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getDeviceList, addDevice, updateDevice, deleteDevice, getDeviceLogs,
|
||||||
|
protocolOptions, protocolLabel, onlineStatusLabel, onlineStatusTag
|
||||||
|
} from '@/api/device'
|
||||||
|
|
||||||
|
// ---------- 列表 ----------
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
list.value = (await getDeviceList({
|
||||||
|
pageIndex: page.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
keyword: keyword.value || undefined
|
||||||
|
})) || []
|
||||||
|
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() { keyword.value = ''; page.value = 1; fetchList() }
|
||||||
|
function onSizeChange() { page.value = 1; fetchList() }
|
||||||
|
|
||||||
|
// ---------- 新增 / 修改 ----------
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const deviceTypes = [
|
||||||
|
{ value: 'TemperatureBox', label: '温度箱' },
|
||||||
|
{ value: 'ChargeCabinet', label: '充放电柜' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
Id: '', Code: '', Name: '', DeviceType: '', GatewayCode: '',
|
||||||
|
SlaveId: 1, ProtocolType: 1, Manufacturer: '', IsEnabled: true, Remark: ''
|
||||||
|
})
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
Code: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
|
||||||
|
Name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
||||||
|
DeviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
|
||||||
|
GatewayCode: [{ required: true, message: '请输入所属网关', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
Object.assign(form, defaultForm(), {
|
||||||
|
Id: row.Id,
|
||||||
|
Code: row.Code,
|
||||||
|
Name: row.Name,
|
||||||
|
DeviceType: row.DeviceType,
|
||||||
|
GatewayCode: row.GatewayCode,
|
||||||
|
SlaveId: row.SlaveId,
|
||||||
|
ProtocolType: row.ProtocolType,
|
||||||
|
Manufacturer: row.Manufacturer || '',
|
||||||
|
IsEnabled: row.IsEnabled,
|
||||||
|
Remark: row.Remark || ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (form.Id) {
|
||||||
|
await updateDevice({ ...form })
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
} else {
|
||||||
|
const payload = { ...form }
|
||||||
|
delete payload.Id
|
||||||
|
await addDevice(payload)
|
||||||
|
ElMessage.success('添加成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch (e) { /* 拦截器已提示 */ } finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 删除 ----------
|
||||||
|
async function onDelete(row) {
|
||||||
|
await ElMessageBox.confirm(`确定删除设备「${row.Name}」吗?`, '提示', { type: 'warning' })
|
||||||
|
try {
|
||||||
|
await deleteDevice(row.Id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
} catch (e) { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 右键菜单 ----------
|
||||||
|
const ctxMenu = reactive({ visible: false, x: 0, y: 0, row: null })
|
||||||
|
|
||||||
|
function onRowContextmenu(row, column, event) {
|
||||||
|
event.preventDefault()
|
||||||
|
ctxMenu.row = row
|
||||||
|
ctxMenu.x = event.clientX
|
||||||
|
ctxMenu.y = event.clientY
|
||||||
|
ctxMenu.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击表格其它区域时收起右键菜单
|
||||||
|
function onRowClick() { ctxMenu.visible = false }
|
||||||
|
|
||||||
|
function closeCtxMenu() { ctxMenu.visible = false }
|
||||||
|
function ctxEdit() { closeCtxMenu(); openEdit(ctxMenu.row) }
|
||||||
|
function ctxDelete() { closeCtxMenu(); onDelete(ctxMenu.row) }
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchList()
|
||||||
|
window.addEventListener('click', closeCtxMenu)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('click', closeCtxMenu))
|
||||||
|
|
||||||
|
// ---------- 设备日志 ----------
|
||||||
|
const logVisible = ref(false)
|
||||||
|
const logLoading = ref(false)
|
||||||
|
const logList = ref([])
|
||||||
|
const logTotal = ref(0)
|
||||||
|
const logPage = ref(1)
|
||||||
|
const logPageSize = ref(10)
|
||||||
|
const logDevice = ref(null)
|
||||||
|
|
||||||
|
async function openLogs(row) {
|
||||||
|
logDevice.value = row
|
||||||
|
logPage.value = 1
|
||||||
|
logVisible.value = true
|
||||||
|
fetchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLogs(reset = false) {
|
||||||
|
if (reset) logPage.value = 1
|
||||||
|
if (!logDevice.value) return
|
||||||
|
logLoading.value = true
|
||||||
|
try {
|
||||||
|
logList.value = (await getDeviceLogs(logDevice.value.Id, {
|
||||||
|
pageIndex: logPage.value,
|
||||||
|
pageSize: logPageSize.value
|
||||||
|
})) || []
|
||||||
|
logTotal.value = logList.value.total || 0
|
||||||
|
} catch (e) {
|
||||||
|
logList.value = []
|
||||||
|
logTotal.value = 0
|
||||||
|
} finally {
|
||||||
|
logLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogSizeChange() { logPage.value = 1; fetchLogs() }
|
||||||
|
|
||||||
|
// ---------- 格式化 ----------
|
||||||
|
const logTagType = (level) => ({ Error: 'danger', Warn: 'warning' }[level] || 'primary')
|
||||||
|
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
|
||||||
</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;
|
||||||
|
}
|
||||||
|
/* 右键菜单 */
|
||||||
|
.ctx-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3000;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.ctx-menu__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ctx-menu__item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.ctx-menu__item.danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.ctx-menu__item.danger:hover {
|
||||||
|
background: #fef0f0;
|
||||||
|
}
|
||||||
|
.log-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.log-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.log-timeline {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.log-type {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.log-message {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.log-pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user