Merge branch 'dev-v2' into pr@dev-v2_export_data

This commit is contained in:
taojinlong 2024-06-24 13:54:34 +08:00
commit 892c395e67
81 changed files with 2232 additions and 263 deletions

View File

@ -14,18 +14,18 @@ public class MybatisPlusGenerator {
* 第一 我嫌麻烦
* 第二 后面配置会放到nacos读起来更麻烦了
*/
private static final String url = "jdbc:mysql://localhost:3306/dataease?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false";
private static final String url = "jdbc:mysql://localhost:3306/de_standalone?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false";
private static final String username = "root";
private static final String password = "123456";
private static final String password = "Password123@mysql";
/**
* 业务模块例如datasource,dataset,panel等
*/
private static final String busi = "visualization";
private static final String busi = "share";
/**
* 这是要生成代码的表名称
*/
private static final String TABLE_NAME = "data_visualization_info";
private static final String TABLE_NAME = "xpack_share";
/**
* 下面两个配置基本上不用动

View File

@ -1,7 +1,28 @@
package io.dataease.datasource.provider;
import io.dataease.api.dataset.dto.DatasetTableDTO;
import io.dataease.api.ds.vo.TableField;
import io.dataease.datasource.dao.auto.entity.CoreDatasource;
import io.dataease.datasource.request.DatasourceRequest;
import io.dataease.exception.DEException;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
/**
* @Author Junjun
*/
public abstract class Provider {
public abstract List<String> getSchema(DatasourceRequest datasourceRequest);
public abstract List<DatasetTableDTO> getTables(DatasourceRequest datasourceRequest);
public abstract Connection getConnection(CoreDatasource coreDatasource) throws DEException;
public abstract String checkStatus(DatasourceRequest datasourceRequest) throws Exception;
public abstract Map<String, Object> fetchResultField(DatasourceRequest datasourceRequest) throws DEException;
public abstract List<TableField> fetchTableField(DatasourceRequest datasourceRequest) throws DEException;
}

View File

@ -1,5 +1,6 @@
package io.dataease.home;
import io.dataease.license.utils.LicenseUtil;
import io.dataease.utils.ModelUtils;
import io.dataease.utils.RsaUtils;
import org.springframework.beans.factory.annotation.Value;
@ -31,7 +32,7 @@ public class RestIndexController {
@GetMapping("/xpackModel")
@ResponseBody
public boolean xpackModel() {
return xpackFrontDistributed;
return xpackFrontDistributed && LicenseUtil.licenseValid();
}
}

View File

@ -0,0 +1,108 @@
package io.dataease.share.dao.auto.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
/**
* <p>
*
* </p>
*
* @author fit2cloud
* @since 2024-06-21
*/
@TableName("core_share_ticket")
public class CoreShareTicket implements Serializable {
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 分享uuid
*/
private String uuid;
/**
* ticket
*/
private String ticket;
/**
* ticket有效期
*/
private Long exp;
/**
* ticket参数
*/
private String args;
/**
* 首次访问时间
*/
private Long accessTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getTicket() {
return ticket;
}
public void setTicket(String ticket) {
this.ticket = ticket;
}
public Long getExp() {
return exp;
}
public void setExp(Long exp) {
this.exp = exp;
}
public String getArgs() {
return args;
}
public void setArgs(String args) {
this.args = args;
}
public Long getAccessTime() {
return accessTime;
}
public void setAccessTime(Long accessTime) {
this.accessTime = accessTime;
}
@Override
public String toString() {
return "CoreShareTicket{" +
"id = " + id +
", uuid = " + uuid +
", ticket = " + ticket +
", exp = " + exp +
", args = " + args +
", accessTime = " + accessTime +
"}";
}
}

View File

@ -9,7 +9,7 @@ import java.io.Serializable;
* </p>
*
* @author fit2cloud
* @since 2024-04-07
* @since 2024-06-21
*/
@TableName("xpack_share")
public class XpackShare implements Serializable {
@ -66,6 +66,11 @@ public class XpackShare implements Serializable {
*/
private Boolean autoPwd;
/**
* ticket必须
*/
private Boolean ticketRequire;
public Long getId() {
return id;
}
@ -146,6 +151,14 @@ public class XpackShare implements Serializable {
this.autoPwd = autoPwd;
}
public Boolean getTicketRequire() {
return ticketRequire;
}
public void setTicketRequire(Boolean ticketRequire) {
this.ticketRequire = ticketRequire;
}
@Override
public String toString() {
return "XpackShare{" +
@ -159,6 +172,7 @@ public class XpackShare implements Serializable {
", oid = " + oid +
", type = " + type +
", autoPwd = " + autoPwd +
", ticketRequire = " + ticketRequire +
"}";
}
}

View File

@ -0,0 +1,18 @@
package io.dataease.share.dao.auto.mapper;
import io.dataease.share.dao.auto.entity.CoreShareTicket;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2024-06-21
*/
@Mapper
public interface CoreShareTicketMapper extends BaseMapper<CoreShareTicket> {
}

View File

@ -10,7 +10,7 @@ import org.apache.ibatis.annotations.Mapper;
* </p>
*
* @author fit2cloud
* @since 2024-04-07
* @since 2024-06-21
*/
@Mapper
public interface XpackShareMapper extends BaseMapper<XpackShare> {

View File

@ -6,6 +6,7 @@ import io.dataease.share.dao.ext.po.XpackSharePO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface XpackShareExtMapper {
@ -28,4 +29,7 @@ public interface XpackShareExtMapper {
@Select("select type from data_visualization_info where id = #{id}")
String visualizationType(@Param("id") Long id);
@Update("update core_share_ticket set uuid = #{ticketUuid} where uuid = #{originUuid}")
void updateTicketUuid(@Param("originUuid") String originUuid, @Param("ticketUuid") String ticketUuid);
}

View File

@ -0,0 +1,153 @@
package io.dataease.share.manage;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.dataease.api.xpack.share.request.TicketCreator;
import io.dataease.api.xpack.share.request.TicketDelRequest;
import io.dataease.api.xpack.share.request.TicketSwitchRequest;
import io.dataease.api.xpack.share.vo.TicketVO;
import io.dataease.api.xpack.share.vo.TicketValidVO;
import io.dataease.commons.utils.CodingUtil;
import io.dataease.exception.DEException;
import io.dataease.share.dao.auto.entity.CoreShareTicket;
import io.dataease.share.dao.auto.entity.XpackShare;
import io.dataease.share.dao.auto.mapper.CoreShareTicketMapper;
import io.dataease.share.dao.auto.mapper.XpackShareMapper;
import io.dataease.share.dao.ext.mapper.XpackShareExtMapper;
import io.dataease.utils.AuthUtils;
import io.dataease.utils.BeanUtils;
import io.dataease.utils.IDUtils;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Component
public class ShareTicketManage {
@Resource
private CoreShareTicketMapper coreShareTicketMapper;
@Resource
private XpackShareMapper xpackShareMapper;
@Resource
private XpackShareExtMapper xpackShareExtMapper;
public CoreShareTicket getByTicket(String ticket) {
QueryWrapper<CoreShareTicket> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("ticket", ticket);
return coreShareTicketMapper.selectOne(queryWrapper);
}
public String saveTicket(TicketCreator creator) {
String ticket = creator.getTicket();
if (StringUtils.isNotBlank(ticket)) {
CoreShareTicket ticketEntity = getByTicket(ticket);
if (ObjectUtils.isNotEmpty(ticketEntity)) {
if (creator.isGenerateNew()) {
ticketEntity.setAccessTime(null);
ticketEntity.setTicket(CodingUtil.shortUuid());
}
ticketEntity.setArgs(creator.getArgs());
ticketEntity.setExp(creator.getExp());
ticketEntity.setUuid(creator.getUuid());
coreShareTicketMapper.updateById(ticketEntity);
return ticketEntity.getTicket();
}
}
ticket = CodingUtil.shortUuid();
CoreShareTicket linkTicket = new CoreShareTicket();
linkTicket.setId(IDUtils.snowID());
linkTicket.setTicket(ticket);
linkTicket.setArgs(creator.getArgs());
linkTicket.setExp(creator.getExp());
linkTicket.setUuid(creator.getUuid());
coreShareTicketMapper.insert(linkTicket);
return ticket;
}
public void deleteTicket(TicketDelRequest request) {
String ticket = request.getTicket();
if (StringUtils.isBlank(ticket)) {
DEException.throwException("ticket为必填参数");
}
QueryWrapper<CoreShareTicket> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("ticket", ticket);
coreShareTicketMapper.delete(queryWrapper);
}
public void switchRequire(TicketSwitchRequest request) {
String resourceId = request.getResourceId();
Boolean require = request.getRequire();
QueryWrapper<XpackShare> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("resource_id", resourceId);
queryWrapper.eq("creator", AuthUtils.getUser().getUserId());
XpackShare xpackShare = xpackShareMapper.selectOne(queryWrapper);
xpackShare.setTicketRequire(require);
xpackShareMapper.updateById(xpackShare);
}
public List<TicketVO> query(Long resourceId) {
QueryWrapper<XpackShare> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("resource_id", resourceId);
queryWrapper.eq("creator", AuthUtils.getUser().getUserId());
XpackShare xpackShare = xpackShareMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(xpackShare)) return null;
String uuid = xpackShare.getUuid();
if (StringUtils.isBlank(uuid)) return null;
QueryWrapper<CoreShareTicket> ticketQueryWrapper = new QueryWrapper<>();
ticketQueryWrapper.eq("uuid", uuid);
List<CoreShareTicket> coreShareTickets = coreShareTicketMapper.selectList(ticketQueryWrapper);
if (CollectionUtils.isEmpty(coreShareTickets)) return null;
return coreShareTickets.stream().map(item -> BeanUtils.copyBean(new TicketVO(), item)).toList();
}
@Transactional
public void updateByUuidChange(String originalUuid, String newUuid) {
xpackShareExtMapper.updateTicketUuid(originalUuid, newUuid);
}
@Transactional
public void deleteByShare(String uuid) {
QueryWrapper<CoreShareTicket> ticketQueryWrapper = new QueryWrapper<>();
ticketQueryWrapper.eq("uuid", uuid);
coreShareTicketMapper.delete(ticketQueryWrapper);
}
public TicketValidVO validateTicket(String ticket, XpackShare share) {
TicketValidVO vo = new TicketValidVO();
if (StringUtils.isBlank(ticket)) {
vo.setTicketValid(!share.getTicketRequire());
return vo;
}
CoreShareTicket linkTicket = getByTicket(ticket);
if (ObjectUtils.isEmpty(linkTicket)) {
vo.setTicketValid(false);
return vo;
}
vo.setTicketValid(true);
vo.setArgs(linkTicket.getArgs());
Long accessTime = linkTicket.getAccessTime();
long now = System.currentTimeMillis();
if (ObjectUtils.isEmpty(accessTime)) {
accessTime = now;
vo.setTicketExp(false);
linkTicket.setAccessTime(accessTime);
coreShareTicketMapper.updateById(linkTicket);
return vo;
}
Long exp = linkTicket.getExp();
if (ObjectUtils.isEmpty(exp) || exp.equals(0L)) {
vo.setTicketExp(false);
return vo;
}
long expTime = exp * 60L * 1000L;
long time = now - accessTime;
vo.setTicketExp(time > expTime);
return vo;
}
}

View File

@ -7,6 +7,7 @@ import io.dataease.api.visualization.request.VisualizationWorkbranchQueryRequest
import io.dataease.api.xpack.share.request.XpackShareProxyRequest;
import io.dataease.api.xpack.share.request.XpackSharePwdValidator;
import io.dataease.api.xpack.share.request.XpackShareUuidEditor;
import io.dataease.api.xpack.share.vo.TicketValidVO;
import io.dataease.api.xpack.share.vo.XpackShareGridVO;
import io.dataease.api.xpack.share.vo.XpackShareProxyVO;
import io.dataease.auth.bo.TokenUserBO;
@ -28,6 +29,7 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
@ -46,6 +48,9 @@ public class XpackShareManage {
@Resource(name = "xpackShareExtMapper")
private XpackShareExtMapper xpackShareExtMapper;
@Resource
private ShareTicketManage shareTicketManage;
public XpackShare queryByResource(Long resourceId) {
Long userId = AuthUtils.getUser().getUserId();
QueryWrapper<XpackShare> queryWrapper = new QueryWrapper<>();
@ -54,10 +59,12 @@ public class XpackShareManage {
return xpackShareMapper.selectOne(queryWrapper);
}
@Transactional
public void switcher(Long resourceId) {
XpackShare originData = queryByResource(resourceId);
if (ObjectUtils.isNotEmpty(originData)) {
xpackShareMapper.deleteById(originData.getId());
shareTicketManage.deleteByShare(originData.getUuid());
return;
}
TokenUserBO user = AuthUtils.getUser();
@ -74,6 +81,7 @@ public class XpackShareManage {
xpackShareMapper.insert(xpackShare);
}
@Transactional
public String editUuid(XpackShareUuidEditor editor) {
Long resourceId = editor.getResourceId();
String uuid = editor.getUuid();
@ -98,6 +106,7 @@ public class XpackShareManage {
if (!matcher.matches()) {
return "仅支持8-16位(字母数字),请重新输入!";
}
shareTicketManage.updateByUuidChange(originData.getUuid(), uuid);
originData.setUuid(uuid);
xpackShareMapper.updateById(originData);
return "";
@ -196,7 +205,8 @@ public class XpackShareManage {
response.addHeader(AuthConstant.LINK_TOKEN_KEY, linkToken);
Integer type = xpackShare.getType();
String typeText = (ObjectUtils.isNotEmpty(type) && type == 1) ? "dashboard" : "dataV";
return new XpackShareProxyVO(xpackShare.getResourceId(), xpackShare.getCreator(), linkExp(xpackShare), pwdValid(xpackShare, request.getCiphertext()), typeText, inIframeError);
TicketValidVO validVO = shareTicketManage.validateTicket(request.getTicket(), xpackShare);
return new XpackShareProxyVO(xpackShare.getResourceId(), xpackShare.getCreator(), linkExp(xpackShare), pwdValid(xpackShare, request.getCiphertext()), typeText, inIframeError, validVO);
}
private boolean linkExp(XpackShare xpackShare) {

View File

@ -0,0 +1,40 @@
package io.dataease.share.server;
import io.dataease.api.xpack.share.ShareTicketApi;
import io.dataease.api.xpack.share.request.TicketCreator;
import io.dataease.api.xpack.share.request.TicketDelRequest;
import io.dataease.api.xpack.share.request.TicketSwitchRequest;
import io.dataease.api.xpack.share.vo.TicketVO;
import io.dataease.share.manage.ShareTicketManage;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/ticket")
public class ShareTicketServer implements ShareTicketApi {
@Resource
private ShareTicketManage shareTicketManage;
@Override
public String saveTicket(TicketCreator creator) {
return shareTicketManage.saveTicket(creator);
}
@Override
public void deleteTicket(TicketDelRequest request) {
shareTicketManage.deleteTicket(request);
}
@Override
public void switchRequire(TicketSwitchRequest request) {
shareTicketManage.switchRequire(request);
}
@Override
public List<TicketVO> query(Long resourceId) {
return shareTicketManage.query(resourceId);
}
}

View File

@ -76,7 +76,7 @@ public class StaticResourceServer implements StaticResourceApi {
return true;
}
// 判断其他图片
if (image == null || image.getWidth() <= 0 || image.getHeight() <= 0 || !isValidSVG(file)) {
if (image == null || image.getWidth() <= 0 || image.getHeight() <= 0) {
return false;
}
return true;

View File

@ -1 +1,36 @@
ALTER TABLE `core_export_task` ADD COLUMN `msg` LONGTEXT NULL COMMENT '错误信息' AFTER `params`;
ALTER TABLE `core_export_task`
ADD COLUMN `msg` LONGTEXT NULL COMMENT '错误信息' AFTER `params`;
DROP TABLE IF EXISTS `xpack_plugin`;
CREATE TABLE `xpack_plugin`
(
`id` bigint NOT NULL COMMENT 'ID',
`name` varchar(255) NOT NULL COMMENT '插件名称',
`icon` longtext NOT NULL COMMENT '图标',
`version` varchar(255) NOT NULL COMMENT '版本',
`install_time` bigint NOT NULL COMMENT '安装时间',
`flag` varchar(255) NOT NULL COMMENT '类型',
`developer` varchar(255) NOT NULL COMMENT '开发者',
`config` longtext NOT NULL COMMENT '插件配置',
`require_version` varchar(255) NOT NULL COMMENT 'DE最低版本',
`module_name` varchar(255) NOT NULL COMMENT '模块名称',
`jar_name` varchar(255) NOT NULL COMMENT 'Jar包名称',
PRIMARY KEY (`id`)
) COMMENT ='插件表';
ALTER TABLE `xpack_share`
ADD COLUMN `ticket_require` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'ticket必须' AFTER `auto_pwd`;
DROP TABLE IF EXISTS `core_share_ticket`;
CREATE TABLE `core_share_ticket`
(
`id` bigint NOT NULL COMMENT 'ID',
`uuid` varchar(255) NOT NULL COMMENT '分享uuid',
`ticket` varchar(255) NOT NULL COMMENT 'ticket',
`exp` bigint DEFAULT NULL COMMENT 'ticket有效期',
`args` longtext COMMENT 'ticket参数',
`access_time` bigint DEFAULT NULL COMMENT '首次访问时间',
PRIMARY KEY (`id`)
) COMMENT ='分享Ticket表';

View File

@ -26,6 +26,6 @@ export default {
}
}
},
sourcemap: false
sourcemap: true
}
}

View File

@ -14,7 +14,7 @@ export default {
],
build: {
rollupOptions: {
external: id => /de-xpack/.test(id) || /extensions-view-3dpie/.test(id),
external: id => /de-xpack/.test(id) || /extensions/.test(id),
output: {
// 用于命名代码拆分时创建的共享块的输出命名
chunkFileNames: `assets/chunk/[name]-${pkg.version}-${pkg.name}.js`,

View File

@ -1,6 +1,7 @@
import request from '@/config/axios'
export const queryUserApi = data => request.post({ url: '/user/byCurOrg', data })
export const queryUserOptionsApi = () => request.get({ url: '/user/org/option' })
export const queryRoleApi = data => request.post({ url: '/role/byCurOrg', data })
export const resourceTreeApi = (flag: string) => request.get({ url: '/auth/busiResource/' + flag })

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="1719155593625" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="32183" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M658.52 0v658.52H0V1024h1024V0H658.52z m23.1 991.3V835.18h-32.7V991.3H465.31V790.06h-32.7V991.3H249.02V835.18h-32.7V991.3H32.7V691.22h658.52V32.7H991.3v183.61H835.18v32.7H991.3v183.61H790.06v32.7H991.3v183.59H835.18v32.7H991.3v309.68H681.62z" p-id="32184"></path></svg>

After

Width:  |  Height:  |  Size: 604 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="" xmlns="http://www.w3.org/2000/svg">
<path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21ZM12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23ZM10.84 14.2799L16.1433 8.97665C16.3386 8.78138 16.6551 8.78138 16.8504 8.97665L17.5575 9.68375C17.7528 9.87901 17.7528 10.1956 17.5575 10.3909L11.1936 16.7548C10.9983 16.9501 10.6817 16.9501 10.4864 16.7548L6.82603 13.0944C6.63077 12.8991 6.63077 12.5826 6.82603 12.3873L7.53314 11.6802C7.7284 11.4849 8.04498 11.4849 8.24024 11.6802L10.84 14.2799Z" fill=""/>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@ -157,6 +157,7 @@ onUnmounted(() => {
background-color: @side-area-background;
border-top: 1px solid @side-outline-border-color;
color: #fff;
z-index: 2;
transition: 0.5s;
.scale-area {
display: flex;

View File

@ -160,6 +160,7 @@ const dragOnEnd = ({ oldIndex, newIndex }) => {
componentData.value.splice(comLength - 1 - oldIndex, 1)
componentData.value.splice(comLength - 1 - newIndex, 0, target)
dvMainStore.setCurComponent({ component: target, index: transformIndex(comLength - oldIndex) })
snapshotStore.recordSnapshotCache()
}
const getIconName = item => {

View File

@ -1490,25 +1490,6 @@ defineExpose({
@linkJumpSetOpen="linkJumpSetOpen(item)"
@linkageSetOpen="linkageSetOpen(item)"
>
<!--如果是图表 则动态获取预存的chart-view数据-->
<!-- <PluginComponent
v-if="item['isPlugin']"
:jsname="item['pluginFlag'] || 'L2NvbXBvbmVudC9pbmRleA=='"
class="component"
:id="'component' + item.id"
:active="item.id === curComponentId"
:dv-type="dvInfo.type"
:scale="curBaseScale"
:style="getComponentStyle(item.style)"
:prop-value="item.propValue"
:is-edit="true"
:view="canvasViewInfo[item.id]"
:element="item"
:request="item.request"
@input="handleInput"
:dv-info="dvInfo"
:canvas-active="canvasActive"
/> -->
<component
:is="findComponent(item.component)"
v-if="item.component === 'UserView' || item['isPlugin']"

View File

@ -2,7 +2,7 @@
import noLic from './nolic.vue'
import { ref, useAttrs, onMounted } from 'vue'
import { execute, randomKey, formatArray } from './convert'
import { loadPluginApi, loadDistributed, xpackModelApi } from '@/api/plugin'
import { loadPluginApi, xpackModelApi } from '@/api/plugin'
import { useCache } from '@/hooks/web/useCache'
import { i18n } from '@/plugins/vue-i18n'
import * as Vue from 'vue'
@ -10,7 +10,7 @@ import axios from 'axios'
import * as Pinia from 'pinia'
import * as vueRouter from 'vue-router'
import { useEmitt } from '@/hooks/web/useEmitt'
import request from '@/config/axios'
const { wsCache } = useCache()
const plugin = ref()
@ -33,15 +33,12 @@ const generateRamStr = (len: number) => {
}
const importProxy = (bytesArray: any[]) => {
/* const promise = import(
`../../../../../../../${formatArray(bytesArray[7])}/${formatArray(bytesArray[8])}/${formatArray(
bytesArray[9]
)}/${formatArray(bytesArray[10])}/${formatArray(bytesArray[11])}.vue`
) */
const promise = import(
`../../../../../../../extensions-view-3dpie/${formatArray(bytesArray[8])}/${formatArray(
`../../../../../../../extensions/${formatArray(bytesArray[8])}/${formatArray(
bytesArray[9]
)}/${formatArray(bytesArray[10])}/${formatArray(bytesArray[11])}.vue`
)}/${formatArray(bytesArray[10])}/${formatArray(bytesArray[11])}/${formatArray(
bytesArray[12]
)}.vue`
)
promise
.then((res: any) => {
@ -53,16 +50,23 @@ const importProxy = (bytesArray: any[]) => {
})
}
const getModuleName = () => {
const jsPath = window.atob(attrs.jsname.toString())
return jsPath.split('/')[0]
}
const loadComponent = () => {
const moduleName = getModuleName()
loading.value = true
const byteArray = wsCache.get(`de-plugin-proxy-plugin`)
const byteArray = wsCache.get(`de-plugin-proxy-${moduleName}`)
if (byteArray) {
importProxy(JSON.parse(byteArray))
loading.value = false
return
}
const key = generateRamStr(randomKey())
loadPluginApi(key)
const moduleNameKey = window.btoa(moduleName)
const saltKey = `${key},${moduleNameKey}`
loadPluginApi(saltKey)
.then(response => {
let code = response.data
const byteArray = execute(code, key)
@ -82,7 +86,8 @@ const storeCacheProxy = byteArray => {
byteArray.forEach(item => {
result.push([...item])
})
wsCache.set(`de-plugin-proxy-plugin`, JSON.stringify(result))
const moduleName = getModuleName()
wsCache.set(`de-plugin-proxy-${moduleName}`, JSON.stringify(result))
}
const pluginProxy = ref(null)
const invokeMethod = param => {
@ -104,8 +109,9 @@ onMounted(async () => {
distributed = wsCache.get(key)
}
if (distributed) {
if (window['DEXPack']) {
const xpack = await window['DEXPack'].mapping[attrs.jsname]
const moduleName = getModuleName()
if (window[moduleName]) {
const xpack = await window[moduleName].mapping[attrs.jsname]
plugin.value = xpack.default
} else {
window['Vue'] = Vue
@ -114,9 +120,10 @@ onMounted(async () => {
window['vueRouter'] = vueRouter
window['MittAll'] = useEmitt().emitter.all
window['I18n'] = i18n
loadDistributed().then(async res => {
new Function(res.data)()
const xpack = await window['DEXPack'].mapping[attrs.jsname]
const url = `/xpackComponent/pluginStaticInfo/${moduleName}`
request.get({ url }).then(async res => {
new Function(res.data || res)()
const xpack = await window[moduleName].mapping[attrs.jsname]
plugin.value = xpack.default
})
}

View File

@ -618,7 +618,7 @@ defineExpose({
}
:deep(.ed-tree--highlight-current .ed-tree-node.is-current > .ed-tree-node__content) {
background-color: #8dbbef !important;
background-color: rgba(51, 112, 255, 0.1) !important;
}
.tree-content ::deep(.ed-input__inner) {

View File

@ -20,7 +20,7 @@
id="input"
ref="files"
type="file"
accept=".jpeg,.jpg,.png,.gif"
accept=".jpeg,.jpg,.png,.gif,.svg"
hidden
@click="
e => {

View File

@ -4,7 +4,7 @@
id="input"
ref="files"
type="file"
accept=".jpeg,.jpg,.png,.gif"
accept=".jpeg,.jpg,.png,.gif,.svg"
hidden
@click="
e => {

View File

@ -4,7 +4,7 @@
id="input"
ref="files"
type="file"
accept=".jpeg,.jpg,.png,.gif"
accept=".jpeg,.jpg,.png,.gif,.svg"
hidden
@click="
e => {

View File

@ -173,6 +173,8 @@ service.interceptors.response.use(
return response
} else if (response.config.url.includes('DEXPack.umd.js')) {
return response
} else if (response.config.url.startsWith('/xpackComponent/pluginStaticInfo/extensions-')) {
return response
} else {
if (
!response?.config?.url.startsWith('/xpackComponent/content') &&
@ -269,18 +271,9 @@ const executeVersionHandler = (response: AxiosResponse) => {
return
}
if (executeVersion && executeVersion !== cacheVal) {
wsCache.clear()
wsCache.set(key, executeVersion)
showMsg('系统有升级请点击刷新页面', '-sys-upgrade-')
/* ElMessageBox.confirm('系统有升级请点击刷新页面', {
confirmButtonType: 'primary',
type: 'warning',
confirmButtonText: '刷新',
cancelButtonText: '取消',
autofocus: false,
showClose: false
}).then(() => {
window.location.reload()
}) */
}
}
export { service, cancelMap }

View File

@ -0,0 +1,219 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
const dvMainStore = dvMainStoreWithOut()
const wRuleRef = ref(null)
const props = defineProps({
tickLabelFormatter: {
type: Function,
default: value => value.toString() //
},
size: {
type: Number,
default: 300 //
},
direction: {
type: String,
default: 'horizontal' //
}
})
const labelInterval = 5
const { canvasStyleData, curComponent } = storeToRefs(dvMainStore)
const rulerSize = computed(() =>
props.direction === 'horizontal' ? canvasStyleData.value.width : canvasStyleData.value.height
)
const curComponentSize = computed(() => {
if (curComponent.value) {
return (
((props.direction === 'horizontal'
? curComponent.value.style.width
: curComponent.value.style.height) *
canvasStyleData.value.scale) /
100
)
} else {
return 0
}
})
const curComponentShadow = computed(() => {
if (curComponent.value) {
return {
left:
(props.direction === 'horizontal'
? curComponent.value.style.left
: curComponent.value.style.top) + 'px',
width:
(props.direction === 'horizontal'
? curComponent.value.style.width
: curComponent.value.style.height) + 'px'
}
} else {
return {}
}
})
const ticks = computed(() => {
const result = []
let currentValue = 0
while (currentValue <= rulerSize.value) {
const isLong = currentValue % (labelInterval * tickSize.value) === 0
const label = isLong ? props.tickLabelFormatter(currentValue) : ''
result.push({ position: (currentValue * canvasStyleData.value.scale) / 100, label, isLong })
currentValue += tickSize.value
}
return result
})
const wStyle = computed(() => {
return {
width: rulerSize.value * 1.5 + 'px'
}
})
const radio = computed(() => rulerSize.value / canvasStyleData.value.width)
const tickSize = computed(
() =>
10 *
Math.max(
Math.floor((200000 * radio.value) / (rulerSize.value * canvasStyleData.value.scale)),
1
)
)
const scaleWidth = computed(() => (rulerSize.value * canvasStyleData.value.scale) / 100)
const rulerScroll = e => {
const left = props.direction === 'vertical' ? e.scrollTop : e.scrollLeft
wRuleRef.value.scrollTo(left, 0)
}
const outerStyle = computed(() => {
return {
width: props.direction === 'vertical' ? props.size - 30 + 'px' : '100%'
}
})
defineExpose({
rulerScroll
})
</script>
<template>
<div
class="ruler-outer"
:style="outerStyle"
:class="{ 'ruler-vertical': direction === 'vertical' }"
ref="wRuleRef"
>
<!--覆盖着尺子上方防止鼠标移到尺子位置滑动-->
<div class="ruler-shadow" :style="outerStyle"></div>
<div :style="wStyle" class="ruler-outer-scroll">
<div class="ruler" :style="{ width: `${scaleWidth}px` }">
<div v-if="curComponent" :style="curComponentShadow" class="cur-shadow"></div>
<div class="ruler-line" :style="{ width: `${scaleWidth}px` }"></div>
<div
v-for="(tick, index) in ticks"
:key="index"
class="ruler-tick"
:class="{ 'long-tick': tick.isLong }"
:style="{ left: `${tick.position}px` }"
>
<span v-if="tick.isLong" class="tick-label">{{ tick.label }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
::-webkit-scrollbar {
width: 0px !important;
height: 0px !important;
}
.ruler-vertical {
position: absolute;
left: 30px;
top: 30px;
transform-origin: top left;
transform: rotate(90deg);
overflow-y: auto;
overflow-x: hidden;
z-index: 1;
.ruler {
.ruler-line {
top: 0;
}
.ruler-tick {
top: 0;
.tick-label {
transform: rotate(180deg);
}
}
}
}
.ruler-shadow {
position: absolute;
height: 30px;
z-index: 10;
overflow: hidden;
}
.ruler-outer {
overflow-x: auto;
background-color: #2c2c2c;
}
.ruler-outer-scroll {
display: flex;
justify-content: center;
}
.ruler {
position: relative;
height: 30px;
display: flex;
align-items: center;
background-color: #2c2c2c;
}
.ruler-line {
position: absolute;
bottom: 0;
height: 1px;
background-color: #ac2a2a;
}
.ruler-tick {
position: absolute;
bottom: 1px;
height: 3px;
width: 1px;
background-color: #e38a8a;
}
.long-tick {
width: 1px;
height: 15px;
}
.tick-label {
position: absolute;
bottom: 2px;
font-size: 8px;
left: 50%;
transform: translateX(2%);
white-space: nowrap;
}
.cur-shadow {
background: rgba(10, 123, 224, 0.3);
height: 30px;
position: absolute;
}
</style>

View File

@ -0,0 +1,144 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
const dvMainStore = dvMainStoreWithOut()
const wRuleRef = ref(null)
const props = defineProps({
tickLabelFormatter: {
type: Function,
default: value => value.toString() //
},
direction: {
type: String,
default: 'horizontal' //
}
})
const labelInterval = 5
const { canvasStyleData } = storeToRefs(dvMainStore)
const ticks = computed(() => {
const result = []
let currentValue = 0
while (currentValue <= canvasStyleData.value.height) {
const isLong = currentValue % (labelInterval * tickSize.value) === 0
const label = isLong ? props.tickLabelFormatter(currentValue) : ''
result.push({ position: (currentValue * canvasStyleData.value.scale) / 100, label, isLong })
currentValue += tickSize.value
}
return result
})
const hStyle = computed(() => {
return {
height: canvasStyleData.value.height * 1.5 + 'px'
}
})
const tickSize = computed(
() =>
10 *
Math.max(Math.floor(200000 / (canvasStyleData.value.height * canvasStyleData.value.scale)), 1)
)
const scaleHeight = computed(
() => (canvasStyleData.value.height * canvasStyleData.value.scale) / 100
)
const rulerScroll = e => {
wRuleRef.value.scrollTo(0, e.scrollHeight)
}
defineExpose({
rulerScroll
})
</script>
<template>
<div class="ruler-outer-vertical" ref="wRuleRef">
testtest
<!--覆盖着尺子上方防止鼠标移到尺子位置滑动-->
<div class="ruler-shadow-vertical"></div>
<div :style="hStyle" class="ruler-outer-vertical-scroll">
<div class="ruler" :style="{ height: `${scaleHeight}px` }">
<div class="ruler-line" :style="{ height: `${scaleHeight}px` }"></div>
<div
v-for="(tick, index) in ticks"
:key="index"
class="ruler-tick"
:class="{ 'long-tick': tick.isLong }"
:style="{ left: `${tick.position}px` }"
>
<span v-if="tick.isLong" class="tick-label">{{ tick.label }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
::-webkit-scrollbar {
width: 0px !important;
height: 0px !important;
}
.ruler-shadow-vertical {
position: absolute;
width: 30px;
height: 100%;
z-index: 10;
overflow: hidden;
}
.ruler-outer-vertical {
position: absolute;
width: 30px;
height: 100%;
overflow-y: auto;
background-color: #2c2c2c;
}
.ruler-outer-vertical-scroll {
display: flex;
align-items: center;
justify-content: center;
}
.ruler {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
height: 100%;
border-left: 1px solid #974e4e;
background-color: #2c2c2c;
}
.ruler-line {
position: absolute;
bottom: 0;
height: 1px;
background-color: #ac2a2a;
}
.ruler-tick {
position: absolute;
bottom: 1px;
height: 3px;
width: 1px;
background-color: #e38a8a;
}
.long-tick {
width: 1px;
height: 15px;
}
.tick-label {
position: absolute;
bottom: 2px;
font-size: 8px;
left: 50%;
transform: translateX(2%);
white-space: nowrap;
}
</style>

View File

@ -48,8 +48,8 @@ const anchorPosition = anchor => {
scrollTo(element.offsetTop)
}
const newComponent = (innerType, isPlugin) => {
eventBus.emit('handleNew', { componentName: 'UserView', innerType: innerType, isPlugin })
const newComponent = (innerType, staticMap) => {
eventBus.emit('handleNew', { componentName: 'UserView', innerType: innerType, staticMap })
}
const handleDragStart = e => {
@ -66,14 +66,15 @@ const groupActiveChange = category => {
}
const loadPluginCategory = data => {
data.forEach(item => {
const { category, title, render, chartValue, chartTitle, icon } = item
const { category, title, render, chartValue, chartTitle, icon, staticMap } = item
const node = {
render,
category,
icon,
value: chartValue,
title: chartTitle,
isPlugin: true
isPlugin: true,
staticMap
}
const stack = [...state.chartGroupList]
let findParent = false
@ -128,7 +129,7 @@ const loadPluginCategory = data => {
:key="chartInfo.title"
>
<div
v-on:click="newComponent(chartInfo.value, chartInfo['isPlugin'])"
v-on:click="newComponent(chartInfo.value, chartInfo['staticMap'])"
class="item-top"
draggable="true"
:data-id="'UserView&' + chartInfo.value"

View File

@ -518,8 +518,9 @@ export function findNewComponentFromList(
componentName,
innerType,
curOriginThemes,
isPlugin?: boolean
staticMap?: object
) {
const isPlugin = !!staticMap
let newComponent
list.forEach(comp => {
if (comp.component === componentName) {
@ -540,6 +541,9 @@ export function findNewComponentFromList(
newComponent.label = viewConfig?.title
newComponent.render = viewConfig?.render
newComponent.isPlugin = !!isPlugin
if (isPlugin) {
newComponent.staticMap = staticMap
}
}
return newComponent
}

View File

@ -99,7 +99,7 @@ onBeforeUnmount(() => {
id="input"
ref="files"
type="file"
accept=".jpeg,.jpg,.png,.gif"
accept=".jpeg,.jpg,.png,.gif,.svg"
hidden
@click="
e => {

View File

@ -19,6 +19,8 @@ import 'tinymce/plugins/table' // 插入表格插件
import 'tinymce/plugins/lists' //
import 'tinymce/plugins/wordcount' //
import 'tinymce/plugins/code' //
import './plugins' //
import '@npkg/tinymce-plugins/letterspacing'
//
import { reactive, ref } from 'vue'
@ -49,7 +51,7 @@ const props = defineProps({
toolbar: {
type: [String, Array],
default:
'codesample bold italic underline alignleft aligncenter alignright alignjustify | undo redo | formatselect | fontselect | fontsizeselect | forecolor backcolor | bullist numlist outdent indent | lists link table code | removeformat '
'codesample bold italic underline alignleft aligncenter alignright alignjustify | undo redo | formatselect | fontselect | fontsizeselect | forecolor backcolor | bullist numlist outdent indent | lists link table code | removeformat letterspacing '
} //
})
//

View File

@ -59,6 +59,8 @@ import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import ChartError from '@/views/chart/components/views/components/ChartError.vue'
import { useEmitt } from '@/hooks/web/useEmitt'
import { valueFormatter } from '@/views/chart/components/js/formatter'
import { parseJson } from '@/views/chart/components/js/util'
import { mappingColor } from '@/views/chart/components/js/panel/common/common_table'
const snapshotStore = snapshotStoreWithOut()
const errMsg = ref('')
const dvMainStore = dvMainStoreWithOut()
@ -103,6 +105,7 @@ const { element, editMode, active, disabled, showPosition } = toRefs(props)
const state = reactive({
data: null,
viewDataInfo: null,
totalItems: 0
})
const dataRowSelect = ref({})
@ -150,7 +153,7 @@ watch(
() => active.value,
val => {
if (!val) {
const ed = tinymce.editors[tinymceId]
const ed = window.tinymce.editors[tinymceId]
if (canEdit.value) {
element.value.propValue.textValue = ed.getContent()
}
@ -158,6 +161,7 @@ watch(
canEdit.value = false
reShow()
myValue.value = assignment(element.value.propValue.textValue)
ed.setContent(myValue.value)
}
}
)
@ -166,7 +170,7 @@ watch(
() => myValue.value,
() => {
if (canEdit.value) {
const ed = tinymce.editors[tinymceId]
const ed = window.tinymce.editors[tinymceId]
element.value.propValue.textValue = ed.getContent()
}
if (initReady.value && canEdit.value) {
@ -216,23 +220,19 @@ const initCurFieldsChange = () => {
const assignment = content => {
const on = content.match(/\[(.+?)\]/g)
if (on) {
const thresholdStyleInfo = conditionAdaptor(state.viewDataInfo)
on.forEach(itm => {
if (dataRowFiledName.value.includes(itm)) {
const ele = itm.slice(1, -1)
let value = dataRowNameSelect.value[ele] !== undefined ? dataRowNameSelect.value[ele] : null
if (value && thresholdStyleInfo && thresholdStyleInfo[ele]) {
const thresholdStyle = thresholdStyleInfo[ele]
value = `<span style="color:${thresholdStyle.color};background-color: ${thresholdStyle.backgroundColor}">${value}</span>`
}
if (initReady.value) {
content = content.replace(
itm,
dataRowNameSelect.value[ele] !== undefined
? dataRowNameSelect.value[ele]
: '[未获取字段值]'
)
content = content.replace(itm, !!value ? value : '[未获取字段值]')
} else {
content = content.replace(
itm,
dataRowNameSelect.value[ele] !== undefined
? dataRowNameSelect.value[ele]
: '[获取中...]'
)
content = content.replace(itm, !!value ? value : '[获取中...]')
}
}
})
@ -245,7 +245,7 @@ const assignment = content => {
return content
}
const fieldSelect = field => {
const ed = tinymce.editors[tinymceId]
const ed = window.tinymce.editors[tinymceId]
const fieldId = 'changeText-' + guid()
const value =
'<span id="' +
@ -262,12 +262,12 @@ const fieldSelect = field => {
}
const onClick = () => {
if (canEdit.value) {
const node = tinymce.activeEditor.selection.getNode()
const node = window.tinymce.activeEditor.selection.getNode()
resetSelect(node)
}
}
const resetSelect = (node?) => {
const edInner = tinymce.get(tinymceId)
const edInner = window.tinymce.get(tinymceId)
if (edInner?.dom) {
const nodeArray = edInner.dom.select('.base-selected')
if (nodeArray) {
@ -316,7 +316,7 @@ const setEdit = () => {
canEdit.value = true
element.value['editing'] = true
myValue.value = element.value.propValue.textValue
const ed = tinymce.editors[tinymceId]
const ed = window.tinymce.editors[tinymceId]
ed.setContent(myValue.value)
reShow()
}
@ -359,6 +359,7 @@ const calcData = (view: Chart, callback) => {
errMsg.value = res.msg
} else {
state.data = res?.data
state.viewDataInfo = res
state.totalItems = res?.totalItems
const curViewInfo = canvasViewInfo.value[element.value.id]
curViewInfo['curFields'] = res.data.fields
@ -449,6 +450,40 @@ const renderChart = () => {
initCurFieldsChange()
}
const conditionAdaptor = (chart: Chart) => {
if (!chart) {
return
}
const { threshold } = parseJson(chart.senior)
if (!threshold.enable) {
return
}
const res = {}
const conditions = threshold.tableThreshold ?? []
if (conditions?.length > 0) {
for (let i = 0; i < conditions.length; i++) {
const field = conditions[i]
let defaultValueColor = 'none'
let defaultBgColor = 'none'
res[field.field.name] = {
color: mappingColor(
dataRowNameSelect.value[field.field.name],
defaultValueColor,
field,
'color'
),
backgroundColor: mappingColor(
dataRowNameSelect.value[field.field.name],
defaultBgColor,
field,
'backgroundColor'
)
}
}
}
return res
}
onMounted(() => {
viewInit()
})

View File

@ -660,6 +660,7 @@ export default {
horizontal: '水平',
vertical: '垂直',
legend: '图例',
legend_num: '图例数',
shape: '形状',
polygon: '多边形',
circle: '圆形',
@ -2268,7 +2269,10 @@ export default {
you_can_type_here: '可以在这里输入其他内容'
},
link_ticket: {
require: 'Ticket 必填'
require: '必选',
back: '返回公共链接设置页面',
refresh: '刷新',
time_tips: '单位: 分钟范围: [0-1440],0代表无期限自首次使用ticket访问开始'
},
pblink: {
key_pwd: '请输入密码打开链接',

View File

@ -612,6 +612,22 @@ declare interface ChartMiscAttr {
* 词云图文字间距
*/
wordSpacing: number
/**
* 自动图例
*/
mapAutoLegend: boolean
/**
* 图例最大值
*/
mapLegendMax: number
/**
* 图例最小值
*/
mapLegendMin: number
/**
* 显示图例个数
*/
mapLegendNumber: number
}
/**
* 动态极值配置

View File

@ -1,4 +1,4 @@
declare interface ChartPlugin {
isPlugin: boolean
pluginResourceId?: string
staticMap?: object
}

View File

@ -391,7 +391,7 @@ export const dvMainStore = defineStore('dataVisualization', {
render: component.render,
plugin: {
isPlugin: component.isPlugin,
pluginResourceId: component.pluginResourceId
staticMap: component.staticMap
}
} as unknown as ChartObj
// 处理配置项默认值不同图表的同一配置项默认值不同

View File

@ -94,6 +94,7 @@ export const selectKey = ['textAlign', 'borderStyle', 'verticalAlign']
export const horizontalPosition = ['headHorizontalPosition']
export const fieldType = ['text', 'time', 'value', 'value', 'value', 'location']
export const fieldTypeText = ['文本', '时间', '数值', '数值(小数)', '数值', '地理位置']
export const optionMap = {
textAlign: textAlignOptions,

View File

@ -73,10 +73,9 @@ const editStyle = computed(() => {
//
const handleNewFromCanvasMain = newComponentInfo => {
const { componentName, innerType, isPlugin } = newComponentInfo
const { componentName, innerType, staticMap } = newComponentInfo
if (componentName) {
const component = findNewComponentFromList(componentName, innerType, curOriginThemes, isPlugin)
component.isPlugin = !!isPlugin
const component = findNewComponentFromList(componentName, innerType, curOriginThemes, staticMap)
syncShapeItemStyle(component, baseWidth.value, baseHeight.value)
component.id = guid()
component.y = 200

View File

@ -64,7 +64,7 @@ const showValueFormatter = computed<boolean>(() => {
})
watch(
[() => props.dimensionData, () => props.item],
[() => props.dimensionData, () => props.item, () => props.chart.type],
() => {
getItemTagType()
},
@ -159,6 +159,10 @@ const removeItem = () => {
}
const getItemTagType = () => {
if (props.chart.type !== 'table-info' && props.item.desensitized) {
tagType.value = '#F54A45'
return
}
tagType.value = getItemType(props.dimensionData, props.quotaData, props.item)
}
@ -204,7 +208,10 @@ onMounted(() => {
:content="item.chartShowName ? item.chartShowName : item.name"
>
<span class="item-span-style">
<span class="item-name">{{ item.chartShowName ? item.chartShowName : item.name }}</span>
<span class="item-name"
>{{ item.chartShowName ? item.chartShowName : item.name
}}{{ item.desensitized ? '(已脱敏)' : '' }}</span
>
</span>
</el-tooltip>

View File

@ -18,6 +18,10 @@ const props = defineProps({
type: Object,
required: true
},
chart: {
type: Object,
required: true
},
index: {
type: Number,
required: true
@ -41,7 +45,7 @@ const emit = defineEmits(['onDimensionItemRemove'])
const { item } = toRefs(props)
watch(
[() => props.dimensionData, () => props.item],
[() => props.dimensionData, () => props.item, () => props.chart.type],
() => {
getItemTagType()
},
@ -70,6 +74,10 @@ const removeItem = () => {
emit('onDimensionItemRemove', item.value)
}
const getItemTagType = () => {
if (props.chart.type !== 'table-info' && props.item.desensitized) {
tagType.value = '#F54A45'
return
}
tagType.value = getItemType(props.dimensionData, props.quotaData, props.item)
}
onMounted(() => {
@ -93,7 +101,9 @@ onMounted(() => {
></Icon>
</el-icon>
</span>
<span class="item-span-style" :title="item.name">{{ item.name }}</span>
<span class="item-span-style" :title="item.name"
>{{ item.name }}{{ item.desensitized ? '(已脱敏)' : '' }}</span
>
<el-icon class="child remove-icon" size="14px">
<Icon name="icon_delete-trash_outlined" class-name="inner-class" @click="removeItem" />
</el-icon>

View File

@ -68,7 +68,7 @@ const toolTip = computed(() => {
return props.themes === 'dark' ? 'ndark' : 'dark'
})
watch(
[() => props.quotaData, () => props.item],
[() => props.quotaData, () => props.item, () => props.chart.type],
() => {
getItemTagType()
},
@ -214,6 +214,10 @@ const removeItem = () => {
}
const getItemTagType = () => {
if (props.chart.type !== 'table-info' && props.item.desensitized) {
tagType.value = '#F54A45'
return
}
tagType.value = getItemType(props.dimensionData, props.quotaData, props.item)
}
@ -304,7 +308,10 @@ onMounted(() => {
:content="item.chartShowName ? item.chartShowName : item.name"
>
<span class="item-span-style">
<span class="item-name">{{ item.chartShowName ? item.chartShowName : item.name }}</span>
<span class="item-name"
>{{ item.chartShowName ? item.chartShowName : item.name
}}{{ item.desensitized ? '(已脱敏)' : '' }}</span
>
<span v-if="item.summary !== ''" class="item-right-summary">
({{ t('chart.' + item.summary) }})
</span>

View File

@ -260,6 +260,7 @@ watch(
:themes="themes"
:chart="chart"
@onLegendChange="onLegendChange"
@onMiscChange="onMiscChange"
/>
</collapse-switch-item>
<el-collapse-item

View File

@ -1,8 +1,14 @@
<script lang="tsx" setup>
import { computed, onMounted, reactive, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_PANEL, DEFAULT_LEGEND_STYLE } from '@/views/chart/components/editor/util/chart'
import { ElSpace } from 'element-plus-secondary'
import {
COLOR_PANEL,
DEFAULT_LEGEND_STYLE,
DEFAULT_MISC
} from '@/views/chart/components/editor/util/chart'
import { ElCol, ElRow, ElSpace } from 'element-plus-secondary'
import { cloneDeep } from 'lodash-es'
import { useEmitt } from '@/hooks/web/useEmitt'
const { t } = useI18n()
@ -14,8 +20,11 @@ const props = withDefaults(
}>(),
{ themes: 'dark' }
)
const emit = defineEmits(['onLegendChange'])
useEmitt({
name: 'map-default-range',
callback: args => mapDefaultRange(args)
})
const emit = defineEmits(['onLegendChange', 'onMiscChange'])
const toolTip = computed(() => {
return props.themes === 'dark' ? 'ndark' : 'dark'
})
@ -36,7 +45,15 @@ const iconSymbolOptions = [
]
const state = reactive({
legendForm: JSON.parse(JSON.stringify(DEFAULT_LEGEND_STYLE))
legendForm: {
...JSON.parse(JSON.stringify(DEFAULT_LEGEND_STYLE)),
miscForm: JSON.parse(JSON.stringify(DEFAULT_MISC)) as ChartMiscAttr
}
})
const chartType = computed(() => {
const chart = JSON.parse(JSON.stringify(props.chart))
return chart?.type
})
const fontSizeList = computed(() => {
@ -54,6 +71,10 @@ const changeLegendStyle = prop => {
emit('onLegendChange', state.legendForm, prop)
}
const changeMisc = prop => {
emit('onMiscChange', { data: state.legendForm.miscForm, requestData: true }, prop)
}
const init = () => {
const chart = JSON.parse(JSON.stringify(props.chart))
if (chart.customStyle) {
@ -63,13 +84,21 @@ const init = () => {
} else {
customStyle = JSON.parse(chart.customStyle)
}
const miscStyle = cloneDeep(props.chart.customAttr.misc)
if (customStyle.legend) {
state.legendForm = customStyle.legend
state.legendForm.miscForm = miscStyle
}
}
}
const showProperty = prop => props.propertyInner?.includes(prop)
const mapDefaultRange = args => {
if (args.from === 'map') {
state.legendForm.miscForm.mapLegendMax = args.data.max
state.legendForm.miscForm.mapLegendMin = args.data.min
state.legendForm.miscForm.mapLegendNumber = args.data.legendNumber
}
}
onMounted(() => {
init()
})
@ -145,6 +174,82 @@ onMounted(() => {
</el-tooltip>
</el-form-item>
</el-space>
<el-space>
<div v-if="chartType === 'map'">
<el-row>
<el-col>
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.legend')"
>
<el-checkbox
size="small"
:effect="themes"
v-model="state.legendForm.miscForm.mapAutoLegend"
@change="changeMisc('mapAutoLegend')"
>
{{ t('chart.margin_model_auto') }}
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<div v-if="!state.legendForm.miscForm.mapAutoLegend">
<el-row :gutter="8">
<el-col :span="12">
<el-form-item
:label="t('chart.max')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input-number
:effect="themes"
v-model="state.legendForm.miscForm.mapLegendMax"
size="small"
controls-position="right"
@change="changeMisc('mapLegendMax')"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
:label="t('chart.min')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input-number
:effect="themes"
v-model="state.legendForm.miscForm.mapLegendMin"
size="small"
controls-position="right"
@change="changeMisc('mapLegendMin')"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col>
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.legend_num')"
>
<el-input-number
:effect="themes"
v-model="state.legendForm.miscForm.mapLegendNumber"
size="small"
:min="1"
:max="9"
controls-position="right"
@change="changeMisc('mapLegendNumber')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
</el-space>
<el-form-item
:label="t('chart.orient')"
class="form-item"

View File

@ -1519,19 +1519,30 @@ const dragOver = (ev: MouseEvent) => {
}
const drop = (ev: MouseEvent, type = 'xAxis') => {
let hasSesensitized = false
ev.preventDefault()
const arr = activeDimension.value.length ? activeDimension.value : activeQuota.value
for (let i = 0; i < arr.length; i++) {
const obj = cloneDeep(arr[i])
if (obj.desensitized && view.value.type !== 'table-info') {
hasSesensitized = true
continue
}
state.moveId = obj.id as unknown as number
view.value[type].push(obj)
const e = { newDraggableIndex: view.value[type].length - 1 }
if ('drillFields' === type) {
addDrill(e)
} else {
addAxis(e, type as AxisType)
}
}
if (hasSesensitized) {
ElMessage.error('脱敏字段不能用于制作该图表!')
}
}
const fieldLoading = ref(false)
@ -1605,7 +1616,7 @@ const deleteChartFieldItem = id => {
</div>
<plugin-component
v-else-if="view.plugin?.isPlugin"
jsname="L2NvbXBvbmVudC9lZGl0b3IvaW5kZXg="
:jsname="view.plugin.staticMap['editor']"
:view="view"
:dimension="state.dimension"
:quota="state.quota"
@ -2277,6 +2288,7 @@ const deleteChartFieldItem = id => {
<drill-item
:key="element.id"
:index="index"
:chart="view"
:item="element"
:dimension-data="state.dimension"
:quota-data="state.quota"

View File

@ -262,7 +262,11 @@ export const DEFAULT_MISC: ChartMiscAttr = {
mapLineSourceColor: '#146C94',
mapLineTargetColor: '#576CBC',
wordSizeRange: [8, 32],
wordSpacing: 6
wordSpacing: 6,
mapAutoLegend: true,
mapLegendMax: 0,
mapLegendMin: 0,
mapLegendNumber: 9
}
export const DEFAULT_MARK = {

View File

@ -3,7 +3,15 @@ import {
L7PlotDrawOptions
} from '@/views/chart/components/js/panel/types/impl/l7plot'
import { Choropleth, ChoroplethOptions } from '@antv/l7plot/dist/esm/plots/choropleth'
import { flow, getGeoJsonFile, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util'
import {
filterChartDataByRange,
flow,
getDynamicColorScale,
getGeoJsonFile,
setMapChartDefaultMaxAndMinValueByData,
hexColorToRGBA,
parseJson
} from '@/views/chart/components/js/util'
import { handleGeoJson } from '@/views/chart/components/js/panel/common/common_antv'
import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
import { cloneDeep } from 'lodash-es'
@ -50,6 +58,30 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
if (!areaId) {
return
}
const sourceData = JSON.parse(JSON.stringify(chart.data?.data || []))
let data = []
const { misc } = parseJson(chart.customAttr)
const { legend } = parseJson(chart.customStyle)
// 自定义图例
if (!misc.mapAutoLegend && legend.show) {
let minValue = misc.mapLegendMin
let maxValue = misc.mapLegendMax
setMapChartDefaultMaxAndMinValueByData(sourceData, maxValue, minValue, (max, min) => {
maxValue = max
minValue = min
action({
from: 'map',
data: {
max: maxValue,
min: minValue,
legendNumber: 9
}
})
})
data = filterChartDataByRange(sourceData, maxValue, minValue)
} else {
data = sourceData
}
const geoJson = cloneDeep(await getGeoJsonFile(areaId))
let options: ChoroplethOptions = {
preserveDrawingBuffer: true,
@ -61,7 +93,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
type: 'geojson'
},
source: {
data: chart.data?.data || [],
data: data,
joinBy: {
sourceField: 'name',
geoField: 'name',
@ -125,7 +157,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
): ChoroplethOptions {
const { areaId }: L7PlotDrawOptions<any> = context.drawOption
const geoJson: FeatureCollection = context.geoJson
const { basicStyle, label } = parseJson(chart.customAttr)
const { basicStyle, label, misc } = parseJson(chart.customAttr)
const senior = parseJson(chart.senior)
const curAreaNameMapping = senior.areaMapping?.[areaId]
handleGeoJson(geoJson, curAreaNameMapping)
@ -141,7 +173,32 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
options.label && (options.label.field = 'name')
return options
}
const data = chart.data.data
const sourceData = JSON.parse(JSON.stringify(chart.data.data))
const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
const { legend } = parseJson(chart.customStyle)
let data = []
let colorScale = []
if (legend.show) {
let minValue = misc.mapLegendMin
let maxValue = misc.mapLegendMax
let mapLegendNumber = misc.mapLegendNumber
setMapChartDefaultMaxAndMinValueByData(sourceData, maxValue, minValue, (max, min) => {
maxValue = max
minValue = min
mapLegendNumber = 9
})
// 非自动过滤数据
if (!misc.mapAutoLegend) {
data = filterChartDataByRange(sourceData, maxValue, minValue)
} else {
mapLegendNumber = 9
}
// 定义最大值最小值区间数量和对应的颜色
colorScale = getDynamicColorScale(minValue, maxValue, mapLegendNumber, colors)
} else {
data = sourceData
colorScale = colors
}
const areaMap = data.reduce((obj, value) => {
obj[value['field']] = value.value
return obj
@ -164,12 +221,11 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
item.properties['_DE_LABEL_'] = content.join('\n\n')
}
})
let colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
if (validArea < colors.length) {
colors = colors.slice(0, validArea)
if (validArea < colorScale.length && !misc.mapAutoLegend) {
colorScale = colorScale.map(item => (item.color ? item.color : item)).slice(0, validArea)
}
if (colors.length) {
options.color['value'] = colors
if (colorScale.length) {
options.color['value'] = colorScale.map(item => (item.color ? item.color : item))
}
return options
}

View File

@ -6,9 +6,10 @@ const { t } = useI18n()
* 富文本图表
*/
export class RichTextChartView extends AbstractChartView {
properties: EditorProperty[] = ['background-overall-component']
properties: EditorProperty[] = ['background-overall-component', 'threshold']
propertyInner: EditorPropertyInner = {
'background-overall-component': ['all']
'background-overall-component': ['all'],
threshold: ['tableThreshold']
}
axis: AxisType[] = ['xAxis', 'yAxis', 'filter']
axisConfig: AxisConfig = {

View File

@ -458,7 +458,7 @@ export function getConditions(chart: Chart) {
return res
}
function mappingColor(value, defaultColor, field, type) {
export function mappingColor(value, defaultColor, field, type) {
let color
for (let i = 0; i < field.conditions.length; i++) {
let flag = false

View File

@ -527,3 +527,68 @@ export const copyString = (content: string, notify = false) => {
}
})
}
/**
* 计算动态区间和颜色
* @param minValue
* @param maxValue
* @param intervals
* @param colors
*/
export const getDynamicColorScale = (
minValue: number,
maxValue: number,
intervals: number,
colors: string[]
) => {
const step = (maxValue - minValue) / intervals
const colorScale = []
for (let i = 0; i < intervals; i++) {
colorScale.push({
value: [minValue + i * step, minValue + (i + 1) * step],
color: colors[i],
label: `${(minValue + i * step).toFixed(2)} - ${(minValue + (i + 1) * step).toFixed(2)}`
})
}
return colorScale
}
/**
* 过滤掉不在区间的数据
* @param data
* @param maxValue
* @param minValue
*/
export const filterChartDataByRange = (data: any[], maxValue: number, minValue: number) => {
return data.filter(
item =>
item.value === null ||
item.value === undefined ||
(item.value >= minValue && item.value <= maxValue)
)
}
/**
* 获取地图默认最大最小值根据数据
* @param data
* @param maxValue
* @param minValue
* @param callback
*/
export const setMapChartDefaultMaxAndMinValueByData = (
data: any[],
maxValue: number,
minValue: number,
callback: (max: number, min: number) => void
) => {
if (minValue === 0 && maxValue === 0) {
const maxResult = data.reduce((max, current) => {
return current.value > max ? current.value : max
}, Number.MIN_SAFE_INTEGER)
const minResult = data.reduce((min, current) => {
return current.value < min ? current.value : min
}, Number.MAX_SAFE_INTEGER)
callback(maxResult, minResult)
}
}

View File

@ -287,6 +287,10 @@ const pointClickTrans = () => {
}
const action = param => {
if (param.from === 'map') {
emitter.emit('map-default-range', param)
return
}
state.pointParam = param.data
//
pointClickTrans()

View File

@ -762,7 +762,7 @@ const showActionIcons = computed(() => {
<div v-if="chartAreaShow" style="flex: 1; overflow: hidden">
<plugin-component
v-if="view.plugin?.isPlugin"
jsname="L2NvbXBvbmVudC9pbmRleA=="
:jsname="view.plugin.staticMap['index']"
:scale="scale"
:dynamic-area-id="dynamicAreaId"
:view="view"

View File

@ -4,7 +4,7 @@
id="input"
ref="files"
type="file"
accept=".jpeg,.jpg,.png,.gif"
accept=".jpeg,.jpg,.png,.gif,.svg"
hidden
@click="
e => {

View File

@ -11,7 +11,7 @@ import { ElMessage } from 'element-plus-secondary'
import { useEmbedded } from '@/store/modules/embedded'
import { useI18n } from '@/hooks/web/useI18n'
import { XpackComponent } from '@/components/plugin'
import { propTypes } from '@/utils/propTypes'
const dvMainStore = dvMainStoreWithOut()
const { t } = useI18n()
const embeddedStore = useEmbedded()
@ -32,7 +32,8 @@ const props = defineProps({
isSelector: {
type: Boolean,
default: false
}
},
ticketArgs: propTypes.string.def(null)
})
const loadCanvasDataAsync = async (dvId, dvType) => {
@ -57,6 +58,14 @@ const loadCanvasDataAsync = async (dvId, dvType) => {
}
}
let argsObject = null
try {
argsObject = JSON.parse(props.ticketArgs)
} catch (error) {
console.error(error)
}
const hasTicketArgs = argsObject && Object.keys(argsObject)
//
let attachParam
await getOuterParamsInfo(dvId).then(rsp => {
@ -65,9 +74,14 @@ const loadCanvasDataAsync = async (dvId, dvType) => {
// iframe iframe
const attachParamsEncode = router.currentRoute.value.query.attachParams
if (attachParamsEncode) {
if (attachParamsEncode || hasTicketArgs) {
try {
attachParam = JSON.parse(Base64.decode(decodeURIComponent(attachParamsEncode)))
if (attachParam) {
attachParam = JSON.parse(Base64.decode(decodeURIComponent(attachParamsEncode)))
}
if (hasTicketArgs) {
attachParam = Object.assign({}, attachParam, argsObject)
}
} catch (e) {
console.error(e)
ElMessage.error(t('visualization.outer_param_decode_error'))

View File

@ -39,6 +39,7 @@ import { Base64 } from 'js-base64'
import CanvasCacheDialog from '@/components/visualization/CanvasCacheDialog.vue'
import { deepCopy } from '@/utils/utils'
import DvPreview from '@/views/data-visualization/DvPreview.vue'
import DeRuler from '@/custom-component/common/DeRuler.vue'
const interactiveStore = interactiveStoreWithOut()
const embeddedStore = useEmbedded()
const { wsCache } = useCache()
@ -63,6 +64,8 @@ const snapshotStore = snapshotStoreWithOut()
const contextmenuStore = contextmenuStoreWithOut()
const composeStore = composeStoreWithOut()
const canvasCacheOutRef = ref(null)
const deWRulerRef = ref(null)
const deHRulerRef = ref(null)
const {
fullscreenFlag,
@ -80,6 +83,7 @@ const canvasInner = ref(null)
const leftSidebarRef = ref(null)
const dvLayout = ref(null)
const canvasCenterRef = ref(null)
const mainHeight = ref(300)
const state = reactive({
datasetTree: [],
scaleHistory: null,
@ -100,9 +104,6 @@ const contentStyle = computed(() => {
}
} else {
return {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: width * 1.5 + 'px',
height: height * 1.5 + 'px'
}
@ -180,9 +181,9 @@ const initScroll = () => {
nextTick(() => {
const { width, height } = canvasStyleData.value
const mainWidth = canvasCenterRef.value.clientWidth
const mainHeight = canvasCenterRef.value.clientHeight
mainHeight.value = canvasCenterRef.value.clientHeight
const scrollX = (1.5 * width - mainWidth) / 2
const scrollY = (1.5 * height - mainHeight) / 2 + 20
const scrollY = (1.5 * height - mainHeight.value) / 2 + 20
//
canvasOut.value.scrollTo(scrollX, scrollY)
})
@ -359,6 +360,12 @@ const canvasPropertiesShow = computed(
const viewsPropertiesShow = computed(
() => !!(curComponent.value && ['UserView', 'VQuery'].includes(curComponent.value.component))
)
const scrollCanvas = e => {
deWRulerRef.value.rulerScroll(e)
deHRulerRef.value.rulerScroll(e)
}
eventBus.on('handleNew', handleNew)
</script>
@ -390,7 +397,19 @@ eventBus.on('handleNew', handleNew)
</dv-sidebar>
<!-- 中间画布 -->
<main id="dv-main-center" class="center" ref="canvasCenterRef">
<el-scrollbar ref="canvasOut" class="content" :class="{ 'preview-content': previewStatus }">
<div class="de-ruler-icon-outer">
<el-icon class="de-ruler-icon">
<Icon name="dv-ruler" />
</el-icon>
</div>
<de-ruler ref="deWRulerRef"></de-ruler>
<de-ruler direction="vertical" :size="mainHeight" ref="deHRulerRef"></de-ruler>
<el-scrollbar
ref="canvasOut"
@scroll="scrollCanvas"
class="content"
:class="{ 'preview-content': previewStatus }"
>
<div
id="canvas-dv-outer"
ref="canvasInner"
@ -400,15 +419,17 @@ eventBus.on('handleNew', handleNew)
@mousedown="handleMouseDown"
@mouseup="deselectCurComponent"
>
<canvas-core
class="canvas-area-shadow editor-main"
v-if="state.canvasInitStatus"
ref="mainCanvasCoreRef"
:component-data="componentData"
:canvas-style-data="canvasStyleData"
:canvas-view-info="canvasViewInfo"
:canvas-id="state.canvasId"
></canvas-core>
<div class="canvas-dv-inner">
<canvas-core
class="canvas-area-shadow editor-main"
v-if="state.canvasInitStatus"
ref="mainCanvasCoreRef"
:component-data="componentData"
:canvas-style-data="canvasStyleData"
:canvas-view-info="canvasViewInfo"
:canvas-id="state.canvasId"
></canvas-core>
</div>
</div>
</el-scrollbar>
<ComponentToolBar :class="{ 'preview-aside-x': previewStatus }"></ComponentToolBar>
@ -535,4 +556,27 @@ eventBus.on('handleNew', handleNew)
height: 1px;
background: #000;
}
.canvas-dv-inner {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.de-ruler-icon-outer {
background: #2c2c2c;
position: absolute;
width: 30px;
height: 30px;
z-index: 3;
color: #ebebeb;
.de-ruler-icon {
margin-left: 6px;
margin-top: 6px;
font-size: 24px;
color: #ebebeb;
}
}
</style>

View File

@ -2,6 +2,12 @@ import request from '@/config/axios'
import { useCache } from '@/hooks/web/useCache'
import { isInIframe } from '@/utils/utils'
const { wsCache } = useCache()
export interface TicketValidVO {
ticketValid: boolean
ticketExp: boolean
args: string
}
export interface ProxyInfo {
resourceId: string
uid: string
@ -9,17 +15,36 @@ export interface ProxyInfo {
pwdValid?: boolean
type: string
inIframeError: boolean
ticketValidVO: TicketValidVO
}
class ShareProxy {
uuid: string
constructor() {
this.uuid = ''
}
getTicket() {
const curLocation = window.location.href
const pmIndex = curLocation.lastIndexOf('?')
if (pmIndex == -1) {
return null
}
const searchText = curLocation.substring(pmIndex + 1)
const regex = /([^&=]+)=([^&]*)/g
let m
while ((m = regex.exec(searchText)) !== null) {
const key = decodeURIComponent(m[1])
if (key === 'ticket') {
return decodeURIComponent(m[2])
}
}
return null
}
setUuid() {
const curLocation = window.location.href
const pmIndex = curLocation.lastIndexOf('?')
const uuidObj = curLocation.substring(
curLocation.lastIndexOf('de-link/') + 8,
curLocation.lastIndexOf('?') > 0 ? curLocation.lastIndexOf('?') : curLocation.length
pmIndex > 0 ? pmIndex : curLocation.length
)
this.uuid = uuidObj
}
@ -31,7 +56,8 @@ class ShareProxy {
const uuid = this.uuid
const url = '/share/proxyInfo'
const inIframe = isInIframe()
const param = { uuid, ciphertext: null, inIframe }
const ticket = this.getTicket()
const param = { uuid, ciphertext: null, inIframe, ticket }
const ciphertext = wsCache.get(`link-${uuid}`)
if (ciphertext) {
param['ciphertext'] = ciphertext

View File

@ -0,0 +1,6 @@
<script lang="ts" setup>
import EmptyBackground from '@/components/empty-background/src/EmptyBackground.vue'
</script>
<template>
<EmptyBackground img-type="noneWhite" description="Ticket参数错误" />
</template>

View File

@ -4,28 +4,40 @@
<LinkError v-else-if="!loading && !linkExist" />
<Exp v-else-if="!loading && linkExp" />
<PwdTips v-else-if="!loading && !pwdValid" />
<TicketError
v-else-if="!loading && (!state.ticketValidVO.ticketValid || state.ticketValidVO.ticketExp)"
/>
<PreviewCanvas
v-else
:class="{ 'hidden-link': loading }"
ref="pcanvas"
public-link-status="true"
public-link-status
:ticket-args="state.ticketValidVO.args"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, ref } from 'vue'
import { onMounted, nextTick, ref, reactive } from 'vue'
import PreviewCanvas from '@/views/data-visualization/PreviewCanvas.vue'
import { ProxyInfo, shareProxy } from './ShareProxy'
import Exp from './exp.vue'
import LinkError from './error.vue'
import PwdTips from './pwd.vue'
import IframeError from './IframeError.vue'
import TicketError from './TicketError.vue'
const pcanvas = ref(null)
const iframeError = ref(true)
const linkExist = ref(false)
const loading = ref(true)
const linkExp = ref(false)
const pwdValid = ref(false)
const state = reactive({
ticketValidVO: {
ticketValid: false,
ticketExp: false,
args: ''
}
})
onMounted(async () => {
const proxyInfo = (await shareProxy.loadProxy()) as ProxyInfo
if (proxyInfo?.inIframeError) {
@ -41,6 +53,7 @@ onMounted(async () => {
linkExist.value = true
linkExp.value = !!proxyInfo.exp
pwdValid.value = !!proxyInfo.pwdValid
state.ticketValidVO = proxyInfo.ticketValidVO
nextTick(() => {
const method = pcanvas?.value?.loadCanvasDataAsync
if (method) {

View File

@ -16,7 +16,10 @@
<el-dialog
v-if="dialogVisible && props.weight >= 7"
class="copy-link_dialog"
:class="{ 'hidden-footer': !shareEnable || showTicket }"
:class="{
'hidden-footer': !shareEnable || showTicket,
'is-ticket-dialog': shareEnable && showTicket
}"
v-model="dialogVisible"
:close-on-click-modal="true"
:append-to-body="true"
@ -118,11 +121,18 @@
</div>
</div>
<div v-if="shareEnable && showTicket" class="share-ticket-container">
<share-ticket :link-url="linkAddr" @close="closeTicket" />
<share-ticket
:link-url="linkAddr"
:uuid="state.detailInfo.uuid"
:resource-id="props.resourceId"
:ticket-require="state.detailInfo.ticketRequire"
@require-change="updateRequireTicket"
@close="closeTicket"
/>
</div>
<template #footer>
<span class="dialog-footer">
<!-- <el-button secondary @click="openTicket">Ticket 设置</el-button> -->
<el-button secondary @click="openTicket">Ticket 设置</el-button>
<el-button :disabled="!shareEnable || expError" type="primary" @click.stop="copyInfo">
{{ t('visualization.copy_link') }}
</el-button>
@ -167,7 +177,8 @@ const state = reactive({
uuid: '',
pwd: '',
exp: 0,
autoPwd: true
autoPwd: true,
ticketRequire: false
} as ShareInfo
})
const emits = defineEmits(['loaded'])
@ -335,12 +346,14 @@ const expChangeHandler = exp => {
}
const beforeClose = async done => {
if (!shareEnable.value) {
showTicket.value = false
done()
return
}
const pwdValid = validatePwdFormat()
const uuidValid = await validateUuid()
if (pwdValid && uuidValid) {
showTicket.value = false
done()
}
}
@ -460,6 +473,9 @@ const openTicket = () => {
const closeTicket = () => {
showTicket.value = false
}
const updateRequireTicket = val => {
state.detailInfo.ticketRequire = val
}
const execute = () => {
share()
@ -480,6 +496,11 @@ onMounted(() => {
})
</script>
<style lang="less">
.is-ticket-dialog {
.ed-dialog__header {
display: none;
}
}
.copy-link_dialog {
.ed-dialog__header {
padding: 16px 16px 10px !important;

View File

@ -4,7 +4,7 @@
<div class="ticket-model-start">
<el-tooltip class="item" effect="dark" :content="$t('link_ticket.back')" placement="top">
<span class="back-tips">
<el-icon class="custom-el-icon back-icon" @click="close">
<el-icon class="custom-el-icon back-icon" @click.stop="close">
<Icon class="toolbar-icon" name="icon_left_outlined" />
</el-icon>
</span>
@ -12,31 +12,33 @@
<span class="ticket-title">{{ 'Ticket ' + $t('commons.setting') }}</span>
</div>
<div class="ticket-model-end">
<el-checkbox v-model="requireTicket" @change="requireTicketChange" />
<span>{{ $t('link_ticket.require') }}</span>
<el-checkbox
v-model="ticketRequire"
@change="requireTicketChange"
:label="t('link_ticket.require')"
/>
</div>
</div>
<div class="ticket-add">
<el-button @click.stop="addRow" text>
<template #icon>
<icon name="icon_add_outlined"></icon>
</template>
{{ t('commons.create') }}
</el-button>
</div>
<div class="ticket-table">
<!-- <div class="text-add-ticket">
<el-button class="de-text-btn mr2" type="text" icon="el-icon-plus" @click="addRow">{{
$t('commons.create')
}}</el-button>
</div>
<el-table :data="tableData" style="width: 100%" size="mini">
<el-table-column prop="ticket" label="ticket" width="120">
<el-table :data="state.tableData" style="width: 100%" size="small">
<el-table-column prop="ticket" label="Ticket" width="130">
<template v-slot="scope">
<div class="ticket-row">
<span>{{ scope.row.ticket }}</span>
<span :title="scope.row.ticket">{{ scope.row.ticket }}</span>
<el-tooltip class="item" effect="dark" :content="$t('commons.copy')" placement="top">
<span
v-clipboard:copy="`${props.linkUrl}?ticket=${scope.row.ticket}`"
v-clipboard:success="onCopy"
v-clipboard:error="onError"
class="copy-i"
>
<svg-icon icon-class="de-icon-copy" />
</span>
<el-button text @click.stop="copyTicket(scope.row.ticket)">
<template #icon>
<Icon name="de-copy"></Icon>
</template>
</el-button>
</el-tooltip>
<el-tooltip
class="item"
@ -44,31 +46,34 @@
:content="`${$t('link_ticket.refresh')} ticket`"
placement="top"
>
<span class="refresh-i">
<i class="el-icon-refresh-right" @click="refreshTicket(scope.row)" />
</span>
<el-button text @click.stop="refreshTicket(scope.row)">
<template #icon>
<Icon name="icon_refresh_outlined"></Icon>
</template>
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column prop="exp" :label="$t('panel.over_time')" width="100">
<el-table-column prop="exp" :label="$t('visualization.over_time')" width="100">
<template v-slot:header>
<span>{{ $t('panel.over_time') }}</span>
<el-tooltip
class="item"
effect="dark"
:content="$t('link_ticket.time_tips')"
placement="top"
>
<span class="check-tips">
<svg-icon icon-class="de-icon-info" @click="closeTicket" />
</span>
</el-tooltip>
<div class="ticket-exp-head">
<span>{{ $t('visualization.over_time') }}</span>
<el-tooltip
class="item"
effect="dark"
:content="$t('link_ticket.time_tips')"
placement="top"
>
<Icon name="dv-info"></Icon>
</el-tooltip>
</div>
</template>
<template v-slot="scope">
<el-input
v-if="scope.row.isEdit"
:ref="el => setExpRef(el, scope.$index)"
v-model="scope.row.exp"
type="number"
:placeholder="$t('commons.input_content')"
@ -86,6 +91,7 @@
<template v-slot="scope">
<el-input
v-if="scope.row.isEdit"
:ref="el => setArgRef(el, scope.$index)"
v-model="scope.row.args"
type="text"
:placeholder="$t('commons.input_content')"
@ -100,16 +106,18 @@
</el-table-column>
<el-table-column :label="$t('commons.operating')" width="80">
<template v-slot="scope">
<div class="ticket-op">
<div class="ticket-row">
<el-tooltip
class="item"
effect="dark"
:content="$t('commons.delete')"
placement="top"
>
<span>
<i class="el-icon-delete" @click="deleteTicket(scope.row, scope.$idnex)" />
</span>
<el-button text @click.stop="deleteTicket(scope.row, scope.$index)">
<template #icon>
<Icon name="icon_delete-trash_outlined"></Icon>
</template>
</el-button>
</el-tooltip>
<el-tooltip
class="item"
@ -117,93 +125,233 @@
:content="scope.row.isEdit ? $t('commons.save') : $t('commons.edit')"
placement="top"
>
<span>
<i v-if="!scope.row.isEdit" class="el-icon-edit" @click="editRow(scope.row)" />
<i
v-else
class="el-icon-circle-check"
@click="saveRow(scope.row, scope.$index)"
/>
</span>
<el-button v-if="!scope.row.isEdit" text @click.stop="editRow(scope.row)">
<template #icon>
<Icon name="icon_edit_outlined"></Icon>
</template>
</el-button>
<el-button v-else text @click.stop="saveRow(scope.row, scope.$index)">
<template #icon>
<Icon name="edit-done"></Icon>
</template>
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table> -->
</el-table>
</div>
<div class="ticket-btn">
<el-button type="primary" @click.stop="finish"> 完成 </el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted, toRefs } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from '@/hooks/web/useI18n'
import request from '@/config/axios'
import { ElMessage, ElMessageBox } from 'element-plus-secondary'
import useClipboard from 'vue-clipboard3'
const { toClipboard } = useClipboard()
const { t } = useI18n()
const props = defineProps({
linkUrl: propTypes.string.def(null)
linkUrl: propTypes.string.def(null),
uuid: propTypes.string.def(null),
resourceId: propTypes.string.def(null),
ticketRequire: propTypes.bool
})
const requireTicket = ref(false)
const tableData = reactive([])
const emits = defineEmits(['close'])
const { ticketRequire } = toRefs(props)
const expRefs = ref({})
const argRefs = ref({})
const state = reactive({
tableData: []
})
const emits = defineEmits(['close', 'requireChange'])
const close = () => {
emits('close')
}
const setExpRef = (el, index) => {
if (el) {
expRefs.value[index] = el
}
}
const setArgRef = (el, index) => {
if (el) {
argRefs.value[index] = el
}
}
const requireTicketChange = val => {
console.log(val)
const url = '/ticket/enableTicket'
const data = {
resourceId: props.resourceId,
require: val
}
request.post({ url, data }).then(() => {
emits('requireChange', val)
})
}
const createLimit = (count?: number) => {
const realCount = count ? count : state.tableData.length || 0
if (realCount > 4) {
ElMessageBox.confirm('提示', {
confirmButtonType: 'primary',
type: 'warning',
confirmButtonText: t('common.roger_that'),
cancelButtonText: t('dataset.cancel'),
autofocus: false,
showClose: false,
showCancelButton: false,
tip: '最多支持创建5个Ticket'
})
return false
}
return true
}
const addRow = () => {
console.log(11)
if (!createLimit()) {
return
}
const row = {
ticket: '',
exp: 30,
args: '',
uuid: props.uuid
}
const url = '/ticket/saveTicket'
request.post({ url, data: row }).then(res => {
row.ticket = res.data
row['isEdit'] = false
state.tableData.splice(0, 0, row)
})
}
const refreshTicket = row => {
console.log(row)
const copyTicket = async ticket => {
const url = `${props.linkUrl}?ticket=${ticket}`
try {
await toClipboard(url)
ElMessage.success(t('common.copy_success'))
} catch (e) {
ElMessage.warning(t('common.copy_unsupported'), e)
}
}
const closeTicket = () => {
console.log(11)
const refreshTicket = row => {
const url = '/ticket/saveTicket'
const param = JSON.parse(JSON.stringify(row))
param['generateNew'] = true
request.post({ url, data: param }).then(res => {
row.ticket = res.data
})
}
const validateExp = (val, index) => {
console.log(val)
console.log(index)
const cref = expRefs.value[index]
const e = cref.input
if (val === null || val === '' || typeof val === 'undefined') {
state.tableData[index]['exp'] = 0
return true
}
if (val > 1440 || val < 0) {
e.style.color = 'red'
e.parentNode.setAttribute('style', 'box-shadow: 0 0 0 1px red inset;')
return false
} else {
e.style.color = null
e.parentNode.removeAttribute('style')
return true
}
}
const validateArgs = (val, index) => {
console.log(val)
console.log(index)
const cref = argRefs.value[index]
const e = cref.input
if (val === null || val === '' || typeof val === 'undefined') {
e.style.color = null
e.parentNode.removeAttribute('style')
const child = e.parentNode.querySelector('.error-msg')
if (child) {
e.parentNode.removeChild(child)
}
return true
}
try {
JSON.parse(val)
e.style.color = null
e.parentNode.removeAttribute('style')
const child = e.parentNode.querySelector('.error-msg')
if (child) {
e.parentNode.removeChild(child)
}
return true
} catch (error) {
e.style.color = 'red'
e.parentNode.setAttribute('style', 'box-shadow: 0 0 0 1px red inset;')
const child = e.parentNode.querySelector('.error-msg')
if (!child) {
const errorDom = document.createElement('div')
errorDom.className = 'error-msg'
errorDom.innerText = '格式错误'
e.parentNode.appendChild(errorDom)
}
return false
}
}
const deleteTicket = (val, index) => {
console.log(val)
console.log(index)
const deleteTicket = (row, index) => {
const param = { ticket: row.ticket }
const url = '/ticket/delTicket'
request.post({ url, data: param }).then(() => {
state.tableData.splice(index, 1)
})
}
const saveRow = (val, index) => {
console.log(val)
console.log(index)
const saveRow = (row, index) => {
const url = '/ticket/saveTicket'
validateExp(row.exp, index) &&
validateArgs(row.args, index) &&
request.post({ url, data: row }).then(() => {
row.isEdit = false
})
}
const editRow = row => {
console.log(row)
row.isEdit = true
}
const onCopy = e => {
console.log(e)
const finish = () => {
close()
}
const onError = e => {
console.log(e)
const loadTicketData = () => {
const resourceId = props.resourceId
const url = `/ticket/query/${resourceId}`
request.get({ url }).then(res => {
state.tableData = res.data || []
})
}
onMounted(() => {
loadTicketData()
})
</script>
<style lang="less" scoped>
.ticket {
height: 261px;
min-height: 280px;
.ticket-model {
display: flex;
height: 22px;
justify-content: space-between;
padding: 16px 0;
padding: 0;
.ticket-model-start {
display: flex;
align-items: center;
color: #1f2329;
font-family: PingFang SC;
font-weight: 500;
@ -212,6 +360,11 @@ const onError = e => {
font-size: 14px;
}
.back-tips {
i {
width: 16px;
height: 16px;
}
width: 22px;
margin-right: 4px;
display: flex;
@ -224,32 +377,109 @@ const onError = e => {
}
}
}
label {
margin-right: 8px;
.ticket-model-end {
display: flex;
label {
height: 22px;
margin-right: 8px;
}
}
}
.ticket-add {
margin: 16px 0;
height: 22px;
button {
height: 22px;
line-height: 22px;
}
}
.ticket-table {
border-top: 1px solid #d5d7d8;
min-height: 156px;
padding: 0 0;
// height: 260px;
height: 50px;
overflow-y: overlay;
position: relative;
::v-deep .error-msg {
height: calc(100% - 124px);
:deep(.error-msg) {
color: red;
position: fixed;
z-index: 9;
font-size: 10px;
height: 10px;
margin-bottom: 12px;
margin-right: -80px;
}
::v-deep .check-tips {
margin-left: 4px;
}
.text-add-ticket {
width: 48px;
:deep(.ticket-exp-head) {
display: flex;
line-height: 22px;
height: 22px;
gap: 4px;
align-items: center;
svg {
margin-left: 4px;
width: 16px;
height: 16px;
cursor: pointer;
}
}
:deep(.ticket-row) {
display: flex;
align-items: center;
height: 22px;
span {
width: 66px;
margin-right: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
button {
height: 16px;
line-height: 16px;
width: 16px;
}
.ed-button + .ed-button {
margin-left: 8px;
}
}
:deep(.ed-table__header) {
background-color: #f5f6f7;
thead {
tr {
th {
background-color: #f5f6f7 !important;
.cell {
line-height: 22px;
padding: 4px 8px;
font-size: 14px;
font-weight: 500;
}
}
}
}
height: 38px;
}
:deep(.ed-table__row) {
height: 39px;
td {
.cell {
line-height: 22px;
height: 22px;
font-size: 14px;
font-weight: 400;
}
}
}
:deep(.ed-input__inner) {
height: 18px;
line-height: 18px;
}
}
.ticket-btn {
margin: 16px 0;
float: right;
}
}
</style>

View File

@ -5,7 +5,7 @@
width="480"
placement="bottom-start"
:show-arrow="false"
popper-class="share-popover"
:popper-class="`share-popover ${showTicket ? 'share-ticket-popover' : ''}`"
@show="share"
>
<template #reference>
@ -21,15 +21,12 @@
{{ t('visualization.share') }}
</el-button>
</template>
<div class="share-container">
<div class="share-container" :class="{ 'hidden-link-container': showTicket }">
<div class="share-title share-padding">公共链接分享</div>
<div class="open-share flex-align-center share-padding">
<el-switch size="small" v-model="shareEnable" @change="enableSwitcher" />
{{ shareTips }}
</div>
<!-- <div v-if="shareEnable" class="text share-padding">
<el-input v-model="linkAddr" disabled />
</div> -->
<div v-if="shareEnable" class="custom-link-line share-padding">
<el-input
ref="linkUuidRef"
@ -117,11 +114,22 @@
<el-divider v-if="shareEnable" class="share-divider" />
<div v-if="shareEnable" class="share-foot share-padding">
<el-button secondary @click="openTicket">Ticket 设置</el-button>
<el-button :disabled="!shareEnable || expError" type="primary" @click="copyInfo">
{{ t('visualization.copy_link') }}
</el-button>
</div>
</div>
<div v-if="shareEnable && showTicket" class="share-ticket-container">
<share-ticket
:link-url="linkAddr"
:uuid="state.detailInfo.uuid"
:resource-id="props.resourceId"
:ticket-require="state.detailInfo.ticketRequire"
@require-change="updateRequireTicket"
@close="closeTicket"
/>
</div>
</el-popover>
</template>
@ -133,6 +141,8 @@ import { propTypes } from '@/utils/propTypes'
import { ShareInfo, SHARE_BASE, shortcuts } from './option'
import { ElMessage, ElLoading } from 'element-plus-secondary'
import useClipboard from 'vue-clipboard3'
import ShareTicket from './ShareTicket.vue'
const { toClipboard } = useClipboard()
const { t } = useI18n()
const props = defineProps({
@ -150,6 +160,7 @@ const linkAddr = ref('')
const expError = ref(false)
const linkCustom = ref(false)
const linkUuidRef = ref(null)
const showTicket = ref(false)
const state = reactive({
detailInfo: {
id: '',
@ -186,6 +197,7 @@ const clickOutPopover = e => {
}
const openPopover = () => {
if (!popoverVisible.value) {
showTicket.value = false
popoverVisible.value = true
}
}
@ -468,6 +480,16 @@ const finishEditUuid = async () => {
linkCustom.value = !uuidValid
}
const openTicket = () => {
showTicket.value = true
}
const closeTicket = () => {
showTicket.value = false
}
const updateRequireTicket = val => {
state.detailInfo.ticketRequire = val
}
const execute = () => {
share()
}
@ -477,12 +499,21 @@ defineExpose({
</script>
<style lang="less">
.share-popover {
.share-popover:not(.share-ticket-popover) {
padding: 16px 0px !important;
}
.share-ticket-popover {
padding: 0 !important;
}
</style>
<style lang="less" scoped>
.hidden-link-container {
display: none;
}
.share-ticket-container {
padding: 16px;
}
.share-container {
.share-title {
font-weight: 500;

View File

@ -4,6 +4,7 @@ export interface ShareInfo {
uuid: string
pwd?: string
autoPwd: boolean
ticketRequire?: boolean
}
export const SHARE_BASE = '/de-link/'

View File

@ -1,5 +1,5 @@
<script lang="tsx" setup>
import { nextTick, reactive, ref, shallowRef } from 'vue'
import { nextTick, reactive, ref, shallowRef, provide } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import type { FormInstance, FormRules } from 'element-plus-secondary'
import { ElIcon, ElMessage } from 'element-plus-secondary'
@ -150,8 +150,10 @@ const rule = reactive<FormRules>({
}
]
})
const initApiItem = (val: ApiItem, apiList) => {
const activeName = ref('third')
provide('api-active-name', activeName)
const initApiItem = (val: ApiItem, apiList, name) => {
activeName.value = name
apiItemList = apiList
Object.assign(apiItem, val)
edit_api_item.value = true
@ -406,7 +408,9 @@ defineExpose({
<span class="icon">
{{ active <= 1 ? '2' : '' }}
</span>
<span class="title">{{ t('datasource.api_step_2') }}</span>
<span class="title">{{
activeName === 'third' ? t('datasource.api_step_2') : '提取参数'
}}</span>
</div>
</template>
</el-step>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { computed, onBeforeMount, PropType, toRefs } from 'vue'
import { computed, onBeforeMount, PropType, toRefs, inject, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { KeyValue } from './ApiTestModel.js'
import draggable from 'vuedraggable'
@ -44,6 +44,8 @@ onBeforeMount(() => {
}
})
const activeName = inject('api-active-name')
const remove = (index: number) => {
if (isDisable()) return
//
@ -66,6 +68,18 @@ const createFilter = (queryString: string) => {
return restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
}
}
const options = [
{
label: '参数',
value: 'params'
},
{
label: '固定值',
value: 'fixed'
}
]
const value = ref('')
</script>
<template>
@ -77,7 +91,7 @@ const createFilter = (queryString: string) => {
<el-icon class="drag handle">
<Icon name="icon_drag_outlined"></Icon>
</el-icon>
<el-col :span="8" v-if="!unShowSelect">
<el-col :span="activeName === 'third' ? 8 : 6" v-if="!unShowSelect">
<el-input
v-if="!suggestions"
v-model="element.name"
@ -96,7 +110,16 @@ const createFilter = (queryString: string) => {
show-word-limit
/>
</el-col>
<el-col :span="3" v-if="activeName === 'fourth'">
<el-select v-model="value">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="8" v-if="unShowSelect">
<el-input
v-if="!!suggestions.length"
@ -108,14 +131,21 @@ const createFilter = (queryString: string) => {
/>
</el-col>
<el-col :span="7">
<el-col :span="activeName === 'third' ? 7 : 6">
<el-input
v-if="!needMock"
v-if="!needMock && activeName === 'third'"
v-model="element.value"
:disabled="isReadOnly"
:placeholder="unShowSelect ? t('common.description') : valueText"
show-word-limit
/>
<el-input
v-if="!needMock && activeName === 'fourth'"
v-model="element.value"
:disabled="isReadOnly"
:placeholder="value === 'params' ? '参数名称' : '值'"
show-word-limit
/>
</el-col>
<el-col :span="7" v-if="showDesc">

View File

@ -6,11 +6,12 @@ import EmptyBackground from '@/components/empty-background/src/EmptyBackground.v
import { cloneDeep } from 'lodash-es'
import ApiHttpRequestDraw from './ApiHttpRequestDraw.vue'
import type { Configuration, ApiConfiguration, SyncSetting } from './option'
import { fieldType, fieldTypeText } from '@/utils/attr'
import { Icon } from '@/components/icon-custom'
import { getSchema } from '@/api/datasource'
import { Base64 } from 'js-base64'
import { CustomPassword } from '@/components/custom-password'
import { ElForm, ElMessage } from 'element-plus-secondary'
import { ElForm, ElMessage, ElMessageBox } from 'element-plus-secondary'
import Cron from '@/components/cron/src/Cron.vue'
import { ComponentPublicInstance } from 'vue'
const { t } = useI18n()
@ -324,9 +325,11 @@ const addApiItem = item => {
: 0
}
nextTick(() => {
editApiItem.value.initApiItem(apiItem, form.value.apiConfiguration)
editApiItem.value.initApiItem(apiItem, form.value.apiConfiguration, activeName.value)
})
}
const activeName = ref('third')
const showPriority = ref(false)
const deleteItem = (item, idx) => {
@ -470,7 +473,149 @@ const apiRule = {
}
]
}
const dialogEditParams = ref(false)
const dialogRenameApi = ref(false)
const activeParamsName = ref('')
const apiParams = ref([
{
id: 1,
name: '接口1'
}
])
const paramsObj = ref({
name: '',
id: 1,
deType: 0
})
const apiObj = ref({
name: '',
id: 1
})
const paramsObjRules = {
name: [
{
required: true,
message: '请输入参数名称',
trigger: 'change'
},
{
min: 2,
max: 64,
message: '参数名称限制264字符',
trigger: 'blur'
}
]
}
const apiObjRules = {
name: [
{
required: true,
message: '请输入接口名称',
trigger: 'change'
},
{
min: 2,
max: 64,
message: '接口名称限制264字符',
trigger: 'blur'
}
]
}
const setActiveName = val => {
activeParamsName.value = val.name
}
const paramsObjRef = ref()
const apiObjRef = ref()
const saveParamsObj = () => {
paramsObjRef.value.validate(result => {
if (result) {
gridData.value.forEach(ele => {
if (ele.id === paramsObj.value.id) {
ele.name = paramsObj.value.name
}
})
}
})
}
const saveApiObj = () => {
apiObjRef.value.validate(result => {
if (result) {
apiParams.value.forEach(ele => {
if (ele.id === apiObj.value.id) {
ele.name = apiObj.value.name
}
})
}
})
}
const paramsResetForm = () => {
dialogEditParams.value = false
}
const apiResetForm = () => {
dialogRenameApi.value = false
}
const gridData = ref([
{
name: 'name',
deType: 0,
id: 0
}
])
const handleApiParams = (cmd: string, data) => {
if (cmd === 'rename') {
dialogRenameApi.value = true
paramsObj.value.name = data.name
}
if (cmd === 'delete') {
ElMessageBox.confirm('确定删除吗?', {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
}).then(() => {
apiParams.value.splice(0, 1)
})
}
if (cmd === 'edit') {
addApiItem(data)
}
}
const editParams = data => {
dialogEditParams.value = true
}
const delParams = data => {
ElMessageBox.confirm('确定删除吗?', {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
}).then(() => {
apiParams.value.splice(0, 1)
})
}
const datasetTypeList = [
{
label: '重命名',
svgName: 'icon_rename_outlined',
command: 'rename'
},
{
label: '删除',
svgName: 'icon_delete-trash_outlined',
command: 'delete'
}
]
defineExpose({
submitForm,
resetForm,
@ -522,7 +667,10 @@ defineExpose({
</el-form-item>
<template v-if="form.type === 'API'">
<div class="title-form_primary flex-space table-info-mr" v-show="activeStep !== 2">
<span>{{ t('datasource.data_table') }}</span>
<el-tabs v-model="activeName" class="api-tabs">
<el-tab-pane :label="t('datasource.data_table')" name="third"></el-tab-pane>
<el-tab-pane label="接口参数" name="fourth"></el-tab-pane>
</el-tabs>
<el-button type="primary" style="margin-left: auto" @click="() => addApiItem(null)">
<template #icon>
<Icon name="icon_add_outlined"></Icon>
@ -536,7 +684,7 @@ defineExpose({
:description="t('datasource.no_data_table')"
img-type="noneWhite"
/>
<template v-if="form.type === 'API' && activeStep === 1">
<template v-if="form.type === 'API' && activeStep === 1 && activeName === 'third'">
<div class="api-card-content">
<div
v-for="(api, idx) in form.apiConfiguration"
@ -609,6 +757,77 @@ defineExpose({
</div>
</div>
</template>
<div
style="display: flex"
v-if="form.type === 'API' && activeStep === 1 && activeName === 'fourth'"
>
<div class="left-api_params">
<div
v-for="ele in apiParams"
:class="[{ active: activeParamsName === ele.name }]"
class="list-item_primary"
:title="ele.name"
:key="ele.id"
@click="setActiveName(ele)"
>
<span class="label">{{ ele.name }}</span>
<span class="name-copy">
<el-icon class="hover-icon" @click.stop="handleApiParams('edit', ele)">
<icon name="icon_edit_outlined"></icon>
</el-icon>
<handle-more
icon-size="24px"
@handle-command="cmd => handleApiParams(cmd, ele)"
:menu-list="datasetTypeList"
placement="bottom-start"
></handle-more>
</span>
</div>
</div>
<div class="right-api_params">
<el-table
height="300"
style="width: 100%"
header-cell-class-name="header-cell"
:data="gridData"
>
<el-table-column :label="t('visualization.param_name')">
<template #default="scope">
{{ scope.row.name || '-' }}
</template>
</el-table-column>
<el-table-column :label="t('deDataset.parameter_type')">
<template #default="scope">
<div class="flex-align-center icon">
<el-icon>
<Icon
:className="`field-icon-${fieldType[scope.row.deType]}`"
:name="`field_${fieldType[scope.row.deType]}`"
></Icon>
</el-icon>
{{ fieldTypeText[scope.row.deType] }}
</div>
</template>
</el-table-column>
<el-table-column :label="t('common.operate')">
<template #default="scope">
<el-button text @click.stop="editParams(scope.row)">
<template #icon>
<Icon name="icon_edit_outlined"></Icon>
</template>
</el-button>
<el-button text @click.stop="delParams(scope.row)">
<template #icon>
<Icon name="icon_delete-trash_outlined"></Icon>
</template>
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<template v-if="notapiexcelconfig">
<el-form-item label="连接方式" prop="type">
@ -957,6 +1176,61 @@ defineExpose({
</el-form-item>
</div>
</el-form>
<el-dialog
title="编辑参数"
v-model="dialogEditParams"
width="420px"
class="create-dialog"
:before-close="paramsResetForm"
>
<el-form
label-position="top"
require-asterisk-position="right"
ref="paramsObjRef"
@keydown.stop.prevent.enter
:model="paramsObj"
:rules="paramsObjRules"
>
<el-form-item :label="t('visualization.param_name')" prop="name">
<el-input placeholder="请输入参数名称" v-model="paramsObj.name" />
</el-form-item>
<el-form-item :label="t('deDataset.parameter_type')" prop="deType">
<el-radio-group v-model="paramsObj.deType">
<el-radio :value="0" label="文本"></el-radio>
<el-radio :value="2" label="数值"></el-radio>
<el-radio :value="3" label="数值(小数)"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button secondary @click="paramsResetForm">{{ t('dataset.cancel') }} </el-button>
<el-button type="primary" @click="saveParamsObj">{{ t('dataset.confirm') }} </el-button>
</template>
</el-dialog>
<el-dialog
title="重命名"
v-model="dialogRenameApi"
width="420px"
class="create-dialog"
:before-close="apiResetForm"
>
<el-form
label-position="top"
require-asterisk-position="right"
ref="paramsObjRef"
@keydown.stop.prevent.enter
:model="apiObj"
:rules="apiObjRules"
>
<el-form-item label="接口名称" prop="name">
<el-input placeholder="请输入接口名称" v-model="apiObj.name" />
</el-form-item>
</el-form>
<template #footer>
<el-button secondary @click="apiResetForm">{{ t('dataset.cancel') }} </el-button>
<el-button type="primary" @click="saveApiObj">{{ t('dataset.confirm') }} </el-button>
</template>
</el-dialog>
<api-http-request-draw @return-item="returnItem" ref="editApiItem"></api-http-request-draw>
</div>
@ -1037,8 +1311,44 @@ defineExpose({
margin: 24px 0 16px 0;
}
.left-api_params {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border: 1px solid #bbbfc4;
width: 300px;
padding: 16px;
.name-copy {
display: none;
line-height: 24px;
margin-left: 4px;
}
.list-item_primary:hover {
.name-copy {
display: inline;
}
.label {
width: 74% !important;
}
}
}
.right-api_params {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border: 1px solid #bbbfc4;
border-left: none;
width: calc(100% - 200px);
}
.table-info-mr {
margin: 28px 0 12px 0;
.api-tabs {
:deep(.ed-tabs__nav-wrap::after) {
display: none;
}
}
}
.info-update {

@ -1 +1 @@
Subproject commit 97d33ae8253dd163a257c5025f21a7facb85004b
Subproject commit 178f8f52209c1ffb81b58c8055733d411aef054a

View File

@ -19,7 +19,7 @@
</parent>
<properties>
<dataease.version>2.7.0</dataease.version>
<dataease.version>2.8.0</dataease.version>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>

View File

@ -20,4 +20,7 @@ public interface XpackComponentApi {
@GetMapping("/viewPlugins")
List<XpackPluginsViewVO> viewPlugins();
@GetMapping("/pluginStaticInfo/{moduleName}")
void pluginStaticInfo(@PathVariable("moduleName") String moduleName);
}

View File

@ -0,0 +1,29 @@
package io.dataease.api.xpack.share;
import io.dataease.api.xpack.share.request.TicketCreator;
import io.dataease.api.xpack.share.request.TicketDelRequest;
import io.dataease.api.xpack.share.request.TicketSwitchRequest;
import io.dataease.api.xpack.share.vo.TicketVO;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@Tag(name = "分享:TICKET")
public interface ShareTicketApi {
@PostMapping("/saveTicket")
String saveTicket(@RequestBody TicketCreator creator);
@PostMapping("/delTicket")
void deleteTicket(@RequestBody TicketDelRequest request);
@PostMapping("/enableTicket")
void switchRequire(@RequestBody TicketSwitchRequest request);
@GetMapping("/query/{resourceId}")
List<TicketVO> query(@PathVariable("resourceId") Long resourceId);
}

View File

@ -0,0 +1,22 @@
package io.dataease.api.xpack.share.request;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class TicketCreator implements Serializable {
@Serial
private static final long serialVersionUID = 8661378104009097296L;
private String ticket;
private Long exp;
private String args;
private String uuid;
private boolean generateNew;
}

View File

@ -0,0 +1,14 @@
package io.dataease.api.xpack.share.request;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class TicketDelRequest implements Serializable {
@Serial
private static final long serialVersionUID = -3978489349675065507L;
private String ticket;
}

View File

@ -0,0 +1,16 @@
package io.dataease.api.xpack.share.request;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class TicketSwitchRequest implements Serializable {
@Serial
private static final long serialVersionUID = 7670768142874123370L;
private String resourceId;
private Boolean require = false;
}

View File

@ -20,4 +20,6 @@ public class XpackShareProxyRequest implements Serializable {
@Schema(description = "密钥", requiredMode = Schema.RequiredMode.REQUIRED)
private String ciphertext;
private boolean inIframe;
private String ticket;
}

View File

@ -0,0 +1,27 @@
package io.dataease.api.xpack.share.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class TicketVO implements Serializable {
@Serial
private static final long serialVersionUID = -599110079356725271L;
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String uuid;
private String ticket;
private Long exp;
private String args;
private Long accessTime;
}

View File

@ -0,0 +1,18 @@
package io.dataease.api.xpack.share.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class TicketValidVO implements Serializable {
@Serial
private static final long serialVersionUID = 2452043685969885580L;
private boolean ticketValid;
private boolean ticketExp;
private String args;
}

View File

@ -32,4 +32,6 @@ public class XpackShareProxyVO implements Serializable {
@Schema(description = "类型")
private String type;
private boolean inIframeError = true;
private TicketValidVO ticketValidVO;
}

View File

@ -29,4 +29,6 @@ public class XpackShareVO implements Serializable {
private String pwd;
@Schema(description = "自动生成密码")
private Boolean autoPwd = true;
@Schema(description = "ticket必须")
private Boolean ticketRequire = false;
}

View File

@ -63,6 +63,7 @@ public class WhitelistUtils {
|| StringUtils.startsWithAny(requestURI, "/appearance/image/")
|| StringUtils.startsWithAny(requestURI, "/share/proxyInfo")
|| StringUtils.startsWithAny(requestURI, "/xpackComponent/content")
|| StringUtils.startsWithAny(requestURI, "/xpackComponent/pluginStaticInfo")
|| StringUtils.startsWithAny(requestURI, "/geo/")
|| StringUtils.startsWithAny(requestURI, "/websocket")
|| StringUtils.startsWithAny(requestURI, "/map/")

View File

@ -1,7 +1,11 @@
package io.dataease.extensions.view.factory;
import io.dataease.exception.DEException;
import io.dataease.extensions.view.template.PluginsChartTemplate;
import io.dataease.extensions.view.vo.XpackPluginsViewVO;
import io.dataease.license.utils.LicenseUtil;
import io.dataease.license.utils.LogUtil;
import io.dataease.plugins.factory.DataEasePluginFactory;
import java.util.List;
import java.util.Map;
@ -11,22 +15,29 @@ public class PluginsChartFactory {
private static final Map<String, PluginsChartTemplate> templateMap = new ConcurrentHashMap<>();
public static PluginsChartTemplate getInstance(String render, String type) {
if (!LicenseUtil.licenseValid()) DEException.throwException("插件功能只对企业版本可用!");
String key = render + "_" + type;
return templateMap.get(key);
}
public static void loadTemplate(String render, String type, PluginsChartTemplate template) {
if (!LicenseUtil.licenseValid()) DEException.throwException("插件功能只对企业版本可用!");
String key = render + "_" + type;
if (templateMap.containsKey(key)) return;
templateMap.put(key, template);
try {
String moduleName = template.getPluginInfo().getModuleName();
DataEasePluginFactory.loadTemplate(moduleName, template);
} catch (Exception e) {
LogUtil.error(e.getMessage(), new Throwable(e));
DEException.throwException(e);
}
}
public static List<XpackPluginsViewVO> getViewConfigList() {
if (!LicenseUtil.licenseValid()) DEException.throwException("插件功能只对企业版本可用!");
return templateMap.values().stream().map(PluginsChartTemplate::getConfig).toList();
}
public static List<String> getAllPlugins() {
return null;
}
}

View File

@ -4,6 +4,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
@Data
public class XpackPluginsViewVO implements Serializable {
@ -26,4 +27,6 @@ public class XpackPluginsViewVO implements Serializable {
private String render;
private Map<String, String> staticMap;
}