20260530:需求第2版探索

This commit is contained in:
George 2026-05-30 10:42:29 +08:00
parent 8ffdd7d5cb
commit d4ad1eab67
24 changed files with 1190 additions and 213 deletions

View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(python3 -c \"import pdfplumber; pdf = pdfplumber.open\\('D:/SoftProject/Java/fastbee/docs/platform/DCP接口文档\\(轴力计\\).pdf'\\); [print\\(p.extract_text\\(\\)\\) for p in pdf.pages[:15]]; pdf.close\\(\\)\")",
"Bash(mvn compile *)"
]
}
}

View File

@ -64,4 +64,12 @@ public class PlatformApiController extends BaseController {
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(platformApiService.deletePlatformApiByIds(ids));
}
@PreAuthorize("@ss.hasPermi('iot:platformConnect:api:edit')")
@PostMapping("/test")
@ApiOperation("测试接口连通性(使用当前表单数据,不依赖已保存记录)")
public AjaxResult test(@RequestBody PlatformApi platformApi,
@RequestParam(required = false) Long tenantChannelId) {
return success(platformApiService.testApi(platformApi, tenantChannelId));
}
}

View File

@ -44,7 +44,11 @@ public class PlatformBindingController extends BaseController {
@PostMapping
@ApiOperation("新增设备绑定")
public AjaxResult add(@RequestBody PlatformBinding binding) {
return toAjax(platformBindingService.insertPlatformBinding(binding));
Boolean result = platformBindingService.insertPlatformBinding(binding);
if (result) {
return success(binding.getId());
}
return error("新增失败");
}
@PreAuthorize("@ss.hasPermi('iot:platformConnect:binding:edit')")
@ -62,4 +66,11 @@ public class PlatformBindingController extends BaseController {
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(platformBindingService.deletePlatformBindingByIds(ids));
}
@PreAuthorize("@ss.hasPermi('iot:platformConnect:binding:edit')")
@PostMapping("/generateDefaultMappings")
@ApiOperation("根据设备类型生成默认字段映射")
public AjaxResult generateDefaultMappings(@RequestParam Long bindingId, @RequestParam String deviceType) {
return success(platformBindingService.generateDefaultMappings(bindingId, deviceType));
}
}

View File

@ -64,4 +64,15 @@ public class PlatformTenantChannelController extends BaseController {
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(platformTenantChannelService.deletePlatformTenantChannelByIds(ids));
}
@PreAuthorize("@ss.hasPermi('iot:platformConnect:tenantChannel:edit')")
@PostMapping("/fetchRemoteInfo")
@ApiOperation("获取第三方平台远程信息(用户ID、项目列表)")
public AjaxResult fetchRemoteInfo(@RequestBody PlatformTenantChannel tenantChannel) {
try {
return success(platformTenantChannelService.fetchRemoteInfo(tenantChannel));
} catch (Exception e) {
return error(e.getMessage());
}
}
}

View File

@ -2,6 +2,7 @@ package com.fastbee.iot.platform.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fastbee.iot.platform.domain.PlatformApi;
import com.fastbee.iot.platform.model.PlatformHttpResponse;
import java.util.List;
@ -15,4 +16,6 @@ public interface IPlatformApiService extends IService<PlatformApi> {
Boolean updatePlatformApi(PlatformApi entity);
Boolean deletePlatformApiByIds(Long[] ids);
PlatformHttpResponse testApi(PlatformApi api, Long tenantChannelId);
}

View File

@ -2,6 +2,7 @@ package com.fastbee.iot.platform.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fastbee.iot.platform.domain.PlatformBinding;
import com.fastbee.iot.platform.domain.PlatformFieldMapping;
import java.util.List;
@ -15,4 +16,9 @@ public interface IPlatformBindingService extends IService<PlatformBinding> {
Boolean updatePlatformBinding(PlatformBinding entity);
Boolean deletePlatformBindingByIds(Long[] ids);
/**
* 根据设备类型生成默认字段映射
*/
List<PlatformFieldMapping> generateDefaultMappings(Long bindingId, String deviceType);
}

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.fastbee.iot.platform.domain.PlatformTenantChannel;
import java.util.List;
import java.util.Map;
public interface IPlatformTenantChannelService extends IService<PlatformTenantChannel> {
List<PlatformTenantChannel> selectPlatformTenantChannelList(PlatformTenantChannel query);
@ -15,4 +16,9 @@ public interface IPlatformTenantChannelService extends IService<PlatformTenantCh
Boolean updatePlatformTenantChannel(PlatformTenantChannel entity);
Boolean deletePlatformTenantChannelByIds(Long[] ids);
/**
* 获取第三方平台远程信息(用户ID、项目列表)
*/
Map<String, Object> fetchRemoteInfo(PlatformTenantChannel tenantChannel);
}

View File

@ -101,25 +101,7 @@ public class PlatformHttpClient {
// 7. 设置 Body
String contentType = StringUtils.isNotEmpty(api.getContentType()) ? api.getContentType() : "application/json";
String body = buildBody(api.getBodyTemplate(), contentType, variables);
if (StringUtils.isNotEmpty(body)) {
if (contentType.contains("json")) {
request.addHeader("Content-Type", "application/json");
request.addBody(body);
} else if (contentType.contains("form")) {
request.addHeader("Content-Type", "application/x-www-form-urlencoded");
JSONObject formData = JSON.parseObject(body);
if (formData != null) {
for (Map.Entry<String, Object> entry : formData.entrySet()) {
request.addBody(entry.getKey(), entry.getValue());
}
}
} else {
request.addHeader("Content-Type", contentType);
request.addBody(body);
}
result.setRequestBody(body);
}
applyBody(request, api.getBodyTemplate(), contentType, variables, result);
// 8. 执行请求
ForestResponse<?> response = request.execute(ForestResponse.class);
@ -170,7 +152,62 @@ public class PlatformHttpClient {
}
/**
* 构建请求体
* 解析 bodyTemplate 并设置到请求中
* bodyTemplate 可能是:
* 1. null/空 — 无 body
* 2. 带 mode 字段的 JSON:{"mode":"x-www-form-urlencoded","urlencoded":[...]} 或 {"mode":"form-data","formData":[...]}
* 3. 普通 JSON 字符串(raw-json 模式直接存的原始内容)
*/
private void applyBody(ForestRequest<?> request, String bodyTemplate, String contentType,
Map<String, Object> variables, PlatformHttpResponse result) {
if (StringUtils.isEmpty(bodyTemplate)) {
return;
}
try {
JSONObject parsed = JSON.parseObject(bodyTemplate);
String mode = parsed != null ? parsed.getString("mode") : null;
if ("x-www-form-urlencoded".equals(mode) || "form-data".equals(mode)) {
// 取 urlencoded 或 formData 数组,转为 key-value 表单参数
String arrayKey = "x-www-form-urlencoded".equals(mode) ? "urlencoded" : "formData";
com.alibaba.fastjson2.JSONArray rows = parsed.getJSONArray(arrayKey);
if (rows != null && !rows.isEmpty()) {
request.addHeader("Content-Type", "application/x-www-form-urlencoded");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rows.size(); i++) {
JSONObject row = rows.getJSONObject(i);
if (row == null) continue;
Boolean enabled = row.getBoolean("enabled");
if (Boolean.FALSE.equals(enabled)) continue;
String key = row.getString("key");
String value = row.getString("value");
if (StringUtils.isEmpty(key)) continue;
value = substituteVariables(value != null ? value : "", variables);
request.addBody(key, value);
if (sb.length() > 0) sb.append('&');
sb.append(key).append('=').append(value);
}
result.setRequestBody(sb.toString());
}
return;
}
} catch (Exception ignored) {
// 不是 JSON 或无 mode 字段,按 raw 处理
}
// raw-json 或普通文本
String body = substituteVariables(bodyTemplate, variables);
if (StringUtils.isNotEmpty(body)) {
if (contentType.contains("json")) {
request.addHeader("Content-Type", "application/json");
} else {
request.addHeader("Content-Type", contentType);
}
request.addBody(body);
result.setRequestBody(body);
}
}
/**
* 构建请求体(保留兼容旧调用,内部不再使用)
*/
private String buildBody(String bodyTemplate, String contentType, Map<String, Object> variables) {
if (StringUtils.isEmpty(bodyTemplate)) {

View File

@ -60,6 +60,12 @@ public class PlatformTokenService {
if (authConfig.containsKey("expiresIn")) {
expiresIn = authConfig.getIntValue("expiresIn");
}
} else if ("oauth2_password".equals(authType)) {
JSONObject result2 = acquireOAuth2PasswordToken(tenantChannel.getBaseUrl(), authConfig);
token = result2.getString("token");
if (result2.containsKey("expiresIn")) {
expiresIn = result2.getIntValue("expiresIn");
}
} else if ("custom_token".equals(authType)) {
JSONObject result = acquireCustomToken(tenantChannel.getBaseUrl(), authConfig);
token = result.getString("token");
@ -178,6 +184,44 @@ public class PlatformTokenService {
throw new RuntimeException("自定义 token 获取失败: " + response.getContent());
}
/**
* OAuth2 password 模式获取 token(DCP 风格:form-urlencoded,grant_type=password)
* authConfig 需包含: tokenUrl, userName, password, clientId, scope
*/
private JSONObject acquireOAuth2PasswordToken(String baseUrl, JSONObject authConfig) {
String tokenUrl = resolveTokenUrl(baseUrl, authConfig.getString("tokenUrl"));
String clientId = authConfig.getString("clientId");
String userName = authConfig.getString("userName");
String password = authConfig.getString("password");
String scope = authConfig.getString("scope");
ForestRequest<?> request = Forest.request();
request.setUrl(tokenUrl);
request.setType(ForestRequestType.POST);
request.addHeader("Content-Type", "application/x-www-form-urlencoded");
request.addBody("grant_type", "password");
if (StringUtils.isNotEmpty(clientId)) {
request.addBody("client_id", clientId);
}
if (StringUtils.isNotEmpty(scope)) {
request.addBody("scope", scope);
}
request.addBody("userName", userName);
request.addBody("password", password);
ForestResponse<?> response = request.execute(ForestResponse.class);
if (response.isSuccess()) {
JSONObject body = JSON.parseObject(response.getContent());
JSONObject result = new JSONObject();
result.put("token", body.getString("access_token"));
if (body.containsKey("expires_in")) {
result.put("expiresIn", body.getIntValue("expires_in"));
}
return result;
}
throw new RuntimeException("OAuth2 password token 获取失败: " + response.getContent());
}
/**
* 解析 authConfig JSON
*/

View File

@ -6,16 +6,33 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.platform.domain.PlatformApi;
import com.fastbee.iot.platform.domain.PlatformTemplate;
import com.fastbee.iot.platform.domain.PlatformTenantChannel;
import com.fastbee.iot.platform.mapper.PlatformApiMapper;
import com.fastbee.iot.platform.model.PlatformHttpResponse;
import com.fastbee.iot.platform.service.IPlatformApiService;
import com.fastbee.iot.platform.service.IPlatformTemplateService;
import com.fastbee.iot.platform.service.IPlatformTenantChannelService;
import com.fastbee.iot.platform.service.PlatformHttpClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Service
public class PlatformApiServiceImpl extends ServiceImpl<PlatformApiMapper, PlatformApi> implements IPlatformApiService {
@Resource
private IPlatformTemplateService platformTemplateService;
@Resource
private IPlatformTenantChannelService platformTenantChannelService;
@Resource
private PlatformHttpClient platformHttpClient;
@Override
public List<PlatformApi> selectPlatformApiList(PlatformApi query) {
LambdaQueryWrapper<PlatformApi> lqw = Wrappers.lambdaQuery();
@ -50,4 +67,35 @@ public class PlatformApiServiceImpl extends ServiceImpl<PlatformApiMapper, Platf
public Boolean deletePlatformApiByIds(Long[] ids) {
return this.removeByIds(Arrays.asList(ids));
}
@Override
public PlatformHttpResponse testApi(PlatformApi api, Long tenantChannelId) {
if (api == null || api.getTemplateId() == null) {
PlatformHttpResponse err = new PlatformHttpResponse();
err.setSuccess(false);
err.setErrorMessage("请先选择平台模板");
return err;
}
PlatformTemplate template = platformTemplateService.getById(api.getTemplateId());
if (template == null) {
PlatformHttpResponse err = new PlatformHttpResponse();
err.setSuccess(false);
err.setErrorMessage("平台模板不存在");
return err;
}
PlatformTenantChannel channel;
if (tenantChannelId != null) {
channel = platformTenantChannelService.getById(tenantChannelId);
} else {
LambdaQueryWrapper<PlatformTenantChannel> lqw = Wrappers.lambdaQuery();
lqw.eq(PlatformTenantChannel::getTemplateId, api.getTemplateId());
lqw.last("LIMIT 1");
channel = platformTenantChannelService.getOne(lqw);
}
if (channel == null) {
channel = new PlatformTenantChannel();
channel.setBaseUrl(template.getBaseUrlTemplate());
}
return platformHttpClient.call(channel, template, api, Collections.emptyMap());
}
}

View File

@ -6,16 +6,21 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.platform.domain.PlatformBinding;
import com.fastbee.iot.platform.domain.PlatformFieldMapping;
import com.fastbee.iot.platform.mapper.PlatformBindingMapper;
import com.fastbee.iot.platform.mapper.PlatformFieldMappingMapper;
import com.fastbee.iot.platform.service.IPlatformBindingService;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Resource;
import java.util.*;
@Service
public class PlatformBindingServiceImpl extends ServiceImpl<PlatformBindingMapper, PlatformBinding> implements IPlatformBindingService {
@Resource
private PlatformFieldMappingMapper fieldMappingMapper;
@Override
public List<PlatformBinding> selectPlatformBindingList(PlatformBinding query) {
LambdaQueryWrapper<PlatformBinding> lqw = Wrappers.lambdaQuery();
@ -56,4 +61,67 @@ public class PlatformBindingServiceImpl extends ServiceImpl<PlatformBindingMappe
public Boolean deletePlatformBindingByIds(Long[] ids) {
return this.removeByIds(Arrays.asList(ids));
}
@Override
public List<PlatformFieldMapping> generateDefaultMappings(Long bindingId, String deviceType) {
// 先检查是否已有映射
LambdaQueryWrapper<PlatformFieldMapping> existQuery = Wrappers.lambdaQuery();
existQuery.eq(PlatformFieldMapping::getBindingId, bindingId);
Long existCount = fieldMappingMapper.selectCount(existQuery);
if (existCount > 0) {
// 已有映射,直接返回现有列表
return fieldMappingMapper.selectList(existQuery);
}
// 根据设备类型生成默认映射
List<PlatformFieldMapping> mappings = buildDefaultMappings(deviceType);
int sortOrder = 1;
for (PlatformFieldMapping mapping : mappings) {
mapping.setBindingId(bindingId);
mapping.setDirection("OUTBOUND");
mapping.setSortOrder(sortOrder++);
mapping.setStatus(1);
fieldMappingMapper.insert(mapping);
}
return mappings;
}
private List<PlatformFieldMapping> buildDefaultMappings(String deviceType) {
List<PlatformFieldMapping> list = new ArrayList<>();
switch (deviceType) {
case "RAIN":
list.add(createMapping("THING_MODEL", "rainfall", "累计雨量", "value", "雨量值", "DIRECT"));
list.add(createMapping("THING_MODEL", "temperature", "温度", "temperature", "温度", "DIRECT"));
break;
case "TILT":
list.add(createMapping("THING_MODEL", "xAngle", "X轴角度", "xDeg", "X轴角度", "DIRECT"));
list.add(createMapping("THING_MODEL", "yAngle", "Y轴角度", "yDeg", "Y轴角度", "DIRECT"));
list.add(createMapping("THING_MODEL", "zAngle", "Z轴角度", "zDeg", "Z轴角度", "DIRECT"));
list.add(createMapping("THING_MODEL", "temperature", "温度", "temperature", "温度", "DIRECT"));
break;
case "STATIC_LEVEL":
list.add(createMapping("THING_MODEL", "height", "液位高度", "height", "液位高度", "DIRECT"));
list.add(createMapping("THING_MODEL", "temperature", "温度", "temperature", "温度", "DIRECT"));
break;
case "AXIAL_FORCE":
list.add(createMapping("THING_MODEL", "strain", "应变值", "e", "应变值", "DIRECT"));
break;
default:
break;
}
return list;
}
private PlatformFieldMapping createMapping(String sourceType, String sourceKey, String sourceName,
String targetPath, String targetName, String transformType) {
PlatformFieldMapping mapping = new PlatformFieldMapping();
mapping.setSourceType(sourceType);
mapping.setSourceKey(sourceKey);
mapping.setSourceName(sourceName);
mapping.setTargetPath(targetPath);
mapping.setTargetName(targetName);
mapping.setTransformType(transformType);
mapping.setRequired(1);
return mapping;
}
}

View File

@ -1,21 +1,39 @@
package com.fastbee.iot.platform.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dtflys.forest.Forest;
import com.dtflys.forest.http.ForestRequest;
import com.dtflys.forest.http.ForestRequestType;
import com.dtflys.forest.http.ForestResponse;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.platform.domain.PlatformTemplate;
import com.fastbee.iot.platform.domain.PlatformTenantChannel;
import com.fastbee.iot.platform.mapper.PlatformTenantChannelMapper;
import com.fastbee.iot.platform.service.IPlatformTenantChannelService;
import com.fastbee.iot.platform.service.IPlatformTemplateService;
import com.fastbee.iot.platform.service.PlatformTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Resource;
import java.util.*;
@Slf4j
@Service
public class PlatformTenantChannelServiceImpl extends ServiceImpl<PlatformTenantChannelMapper, PlatformTenantChannel> implements IPlatformTenantChannelService {
@Resource
private PlatformTokenService platformTokenService;
@Resource
private IPlatformTemplateService platformTemplateService;
@Override
public List<PlatformTenantChannel> selectPlatformTenantChannelList(PlatformTenantChannel query) {
LambdaQueryWrapper<PlatformTenantChannel> lqw = Wrappers.lambdaQuery();
@ -53,4 +71,75 @@ public class PlatformTenantChannelServiceImpl extends ServiceImpl<PlatformTenant
public Boolean deletePlatformTenantChannelByIds(Long[] ids) {
return this.removeByIds(Arrays.asList(ids));
}
@Override
public Map<String, Object> fetchRemoteInfo(PlatformTenantChannel tenantChannel) {
Map<String, Object> result = new HashMap<>();
// 获取模板信息
PlatformTemplate template = platformTemplateService.selectPlatformTemplateById(tenantChannel.getTemplateId());
if (template == null) {
throw new RuntimeException("平台模板不存在");
}
// 获取 token
String token = platformTokenService.getToken(tenantChannel, template);
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("获取平台 token 失败,请检查凭证配置");
}
String baseUrl = StringUtils.isNotEmpty(tenantChannel.getBaseUrl()) ? tenantChannel.getBaseUrl() : template.getBaseUrlTemplate();
if (StringUtils.isEmpty(baseUrl)) {
throw new RuntimeException("baseUrl 未配置");
}
baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
// 1. 获取用户信息
String userUrl = baseUrl + "/api/app/user/user-id";
ForestRequest<?> userRequest = Forest.request();
userRequest.setUrl(userUrl);
userRequest.setType(ForestRequestType.GET);
userRequest.addHeader("Authorization", "Bearer " + token);
ForestResponse<?> userResponse = userRequest.execute(ForestResponse.class);
if (!userResponse.isSuccess()) {
throw new RuntimeException("获取用户信息失败: " + userResponse.getContent());
}
JSONObject userInfo = JSON.parseObject(userResponse.getContent());
String userId = userInfo.getString("id");
String userName = userInfo.getString("userName");
result.put("userId", userId);
result.put("userName", userName);
// 2. 获取项目列表
if (StringUtils.isNotEmpty(userId)) {
String projectUrl = baseUrl + "/api/app/project/by-user-id/" + userId;
ForestRequest<?> projectRequest = Forest.request();
projectRequest.setUrl(projectUrl);
projectRequest.setType(ForestRequestType.GET);
projectRequest.addHeader("Authorization", "Bearer " + token);
ForestResponse<?> projectResponse = projectRequest.execute(ForestResponse.class);
if (projectResponse.isSuccess()) {
JSONArray projects = JSON.parseArray(projectResponse.getContent());
List<Map<String, String>> projectList = new ArrayList<>();
if (projects != null) {
for (int i = 0; i < projects.size(); i++) {
JSONObject proj = projects.getJSONObject(i);
Map<String, String> item = new HashMap<>();
item.put("id", proj.getString("id"));
item.put("name", proj.getString("name"));
projectList.add(item);
}
}
result.put("projects", projectList);
} else {
log.warn("获取项目列表失败: {}", projectResponse.getContent());
result.put("projects", Collections.emptyList());
}
}
return result;
}
}

View File

@ -0,0 +1,20 @@
-- =====================================================================
-- 平台对接菜单重组:8个平级菜单 → 3个主入口 + 1个"高级配置"子目录
-- 执行前提:已执行过 iot_platform_connect.sql(菜单已存在)
-- =====================================================================
-- 1. 新增"高级配置"目录(menu_id=3580, parent=3500)
INSERT IGNORE INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (3580, '高级配置', 3500, 4, 'advanced', NULL, NULL, 1, 0, 'M', '0', '0', '', 'system', 'admin', NOW(), 'admin', NOW(), '平台对接高级配置目录');
-- 2. 主入口菜单调整(直接挂在3500下,调整排序和名称)
UPDATE `sys_menu` SET `menu_name` = '连接管理', `order_num` = 1, `icon` = 'peoples' WHERE `menu_id` = 3540;
UPDATE `sys_menu` SET `order_num` = 2 WHERE `menu_id` = 3560;
UPDATE `sys_menu` SET `menu_name` = '推送记录', `order_num` = 3, `icon` = 'log' WHERE `menu_id` = 3510;
-- 3. 高级配置子菜单(parent_id 改为3580)
UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 1 WHERE `menu_id` = 3501;
UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 2 WHERE `menu_id` = 3520;
UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 3 WHERE `menu_id` = 3530;
UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 4 WHERE `menu_id` = 3550;
UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 5 WHERE `menu_id` = 3570;

View File

@ -19,3 +19,12 @@ export function updateApi(data) {
export function delApi(id) {
return request({ url: '/iot/platformConnect/api/' + id, method: 'delete' })
}
export function testApi(data, tenantChannelId) {
return request({
url: '/iot/platformConnect/api/test',
method: 'post',
params: tenantChannelId ? { tenantChannelId } : {},
data: data
})
}

View File

@ -19,3 +19,7 @@ export function updateBinding(data) {
export function delBinding(id) {
return request({ url: '/iot/platformConnect/binding/' + id, method: 'delete' })
}
export function generateDefaultMappings(bindingId, deviceType) {
return request({ url: '/iot/platformConnect/binding/generateDefaultMappings', method: 'post', params: { bindingId, deviceType } })
}

View File

@ -19,3 +19,7 @@ export function updateTenantChannel(data) {
export function delTenantChannel(id) {
return request({ url: '/iot/platformConnect/tenantChannel/' + id, method: 'delete' })
}
export function fetchRemoteInfo(data) {
return request({ url: '/iot/platformConnect/tenantChannel/fetchRemoteInfo', method: 'post', data: data })
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1780062094720" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4900" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M975.657143 96.914286l-48.457143-48.457143c-1.828571-1.828571-4.114286-2.628571-6.514286-2.628572s-4.685714 0.914286-6.514285 2.628572l-86.971429 86.971428a227.737143 227.737143 0 0 0-128.114286-39.2c-58.514286 0-117.028571 22.285714-161.714285 66.971429L420.914286 279.657143a9.177143 9.177143 0 0 0 0 12.914286L731.428571 603.085714c1.828571 1.828571 4.114286 2.628571 6.514286 2.628572 2.285714 0 4.685714-0.914286 6.514286-2.628572l116.457143-116.457143c78.742857-78.857143 88-200.8 27.771428-289.714285l86.971429-86.971429c3.542857-3.657143 3.542857-9.485714 0-13.028571zM805.828571 431.657143l-67.885714 67.885714-213.485714-213.485714 67.885714-67.885714c28.457143-28.457143 66.4-44.228571 106.742857-44.228572 40.342857 0 78.171429 15.657143 106.742857 44.228572 28.457143 28.457143 44.228571 66.4 44.228572 106.742857 0 40.342857-15.771429 78.171429-44.228572 106.742857z m-217.371428 120a9.177143 9.177143 0 0 0-12.914286 0L499.428571 627.771429 396.228571 524.571429l76.228572-76.228572c3.542857-3.542857 3.542857-9.371429 0-12.914286L430.857143 393.828571a9.177143 9.177143 0 0 0-12.914286 0L341.714286 470.057143l-49.142857-49.142857a8.971429 8.971429 0 0 0-6.514286-2.628572c-2.285714 0-4.685714 0.914286-6.514286 2.628572L163.2 537.371429c-78.742857 78.857143-88 200.8-27.771429 289.714285l-86.971428 86.971429a9.177143 9.177143 0 0 0 0 12.914286l48.457143 48.457142c1.828571 1.828571 4.114286 2.628571 6.514285 2.628572s4.685714-0.914286 6.514286-2.628572l86.971429-86.971428c38.514286 26.171429 83.314286 39.2 128.114285 39.2 58.514286 0 117.028571-22.285714 161.714286-66.971429l116.457143-116.457143c3.542857-3.542857 3.542857-9.371429 0-12.914285l-49.142857-49.142857 76.228571-76.228572c3.542857-3.542857 3.542857-9.371429 0-12.914286l-41.828571-41.371428zM431.657143 805.828571a150.08 150.08 0 0 1-106.742857 44.228572c-40.342857 0-78.171429-15.657143-106.742857-44.228572a150.08 150.08 0 0 1-44.228572-106.742857c0-40.342857 15.657143-78.171429 44.228572-106.742857l67.885714-67.885714 213.485714 213.485714-67.885714 67.885714z" p-id="4901"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -102,21 +102,45 @@
</el-form-item>
</el-col>
</el-row>
<el-form-item label="请求设计" required>
<http-request-designer v-model="designer" />
<el-form-item label="请求设计" required class="request-design-item">
<http-request-designer ref="httpDesigner" v-model="designer" />
</el-form-item>
</el-form>
<div v-if="testResult" class="test-result" style="margin:0 0 12px 0">
<el-alert
:title="testResult.success ? '连通成功' : '连通失败'"
:type="testResult.success ? 'success' : 'error'"
:closable="false"
style="margin-bottom:8px"
/>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="请求地址">{{ testResult.requestUrl }}</el-descriptions-item>
<el-descriptions-item label="状态码">{{ testResult.statusCode }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ testResult.durationMs }} ms</el-descriptions-item>
<el-descriptions-item v-if="testResult.errorMessage" label="错误信息">{{ testResult.errorMessage }}</el-descriptions-item>
</el-descriptions>
<div v-if="testResult.requestBody" style="margin-top:8px">
<div style="font-size:12px;color:#909399;margin-bottom:4px">请求体</div>
<pre class="result-pre">{{ formatJson(testResult.requestBody) }}</pre>
</div>
<div v-if="testResult.body" style="margin-top:8px">
<div style="font-size:12px;color:#909399;margin-bottom:4px">响应体</div>
<pre class="result-pre">{{ formatJson(testResult.body) }}</pre>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="success" :loading="testLoading" @click="doTestInDialog">测试连通</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
<api-field-dialog ref="apiFieldDialog" :api-id="currentApiId" />
</div>
</template>
<script>
import { listApi, getApi, delApi, addApi, updateApi } from '@/api/iot/platformConnect/api';
import { listApi, getApi, delApi, addApi, updateApi, testApi } from '@/api/iot/platformConnect/api';
import { listTemplate } from '@/api/iot/platformConnect/template';
import EntitySelect from '../components/EntitySelect';
import HttpRequestDesigner from '../components/HttpRequestDesigner';
@ -157,6 +181,8 @@ export default {
apiName: [{ required: true, message: '接口名称不能为空', trigger: 'blur' }],
apiType: [{ required: true, message: '接口类型不能为空', trigger: 'change' }],
},
testLoading: false,
testResult: null,
};
},
created() {
@ -237,6 +263,9 @@ export default {
this.designer = deserializeDesigner(this.form);
},
submitForm() {
if (this.$refs.httpDesigner) {
this.$refs.httpDesigner.flushEditors();
}
const requestConfig = serializeDesigner(this.designer);
if (!validateJson(requestConfig.headerTemplate) || !validateJson(requestConfig.queryTemplate) || !validateJson(requestConfig.requestConfig)) {
this.$modal.msgError('Header、Query 或请求配置格式不正确');
@ -274,6 +303,25 @@ export default {
this.currentApiId = row.id;
this.$nextTick(() => { this.$refs.apiFieldDialog.open(); });
},
doTestInDialog() {
this.testLoading = true;
this.testResult = null;
const requestConfig = serializeDesigner(this.designer);
const payload = Object.assign({}, this.form, requestConfig);
testApi(payload, null).then(res => {
this.testResult = res.data;
}).finally(() => {
this.testLoading = false;
});
},
formatJson(text) {
if (!text) return text;
try {
return JSON.stringify(JSON.parse(text), null, 2);
} catch (e) {
return text;
}
},
formatOption(options, value) {
const item = options.find(option => option.value === value);
return item ? item.label : value;
@ -286,4 +334,22 @@ export default {
.form-tip {
margin-bottom: 16px;
}
.request-design-item ::v-deep .el-form-item__content {
display: block;
line-height: normal;
}
.test-result {
margin-top: 4px;
}
.result-pre {
padding: 10px;
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
font-size: 12px;
max-height: 200px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@ -1,14 +1,23 @@
<template>
<el-dialog title="字段映射配置" :visible.sync="visible" width="1000px" append-to-body @close="handleClose">
<el-alert title="左侧为本地物模型字段,右侧为第三方平台接口字段。配置映射关系后,数据交换时将自动转换。" type="info" :closable="false" class="mb8" />
<el-alert v-if="mappingList.length === 0 && !loadingMappings" title="当前无映射配置" type="warning" :closable="false" class="mb8">
<template slot="title">
<span>当前无映射配置。</span>
<el-button v-if="externalDeviceType && externalDeviceType !== 'CUSTOM'" type="text" style="margin-left: 8px" @click="handleAutoGenerate">根据设备类型自动生成默认映射</el-button>
</template>
</el-alert>
<el-alert v-else title="左侧为本地物模型字段,右侧为第三方平台接口字段。配置映射关系后,数据交换时将自动转换。" type="info" :closable="false" class="mb8" />
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">添加映射</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-if="externalDeviceType && externalDeviceType !== 'CUSTOM' && mappingList.length > 0" type="warning" plain icon="el-icon-refresh" size="mini" @click="handleRegenerate">重新生成默认映射</el-button>
</el-col>
</el-row>
<el-table :data="mappingList" size="small" border>
<el-table :data="mappingList" size="small" border v-loading="loadingMappings">
<el-table-column label="来源类型" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.sourceType === 'THING_MODEL'" size="mini">物模型</el-tag>
@ -16,15 +25,22 @@
<el-tag v-else size="mini" type="warning">表达式</el-tag>
</template>
</el-table-column>
<el-table-column label="来源键" align="center" prop="sourceKey" min-width="140" show-overflow-tooltip />
<el-table-column label="来源键" align="center" prop="sourceKey" min-width="120">
<template slot-scope="scope">
<span>{{ scope.row.sourceName ? scope.row.sourceName + ' (' + scope.row.sourceKey + ')' : scope.row.sourceKey }}</span>
</template>
</el-table-column>
<el-table-column label="" align="center" width="50">
<template><i class="el-icon-right" /></template>
</el-table-column>
<el-table-column label="目标路径" align="center" prop="targetPath" min-width="160" show-overflow-tooltip />
<el-table-column label="目标路径" align="center" prop="targetPath" min-width="120">
<template slot-scope="scope">
<span>{{ scope.row.targetName ? scope.row.targetName + ' (' + scope.row.targetPath + ')' : scope.row.targetPath }}</span>
</template>
</el-table-column>
<el-table-column label="转换类型" align="center" prop="transformType" width="100">
<template slot-scope="scope">{{ formatTransform(scope.row.transformType) }}</template>
</el-table-column>
<el-table-column label="转换表达式" align="center" prop="transformExpr" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="120">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
@ -86,6 +102,7 @@
<script>
import { listFieldMapping, addFieldMapping, updateFieldMapping, delFieldMapping } from '@/api/iot/platformConnect/fieldMapping'
import { listApiField } from '@/api/iot/platformConnect/apiField'
import { generateDefaultMappings } from '@/api/iot/platformConnect/binding'
import request from '@/utils/request'
export default {
@ -93,11 +110,13 @@ export default {
props: {
bindingId: { type: [Number, String], default: null },
flowId: { type: [Number, String], default: null },
productId: { type: [Number, String], default: null }
productId: { type: [Number, String], default: null },
externalDeviceType: { type: String, default: null }
},
data() {
return {
visible: false,
loadingMappings: false,
mappingList: [],
thingModelFields: [],
apiFields: [],
@ -113,14 +132,21 @@ export default {
methods: {
open() {
this.visible = true
this.loadingMappings = true
this.loadMappings()
this.loadThingModelFields()
this.loadApiFields()
},
loadMappings() {
if (!this.bindingId) return
if (!this.bindingId) {
this.loadingMappings = false
return
}
listFieldMapping({ bindingId: this.bindingId, pageNum: 1, pageSize: 200 }).then(response => {
this.mappingList = (response.rows || []).sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
this.loadingMappings = false
}).catch(() => {
this.loadingMappings = false
})
},
loadThingModelFields() {
@ -133,12 +159,30 @@ export default {
}).catch(() => { this.thingModelFields = [] })
},
loadApiFields() {
// 尝试通过 flowId 关联的 API 加载字段
if (!this.flowId) return
listApiField({ flowId: this.flowId, direction: 'REQUEST', pageNum: 1, pageSize: 200 }).then(response => {
this.apiFields = response.rows || []
}).catch(() => { this.apiFields = [] })
},
handleAutoGenerate() {
if (!this.bindingId || !this.externalDeviceType) return
generateDefaultMappings(this.bindingId, this.externalDeviceType).then(res => {
this.mappingList = res.data || []
this.$modal.msgSuccess('默认映射已生成')
})
},
handleRegenerate() {
this.$modal.confirm('重新生成将覆盖现有映射,是否继续?').then(() => {
// 先删除现有映射
const deletePromises = this.mappingList.filter(m => m.id).map(m => delFieldMapping(m.id))
return Promise.all(deletePromises)
}).then(() => {
return generateDefaultMappings(this.bindingId, this.externalDeviceType)
}).then(res => {
this.mappingList = res.data || []
this.$modal.msgSuccess('默认映射已重新生成')
}).catch(() => {})
},
handleClose() {
this.mappingList = []
this.thingModelFields = []

View File

@ -11,15 +11,10 @@
style="width: 220px"
/>
</el-form-item>
<el-form-item label="流程" prop="flowId">
<entity-select
v-model="queryParams.flowId"
:loader="loadFlows"
label-key="flowName"
sub-label-key="flowType"
placeholder="请选择流程"
style="width: 220px"
/>
<el-form-item label="设备类型" prop="externalDeviceType">
<el-select v-model="queryParams.externalDeviceType" clearable placeholder="请选择" style="width: 160px">
<el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="设备编号" prop="serialNumber"><el-input v-model="queryParams.serialNumber" clearable @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button></el-form-item>
@ -34,45 +29,38 @@
<el-table v-loading="loading" :data="bindingList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="实例ID" align="center" prop="tenantChannelId" width="90" />
<el-table-column label="流程ID" align="center" prop="flowId" width="90" />
<el-table-column label="绑定名称" align="center" prop="bindingName" min-width="140" show-overflow-tooltip />
<el-table-column label="产品" align="center" prop="productName" min-width="120" show-overflow-tooltip />
<el-table-column label="设备编号" align="center" prop="serialNumber" min-width="140" />
<el-table-column label="第三方用户" align="center" prop="externalUserId" min-width="120" show-overflow-tooltip />
<el-table-column label="第三方项目" align="center" prop="externalProjectId" min-width="120" show-overflow-tooltip />
<el-table-column label="设备类型" align="center" prop="externalDeviceType" width="120">
<el-table-column label="绑定名称" align="center" prop="bindingName" min-width="160" show-overflow-tooltip />
<el-table-column label="设备类型" align="center" prop="externalDeviceType" width="110">
<template slot-scope="scope">{{ formatOption(deviceTypeOptions, scope.row.externalDeviceType) }}</template>
</el-table-column>
<el-table-column label="交换模式" align="center" prop="exchangeMode" width="120">
<template slot-scope="scope">{{ formatOption(exchangeModeOptions, scope.row.exchangeMode) }}</template>
</el-table-column>
<el-table-column label="产品" align="center" prop="productName" min-width="120" show-overflow-tooltip />
<el-table-column label="设备编号" align="center" prop="serialNumber" min-width="140" />
<el-table-column label="设备名称" align="center" prop="deviceName" min-width="120" show-overflow-tooltip />
<el-table-column label="状态" align="center" prop="status" width="90"><template slot-scope="scope"><el-tag v-if="scope.row.status === 1" type="success">启用</el-tag><el-tag v-else type="info">停用</el-tag></template></el-table-column>
<el-table-column label="操作" align="center" width="180"><template slot-scope="scope"><el-button v-hasPermi="['iot:platformConnect:binding:edit']" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button><el-button size="mini" type="text" icon="el-icon-connection" @click="handleFieldMapping(scope.row)">映射</el-button><el-button v-hasPermi="['iot:platformConnect:binding:remove']" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button></template></el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<el-dialog :title="title" :visible.sync="open" width="860px" append-to-body>
<el-alert
title="DCP 场景中一个测点对应一个 FastBee 设备;第三方用户/项目 ID 是对方平台资源,不是 FastBee 用户 ID。"
type="info"
:closable="false"
class="form-tip"
/>
<el-form ref="form" :model="form" :rules="rules" label-width="125px">
<el-row :gutter="12">
<el-col :span="12">
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="平台实例" prop="tenantChannelId">
<entity-select
v-model="form.tenantChannelId"
:loader="loadTenantChannels"
label-key="channelName"
sub-label-key="channelCode"
placeholder="请选择租户平台实例"
placeholder="请选择平台实例"
@change="handleTenantChannelChange"
/>
</el-form-item>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="设备类型" prop="externalDeviceType">
<el-select v-model="form.externalDeviceType" placeholder="请选择设备类型" style="width: 100%" @change="handleDeviceTypeChange">
<el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="流程" prop="flowId">
@ -82,7 +70,7 @@
:query="flowQuery"
label-key="flowName"
sub-label-key="flowType"
placeholder="请选择数据交换流程"
placeholder="自动匹配(可手动选择)"
/>
</el-form-item>
</el-col>
@ -110,54 +98,38 @@
label-key="deviceName"
sub-label-key="serialNumber"
:disabled="!form.productId"
placeholder="请先选择产品,再选择设备"
placeholder="请先选择产品"
@change="handleDeviceChange"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="绑定名称" prop="bindingName"><el-input v-model="form.bindingName" placeholder="如 DCP轴力计-1号测点" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="设备编号" prop="serialNumber"><el-input v-model="form.serialNumber" disabled /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="绑定名称" prop="bindingName"><el-input v-model="form.bindingName" placeholder="自动生成,可修改" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="设备编号"><el-input v-model="form.serialNumber" disabled /></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="设备名称" prop="deviceName"><el-input v-model="form.deviceName" disabled /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="产品名称" prop="productName"><el-input v-model="form.productName" disabled /></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="第三方用户ID" prop="externalUserId"><el-input v-model="form.externalUserId" placeholder="对方平台用户/账号标识" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="第三方项目ID" prop="externalProjectId"><el-input v-model="form.externalProjectId" placeholder="对方平台项目标识" /></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="第三方设备类型" prop="externalDeviceType">
<el-select v-model="form.externalDeviceType" placeholder="请选择设备类型" style="width: 100%">
<el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交换模式" prop="exchangeMode">
<el-select v-model="form.exchangeMode" placeholder="请选择交换模式" style="width: 100%">
<el-option v-for="item in exchangeModeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="高级配置" prop="configJson">
<codemirror ref="configEditor" :value="form.configJson || ''" :height="'150px'" :myMode="'application/json'" />
</el-form-item>
<el-form-item label="状态" prop="status"><el-radio-group v-model="form.status"><el-radio :label="1">启用</el-radio><el-radio :label="0">停用</el-radio></el-radio-group></el-form-item>
<!-- 高级配置折叠 -->
<el-collapse class="advanced-collapse">
<el-collapse-item title="高级配置" name="advanced">
<el-form-item label="第三方用户ID"><el-input v-model="form.externalUserId" placeholder="默认从连接配置继承" /></el-form-item>
<el-form-item label="第三方项目ID"><el-input v-model="form.externalProjectId" placeholder="默认从连接配置继承" /></el-form-item>
<el-form-item label="扩展配置">
<codemirror ref="configEditor" :value="form.configJson || ''" :height="'120px'" :myMode="'application/json'" />
</el-form-item>
</el-collapse-item>
</el-collapse>
</el-form>
<div slot="footer" class="dialog-footer"><el-button type="primary" @click="submitForm">确 定</el-button><el-button @click="cancel">取 消</el-button></div>
</el-dialog>
<field-mapping-dialog ref="fieldMappingDialog" :binding-id="currentBindingId" :flow-id="currentFlowId" :product-id="currentProductId" />
<field-mapping-dialog ref="fieldMappingDialog" :binding-id="currentBindingId" :flow-id="currentFlowId" :product-id="currentProductId" :external-device-type="currentDeviceType" />
</div>
</template>
<script>
import { listBinding, getBinding, delBinding, addBinding, updateBinding } from '@/api/iot/platformConnect/binding';
import { listTenantChannel } from '@/api/iot/platformConnect/tenantChannel';
import { listBinding, getBinding, delBinding, addBinding, updateBinding, generateDefaultMappings } from '@/api/iot/platformConnect/binding';
import { listTenantChannel, getTenantChannel } from '@/api/iot/platformConnect/tenantChannel';
import { listFlow } from '@/api/iot/platformConnect/flow';
import { listShortProduct } from '@/api/iot/product';
import { listDeviceShort } from '@/api/iot/device';
@ -178,15 +150,11 @@ export default {
currentBindingId: null,
currentFlowId: null,
currentProductId: null,
currentDeviceType: null,
total: 0,
bindingList: [],
title: '',
open: false,
exchangeModeOptions: [
{ label: '推送数据', value: 'OUTBOUND_PUSH' },
{ label: '拉取数据', value: 'INBOUND_PULL' },
{ label: '接收数据', value: 'INBOUND_RECEIVE' },
],
deviceTypeOptions: [
{ label: '轴力计', value: 'AXIAL_FORCE' },
{ label: '雨量计', value: 'RAIN' },
@ -194,17 +162,16 @@ export default {
{ label: '静力水准', value: 'STATIC_LEVEL' },
{ label: '自定义', value: 'CUSTOM' },
],
queryParams: { pageNum: 1, pageSize: 10, tenantChannelId: null, flowId: null, serialNumber: null },
queryParams: { pageNum: 1, pageSize: 10, tenantChannelId: null, externalDeviceType: null, serialNumber: null },
form: {},
flowQuery: {},
deviceQuery: {},
selectedChannel: null,
rules: {
tenantChannelId: [{ required: true, message: '平台实例不能为空', trigger: 'change' }],
flowId: [{ required: true, message: '流程不能为空', trigger: 'change' }],
externalDeviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
deviceId: [{ required: true, message: '设备不能为空', trigger: 'change' }],
serialNumber: [{ required: true, message: '设备编号不能为空', trigger: 'blur' }],
exchangeMode: [{ required: true, message: '交换模式不能为空', trigger: 'change' }],
},
};
},
@ -252,7 +219,7 @@ export default {
serialNumber: null,
externalUserId: null,
externalProjectId: null,
externalDeviceType: 'AXIAL_FORCE',
externalDeviceType: null,
bindingName: null,
exchangeMode: 'OUTBOUND_PUSH',
configJson: null,
@ -260,6 +227,7 @@ export default {
};
this.flowQuery = {};
this.deviceQuery = {};
this.selectedChannel = null;
this.resetForm('form');
},
handleQuery() {
@ -292,8 +260,33 @@ export default {
});
},
handleTenantChannelChange(value, selected) {
this.selectedChannel = selected;
this.form.flowId = null;
this.flowQuery = selected && selected.templateId ? { templateId: selected.templateId } : {};
// 继承通道的默认用户和项目
if (selected) {
if (selected.defaultUserId && !this.form.externalUserId) {
this.form.externalUserId = selected.defaultUserId;
}
if (selected.defaultProjectId && !this.form.externalProjectId) {
this.form.externalProjectId = selected.defaultProjectId;
}
}
this.autoMatchFlow();
},
handleDeviceTypeChange() {
this.autoMatchFlow();
this.autoGenerateBindingName();
},
autoMatchFlow() {
if (!this.form.tenantChannelId || !this.form.externalDeviceType) return;
const query = Object.assign({ pageSize: 100, status: 1, flowType: 'OUTBOUND_PUSH' }, this.flowQuery);
listFlow(query).then(res => {
const flows = res.rows || [];
if (flows.length === 1) {
this.form.flowId = flows[0].id;
}
});
},
handleProductChange(value, selected) {
this.form.productName = selected ? selected.productName : null;
@ -313,42 +306,54 @@ export default {
this.form.serialNumber = selected.serialNumber;
this.form.productId = selected.productId || this.form.productId;
this.form.productName = selected.productName || this.form.productName;
if (!this.form.bindingName) {
this.form.bindingName = selected.deviceName || selected.serialNumber;
this.autoGenerateBindingName();
},
autoGenerateBindingName() {
if (this.form.bindingName && this.form.id) return;
const deviceName = this.form.deviceName || this.form.serialNumber || '';
const typeLabel = this.formatOption(this.deviceTypeOptions, this.form.externalDeviceType);
if (deviceName && typeLabel) {
this.form.bindingName = deviceName + '-' + typeLabel;
}
},
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
// 从 Codemirror 获取 configJson
if (this.$refs.configEditor) {
this.form.configJson = this.$refs.configEditor.getValue()
this.form.configJson = this.$refs.configEditor.getValue();
}
if (this.form.id != null) {
updateBinding(this.form).then(() => {
this.$modal.msgSuccess('修改成功');
this.open = false;
this.getList();
});
} else {
addBinding(this.form).then(() => {
this.$modal.msgSuccess('新增成功');
// exchangeMode 从 flow 推导,默认 OUTBOUND_PUSH
this.form.exchangeMode = 'OUTBOUND_PUSH';
const isNew = this.form.id == null;
const action = isNew ? addBinding(this.form) : updateBinding(this.form);
action.then((res) => {
this.$modal.msgSuccess(isNew ? '新增成功' : '修改成功');
this.open = false;
this.getList();
// 新增时自动生成默认字段映射
if (isNew && this.form.externalDeviceType && this.form.externalDeviceType !== 'CUSTOM') {
const bindingId = res.data;
if (bindingId) {
generateDefaultMappings(bindingId, this.form.externalDeviceType).then(() => {
this.$notify({ title: '提示', message: '已自动生成默认字段映射,可在"映射"中查看调整', type: 'success', duration: 4000 });
});
}
}
});
}
});
},
handleFieldMapping(row) {
this.currentBindingId = row.id;
this.currentFlowId = row.flowId;
this.currentProductId = row.productId;
this.currentDeviceType = row.externalDeviceType;
this.$nextTick(() => { this.$refs.fieldMappingDialog.open(); });
},
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除设备绑定编号为"' + ids + '"的数据项?').then(function() {
this.$modal.confirm('是否确认删除该设备绑定?').then(function() {
return delBinding(ids);
}).then(() => {
this.getList();
@ -364,7 +369,7 @@ export default {
</script>
<style scoped>
.form-tip {
margin-bottom: 16px;
.advanced-collapse {
margin-bottom: 12px;
}
</style>

View File

@ -1,21 +1,21 @@
<template>
<div class="http-request-designer">
<el-row :gutter="8">
<el-col :span="5">
<el-select v-model="designer.method" placeholder="请求方法" style="width: 100%">
<div class="request-url-bar">
<el-select v-model="designer.method" class="request-method" placeholder="请求方法">
<el-option v-for="item in methodOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-col>
<el-col :span="19">
<el-input v-model="designer.pathTemplate" placeholder="请输入接口路径,例如 /api/v1/data" />
</el-col>
</el-row>
<el-input
v-model="designer.pathTemplate"
class="request-path"
placeholder="请输入接口路径,例如 /api/v1/data"
/>
</div>
<el-tabs v-model="activeTab" class="request-tabs">
<el-tab-pane label="Params" name="params">
<key-value-editor v-model="designer.params" add-text="添加 Query 参数" />
<key-value-editor ref="paramsEditor" v-model="designer.params" add-text="添加 Query 参数" />
</el-tab-pane>
<el-tab-pane label="Headers" name="headers">
<key-value-editor v-model="designer.headers" add-text="添加 Header" />
<key-value-editor ref="headersEditor" v-model="designer.headers" add-text="添加 Header" />
</el-tab-pane>
<el-tab-pane label="Body" name="body">
<el-radio-group v-model="designer.body.mode" size="small" class="body-mode">
@ -24,30 +24,63 @@
<el-radio-button label="x-www-form-urlencoded">x-www-form-urlencoded</el-radio-button>
<el-radio-button label="raw-json">raw JSON</el-radio-button>
</el-radio-group>
<key-value-editor v-if="designer.body.mode === 'form-data'" v-model="designer.body.formData" add-text="添加 Form 字段" />
<key-value-editor v-if="designer.body.mode === 'x-www-form-urlencoded'" v-model="designer.body.urlencoded" add-text="添加表单字段" />
<el-input v-if="designer.body.mode === 'raw-json'" v-model="designer.body.raw" type="textarea" :rows="8" placeholder="请输入 JSON 请求体,可使用 ${变量名} 作为模板变量" />
<key-value-editor
v-if="designer.body.mode === 'form-data'"
ref="formDataEditor"
v-model="designer.body.formData"
add-text="添加 Form 字段"
/>
<key-value-editor
v-if="designer.body.mode === 'x-www-form-urlencoded'"
ref="urlencodedEditor"
v-model="designer.body.urlencoded"
add-text="添加表单字段"
/>
<div v-if="designer.body.mode === 'raw-json'">
<div style="margin-bottom:6px;text-align:right">
<el-button size="mini" icon="el-icon-upload2" @click="importJsonDialogVisible = true">导入 JSON</el-button>
</div>
<el-input v-model="designer.body.raw" type="textarea" :rows="8" placeholder="请输入 JSON 请求体,可使用 ${变量名} 作为模板变量" />
</div>
<el-alert v-if="designer.body.mode === 'none'" title="该接口不配置请求体" type="info" :closable="false" />
</el-tab-pane>
<el-tab-pane label="高级配置" name="config">
<el-form label-width="110px">
<el-form-item label="Content-Type">
<el-select v-model="designer.contentType" clearable placeholder="请选择 Content-Type" style="width: 100%">
<div class="config-panel">
<div class="config-field">
<label class="config-label">Content-Type</label>
<el-select
v-model="designer.contentType"
clearable
placeholder="请选择 Content-Type"
class="config-control"
>
<el-option label="application/json" value="application/json" />
<el-option label="application/x-www-form-urlencoded" value="application/x-www-form-urlencoded" />
<el-option label="multipart/form-data" value="multipart/form-data" />
<el-option label="text/plain" value="text/plain" />
</el-select>
</el-form-item>
<el-form-item label="请求配置">
<key-value-editor v-model="designer.config" add-text="添加配置" />
</el-form-item>
</el-form>
</div>
<div class="config-field config-field--stack">
<label class="config-label">请求配置</label>
<div class="config-control">
<key-value-editor ref="configEditor" v-model="designer.config" add-text="添加配置" />
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="保存预览" name="preview">
<pre class="preview-json">{{ previewText }}</pre>
</el-tab-pane>
</el-tabs>
<el-dialog title="导入 JSON" :visible.sync="importJsonDialogVisible" width="600px" append-to-body>
<el-input v-model="importJsonText" type="textarea" :rows="12" placeholder="粘贴 JSON 内容,支持 ${变量名} 模板变量" />
<div v-if="importJsonError" style="color:#f56c6c;margin-top:6px;font-size:12px">{{ importJsonError }}</div>
<div slot="footer">
<el-button @click="importJsonDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="confirmImportJson">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
@ -65,8 +98,40 @@ export default {
return {
activeTab: 'params',
methodOptions: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
importJsonDialogVisible: false,
importJsonText: '',
importJsonError: '',
};
},
methods: {
flushEditors() {
['paramsEditor', 'headersEditor', 'configEditor', 'formDataEditor', 'urlencodedEditor'].forEach(refName => {
const editor = this.$refs[refName];
if (editor && typeof editor.flushBulk === 'function') {
editor.flushBulk();
}
});
},
confirmImportJson() {
const text = this.importJsonText.trim();
if (!text) {
this.importJsonError = '内容不能为空';
return;
}
// 将模板变量临时替换为合法值再做 JSON 校验
const sanitized = text.replace(/\$\{[^}]+\}/g, '"__var__"');
try {
JSON.parse(sanitized);
} catch (e) {
this.importJsonError = 'JSON 格式不正确:' + e.message;
return;
}
this.designer.body.raw = text;
this.importJsonDialogVisible = false;
this.importJsonText = '';
this.importJsonError = '';
},
},
computed: {
designer: {
get() {
@ -84,9 +149,60 @@ export default {
</script>
<style scoped>
.http-request-designer {
width: 100%;
box-sizing: border-box;
}
.request-url-bar {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.request-url-bar .request-method {
width: 108px;
flex-shrink: 0;
}
.request-url-bar .request-path {
flex: 1;
min-width: 0;
}
.request-tabs {
margin-top: 12px;
}
.request-tabs ::v-deep .el-tabs__header {
margin-bottom: 12px;
}
.request-tabs ::v-deep .el-tabs__content {
padding-top: 4px;
}
.config-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.config-field {
display: flex;
align-items: center;
gap: 12px;
}
.config-field--stack {
align-items: flex-start;
}
.config-field--stack .config-label {
padding-top: 8px;
}
.config-label {
flex: 0 0 100px;
font-size: 14px;
color: #606266;
text-align: right;
line-height: 32px;
}
.config-control {
flex: 1;
min-width: 0;
}
.body-mode {
margin-bottom: 12px;
}

View File

@ -1,31 +1,73 @@
<template>
<div class="key-value-editor">
<el-row v-for="(item, index) in rows" :key="index" :gutter="8" class="kv-row">
<el-col v-if="showEnabled" :span="2">
<div v-if="bulkEdit" class="kv-toolbar">
<el-button type="text" size="mini" class="kv-mode-btn" @click="toggleBulkMode">
{{ bulkMode ? '键值编辑' : '批量编辑' }}
</el-button>
</div>
<div v-if="!bulkMode" class="kv-table">
<div class="kv-header" :class="rowLayoutClass">
<span v-if="showEnabled" class="col-check" />
<span class="col-key">Key</span>
<span class="col-value">Value</span>
<span v-if="showDescription" class="col-desc">Description</span>
<span class="col-action" />
</div>
<div
v-for="(item, index) in rows"
:key="index"
class="kv-row"
:class="[rowLayoutClass, { 'is-disabled': showEnabled && item.enabled === false }]"
>
<div v-if="showEnabled" class="col-check">
<el-checkbox v-model="item.enabled" />
</el-col>
<el-col :span="showEnabled ? 7 : 8">
<el-input v-model="item.key" size="small" :placeholder="keyPlaceholder">
<template slot="prepend">Key</template>
</el-input>
</el-col>
<el-col :span="8">
<el-input v-model="item.value" size="small" :placeholder="valuePlaceholder">
<template slot="prepend">Value</template>
</el-input>
</el-col>
<el-col :span="5">
</div>
<div class="col-key">
<el-input v-model="item.key" size="small" :placeholder="keyPlaceholder" />
</div>
<div class="col-value">
<el-input v-model="item.value" size="small" :placeholder="valuePlaceholder" />
</div>
<div v-if="showDescription" class="col-desc">
<el-input v-model="item.description" size="small" placeholder="说明" />
</el-col>
<el-col :span="2">
<el-button size="small" plain type="danger" icon="el-icon-delete" @click="removeRow(index)" />
</el-col>
</el-row>
<el-button size="mini" type="text" icon="el-icon-plus" @click="addRow">{{ addText }}</el-button>
</div>
<div class="col-action">
<el-button
type="text"
size="small"
class="kv-delete-btn"
icon="el-icon-delete"
@click="removeRow(index)"
/>
</div>
</div>
<div v-if="!rows.length" class="kv-empty">暂无参数,点击下方按钮添加或使用批量编辑</div>
</div>
<div v-else class="kv-bulk">
<el-input
v-model="bulkText"
type="textarea"
:rows="bulkRows"
:placeholder="bulkPlaceholder"
@blur="syncBulkToRows"
/>
<p v-if="bulkError" class="kv-bulk-error">{{ bulkError }}</p>
<p class="kv-bulk-hint">{{ bulkHint }}</p>
</div>
<div v-if="!bulkMode" class="kv-footer">
<el-button type="text" size="mini" icon="el-icon-plus" class="kv-add-btn" @click="addRow">
{{ addText }}
</el-button>
</div>
</div>
</template>
<script>
import { bulkTextToRows, rowsToBulkText } from '../utils/requestTemplate';
export default {
name: 'PlatformKeyValueEditor',
props: {
@ -34,6 +76,24 @@ export default {
keyPlaceholder: { type: String, default: '参数名' },
valuePlaceholder: { type: String, default: '参数值' },
showEnabled: { type: Boolean, default: true },
showDescription: { type: Boolean, default: true },
bulkEdit: { type: Boolean, default: true },
bulkRows: { type: Number, default: 10 },
bulkPlaceholder: {
type: String,
default: '每行一个参数,格式 key:value\n以 # 开头的行表示禁用,例如:\n#disabledKey:value\nactiveKey:${token}',
},
bulkHint: {
type: String,
default: '每行格式为 key:value,仅按第一个冒号分割;以 # 开头的行在保存时视为禁用项。',
},
},
data() {
return {
bulkMode: false,
bulkText: '',
bulkError: '',
};
},
computed: {
rows: {
@ -44,8 +104,64 @@ export default {
this.$emit('input', value);
},
},
rowLayoutClass() {
return {
'no-desc': !this.showDescription,
'no-check': !this.showEnabled,
};
},
},
beforeDestroy() {
if (this._bulkTimer) {
clearTimeout(this._bulkTimer);
}
},
methods: {
/** 提交表单前调用,将批量编辑内容写回 v-model */
flushBulk() {
if (!this.bulkMode) {
return;
}
this.applyBulkText(true);
},
toggleBulkMode() {
if (this.bulkMode) {
if (!this.applyBulkText()) {
return;
}
this.bulkMode = false;
this.bulkError = '';
return;
}
this.bulkText = rowsToBulkText(this.rows);
this.bulkError = '';
this.bulkMode = true;
},
syncBulkToRows() {
if (!this.bulkMode) {
return;
}
this.applyBulkText(true);
},
applyBulkText(silent) {
const parsed = bulkTextToRows(this.bulkText);
if (!silent && this.bulkText.trim() && !parsed.length) {
this.bulkError = '未能解析出有效参数,请检查每行是否为 key:value 格式';
return false;
}
const descMap = {};
(this.rows || []).forEach(row => {
if (row && row.key) {
descMap[row.key] = row.description || '';
}
});
parsed.forEach(row => {
row.description = descMap[row.key] || '';
});
this.rows = parsed.length ? parsed : [];
this.bulkError = '';
return true;
},
addRow() {
const rows = this.rows.slice();
rows.push({ enabled: true, key: '', value: '', description: '' });
@ -61,7 +177,132 @@ export default {
</script>
<style scoped>
.key-value-editor {
width: 100%;
}
.kv-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 4px;
}
.kv-mode-btn {
padding: 0 4px;
font-size: 12px;
color: #409eff;
}
.kv-table {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
background: #fff;
}
.kv-header,
.kv-row {
margin-bottom: 8px;
display: grid;
grid-template-columns: 36px minmax(0, 1fr) minmax(0, 1fr) 120px 36px;
align-items: center;
column-gap: 8px;
padding: 0 8px;
}
.kv-header.no-desc,
.kv-row.no-desc {
grid-template-columns: 36px minmax(0, 1fr) minmax(0, 1fr) 36px;
}
.kv-header.no-check,
.kv-row.no-check {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 120px 36px;
}
.kv-header.no-check.no-desc,
.kv-row.no-check.no-desc {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 36px;
}
.kv-header {
height: 36px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
font-size: 12px;
font-weight: 600;
color: #909399;
}
.kv-row {
min-height: 40px;
padding-top: 4px;
padding-bottom: 4px;
border-bottom: 1px solid #f0f2f5;
}
.kv-row:last-child {
border-bottom: none;
}
.kv-row.is-disabled .col-key ::v-deep .el-input__inner,
.kv-row.is-disabled .col-value ::v-deep .el-input__inner {
color: #c0c4cc;
}
.col-check {
display: flex;
align-items: center;
justify-content: center;
}
.col-action {
display: flex;
align-items: center;
justify-content: center;
}
.kv-delete-btn {
padding: 4px;
color: #f56c6c;
font-size: 16px;
}
.kv-delete-btn:hover {
color: #f78989;
}
.kv-empty {
padding: 16px;
text-align: center;
font-size: 12px;
color: #c0c4cc;
}
.kv-footer {
margin-top: 6px;
}
.kv-add-btn {
padding-left: 0;
color: #409eff;
}
.kv-bulk ::v-deep .el-textarea__inner {
font-family: Consolas, Monaco, 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
}
.kv-bulk-hint {
margin: 6px 0 0;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
.kv-bulk-error {
margin: 6px 0 0;
font-size: 12px;
color: #f56c6c;
}
</style>

View File

@ -1,8 +1,16 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="租户ID" prop="tenantId"><el-input v-model="queryParams.tenantId" clearable @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="模板ID" prop="templateId"><el-input v-model="queryParams.templateId" clearable @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="平台模板" prop="templateId">
<entity-select
v-model="queryParams.templateId"
:loader="loadTemplates"
label-key="platformName"
sub-label-key="platformCode"
placeholder="请选择平台模板"
style="width: 220px"
/>
</el-form-item>
<el-form-item label="实例编码" prop="channelCode"><el-input v-model="queryParams.channelCode" clearable @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
@ -14,13 +22,12 @@
</el-row>
<el-table v-loading="loading" :data="channelList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="租户ID" align="center" prop="tenantId" width="90" />
<el-table-column label="平台模板" align="center" prop="templateId" width="90" />
<el-table-column label="实例编码" align="center" prop="channelCode" />
<el-table-column label="实例名称" align="center" prop="channelName" />
<el-table-column label="实例编码" align="center" prop="channelCode" min-width="120" />
<el-table-column label="实例名称" align="center" prop="channelName" min-width="140" />
<el-table-column label="baseURL" align="center" prop="baseUrl" min-width="180" show-overflow-tooltip />
<el-table-column label="默认用户" align="center" prop="defaultUserId" />
<el-table-column label="默认项目" align="center" prop="defaultProjectId" />
<el-table-column label="默认用户" align="center" prop="defaultUserId" min-width="120" show-overflow-tooltip />
<el-table-column label="默认项目" align="center" prop="defaultProjectId" min-width="120" show-overflow-tooltip />
<el-table-column label="状态" align="center" prop="status" width="90">
<template slot-scope="scope"><el-tag v-if="scope.row.status === 1" type="success">启用</el-tag><el-tag v-else type="info">停用</el-tag></template>
</el-table-column>
@ -36,12 +43,13 @@
<!-- 新增/修改对话框 -->
<el-dialog :title="title" :visible.sync="open" width="780px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="租户ID" prop="tenantId"><el-input v-model="form.tenantId" /></el-form-item>
<el-form-item label="平台模板" prop="templateId">
<entity-select v-model="form.templateId" :loader="loadTemplates" label-key="platformName" sub-label-key="platformCode" @change="handleTemplateChange" />
</el-form-item>
<el-form-item label="实例编码" prop="channelCode"><el-input v-model="form.channelCode" /></el-form-item>
<el-form-item label="实例名称" prop="channelName"><el-input v-model="form.channelName" /></el-form-item>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="实例编码" prop="channelCode"><el-input v-model="form.channelCode" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="实例名称" prop="channelName"><el-input v-model="form.channelName" /></el-form-item></el-col>
</el-row>
<el-form-item label="baseURL" prop="baseUrl"><el-input v-model="form.baseUrl" placeholder="第三方平台基础地址" /></el-form-item>
<!-- 动态授权表单 -->
@ -59,14 +67,38 @@
<codemirror ref="authEditor" :value="form.authConfig" :height="'150px'" :myMode="'application/json'" />
</el-form-item>
<!-- 接收安全配置 -->
<el-divider content-position="left">接收安全配置</el-divider>
<!-- 远程信息获取 -->
<el-divider content-position="left">平台关联信息</el-divider>
<el-form-item label="远程获取">
<el-button type="primary" size="small" :loading="fetchLoading" @click="handleFetchRemoteInfo">获取用户和项目信息</el-button>
<span v-if="remoteUserName" class="remote-info-tag">
<el-tag size="small" type="success">{{ remoteUserName }}</el-tag>
</span>
</el-form-item>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="默认用户ID" prop="defaultUserId">
<el-input v-model="form.defaultUserId" placeholder="点击上方按钮自动获取" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="默认项目ID" prop="defaultProjectId">
<el-select v-if="projectOptions.length > 0" v-model="form.defaultProjectId" placeholder="请选择项目" style="width: 100%" filterable>
<el-option v-for="item in projectOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-input v-else v-model="form.defaultProjectId" placeholder="点击上方按钮自动获取项目列表" />
</el-form-item>
</el-col>
</el-row>
<!-- 接收安全配置(折叠) -->
<el-collapse class="advanced-collapse">
<el-collapse-item title="接收安全配置" name="receive">
<el-form-item label="接收Token"><el-input v-model="receiveFormData.token" placeholder="第三方调用时需携带的 token" /></el-form-item>
<el-form-item label="IP白名单"><el-input v-model="receiveFormData.ipWhitelist" placeholder="多个IP用逗号分隔" /></el-form-item>
</el-collapse-item>
</el-collapse>
<el-divider content-position="left">默认映射</el-divider>
<el-form-item label="默认用户ID" prop="defaultUserId"><el-input v-model="form.defaultUserId" /></el-form-item>
<el-form-item label="默认项目ID" prop="defaultProjectId"><el-input v-model="form.defaultProjectId" /></el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status"><el-radio :label="1">启用</el-radio><el-radio :label="0">停用</el-radio></el-radio-group>
</el-form-item>
@ -80,7 +112,7 @@
</template>
<script>
import { listTenantChannel, getTenantChannel, delTenantChannel, addTenantChannel, updateTenantChannel } from '@/api/iot/platformConnect/tenantChannel'
import { listTenantChannel, getTenantChannel, delTenantChannel, addTenantChannel, updateTenantChannel, fetchRemoteInfo } from '@/api/iot/platformConnect/tenantChannel'
import { listTemplate, getTemplate } from '@/api/iot/platformConnect/template'
import EntitySelect from '../components/EntitySelect.vue'
@ -98,13 +130,15 @@ export default {
channelList: [],
title: '',
open: false,
fetchLoading: false,
remoteUserName: '',
projectOptions: [],
queryParams: { pageNum: 1, pageSize: 10, tenantId: null, templateId: null, channelCode: null },
form: {},
authSchemaFields: [],
authFormData: {},
receiveFormData: { token: '', ipWhitelist: '' },
rules: {
tenantId: [{ required: true, message: '租户ID不能为空', trigger: 'blur' }],
templateId: [{ required: true, message: '平台模板不能为空', trigger: 'blur' }],
channelCode: [{ required: true, message: '实例编码不能为空', trigger: 'blur' }],
channelName: [{ required: true, message: '实例名称不能为空', trigger: 'blur' }]
@ -128,9 +162,7 @@ export default {
},
handleTemplateChange(val, templateObj) {
if (templateObj) {
// 解析 authSchema 生成动态表单
this.parseAuthSchema(templateObj.authSchema)
// 自动填充 baseUrl
if (!this.form.baseUrl && templateObj.baseUrlTemplate) {
this.form.baseUrl = templateObj.baseUrlTemplate
}
@ -154,15 +186,59 @@ export default {
this.authSchemaFields = []
}
},
handleFetchRemoteInfo() {
// 序列化当前表单的 authConfig
let authConfig = null
if (this.authSchemaFields.length > 0) {
authConfig = JSON.stringify(this.authFormData)
} else if (this.$refs.authEditor) {
authConfig = this.$refs.authEditor.getValue()
}
if (!this.form.baseUrl) {
this.$modal.msgWarning('请先填写 baseURL')
return
}
if (!authConfig || authConfig === '{}') {
this.$modal.msgWarning('请先填写授权配置')
return
}
this.fetchLoading = true
const payload = {
templateId: this.form.templateId,
baseUrl: this.form.baseUrl,
authConfig: authConfig
}
fetchRemoteInfo(payload).then(res => {
const data = res.data
if (data.userId) {
this.form.defaultUserId = data.userId
this.remoteUserName = data.userName || data.userId
}
if (data.projects && data.projects.length > 0) {
this.projectOptions = data.projects
if (!this.form.defaultProjectId) {
this.form.defaultProjectId = data.projects[0].id
}
}
this.$modal.msgSuccess('获取成功')
}).catch(() => {
this.$modal.msgError('获取远程信息失败,请检查凭证和网络')
}).finally(() => {
this.fetchLoading = false
})
},
cancel() {
this.open = false
this.reset()
},
reset() {
this.form = { id: null, tenantId: null, templateId: null, channelCode: null, channelName: null, baseUrl: null, authConfig: null, receiveConfig: null, defaultUserId: null, defaultProjectId: null, status: 1 }
this.form = { id: null, tenantId: null, templateId: null, channelCode: null, channelName: null, baseUrl: null, authConfig: null, receiveConfig: null, defaultUserId: null, defaultProjectId: null, configJson: null, status: 1 }
this.authFormData = {}
this.receiveFormData = { token: '', ipWhitelist: '' }
this.authSchemaFields = []
this.remoteUserName = ''
this.projectOptions = []
this.resetForm('form')
},
handleQuery() {
@ -181,14 +257,13 @@ export default {
handleAdd() {
this.reset()
this.open = true
this.title = '添加租户接入'
this.title = '添加连接配置'
},
handleUpdate(row) {
this.reset()
const id = row.id || this.ids
getTenantChannel(id).then(response => {
this.form = response.data
// 解析 authConfig 到动态表单
if (this.form.authConfig) {
try {
this.authFormData = JSON.parse(this.form.authConfig)
@ -196,7 +271,6 @@ export default {
this.authFormData = {}
}
}
// 解析 receiveConfig
if (this.form.receiveConfig) {
try {
this.receiveFormData = JSON.parse(this.form.receiveConfig)
@ -204,7 +278,6 @@ export default {
this.receiveFormData = { token: '', ipWhitelist: '' }
}
}
// 加载模板的 authSchema
if (this.form.templateId) {
getTemplate(this.form.templateId).then(res => {
if (res.data) {
@ -213,13 +286,12 @@ export default {
})
}
this.open = true
this.title = '修改租户接入'
this.title = '修改连接配置'
})
},
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
// 序列化动态表单到 JSON
if (this.authSchemaFields.length > 0) {
this.form.authConfig = JSON.stringify(this.authFormData)
} else if (this.$refs.authEditor) {
@ -245,7 +317,7 @@ export default {
},
handleDelete(row) {
const ids = row.id || this.ids
this.$modal.confirm('是否确认删除租户接入编号为"' + ids + '"的数据项?').then(() => {
this.$modal.confirm('是否确认删除该连接配置?').then(() => {
return delTenantChannel(ids)
}).then(() => {
this.getList()
@ -255,3 +327,12 @@ export default {
}
}
</script>
<style scoped>
.remote-info-tag {
margin-left: 12px;
}
.advanced-collapse {
margin-bottom: 16px;
}
</style>

View File

@ -1,3 +1,50 @@
/** 键值行 -> Postman 风格批量文本(每行 key:value) */
export function rowsToBulkText(rows) {
return (rows || [])
.map(item => {
if (!item || !item.key) {
return '';
}
const prefix = item.enabled === false ? '#' : '';
const value = item.value == null ? '' : String(item.value);
return `${prefix}${item.key}:${value}`;
})
.filter(Boolean)
.join('\n');
}
/** 批量文本 -> 键值行;# 开头表示禁用 */
export function bulkTextToRows(text) {
const rows = [];
(text || '').split(/\r?\n/).forEach(line => {
const trimmed = line.trim();
if (!trimmed) {
return;
}
let enabled = true;
let content = trimmed;
if (content.startsWith('#')) {
enabled = false;
content = content.slice(1).trim();
if (!content) {
return;
}
}
const colonIndex = content.indexOf(':');
if (colonIndex < 0) {
rows.push({ enabled, key: content, value: '', description: '' });
return;
}
rows.push({
enabled,
key: content.slice(0, colonIndex).trim(),
value: content.slice(colonIndex + 1),
description: '',
});
});
return rows;
}
export function rowsToObject(rows) {
const result = {}
;(rows || []).forEach(item => {
@ -76,7 +123,7 @@ export function serializeDesigner(value) {
} else if (body.mode === 'x-www-form-urlencoded') {
bodyTemplate = JSON.stringify({ mode: body.mode, urlencoded: body.urlencoded || [] });
} else {
bodyTemplate = '';
bodyTemplate = null;
}
return {
method: value.method,