左侧区域树 + 右侧设备卡片网格布局,点击卡片打开详情 Drawer, 支持设备基础信息查看、类型化实时数据展示(客流趋势/工牌状态)、 业务配置编辑及设备绑定/解绑操作。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
261 lines
5.6 KiB
Vue
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>
|