Files
aiot-platform-ui/apps/web-antd/src/views/ops/area-device/modules/device-card.vue
lzh 975bf975b9 feat(@vben/web-antd): 新增区域设备可视化管理页面
左侧区域树 + 右侧设备卡片网格布局,点击卡片打开详情 Drawer,
支持设备基础信息查看、类型化实时数据展示(客流趋势/工牌状态)、
业务配置编辑及设备绑定/解绑操作。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 00:14:04 +08:00

261 lines
5.6 KiB
Vue

<script setup lang="ts">
import type { OpsAreaApi } from '#/api/ops/area';
import { computed } from 'vue';
import { Badge, Card, Dropdown, Menu } from 'ant-design-vue';
import { RELATION_TYPE_COLORS, RELATION_TYPE_OPTIONS } from '../../area/data';
const props = defineProps<{
deviceState?: number;
relation: OpsAreaApi.AreaDeviceRelation;
}>();
const emit = defineEmits<{
(e: 'click', relation: OpsAreaApi.AreaDeviceRelation): void;
(e: 'unbind', relation: OpsAreaApi.AreaDeviceRelation): void;
}>();
const nicknameLabel = computed(() => {
return props.relation.nickname || '--';
});
const deviceNameLabel = computed(() => {
return props.relation.deviceName || props.relation.deviceKey || '--';
});
const productLabel = computed(() => {
return props.relation.productName || props.relation.productKey || '--';
});
const relationLabel = computed(() => {
return (
RELATION_TYPE_OPTIONS.find((o) => o.value === props.relation.relationType)
?.label ??
props.relation.relationType ??
'--'
);
});
const relationTagColor = computed(() => {
return (
RELATION_TYPE_COLORS[props.relation.relationType ?? ''] ?? {
bg: '#f5f5f5',
text: '#8c8c8c',
}
);
});
function handleCardClick() {
emit('click', props.relation);
}
function handleMenuClick({ key }: { key: string }) {
if (key === 'detail') {
emit('click', props.relation);
} else if (key === 'unbind') {
emit('unbind', props.relation);
}
}
</script>
<template>
<Card
class="device-card"
hoverable
:body-style="{ padding: '0' }"
@click="handleCardClick"
>
<div class="device-card__body">
<!-- Header: nickname + type badge + dropdown -->
<div class="device-card__header">
<span class="device-card__name" :title="nicknameLabel">
{{ nicknameLabel }}
</span>
<div class="device-card__header-right">
<span
class="device-card__type-badge"
:style="{
background: relationTagColor.bg,
color: relationTagColor.text,
}"
>
{{ relationLabel }}
</span>
<Dropdown :trigger="['click']">
<span class="device-card__more" @click.stop>...</span>
<template #overlay>
<Menu @click="handleMenuClick">
<Menu.Item key="detail">查看详情</Menu.Item>
<Menu.Item key="unbind" danger>解除绑定</Menu.Item>
</Menu>
</template>
</Dropdown>
</div>
</div>
<!-- Key info: deviceName + product -->
<div class="device-card__info">
<div class="device-card__info-row">
<span class="device-card__info-label">设备标识</span>
<span class="device-card__info-value">{{ deviceNameLabel }}</span>
</div>
<div class="device-card__info-row">
<span class="device-card__info-label">产品所属</span>
<span class="device-card__info-value">{{ productLabel }}</span>
</div>
</div>
<!-- Footer: device state + enabled status -->
<div class="device-card__footer">
<Badge
:status="
props.deviceState === 1
? 'success'
: props.deviceState === 0
? 'error'
: 'default'
"
:text="
props.deviceState === 1
? '在线'
: props.deviceState === 0
? '离线'
: '未知'
"
/>
<span class="device-card__footer-sep">|</span>
<Badge
:status="relation.enabled ? 'success' : 'error'"
:text="relation.enabled ? '启用' : '停用'"
/>
</div>
</div>
</Card>
</template>
<style scoped lang="scss">
.device-card {
overflow: hidden;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
transform: translateY(-2px);
}
&__body {
padding: 16px;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
&__name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
font-size: 15px;
font-weight: 600;
color: #262626;
white-space: nowrap;
}
&__header-right {
display: flex;
flex-shrink: 0;
gap: 6px;
align-items: center;
}
&__type-badge {
padding: 2px 10px;
font-size: 12px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
border-radius: 10px;
}
&__more {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 16px;
font-weight: bold;
color: #8c8c8c;
letter-spacing: 1px;
cursor: pointer;
border-radius: 4px;
&:hover {
color: #1677ff;
background: #f0f0f0;
}
}
&__info {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 10px;
font-size: 13px;
}
&__info-row {
display: flex;
gap: 8px;
align-items: center;
}
&__info-label {
flex-shrink: 0;
color: #8c8c8c;
}
&__info-value {
overflow: hidden;
text-overflow: ellipsis;
color: #595959;
white-space: nowrap;
}
&__footer {
display: flex;
gap: 8px;
align-items: center;
}
&__footer-sep {
color: #d9d9d9;
}
}
html.dark {
.device-card__name {
color: rgb(255 255 255 / 85%);
}
.device-card__info-label {
color: rgb(255 255 255 / 45%);
}
.device-card__info-value {
color: rgb(255 255 255 / 65%);
}
.device-card__more:hover {
background: rgb(255 255 255 / 8%);
}
}
</style>