Browse Source
feat(tabs): 添加标签页显示控制功能 新增getShowTabs计算属性来控制标签页的显示, 当路由元数据包含hideTab或路径为/interface、/run/interface时隐藏标签页 ```pull/134/head
3 changed files with 522 additions and 1 deletions
@ -0,0 +1,28 @@ |
|||
import type { AppRouteModule } from '@/router/types' |
|||
|
|||
import { LAYOUT } from '@/router/constant' |
|||
|
|||
const run: AppRouteModule = { |
|||
path: '/run', |
|||
name: 'Run', |
|||
component: LAYOUT, |
|||
meta: { |
|||
orderNo: 30, |
|||
icon: 'ant-design:control-outlined', |
|||
title: '运行管理', |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: 'interface', |
|||
name: 'InterfaceManage', |
|||
component: () => import('@/views/run/interface/index.vue'), |
|||
meta: { |
|||
title: '接口管理', |
|||
icon: 'ant-design:api-outlined', |
|||
hideTab: true, |
|||
}, |
|||
}, |
|||
], |
|||
} |
|||
|
|||
export default run |
|||
@ -0,0 +1,486 @@ |
|||
<script lang="ts" setup> |
|||
import { computed, onMounted, ref } from 'vue' |
|||
import { useRoute } from 'vue-router' |
|||
import { Badge, Card, Empty, Modal, Tabs } from 'ant-design-vue' |
|||
|
|||
import { BasicTable, TableAction, useTable } from '@/components/Table' |
|||
import { useMessage } from '@/hooks/web/useMessage' |
|||
|
|||
interface InterfaceInfo { |
|||
mp_id?: string[] |
|||
} |
|||
|
|||
interface InterfaceRow { |
|||
ID: number |
|||
block_description?: string |
|||
Description?: string |
|||
BLOCK_Name?: string |
|||
point_id?: string |
|||
instant_Num?: number | string |
|||
Info?: InterfaceInfo |
|||
COMPOUND?: string |
|||
CP_HOST?: string |
|||
Used?: number | string |
|||
is_update?: number | string |
|||
} |
|||
|
|||
interface StatusMeta { |
|||
key: 'done' | 'pending' | 'uncovered' | 'update' |
|||
label: string |
|||
color: string |
|||
} |
|||
|
|||
const { createMessage } = useMessage() |
|||
const route = useRoute() |
|||
const query = ref({ |
|||
unitId: undefined as undefined | string, |
|||
typeName: undefined as undefined | string, |
|||
keyword: '' |
|||
}) |
|||
|
|||
const statusFilter = ref<'all' | StatusMeta['key']>('all') |
|||
|
|||
const AModal = Modal |
|||
const ACard = Card |
|||
const ATabs = Tabs |
|||
const ATabPane = Tabs.TabPane |
|||
const AEmpty = Empty |
|||
|
|||
const unitTitle = computed(() => { |
|||
const unitId = route.query.unitid ? decodeURIComponent(String(route.query.unitid)) : '' |
|||
return unitId ? `#${unitId}号机智能高级应用至DCS通讯接口` : '接口管理' |
|||
}) |
|||
|
|||
const allRows = ref<InterfaceRow[]>([]) |
|||
const selectedRows = ref<InterfaceRow[]>([]) |
|||
|
|||
const statusList: StatusMeta[] = [ |
|||
{ key: 'done', label: '已完成', color: '#fa8c16' }, |
|||
{ key: 'pending', label: '未完成', color: '#1677ff' }, |
|||
{ key: 'uncovered', label: '未覆盖', color: '#ff4d4f' }, |
|||
{ key: 'update', label: '版本更新', color: '#52c41a' }, |
|||
] |
|||
|
|||
const columns = [ |
|||
{ title: '序号', dataIndex: 'ID', key: 'id', width: 90 }, |
|||
{ title: 'DCS目标描述', dataIndex: 'block_description', key: 'block_description', minWidth: 240 }, |
|||
{ title: 'EXA描述', dataIndex: 'Description', key: 'Description', minWidth: 240 }, |
|||
{ title: '模块名称', dataIndex: 'BLOCK_Name', key: 'BLOCK_Name', minWidth: 140 }, |
|||
{ title: '点号', dataIndex: 'point_id', key: 'point_id', minWidth: 140 }, |
|||
{ title: '实例数量', dataIndex: 'instant_Num', key: 'instant_Num', width: 100 }, |
|||
{ title: '实例id', dataIndex: 'Info', key: 'instanceId', minWidth: 160 }, |
|||
{ title: 'COMPOUND', dataIndex: 'COMPOUND', key: 'COMPOUND', minWidth: 120 }, |
|||
{ title: 'CP_HOST', dataIndex: 'CP_HOST', key: 'CP_HOST', minWidth: 220 }, |
|||
{ title: '操作', dataIndex: 'action', key: 'action', width: 240, fixed: 'right' }, |
|||
] |
|||
|
|||
const [registerTable, { setTableData, clearSelectedRowKeys, setSelectedRowKeys, getDataSource }] = useTable({ |
|||
title: '接口列表', |
|||
columns, |
|||
rowKey: 'ID', |
|||
showTableSetting: true, |
|||
showIndexColumn: false, |
|||
pagination: { |
|||
pageSize: 15, |
|||
}, |
|||
rowSelection: { |
|||
type: 'checkbox', |
|||
onChange: (_keys, rows: InterfaceRow[]) => { |
|||
selectedRows.value = rows |
|||
}, |
|||
}, |
|||
}) |
|||
|
|||
function toNumber(value: string | number | undefined) { |
|||
const num = Number(value) |
|||
return Number.isNaN(num) ? 0 : num |
|||
} |
|||
|
|||
function getRowStatus(record: InterfaceRow): StatusMeta { |
|||
const isUpdate = toNumber(record.is_update) === 1 |
|||
if (isUpdate) |
|||
return statusList[3] |
|||
|
|||
if (toNumber(record.Used) === 1) |
|||
return statusList[0] |
|||
|
|||
if (toNumber(record.instant_Num) > 0) |
|||
return statusList[1] |
|||
|
|||
return statusList[2] |
|||
} |
|||
|
|||
const statusCounts = computed(() => { |
|||
const result = { |
|||
done: 0, |
|||
pending: 0, |
|||
uncovered: 0, |
|||
update: 0, |
|||
} |
|||
|
|||
allRows.value.forEach((row) => { |
|||
const status = getRowStatus(row) |
|||
result[status.key] += 1 |
|||
}) |
|||
|
|||
return result |
|||
}) |
|||
|
|||
function getInstanceIds(record: InterfaceRow) { |
|||
if (!record.Info?.mp_id || record.Info.mp_id.length === 0) |
|||
return '-' |
|||
return record.Info.mp_id.join(',') |
|||
} |
|||
|
|||
function applyFilters() { |
|||
const keyword = query.value.keyword.trim() |
|||
let rows = [...allRows.value] |
|||
|
|||
if (statusFilter.value !== 'all') { |
|||
rows = rows.filter(item => getRowStatus(item).key === statusFilter.value) |
|||
} |
|||
|
|||
if (keyword) { |
|||
rows = rows.filter(item => |
|||
`${item.point_id ?? ''}${item.block_description ?? ''}${item.Description ?? ''}`.includes(keyword), |
|||
) |
|||
} |
|||
|
|||
setTableData(rows) |
|||
} |
|||
|
|||
function handleSearch() { |
|||
applyFilters() |
|||
} |
|||
|
|||
function handleResetSelection() { |
|||
selectedRows.value = [] |
|||
clearSelectedRowKeys() |
|||
} |
|||
|
|||
function handleSelectAll() { |
|||
const dataSource = getDataSource() |
|||
const keys = dataSource.map((item: InterfaceRow) => item.ID) |
|||
setSelectedRowKeys(keys) |
|||
} |
|||
|
|||
const bindModalOpen = ref(false) |
|||
const detailModalOpen = ref(false) |
|||
const detailRecord = ref<InterfaceRow | null>(null) |
|||
|
|||
function handleBatchBind() { |
|||
if (selectedRows.value.length === 0) { |
|||
createMessage.warning('请先勾选需要批量绑定的记录') |
|||
return |
|||
} |
|||
bindModalOpen.value = true |
|||
} |
|||
|
|||
function handleBatchReset() { |
|||
if (selectedRows.value.length === 0) { |
|||
createMessage.warning('请先勾选需要复位的记录') |
|||
return |
|||
} |
|||
createMessage.success('已提交批量复位请求') |
|||
} |
|||
|
|||
function handleBindModel(record: InterfaceRow) { |
|||
bindModalOpen.value = true |
|||
selectedRows.value = [record] |
|||
} |
|||
|
|||
function handleDetail(record: InterfaceRow) { |
|||
detailRecord.value = record |
|||
detailModalOpen.value = true |
|||
} |
|||
|
|||
function handleReset(record: InterfaceRow) { |
|||
createMessage.success(`已提交${record.ID}复位请求`) |
|||
} |
|||
|
|||
function handleConfirm(record: InterfaceRow) { |
|||
createMessage.success(`已提交${record.ID}确认请求`) |
|||
} |
|||
|
|||
function handleStatusClick(key: StatusMeta['key']) { |
|||
statusFilter.value = key |
|||
applyFilters() |
|||
} |
|||
|
|||
function handleStatusReset() { |
|||
statusFilter.value = 'all' |
|||
applyFilters() |
|||
} |
|||
|
|||
onMounted(() => { |
|||
setTableData(allRows.value) |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="interface-page"> |
|||
<ACard class="interface-title-card"> |
|||
<div class="interface-title">{{ unitTitle }}</div> |
|||
</ACard> |
|||
|
|||
<ACard class="interface-legend-card"> |
|||
<div class="interface-legend"> |
|||
<Badge |
|||
:class="statusCounts.done === 0 ? 'runningStatus' : 'runningStatus alarm'" |
|||
:color="statusList[0].color" |
|||
text="已完成" |
|||
@click="handleStatusClick('done')" |
|||
/> |
|||
<Badge |
|||
:class="statusCounts.pending === 0 ? 'runningStatus' : 'runningStatus alarm'" |
|||
:color="statusList[1].color" |
|||
text="未完成" |
|||
@click="handleStatusClick('pending')" |
|||
/> |
|||
<Badge |
|||
:class="statusCounts.uncovered === 0 ? 'runningStatus' : 'runningStatus alarm'" |
|||
:color="statusList[2].color" |
|||
text="未覆盖" |
|||
@click="handleStatusClick('uncovered')" |
|||
/> |
|||
<Badge |
|||
:class="statusCounts.update === 0 ? 'runningStatus' : 'runningStatus alarm'" |
|||
:color="statusList[3].color" |
|||
text="版本更新" |
|||
@click="handleStatusClick('update')" |
|||
/> |
|||
</div> |
|||
</ACard> |
|||
|
|||
<ACard class="interface-filter-card"> |
|||
<div class="filter-row"> |
|||
<a-select |
|||
v-model:value="query.unitId" |
|||
class="filter-item" |
|||
allow-clear |
|||
placeholder="2号机组" |
|||
:options="[]" |
|||
/> |
|||
<a-select |
|||
v-model:value="query.typeName" |
|||
class="filter-item" |
|||
allow-clear |
|||
placeholder="测点级" |
|||
:options="[]" |
|||
/> |
|||
<a-input |
|||
v-model:value="query.keyword" |
|||
class="filter-input" |
|||
placeholder="点号/描述" |
|||
@press-enter="handleSearch" |
|||
/> |
|||
<div class="filter-actions"> |
|||
<a-button type="primary" @click="handleSearch">搜索</a-button> |
|||
<a-button @click="handleBatchBind">批量绑定</a-button> |
|||
<a-button @click="handleBatchReset">批量复位</a-button> |
|||
</div> |
|||
</div> |
|||
</ACard> |
|||
|
|||
<ACard class="interface-table-card"> |
|||
<BasicTable @register="registerTable"> |
|||
<template #title> |
|||
<div class="table-title-bar"> |
|||
<div class="table-title-left"> |
|||
<a-button size="small" type="primary" @click="handleSelectAll">全选</a-button> |
|||
<a-button size="small" class="ml-2" @click="handleResetSelection">取消全选</a-button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<template #bodyCell="{ column, record }"> |
|||
<template v-if="column.key === 'id'"> |
|||
<span class="status-dot" :style="{ backgroundColor: getRowStatus(record).color }" /> |
|||
{{ record.ID }} |
|||
</template> |
|||
|
|||
<template v-else-if="column.key === 'instanceId'"> |
|||
{{ getInstanceIds(record) }} |
|||
</template> |
|||
|
|||
<template v-else-if="column.key === 'action'"> |
|||
<TableAction |
|||
:actions="[ |
|||
{ |
|||
label: '绑定模型测点', |
|||
onClick: handleBindModel.bind(null, record), |
|||
}, |
|||
{ |
|||
label: '详情', |
|||
onClick: handleDetail.bind(null, record), |
|||
}, |
|||
{ |
|||
label: '复位', |
|||
ifShow: toNumber(record.is_update) !== 1, |
|||
onClick: handleReset.bind(null, record), |
|||
}, |
|||
{ |
|||
label: '确定', |
|||
ifShow: toNumber(record.is_update) === 1, |
|||
onClick: handleConfirm.bind(null, record), |
|||
}, |
|||
]" |
|||
/> |
|||
</template> |
|||
</template> |
|||
</BasicTable> |
|||
</ACard> |
|||
|
|||
<AModal v-model:open="bindModalOpen" title="批量绑定" width="900px" :footer="null"> |
|||
<div class="modal-section"> |
|||
<div class="modal-toolbar"> |
|||
<span>已选择 {{ selectedRows.length }} 条记录</span> |
|||
<a-button type="link" @click="handleResetSelection">清空选择</a-button> |
|||
</div> |
|||
<ATabs> |
|||
<ATabPane key="rule" tab="规则类测点"> |
|||
<AEmpty description="暂无规则类测点数据" /> |
|||
</ATabPane> |
|||
<ATabPane key="model" tab="模型测点"> |
|||
<AEmpty description="暂无模型测点数据" /> |
|||
</ATabPane> |
|||
<ATabPane key="raw" tab="原始测点"> |
|||
<AEmpty description="暂无原始测点数据" /> |
|||
</ATabPane> |
|||
</ATabs> |
|||
</div> |
|||
</AModal> |
|||
|
|||
<AModal v-model:open="detailModalOpen" title="详情" width="900px" :footer="null"> |
|||
<div class="detail-panel"> |
|||
<div class="detail-row"> |
|||
<span class="detail-label">模块名称:</span> |
|||
<span>{{ detailRecord?.BLOCK_Name || '-' }}</span> |
|||
</div> |
|||
<div class="detail-row"> |
|||
<span class="detail-label">参数名称:</span> |
|||
<span>{{ detailRecord?.Description || '-' }}</span> |
|||
</div> |
|||
<AEmpty description="暂无实例详情数据" class="detail-empty" /> |
|||
</div> |
|||
</AModal> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" scoped> |
|||
.interface-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 10px; |
|||
} |
|||
|
|||
.interface-title-card { |
|||
.interface-title { |
|||
text-align: center; |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
color: #111827; |
|||
} |
|||
} |
|||
|
|||
.interface-legend-card { |
|||
.interface-legend { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 18px; |
|||
flex-wrap: wrap; |
|||
} |
|||
} |
|||
|
|||
.interface-page :deep(.ant-card-body) { |
|||
padding: 12px 16px; |
|||
} |
|||
|
|||
.interface-filter-card { |
|||
.filter-row { |
|||
display: grid; |
|||
grid-template-columns: 220px 220px minmax(240px, 1fr) auto; |
|||
align-items: center; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.filter-item { |
|||
width: 100%; |
|||
} |
|||
|
|||
.filter-input { |
|||
width: 100%; |
|||
} |
|||
|
|||
.filter-actions { |
|||
display: flex; |
|||
gap: 10px; |
|||
justify-content: flex-end; |
|||
} |
|||
} |
|||
|
|||
.interface-table-card { |
|||
:deep(.ant-table-title) { |
|||
padding: 0 0 8px; |
|||
} |
|||
} |
|||
|
|||
.table-title-bar { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.modal-section { |
|||
.modal-toolbar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 12px; |
|||
} |
|||
} |
|||
|
|||
.detail-panel { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.detail-row { |
|||
display: flex; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.detail-label { |
|||
color: #6b7280; |
|||
} |
|||
|
|||
.detail-empty { |
|||
margin-top: 12px; |
|||
} |
|||
|
|||
.status-dot { |
|||
display: inline-block; |
|||
width: 8px; |
|||
height: 8px; |
|||
border-radius: 50%; |
|||
margin-right: 6px; |
|||
} |
|||
|
|||
:deep(.alarm .ant-badge-status-dot) { |
|||
animation: flash 1s linear infinite; |
|||
} |
|||
|
|||
@keyframes flash { |
|||
from { |
|||
opacity: 0; |
|||
} |
|||
|
|||
to { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
.runningStatus { |
|||
cursor: pointer; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue