You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

486 lines
12 KiB

<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>