perf:【IoT 物联网】场景联动属性选择器属性详情展示逻辑优化,直接使用el-popover
parent
6c954c4ff1
commit
ab54879203
|
|
@ -37,90 +37,122 @@
|
||||||
</el-option-group>
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<!-- 属性详情触发按钮 -->
|
<!-- 属性详情弹出层 -->
|
||||||
<div class="relative">
|
<el-popover
|
||||||
<el-button
|
v-if="selectedProperty"
|
||||||
v-if="selectedProperty"
|
placement="right-start"
|
||||||
ref="detailTriggerRef"
|
:width="350"
|
||||||
type="info"
|
trigger="click"
|
||||||
:icon="InfoFilled"
|
:show-arrow="true"
|
||||||
circle
|
:offset="8"
|
||||||
size="small"
|
popper-class="property-detail-popover"
|
||||||
@click="togglePropertyDetail"
|
>
|
||||||
class="flex-shrink-0"
|
<template #reference>
|
||||||
title="查看属性详情"
|
<el-button
|
||||||
/>
|
type="info"
|
||||||
|
:icon="InfoFilled"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
title="查看属性详情"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 属性详情弹出层 -->
|
<!-- 弹出层内容 -->
|
||||||
<Teleport to="body">
|
<div class="property-detail-content">
|
||||||
<div
|
<div class="flex items-center gap-8px mb-12px">
|
||||||
v-if="showPropertyDetail && selectedProperty"
|
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
|
||||||
ref="propertyDetailRef"
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
class="property-detail-popover"
|
{{ selectedProperty.name }}
|
||||||
:style="popoverStyle"
|
</span>
|
||||||
>
|
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
|
||||||
|
{{ getPropertyTypeName(selectedProperty.dataType) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-8px ml-24px">
|
||||||
|
<div class="flex items-start gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
|
标识符:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.identifier }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedProperty.description" class="flex items-start gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
|
描述:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedProperty.unit" class="flex items-start gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
|
单位:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.unit }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedProperty.range" class="flex items-start gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
|
取值范围:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.range }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 根据属性类型显示额外信息 -->
|
||||||
<div
|
<div
|
||||||
class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-300px max-w-400px"
|
v-if="
|
||||||
|
selectedProperty.type === IoTThingModelTypeEnum.PROPERTY &&
|
||||||
|
selectedProperty.accessMode
|
||||||
|
"
|
||||||
|
class="flex items-start gap-8px"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-8px mb-12px">
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-4px" />
|
访问模式:
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
</span>
|
||||||
{{ selectedProperty.name }}
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
</span>
|
{{ getAccessModeText(selectedProperty.accessMode) }}
|
||||||
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
|
</span>
|
||||||
{{ getPropertyTypeName(selectedProperty.dataType) }}
|
</div>
|
||||||
</el-tag>
|
|
||||||
</div>
|
<div
|
||||||
<div class="space-y-8px ml-24px">
|
v-if="
|
||||||
<div class="flex items-start gap-8px">
|
selectedProperty.type === IoTThingModelTypeEnum.EVENT && selectedProperty.eventType
|
||||||
<span
|
"
|
||||||
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
class="flex items-start gap-8px"
|
||||||
>
|
>
|
||||||
标识符:
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
</span>
|
事件类型:
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
</span>
|
||||||
{{ selectedProperty.identifier }}
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
</span>
|
{{ getEventTypeText(selectedProperty.eventType) }}
|
||||||
</div>
|
</span>
|
||||||
<div v-if="selectedProperty.description" class="flex items-start gap-8px">
|
</div>
|
||||||
<span
|
|
||||||
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
<div
|
||||||
>
|
v-if="
|
||||||
描述:
|
selectedProperty.type === IoTThingModelTypeEnum.SERVICE && selectedProperty.callType
|
||||||
</span>
|
"
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
class="flex items-start gap-8px"
|
||||||
{{ selectedProperty.description }}
|
>
|
||||||
</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
</div>
|
调用类型:
|
||||||
<div v-if="selectedProperty.unit" class="flex items-start gap-8px">
|
</span>
|
||||||
<span
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
{{ getCallTypeText(selectedProperty.callType) }}
|
||||||
>
|
</span>
|
||||||
单位:
|
|
||||||
</span>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
|
||||||
{{ selectedProperty.unit }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedProperty.range" class="flex items-start gap-8px">
|
|
||||||
<span
|
|
||||||
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
|
||||||
>
|
|
||||||
取值范围:
|
|
||||||
</span>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
|
||||||
{{ selectedProperty.range }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 关闭按钮 -->
|
|
||||||
<div class="flex justify-end mt-12px">
|
|
||||||
<el-button size="small" @click="hidePropertyDetail">关闭</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</div>
|
||||||
</div>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -153,25 +185,6 @@ const loading = ref(false)
|
||||||
const propertyList = ref<PropertySelectorItem[]>([])
|
const propertyList = ref<PropertySelectorItem[]>([])
|
||||||
const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
|
const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
|
||||||
|
|
||||||
// 属性详情弹出层相关状态
|
|
||||||
const showPropertyDetail = ref(false)
|
|
||||||
const detailTriggerRef = ref()
|
|
||||||
const propertyDetailRef = ref()
|
|
||||||
const popoverStyle = ref({})
|
|
||||||
|
|
||||||
// 点击外部关闭弹出层
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
showPropertyDetail.value &&
|
|
||||||
propertyDetailRef.value &&
|
|
||||||
detailTriggerRef.value &&
|
|
||||||
!propertyDetailRef.value.contains(event.target as Node) &&
|
|
||||||
!detailTriggerRef.value.$el.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
hidePropertyDetail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const propertyGroups = computed(() => {
|
const propertyGroups = computed(() => {
|
||||||
const groups: { label: string; options: any[] }[] = []
|
const groups: { label: string; options: any[] }[] = []
|
||||||
|
|
@ -235,65 +248,33 @@ const getPropertyTypeTag = (dataType: string) => {
|
||||||
return tagMap[dataType] || 'info'
|
return tagMap[dataType] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹出层控制方法
|
// 工具函数 - 获取访问模式文本
|
||||||
const togglePropertyDetail = () => {
|
const getAccessModeText = (accessMode: string) => {
|
||||||
if (showPropertyDetail.value) {
|
const modeMap = {
|
||||||
hidePropertyDetail()
|
r: '只读',
|
||||||
} else {
|
w: '只写',
|
||||||
showPropertyDetailPopover()
|
rw: '读写'
|
||||||
}
|
}
|
||||||
|
return modeMap[accessMode] || accessMode
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPropertyDetailPopover = () => {
|
// 工具函数 - 获取事件类型文本
|
||||||
if (!selectedProperty.value || !detailTriggerRef.value) return
|
const getEventTypeText = (eventType: string) => {
|
||||||
|
const typeMap = {
|
||||||
showPropertyDetail.value = true
|
info: '信息',
|
||||||
|
alert: '告警',
|
||||||
nextTick(() => {
|
error: '故障'
|
||||||
updatePopoverPosition()
|
}
|
||||||
})
|
return typeMap[eventType] || eventType
|
||||||
}
|
}
|
||||||
|
|
||||||
const hidePropertyDetail = () => {
|
// 工具函数 - 获取调用类型文本
|
||||||
showPropertyDetail.value = false
|
const getCallTypeText = (callType: string) => {
|
||||||
}
|
const typeMap = {
|
||||||
|
sync: '同步',
|
||||||
const updatePopoverPosition = () => {
|
async: '异步'
|
||||||
if (!detailTriggerRef.value || !propertyDetailRef.value) return
|
|
||||||
|
|
||||||
const triggerEl = detailTriggerRef.value.$el
|
|
||||||
const triggerRect = triggerEl.getBoundingClientRect()
|
|
||||||
const popoverEl = propertyDetailRef.value
|
|
||||||
|
|
||||||
// 计算弹出层位置
|
|
||||||
const left = triggerRect.left + triggerRect.width + 8
|
|
||||||
const top = triggerRect.top
|
|
||||||
|
|
||||||
// 检查是否超出视窗右边界
|
|
||||||
const popoverWidth = 400 // 最大宽度
|
|
||||||
const viewportWidth = window.innerWidth
|
|
||||||
|
|
||||||
let finalLeft = left
|
|
||||||
if (left + popoverWidth > viewportWidth - 16) {
|
|
||||||
// 如果超出右边界,显示在左侧
|
|
||||||
finalLeft = triggerRect.left - popoverWidth - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否超出视窗下边界
|
|
||||||
let finalTop = top
|
|
||||||
const popoverHeight = popoverEl.offsetHeight || 200
|
|
||||||
const viewportHeight = window.innerHeight
|
|
||||||
|
|
||||||
if (top + popoverHeight > viewportHeight - 16) {
|
|
||||||
finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
popoverStyle.value = {
|
|
||||||
position: 'fixed',
|
|
||||||
left: `${finalLeft}px`,
|
|
||||||
top: `${finalTop}px`,
|
|
||||||
zIndex: 9999
|
|
||||||
}
|
}
|
||||||
|
return typeMap[callType] || callType
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
|
|
@ -305,8 +286,7 @@ const handleChange = (value: string) => {
|
||||||
config: property
|
config: property
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 选择变化时隐藏详情弹出层
|
// 选择变化时,el-popover 会自动关闭
|
||||||
hidePropertyDetail()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取物模型TSL数据
|
// 获取物模型TSL数据
|
||||||
|
|
@ -441,21 +421,6 @@ const getPropertyRange = (property: any) => {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据范围描述(保留兼容性)
|
|
||||||
const getDataRange = (dataSpecs: any) => {
|
|
||||||
if (!dataSpecs) return undefined
|
|
||||||
|
|
||||||
if (dataSpecs.min !== undefined && dataSpecs.max !== undefined) {
|
|
||||||
return `${dataSpecs.min}~${dataSpecs.max}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataSpecs.dataSpecsList && Array.isArray(dataSpecs.dataSpecsList)) {
|
|
||||||
return dataSpecs.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听产品变化
|
// 监听产品变化
|
||||||
watch(
|
watch(
|
||||||
() => props.productId,
|
() => props.productId,
|
||||||
|
|
@ -470,74 +435,30 @@ watch(
|
||||||
() => props.triggerType,
|
() => props.triggerType,
|
||||||
() => {
|
() => {
|
||||||
localValue.value = ''
|
localValue.value = ''
|
||||||
hidePropertyDetail()
|
// el-popover 会自动关闭,无需手动处理
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听窗口大小变化,重新计算弹出层位置
|
|
||||||
const handleResize = () => {
|
|
||||||
if (showPropertyDetail.value) {
|
|
||||||
updatePopoverPosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('click', handleClickOutside)
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('click', handleClickOutside)
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@keyframes fadeInScale {
|
/* 下拉选项样式 */
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9) translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select-dropdown__item) {
|
:deep(.el-select-dropdown__item) {
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-detail-popover {
|
/* 弹出层内容样式 */
|
||||||
animation: fadeInScale 0.2s ease-out;
|
.property-detail-content {
|
||||||
transform-origin: top left;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹出层箭头效果(可选) */
|
/* 弹出层自定义样式 */
|
||||||
.property-detail-popover::before {
|
:global(.property-detail-popover) {
|
||||||
position: absolute;
|
/* 可以在这里添加全局弹出层样式 */
|
||||||
top: 20px;
|
max-width: 400px !important;
|
||||||
left: -8px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 8px solid transparent;
|
|
||||||
border-right: 8px solid var(--el-border-color);
|
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-detail-popover::after {
|
:global(.property-detail-popover .el-popover__content) {
|
||||||
position: absolute;
|
padding: 16px !important;
|
||||||
top: 20px;
|
|
||||||
left: -7px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 8px solid transparent;
|
|
||||||
border-right: 8px solid white;
|
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,446 +0,0 @@
|
||||||
<!-- 服务选择器组件 -->
|
|
||||||
<template>
|
|
||||||
<div class="w-full">
|
|
||||||
<el-select
|
|
||||||
:model-value="modelValue"
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
placeholder="请选择服务"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
class="w-full"
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="!productId"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="service in serviceList"
|
|
||||||
:key="service.identifier"
|
|
||||||
:label="service.name"
|
|
||||||
:value="service.identifier"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
|
||||||
<div class="flex items-center gap-12px flex-1">
|
|
||||||
<Icon
|
|
||||||
icon="ep:service"
|
|
||||||
class="text-18px text-[var(--el-color-success)] flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
|
|
||||||
{{ service.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
|
|
||||||
{{ service.identifier }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="service.description"
|
|
||||||
class="text-11px text-[var(--el-text-color-secondary)] mt-2px"
|
|
||||||
>
|
|
||||||
{{ service.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getCallTypeTag(service.callType)" size="small">
|
|
||||||
{{ getCallTypeLabel(service.callType) }}
|
|
||||||
</el-tag>
|
|
||||||
<el-button
|
|
||||||
ref="detailTriggerRef"
|
|
||||||
type="info"
|
|
||||||
:icon="InfoFilled"
|
|
||||||
circle
|
|
||||||
size="small"
|
|
||||||
@click.stop="showServiceDetail(service)"
|
|
||||||
title="查看服务详情"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<!-- 服务详情弹出层 -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<div
|
|
||||||
v-if="showServiceDetailPopover && selectedService"
|
|
||||||
ref="serviceDetailRef"
|
|
||||||
class="service-detail-popover"
|
|
||||||
:style="servicePopoverStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-8px mb-16px">
|
|
||||||
<Icon icon="ep:service" class="text-[var(--el-color-success)] text-18px" />
|
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ selectedService.name }}
|
|
||||||
</span>
|
|
||||||
<el-tag :type="getCallTypeTag(selectedService.callType)" size="small">
|
|
||||||
{{ getCallTypeLabel(selectedService.callType) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-16px">
|
|
||||||
<!-- 基本信息 -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:info" class="text-[var(--el-color-info)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">基本信息</span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-22px space-y-4px">
|
|
||||||
<div class="text-12px">
|
|
||||||
<span class="text-[var(--el-text-color-secondary)]">标识符:</span>
|
|
||||||
<span class="text-[var(--el-text-color-primary)]">{{
|
|
||||||
selectedService.identifier
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedService.description" class="text-12px">
|
|
||||||
<span class="text-[var(--el-text-color-secondary)]">描述:</span>
|
|
||||||
<span class="text-[var(--el-text-color-primary)]">{{
|
|
||||||
selectedService.description
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-12px">
|
|
||||||
<span class="text-[var(--el-text-color-secondary)]">调用方式:</span>
|
|
||||||
<span class="text-[var(--el-text-color-primary)]">{{
|
|
||||||
getCallTypeLabel(selectedService.callType)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 输入参数 -->
|
|
||||||
<div v-if="selectedService.inputParams && selectedService.inputParams.length > 0">
|
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:download" class="text-[var(--el-color-primary)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">输入参数</span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-22px space-y-8px">
|
|
||||||
<div
|
|
||||||
v-for="param in selectedService.inputParams"
|
|
||||||
:key="param.identifier"
|
|
||||||
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ param.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-11px text-[var(--el-text-color-secondary)]">
|
|
||||||
{{ param.identifier }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getParamTypeTag(param.dataType)" size="small">
|
|
||||||
{{ getParamTypeName(param.dataType) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 输出参数 -->
|
|
||||||
<div v-if="selectedService.outputParams && selectedService.outputParams.length > 0">
|
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:upload" class="text-[var(--el-color-warning)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">输出参数</span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-22px space-y-8px">
|
|
||||||
<div
|
|
||||||
v-for="param in selectedService.outputParams"
|
|
||||||
:key="param.identifier"
|
|
||||||
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ param.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-11px text-[var(--el-text-color-secondary)]">
|
|
||||||
{{ param.identifier }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getParamTypeTag(param.dataType)" size="small">
|
|
||||||
{{ getParamTypeName(param.dataType) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 关闭按钮 -->
|
|
||||||
<div class="flex justify-end mt-16px">
|
|
||||||
<el-button size="small" @click="hideServiceDetail">关闭</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
|
||||||
import { ThingModelApi } from '@/api/iot/thingmodel'
|
|
||||||
import { ThingModelService } from '@/api/iot/rule/scene/scene.types'
|
|
||||||
import { getThingModelServiceCallTypeLabel } from '@/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 服务选择器组件 */
|
|
||||||
defineOptions({ name: 'ServiceSelector' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue?: string
|
|
||||||
productId?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value?: string): void
|
|
||||||
(e: 'change', value?: string, service?: ThingModelService): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// TODO @puhui999:这里不用的
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
// 状态
|
|
||||||
const loading = ref(false)
|
|
||||||
const serviceList = ref<ThingModelService[]>([])
|
|
||||||
const showServiceDetailPopover = ref(false)
|
|
||||||
const selectedService = ref<ThingModelService | null>(null)
|
|
||||||
const detailTriggerRef = ref()
|
|
||||||
const serviceDetailRef = ref()
|
|
||||||
const servicePopoverStyle = ref({})
|
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const handleChange = (value?: string) => {
|
|
||||||
// 更新 modelValue(这是 v-model 绑定的关键)
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
|
|
||||||
// 触发 change 事件,传递服务对象
|
|
||||||
const service = serviceList.value.find((s) => s.identifier === value)
|
|
||||||
emit('change', value, service)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取物模型TSL数据
|
|
||||||
const getThingModelTSL = async () => {
|
|
||||||
if (!props.productId) {
|
|
||||||
serviceList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
|
|
||||||
serviceList.value = tslData?.services || []
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取物模型TSL失败:', error)
|
|
||||||
serviceList.value = []
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
const getCallTypeLabel = (callType: string) => {
|
|
||||||
return getThingModelServiceCallTypeLabel(callType) || callType
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCallTypeTag = (callType: string) => {
|
|
||||||
return callType === 'sync' ? 'primary' : 'success'
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @puhui999:一些注释风格;
|
|
||||||
const getParamTypeName = (dataType: string) => {
|
|
||||||
const typeMap = {
|
|
||||||
int: '整数',
|
|
||||||
float: '浮点数',
|
|
||||||
double: '双精度',
|
|
||||||
text: '字符串',
|
|
||||||
bool: '布尔值',
|
|
||||||
enum: '枚举',
|
|
||||||
date: '日期',
|
|
||||||
struct: '结构体',
|
|
||||||
array: '数组'
|
|
||||||
}
|
|
||||||
return typeMap[dataType] || dataType
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParamTypeTag = (dataType: string) => {
|
|
||||||
const tagMap = {
|
|
||||||
int: 'primary',
|
|
||||||
float: 'success',
|
|
||||||
double: 'success',
|
|
||||||
text: 'info',
|
|
||||||
bool: 'warning',
|
|
||||||
enum: 'danger',
|
|
||||||
date: 'primary',
|
|
||||||
struct: 'info',
|
|
||||||
array: 'warning'
|
|
||||||
}
|
|
||||||
return tagMap[dataType] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务详情弹出层控制
|
|
||||||
const showServiceDetail = (service: ThingModelService) => {
|
|
||||||
selectedService.value = service
|
|
||||||
showServiceDetailPopover.value = true
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
updateServicePopoverPosition()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideServiceDetail = () => {
|
|
||||||
showServiceDetailPopover.value = false
|
|
||||||
selectedService.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateServicePopoverPosition = () => {
|
|
||||||
if (!detailTriggerRef.value || !serviceDetailRef.value) return
|
|
||||||
|
|
||||||
const triggerEl = detailTriggerRef.value.$el
|
|
||||||
const triggerRect = triggerEl.getBoundingClientRect()
|
|
||||||
|
|
||||||
// 计算弹出层位置
|
|
||||||
const left = triggerRect.left + triggerRect.width + 8
|
|
||||||
const top = triggerRect.top
|
|
||||||
|
|
||||||
// 检查是否超出视窗右边界
|
|
||||||
const popoverWidth = 500
|
|
||||||
const viewportWidth = window.innerWidth
|
|
||||||
|
|
||||||
let finalLeft = left
|
|
||||||
if (left + popoverWidth > viewportWidth - 16) {
|
|
||||||
finalLeft = triggerRect.left - popoverWidth - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否超出视窗下边界
|
|
||||||
let finalTop = top
|
|
||||||
const popoverHeight = serviceDetailRef.value.offsetHeight || 300
|
|
||||||
const viewportHeight = window.innerHeight
|
|
||||||
|
|
||||||
if (top + popoverHeight > viewportHeight - 16) {
|
|
||||||
finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
servicePopoverStyle.value = {
|
|
||||||
position: 'fixed',
|
|
||||||
left: `${finalLeft}px`,
|
|
||||||
top: `${finalTop}px`,
|
|
||||||
zIndex: 9999
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听产品变化
|
|
||||||
watch(
|
|
||||||
() => props.productId,
|
|
||||||
() => {
|
|
||||||
getThingModelTSL()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听modelValue变化,处理编辑模式的回显
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue && serviceList.value.length > 0) {
|
|
||||||
// 确保服务列表已加载,然后设置选中的服务
|
|
||||||
const service = serviceList.value.find((s) => s.identifier === newValue)
|
|
||||||
if (service) {
|
|
||||||
selectedService.value = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听服务列表变化,处理异步加载后的回显
|
|
||||||
watch(
|
|
||||||
() => serviceList.value,
|
|
||||||
(newServiceList) => {
|
|
||||||
if (newServiceList.length > 0 && props.modelValue) {
|
|
||||||
// 服务列表加载完成后,如果有modelValue,设置选中的服务
|
|
||||||
const service = newServiceList.find((s) => s.identifier === props.modelValue)
|
|
||||||
if (service) {
|
|
||||||
selectedService.value = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听窗口大小变化
|
|
||||||
const handleResize = () => {
|
|
||||||
if (showServiceDetailPopover.value) {
|
|
||||||
updateServicePopoverPosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击外部关闭弹出层
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
showServiceDetailPopover.value &&
|
|
||||||
serviceDetailRef.value &&
|
|
||||||
detailTriggerRef.value &&
|
|
||||||
!serviceDetailRef.value.contains(event.target as Node) &&
|
|
||||||
!detailTriggerRef.value.$el.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
hideServiceDetail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('click', handleClickOutside)
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('click', handleClickOutside)
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@keyframes fadeInScale {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9) translateY(-4px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-detail-popover {
|
|
||||||
animation: fadeInScale 0.2s ease-out;
|
|
||||||
transform-origin: top left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-detail-popover::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: -8px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 8px solid transparent;
|
|
||||||
border-right: 8px solid var(--el-border-color);
|
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-detail-popover::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: -7px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 8px solid transparent;
|
|
||||||
border-right: 8px solid white;
|
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select-dropdown__item) {
|
|
||||||
height: auto;
|
|
||||||
padding: 8px 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Loading…
Reference in New Issue