feat: 新增产品接入方式选择功能
- 添加 AccessSelector组件用于选择接入方式 - 实现设备接入方式的查询和展示 - 增加接入方式选择和更换功能 - 优化产品详情页面布局
This commit is contained in:
parent
1e5302ee64
commit
b86957758a
|
@ -1,18 +1,19 @@
|
||||||
import type { GatewayVO, GatewayForm, GatewayQuery } from './model';
|
import type { GatewayForm, GatewayQuery, GatewayVO } from './model';
|
||||||
|
|
||||||
import type { ID, IDS } from '#/api/common';
|
import type { ID, IDS, PageResult } from '#/api/common';
|
||||||
import type { PageResult } from '#/api/common';
|
|
||||||
|
|
||||||
import { commonExport } from '#/api/helper';
|
import { commonExport } from '#/api/helper';
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询设备接入网关列表
|
* 查询设备接入网关列表
|
||||||
* @param params
|
* @param params
|
||||||
* @returns 设备接入网关列表
|
* @returns 设备接入网关列表
|
||||||
*/
|
*/
|
||||||
export function gatewayList(params?: GatewayQuery) {
|
export function gatewayList(params?: GatewayQuery) {
|
||||||
return requestClient.get<PageResult<GatewayVO>>('/operations/gateway/list', { params });
|
return requestClient.get<PageResult<GatewayVO>>('/operations/gateway/list', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,460 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Pagination,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { gatewayList } from '#/api/operations/gateway';
|
||||||
|
import { enabledOptions, networkTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
select: [access: any];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const selectedGateway = ref<any>(null);
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = ref({
|
||||||
|
name: '',
|
||||||
|
provider: undefined,
|
||||||
|
enabled: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 4,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total: number, range: [number, number]) =>
|
||||||
|
`第 ${range[0]}-${range[1]}条/总共${total}条`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 网关列表数据
|
||||||
|
const gatewayListData = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 接入方式选项
|
||||||
|
const providerOptions = [
|
||||||
|
...networkTypeOptions,
|
||||||
|
{
|
||||||
|
label: '网关子设备接入',
|
||||||
|
value: 'child-device',
|
||||||
|
channel: 'child-device',
|
||||||
|
transport: 'Gateway',
|
||||||
|
description:
|
||||||
|
'需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 加载网关列表数据
|
||||||
|
const loadGatewayList = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response = await gatewayList({
|
||||||
|
pageNum: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
...searchForm.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.rows) {
|
||||||
|
gatewayListData.value = response.rows.map((item) => ({
|
||||||
|
...item,
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
|
pagination.total = response.total;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载网关列表失败:', error);
|
||||||
|
message.error('加载网关列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.value.name = '';
|
||||||
|
searchForm.value.provider = undefined;
|
||||||
|
searchForm.value.enabled = undefined;
|
||||||
|
pagination.current = 1;
|
||||||
|
loadGatewayList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1;
|
||||||
|
loadGatewayList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
|
pagination.current = page;
|
||||||
|
pagination.pageSize = pageSize;
|
||||||
|
loadGatewayList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择网关
|
||||||
|
const selectGateway = (gateway: any) => {
|
||||||
|
// 清除其他选中状态
|
||||||
|
gatewayListData.value.forEach((item) => {
|
||||||
|
item.selected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置当前选中状态
|
||||||
|
gateway.selected = true;
|
||||||
|
selectedGateway.value = gateway;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
const confirmSelection = () => {
|
||||||
|
if (!selectedGateway.value) {
|
||||||
|
message.warning('请先选择一个接入方式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('select', selectedGateway.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态标签颜色
|
||||||
|
const getStatusColor = (enabled: string) => {
|
||||||
|
return enabled === '1' ? 'success' : 'error';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态标签文本
|
||||||
|
const getStatusText = (enabled: string) => {
|
||||||
|
return enabled === '1' ? '启用' : '禁用';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取接入方式标签颜色
|
||||||
|
const getProviderColor = (provider: string) => {
|
||||||
|
switch (provider) {
|
||||||
|
case 'child-device': {
|
||||||
|
return 'purple';
|
||||||
|
}
|
||||||
|
case 'HTTP_SERVER': {
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
case 'MQTT_CLIENT': {
|
||||||
|
return 'blue';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取接入方式标签文本
|
||||||
|
const getProviderText = (provider: string) => {
|
||||||
|
const option = providerOptions.find((item) => item.value === provider);
|
||||||
|
return option ? option.label : provider;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadGatewayList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="access-selector">
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<div class="search-section">
|
||||||
|
<Form layout="inline" :model="searchForm">
|
||||||
|
<FormItem label="名称">
|
||||||
|
<Input
|
||||||
|
v-model="searchForm.name"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
allow-clear
|
||||||
|
style="width: 140px"
|
||||||
|
@input="(e) => (searchForm.name = e.target.value)"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="接入方式">
|
||||||
|
<Select
|
||||||
|
v-model="searchForm.provider"
|
||||||
|
placeholder="请选择"
|
||||||
|
allow-clear
|
||||||
|
style="width: 140px"
|
||||||
|
:options="providerOptions"
|
||||||
|
@change="(value) => (searchForm.provider = value)"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="状态">
|
||||||
|
<Select
|
||||||
|
v-model="searchForm.enabled"
|
||||||
|
placeholder="请选择"
|
||||||
|
allow-clear
|
||||||
|
style="width: 140px"
|
||||||
|
:options="enabledOptions"
|
||||||
|
@change="(value) => (searchForm.enabled = value)"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Space>
|
||||||
|
<Button @click="resetSearch">重置</Button>
|
||||||
|
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||||
|
</Space>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 网关列表 -->
|
||||||
|
<div class="gateway-list" v-loading="loading">
|
||||||
|
<Row :gutter="[16, 16]">
|
||||||
|
<Col v-for="gateway in gatewayListData" :key="gateway.id" :span="12">
|
||||||
|
<Card
|
||||||
|
class="gateway-card"
|
||||||
|
:class="[{ selected: gateway.selected }]"
|
||||||
|
hoverable
|
||||||
|
@click="selectGateway(gateway)"
|
||||||
|
>
|
||||||
|
<div class="gateway-main">
|
||||||
|
<div class="gateway-icon">
|
||||||
|
<div class="icon-placeholder">
|
||||||
|
{{ gateway.provider.toUpperCase().slice(0, 12) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gateway-content">
|
||||||
|
<div class="gateway-header">
|
||||||
|
<h4 class="gateway-name">{{ gateway.name }}</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tag :color="getProviderColor(gateway.provider)">
|
||||||
|
{{ getProviderText(gateway.provider) }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<Tag
|
||||||
|
class="gateway-status"
|
||||||
|
:color="getStatusColor(gateway.enabled)"
|
||||||
|
>
|
||||||
|
{{ getStatusText(gateway.enabled) }}
|
||||||
|
</Tag>
|
||||||
|
<div class="gateway-footer">
|
||||||
|
<div class="gateway-footer-item" v-if="gateway.channelName">
|
||||||
|
<div class="gateway-footer-item-label">网络组件</div>
|
||||||
|
<div class="gateway-footer-item-value">
|
||||||
|
{{ gateway.channelName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gateway-footer-item">
|
||||||
|
<div class="gateway-footer-item-label">消息协议</div>
|
||||||
|
<div class="gateway-footer-item-value">
|
||||||
|
{{ gateway.protocolName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="selected-indicator" v-if="gateway.selected">
|
||||||
|
<div class="checkmark">✓</div>
|
||||||
|
</div>
|
||||||
|
<!-- <p class="gateway-description">{{ gateway.description }}</p> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="gatewayListData.length === 0" class="empty-state">
|
||||||
|
<Empty description="暂无符合条件的接入方式" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div v-if="gatewayListData.length > 0" class="pagination-section">
|
||||||
|
<Pagination
|
||||||
|
:current="pagination.current"
|
||||||
|
:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:show-size-changer="pagination.showSizeChanger"
|
||||||
|
:show-quick-jumper="pagination.showQuickJumper"
|
||||||
|
:show-total="pagination.showTotal"
|
||||||
|
@change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<div class="footer-actions">
|
||||||
|
<Space>
|
||||||
|
<Button @click="handleCancel">取消</Button>
|
||||||
|
<Button type="primary" @click="confirmSelection">确定</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.access-selector {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-list {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.gateway-card {
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #e4e4e7;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 4px 12px rgb(24 144 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: #f0f8ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 4px 12px rgb(24 144 255 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-main {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.gateway-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.icon-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
background: #1890ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.gateway-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.gateway-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-description {
|
||||||
|
display: -webkit-box;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.gateway-footer-item {
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
.gateway-footer-item-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-footer-item-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
background: #1890ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
padding-top: 16px;
|
||||||
|
text-align: right;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,543 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ProductVO } from '#/api/device/product/model';
|
||||||
|
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Col,
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
InputPassword,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
Tooltip,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getPoliciesList, productUpdateById } from '#/api/device/product';
|
||||||
|
import { gatewayInfo } from '#/api/operations/gateway';
|
||||||
|
|
||||||
|
import AccessSelector from './AccessSelector.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
productInfo: ProductVO;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
refresh: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
const accessModalVisible = ref(false);
|
||||||
|
const saveLoading = ref(false);
|
||||||
|
// const selectedProductType = ref('');
|
||||||
|
const selectedStorePolicy = ref('default');
|
||||||
|
|
||||||
|
const formData = reactive({});
|
||||||
|
|
||||||
|
// 接入信息
|
||||||
|
const accessInfo = ref({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
protocolName: '',
|
||||||
|
document: '',
|
||||||
|
addresses: [],
|
||||||
|
routes: [],
|
||||||
|
provider: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存储策略选项
|
||||||
|
const storePolicyOptions = ref([
|
||||||
|
{ label: 'influxdb-列式', value: 'influxdb' },
|
||||||
|
// { label: '高性能策略', value: 'high-performance' },
|
||||||
|
// { label: '低成本策略', value: 'low-cost' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 接入配置
|
||||||
|
const accessConfigs = ref([]);
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const tableColumns = computed(() => {
|
||||||
|
const isMQTT =
|
||||||
|
accessInfo.value.provider === 'mqtt-server-gateway' ||
|
||||||
|
accessInfo.value.provider === 'mqtt-client-gateway';
|
||||||
|
|
||||||
|
return isMQTT
|
||||||
|
? [
|
||||||
|
{ title: 'Topic', dataIndex: 'topic', key: 'topic', width: 200 },
|
||||||
|
{ title: '描述', dataIndex: 'description', key: 'description' },
|
||||||
|
{ title: '示例', dataIndex: 'example', key: 'example', width: 150 },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ title: '地址', dataIndex: 'address', key: 'address', width: 200 },
|
||||||
|
{ title: '描述', dataIndex: 'description', key: 'description' },
|
||||||
|
{ title: '示例', dataIndex: 'example', key: 'example', width: 150 },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Markdown转HTML
|
||||||
|
const markdownToHtml = computed(() => {
|
||||||
|
// 这里可以使用markdown-it等库转换markdown为HTML
|
||||||
|
return accessInfo.value.document || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选择接入方式
|
||||||
|
const handleSelectAccess = () => {
|
||||||
|
accessModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更换接入方式
|
||||||
|
const handleChangeAccess = () => {
|
||||||
|
accessModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接入方式选择回调
|
||||||
|
const handleModalSelect = (access: any) => {
|
||||||
|
console.log('access', access);
|
||||||
|
accessInfo.value = access;
|
||||||
|
accessModalVisible.value = false;
|
||||||
|
emit('refresh');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭接入方式抽屉
|
||||||
|
const handleAccessModalClose = () => {
|
||||||
|
accessModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 产品类型变更
|
||||||
|
// const handleProductTypeChange = (value: string) => {
|
||||||
|
// selectedProductType.value = value;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 获取选项
|
||||||
|
const getOptions = (item: any) => {
|
||||||
|
if (item?.type?.type === 'enum' && item?.type?.elements) {
|
||||||
|
return item.type.elements.map((el: any) => ({
|
||||||
|
label: el.text,
|
||||||
|
value: el.value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取流信息
|
||||||
|
const getStream = (record: any) => {
|
||||||
|
return record.stream || '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
saveLoading.value = true;
|
||||||
|
await productUpdateById(props.productInfo.id, {
|
||||||
|
id: props.productInfo.id,
|
||||||
|
provider: accessInfo.value.id,
|
||||||
|
storePolicy: selectedStorePolicy.value,
|
||||||
|
protocolConf: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success('保存成功');
|
||||||
|
emit('refresh');
|
||||||
|
} catch {
|
||||||
|
message.error('保存失败');
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadStorePolicy = async () => {
|
||||||
|
const res = await getPoliciesList();
|
||||||
|
storePolicyOptions.value = res.map((item: any) => ({
|
||||||
|
label: item.policyName,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载接入信息
|
||||||
|
const loadAccessInfo = async () => {
|
||||||
|
if (props.productInfo.provider) {
|
||||||
|
// 这里调用API加载接入信息
|
||||||
|
const res = await gatewayInfo(props.productInfo.provider);
|
||||||
|
accessInfo.value = res;
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
// accessInfo.value = {
|
||||||
|
// name: 'MQTT接入',
|
||||||
|
// description: '支持MQTT协议的设备接入',
|
||||||
|
// protocolName: 'MQTT',
|
||||||
|
// document: '',
|
||||||
|
// addresses: [
|
||||||
|
// { address: 'mqtt://localhost:1883', health: 1 },
|
||||||
|
// { address: 'mqtt://192.168.1.100:1883', health: -1 },
|
||||||
|
// ],
|
||||||
|
// routes: [
|
||||||
|
// {
|
||||||
|
// topic: '/device/+/data',
|
||||||
|
// description: '设备数据上报',
|
||||||
|
// example: '{"temp": 25}',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// topic: '/device/+/command',
|
||||||
|
// description: '设备命令下发',
|
||||||
|
// example: '{"cmd": "restart"}',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// provider: 'mqtt-server-gateway',
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadAccessInfo();
|
||||||
|
loadStorePolicy();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="device-access">
|
||||||
|
<!-- 未配置接入方式时的空状态 -->
|
||||||
|
<div v-if="!accessInfo.id" class="empty-state">
|
||||||
|
<Empty>
|
||||||
|
<template #description>
|
||||||
|
<span v-if="hasAccessByCodes('device:product:edit')">
|
||||||
|
请先
|
||||||
|
<a-button type="link" @click="handleSelectAccess">选择</a-button>
|
||||||
|
设备接入网关,用以提供设备接入能力
|
||||||
|
</span>
|
||||||
|
<span v-else>请联系管理员配置产品接入方式</span>
|
||||||
|
</template>
|
||||||
|
</Empty>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已配置接入方式 -->
|
||||||
|
<div v-else class="access-config">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="12">
|
||||||
|
<!-- 接入方式 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h4>接入方式</h4>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleChangeAccess"
|
||||||
|
v-access:code="['device:product:edit']"
|
||||||
|
:disabled="productInfo.deviceNum > 0"
|
||||||
|
>
|
||||||
|
更换
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<p class="access-name">{{ accessInfo.name }}</p>
|
||||||
|
<p class="access-desc">{{ accessInfo.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息协议 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h4>消息协议</h4>
|
||||||
|
<Tooltip title="此配置来自于产品接入方式所选择的协议">
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<p>{{ accessInfo.protocolName }}</p>
|
||||||
|
<div v-if="accessInfo.document" v-html="markdownToHtml"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 连接信息 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h4>连接信息</h4>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div
|
||||||
|
v-if="accessInfo.addresses && accessInfo.addresses.length > 0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="item in accessInfo.addresses"
|
||||||
|
:key="item.address"
|
||||||
|
class="address-item"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
:color="item.health === -1 ? 'red' : 'green'"
|
||||||
|
:text="item.address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-address">暂无连接信息</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 产品类型 -->
|
||||||
|
<!-- <div class="config-section" v-if="productTypes.length > 0">
|
||||||
|
<div class="section-header">
|
||||||
|
<h4>产品类型</h4>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<Form layout="vertical">
|
||||||
|
<FormItem
|
||||||
|
label="产品类型"
|
||||||
|
:rules="[{ required: true, message: '请选择产品类型' }]"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="selectedProductType"
|
||||||
|
:options="productTypes"
|
||||||
|
placeholder="请选择产品类型"
|
||||||
|
@change="handleProductTypeChange"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 其它接入配置 -->
|
||||||
|
<div
|
||||||
|
v-for="(config, index) in accessConfigs"
|
||||||
|
:key="index"
|
||||||
|
class="config-section"
|
||||||
|
>
|
||||||
|
<div class="section-header">
|
||||||
|
<h4>{{ config.name }}</h4>
|
||||||
|
<Tooltip title="此配置来自于产品接入方式所选择的协议">
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<Form layout="vertical">
|
||||||
|
<FormItem
|
||||||
|
v-for="item in config.properties"
|
||||||
|
:key="item.property"
|
||||||
|
:label="item.name"
|
||||||
|
:name="item.property"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: !!item?.type?.expands?.required,
|
||||||
|
message: `${
|
||||||
|
item.type.type === 'enum' || 'boolean'
|
||||||
|
? '请选择'
|
||||||
|
: '请输入'
|
||||||
|
}${item.name}`,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-if="item.type.type === 'string'"
|
||||||
|
v-model:value="formData[item.property]"
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
<InputPassword
|
||||||
|
v-if="item.type.type === 'password'"
|
||||||
|
v-model:value="formData[item.property]"
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-if="
|
||||||
|
item.type.type === 'enum' || item.type.type === 'boolean'
|
||||||
|
"
|
||||||
|
v-model:value="formData[item.property]"
|
||||||
|
placeholder="请选择"
|
||||||
|
:options="getOptions(item)"
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
v-if="
|
||||||
|
['int', 'float', 'double', 'long'].includes(
|
||||||
|
item.type.type,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-model:value="formData[item.property]"
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 存储策略 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h4>存储策略</h4>
|
||||||
|
<Tooltip
|
||||||
|
title="若修改存储策略,需要手动做数据迁移,平台只能搜索最新存储策略中的数据"
|
||||||
|
>
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<Select
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="selectedStorePolicy"
|
||||||
|
placeholder="请选择存储策略"
|
||||||
|
:options="storePolicyOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 保存按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
:loading="saveLoading"
|
||||||
|
v-access:code="['device:product:edit']"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 右侧信息展示 -->
|
||||||
|
<Col
|
||||||
|
:span="12"
|
||||||
|
v-if="accessInfo.routes && accessInfo.routes.length > 0"
|
||||||
|
>
|
||||||
|
<div class="info-panel">
|
||||||
|
<h4>
|
||||||
|
{{
|
||||||
|
accessInfo.provider === 'mqtt-server-gateway' ||
|
||||||
|
accessInfo.provider === 'mqtt-client-gateway'
|
||||||
|
? 'Topic信息'
|
||||||
|
: 'URL信息'
|
||||||
|
}}
|
||||||
|
</h4>
|
||||||
|
<Table
|
||||||
|
:columns="tableColumns"
|
||||||
|
:data-source="accessInfo.routes"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ y: 500 }"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ text, column, record }">
|
||||||
|
<template v-if="column.key === 'topic'">
|
||||||
|
<a-tooltip placement="topLeft" :title="text">
|
||||||
|
<div class="ellipsis-text">{{ text }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'stream'">
|
||||||
|
<div>{{ getStream(record) }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'description'">
|
||||||
|
<a-tooltip placement="topLeft" :title="text">
|
||||||
|
<div class="ellipsis-text">{{ text }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'address'">
|
||||||
|
<a-tooltip placement="topLeft" :title="text">
|
||||||
|
<div class="ellipsis-text">{{ text }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'example'">
|
||||||
|
<a-tooltip placement="topLeft" :title="text">
|
||||||
|
<div class="ellipsis-text">{{ text }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 选择接入方式抽屉 -->
|
||||||
|
<Modal
|
||||||
|
:open="accessModalVisible"
|
||||||
|
title="选择接入方式"
|
||||||
|
width="1000px"
|
||||||
|
centered
|
||||||
|
@cancel="handleAccessModalClose"
|
||||||
|
:footer="null"
|
||||||
|
>
|
||||||
|
<AccessSelector
|
||||||
|
:product-id="productInfo.id"
|
||||||
|
@select="handleModalSelect"
|
||||||
|
@close="handleAccessModalClose"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-access {
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-config {
|
||||||
|
.config-section {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
.access-name {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-desc {
|
||||||
|
margin: 0;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-address {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis-text {
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,12 +2,10 @@
|
||||||
import { computed, onMounted, onUnmounted } from 'vue';
|
import { computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||||
import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import { productUpdate, productUpdateStatus } from '#/api/device/product';
|
import { productUpdate, productUpdateStatus } from '#/api/device/product';
|
||||||
import { useProductStore } from '#/store/product';
|
import { useProductStore } from '#/store/product';
|
||||||
|
|
Loading…
Reference in New Issue