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 { PageResult } from '#/api/common';
|
||||
import type { ID, IDS, PageResult } from '#/api/common';
|
||||
|
||||
import { commonExport } from '#/api/helper';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 查询设备接入网关列表
|
||||
* @param params
|
||||
* @returns 设备接入网关列表
|
||||
*/
|
||||
* 查询设备接入网关列表
|
||||
* @param params
|
||||
* @returns 设备接入网关列表
|
||||
*/
|
||||
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 { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { productUpdate, productUpdateStatus } from '#/api/device/product';
|
||||
import { useProductStore } from '#/store/product';
|
||||
|
|
Loading…
Reference in New Issue