20260530:需求第2版探索
This commit is contained in:
parent
8ffdd7d5cb
commit
d4ad1eab67
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal 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 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
20
sql/iot_platform_connect_menu_upgrade.sql
Normal file
20
sql/iot_platform_connect_menu_upgrade.sql
Normal 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;
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 } })
|
||||
}
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
|
||||
1
yack-web/src/assets/icons/svg/api.svg
Normal file
1
yack-web/src/assets/icons/svg/api.svg
Normal 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 |
@ -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>
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user