Browse Source

CHART-12746 图表埋点

feature/big-screen
白岳 5 years ago
parent
commit
4c198a62ab
  1. 4
      designer-base/src/main/java/com/fr/design/mainframe/JTemplate.java
  2. 256
      designer-base/src/main/java/com/fr/design/mainframe/chart/info/ChartInfo.java
  3. 333
      designer-base/src/main/java/com/fr/design/mainframe/chart/info/ChartInfoCollector.java
  4. 40
      designer-base/src/main/java/com/fr/design/mainframe/chart/info/ChartSendHelper.java
  5. 24
      designer-chart/src/main/java/com/fr/design/chart/ChartDialog.java
  6. 11
      designer-chart/src/main/java/com/fr/design/chart/ChartTypePane.java
  7. 14
      designer-chart/src/main/java/com/fr/design/mainframe/chart/ChartEditPane.java
  8. 11
      designer-chart/src/main/java/com/fr/design/mainframe/chart/gui/ChartTypeButtonPane.java
  9. 5
      designer-chart/src/main/java/com/fr/van/chart/designer/type/AbstractVanChartTypePane.java
  10. 36
      designer-form/src/main/java/com/fr/design/mainframe/FormCreatorDropTarget.java
  11. 2
      designer-realize/src/main/java/com/fr/design/mainframe/InformationCollector.java
  12. 18
      designer-realize/src/main/java/com/fr/poly/hanlder/PolyDesignerDropTarget.java

4
designer-base/src/main/java/com/fr/design/mainframe/JTemplate.java

@ -36,6 +36,7 @@ import com.fr.design.gui.imenu.UIMenuItem;
import com.fr.design.gui.itree.filetree.TemplateFileTree; import com.fr.design.gui.itree.filetree.TemplateFileTree;
import com.fr.design.i18n.Toolkit; import com.fr.design.i18n.Toolkit;
import com.fr.design.layout.FRGUIPaneFactory; import com.fr.design.layout.FRGUIPaneFactory;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.design.mainframe.template.info.TemplateInfoCollector; import com.fr.design.mainframe.template.info.TemplateInfoCollector;
import com.fr.design.mainframe.template.info.TemplateProcessInfo; import com.fr.design.mainframe.template.info.TemplateProcessInfo;
import com.fr.design.mainframe.template.info.TimeConsumeTimer; import com.fr.design.mainframe.template.info.TimeConsumeTimer;
@ -70,11 +71,11 @@ import javax.swing.JComponent;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.undo.UndoManager; import javax.swing.undo.UndoManager;
import java.awt.BorderLayout;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.awt.BorderLayout;
/** /**
* 报表设计和表单设计的编辑区域(设计器编辑的IO文件) * 报表设计和表单设计的编辑区域(设计器编辑的IO文件)
@ -164,6 +165,7 @@ public abstract class JTemplate<T extends BaseBook, U extends BaseUndoState<?>>
} }
private void collectInfo(String originID) { // 执行收集操作 private void collectInfo(String originID) { // 执行收集操作
ChartInfoCollector.getInstance().collectInfo(template.getTemplateID(), originID, getProcessInfo());
if (!consumeTimer.isEnabled()) { if (!consumeTimer.isEnabled()) {
return; return;
} }

256
designer-base/src/main/java/com/fr/design/mainframe/chart/info/ChartInfo.java

@ -0,0 +1,256 @@
package com.fr.design.mainframe.chart.info;
import com.fr.base.io.BaseBook;
import com.fr.config.MarketConfig;
import com.fr.design.DesignModelAdapter;
import com.fr.design.DesignerEnvManager;
import com.fr.design.file.HistoryTemplateListCache;
import com.fr.general.GeneralUtils;
import com.fr.json.JSONObject;
import com.fr.stable.ProductConstants;
import com.fr.stable.StringUtils;
import com.fr.stable.xml.XMLPrintWriter;
import com.fr.stable.xml.XMLReadable;
import com.fr.stable.xml.XMLWriter;
import com.fr.stable.xml.XMLableReader;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
/**
* @author Bjorn
* @version 10.0
* Created by Bjorn on 2020-02-17
*/
public class ChartInfo implements XMLReadable, XMLWriter {
public static final String XML_TAG = "ChartInfo";
private static final String XML_CHART_CONSUMING_MAP = "chartConsumingMap";
private static final String ATTR_TEST_TEMPLATE = "testTemplate";
private static final String ATTR_DAY_COUNT = "day_count";
private static final String ATTR_USERNAME = "username";
private static final String ATTR_UUID = "uuid";
private static final String ATTR_ACTIVITYKEY = "activityKey";
private static final String ATTR_TEMPLATE_ID = "templateID";
private static final String ATTR_REPORT_TYPE = "type";
private static final String ATTR_CHART_ID = "chartId";
private static final String ATTR_CHART_TYPE = "chartType";
private static final String ATTR_CHART_CREATE_TIME = "chartCreateTime";
private static final String ATTR_CHART_TYPE_TIME = "chartTypeTime";
private static final String ATTR_CHART_PROPERTY_FIRST_TIME = "chartPropertyFirstTime";
private static final String ATTR_CHART_PROPERTY_END_TIME = "chartPropertyEndTime";
private static final String ATTR_JAR_TIME = "jarTime";
private static final String ATTR_VERSION = "version";
private static final int COMPLETE_DAY_COUNT = 3; // 判断图表是否可以上传的天数
private int idleDayCount; // 到现在为止,图表闲置(上次保存后没有再编辑过)的天数
private String chartId = StringUtils.EMPTY;
private String templateId = StringUtils.EMPTY;
private Map<String, Object> chartConsumingMap = new HashMap<>();
private BaseBook book;
private boolean testTemplate;
private ChartInfo() {
}
private ChartInfo(String chartId, String templateId, BaseBook book) {
this.chartId = chartId;
this.templateId = templateId;
this.book = book;
}
public String getChartId() {
return chartId;
}
public String getTemplateId() {
return templateId;
}
public void setTemplateId(String templateId) {
this.templateId = templateId;
this.chartConsumingMap.put(ATTR_TEMPLATE_ID, templateId);
}
public BaseBook getBook() {
return book;
}
public boolean isTestTemplate() {
return testTemplate;
}
public void setTestTemplate(boolean testTemplate) {
this.testTemplate = testTemplate;
}
static ChartInfo newInstanceByRead(XMLableReader reader) {
ChartInfo chartInfo = new ChartInfo();
reader.readXMLObject(chartInfo);
return chartInfo;
}
public static ChartInfo newInstance(String chartId, String chartType) {
return newInstance(chartId, chartType, null);
}
public static ChartInfo newInstance(String chartId, String chartType, String createTime) {
HashMap<String, Object> chartConsumingMap = new HashMap<>();
String username = MarketConfig.getInstance().getBbsUsername();
String uuid = DesignerEnvManager.getEnvManager().getUUID();
String activitykey = DesignerEnvManager.getEnvManager().getActivationKey();
BaseBook book = DesignModelAdapter.getCurrentModelAdapter().getBook();
String templateId = book.getTemplateID();
int reportType = HistoryTemplateListCache.getInstance().getCurrentEditingTemplate().getProcessInfo().getReportType();
String typeTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance().getTime());
createTime = createTime == null ? typeTime : createTime;
String jarTime = GeneralUtils.readBuildNO();
String version = ProductConstants.VERSION;
chartConsumingMap.put(ATTR_USERNAME, username);
chartConsumingMap.put(ATTR_UUID, uuid);
chartConsumingMap.put(ATTR_ACTIVITYKEY, activitykey);
chartConsumingMap.put(ATTR_TEMPLATE_ID, templateId);
chartConsumingMap.put(ATTR_REPORT_TYPE, reportType);
chartConsumingMap.put(ATTR_CHART_ID, chartId);
chartConsumingMap.put(ATTR_CHART_TYPE, chartType);
chartConsumingMap.put(ATTR_CHART_CREATE_TIME, createTime);
chartConsumingMap.put(ATTR_CHART_TYPE_TIME, typeTime);
chartConsumingMap.put(ATTR_CHART_PROPERTY_FIRST_TIME, "");
chartConsumingMap.put(ATTR_CHART_PROPERTY_END_TIME, "");
chartConsumingMap.put(ATTR_JAR_TIME, jarTime);
chartConsumingMap.put(ATTR_VERSION, version);
ChartInfo chartInfo = new ChartInfo(chartId, templateId, book);
chartInfo.chartConsumingMap = chartConsumingMap;
return chartInfo;
}
public void writeXML(XMLPrintWriter writer) {
writer.startTAG(XML_TAG);
if (StringUtils.isNotEmpty(chartId)) {
writer.attr(ATTR_CHART_ID, this.chartId);
}
if (StringUtils.isNotEmpty(templateId)) {
writer.attr(ATTR_TEMPLATE_ID, this.templateId);
}
if (idleDayCount >= 0) {
writer.attr(ATTR_DAY_COUNT, this.idleDayCount);
}
writer.attr(ATTR_TEST_TEMPLATE, this.testTemplate);
writer.startTAG(XML_CHART_CONSUMING_MAP);
writer.attr(ATTR_USERNAME, (String) chartConsumingMap.get(ATTR_USERNAME));
writer.attr(ATTR_UUID, (String) chartConsumingMap.get(ATTR_UUID));
writer.attr(ATTR_ACTIVITYKEY, (String) chartConsumingMap.get(ATTR_ACTIVITYKEY));
writer.attr(ATTR_REPORT_TYPE, (int) chartConsumingMap.get(ATTR_REPORT_TYPE));
writer.attr(ATTR_CHART_TYPE, (String) chartConsumingMap.get(ATTR_CHART_TYPE));
writer.attr(ATTR_CHART_CREATE_TIME, (String) chartConsumingMap.get(ATTR_CHART_CREATE_TIME));
writer.attr(ATTR_CHART_TYPE_TIME, (String) chartConsumingMap.get(ATTR_CHART_TYPE_TIME));
writer.attr(ATTR_CHART_PROPERTY_FIRST_TIME, (String) chartConsumingMap.get(ATTR_CHART_PROPERTY_FIRST_TIME));
writer.attr(ATTR_CHART_PROPERTY_END_TIME, (String) chartConsumingMap.get(ATTR_CHART_PROPERTY_END_TIME));
writer.attr(ATTR_JAR_TIME, (String) chartConsumingMap.get(ATTR_JAR_TIME));
writer.attr(ATTR_VERSION, (String) chartConsumingMap.get(ATTR_VERSION));
writer.end();
writer.end();
}
public void readXML(XMLableReader reader) {
if (!reader.isChildNode()) {
idleDayCount = reader.getAttrAsInt(ATTR_DAY_COUNT, 0);
chartId = reader.getAttrAsString(ATTR_CHART_ID, StringUtils.EMPTY);
templateId = reader.getAttrAsString(ATTR_TEMPLATE_ID, StringUtils.EMPTY);
testTemplate = reader.getAttrAsBoolean(ATTR_TEST_TEMPLATE, true);
} else {
try {
String name = reader.getTagName();
if (XML_CHART_CONSUMING_MAP.equals(name)) {
chartConsumingMap.put(ATTR_USERNAME, reader.getAttrAsString(ATTR_USERNAME, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_UUID, reader.getAttrAsString(ATTR_UUID, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_ACTIVITYKEY, reader.getAttrAsString(ATTR_ACTIVITYKEY, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_TEMPLATE_ID, templateId);
chartConsumingMap.put(ATTR_REPORT_TYPE, reader.getAttrAsInt(ATTR_REPORT_TYPE, 0));
chartConsumingMap.put(ATTR_CHART_ID, chartId);
chartConsumingMap.put(ATTR_CHART_TYPE, reader.getAttrAsString(ATTR_CHART_TYPE, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_CHART_CREATE_TIME, reader.getAttrAsString(ATTR_CHART_CREATE_TIME, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_CHART_TYPE_TIME, reader.getAttrAsString(ATTR_CHART_TYPE_TIME, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_CHART_PROPERTY_FIRST_TIME, reader.getAttrAsString(ATTR_CHART_PROPERTY_FIRST_TIME, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_CHART_PROPERTY_END_TIME, reader.getAttrAsString(ATTR_CHART_PROPERTY_END_TIME, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_JAR_TIME, reader.getAttrAsString(ATTR_JAR_TIME, StringUtils.EMPTY));
chartConsumingMap.put(ATTR_VERSION, reader.getAttrAsString(ATTR_VERSION, "8.0"));
}
} catch (Exception ex) {
// 什么也不做,使用默认值
}
}
}
public void resetIdleDayCount() {
this.idleDayCount = 0;
}
public void addIdleDayCountByOne() {
this.idleDayCount += 1;
}
public int getIdleDayCount() {
return this.idleDayCount;
}
boolean isComplete() {
// 连续3天打开了设计器但是没有编辑
return idleDayCount > COMPLETE_DAY_COUNT;
}
String getChartConsumingMapJsonString() {
return new JSONObject(chartConsumingMap).toString();
}
public void updatePropertyTime() {
String propertyTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance().getTime());
if (StringUtils.isEmpty((String) chartConsumingMap.get(ATTR_CHART_PROPERTY_FIRST_TIME))) {
chartConsumingMap.put(ATTR_CHART_PROPERTY_FIRST_TIME, propertyTime);
}
chartConsumingMap.put(ATTR_CHART_PROPERTY_END_TIME, propertyTime);
}
public void updateChartType(String chartType) {
String typeTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance().getTime());
chartConsumingMap.put(ATTR_CHART_TYPE_TIME, typeTime);
chartConsumingMap.put(ATTR_CHART_TYPE, chartType);
chartConsumingMap.put(ATTR_CHART_PROPERTY_FIRST_TIME, "");
chartConsumingMap.put(ATTR_CHART_PROPERTY_END_TIME, "");
}
@Override
public ChartInfo clone() {
ChartInfo chartInfo = new ChartInfo();
chartInfo.chartId = this.chartId;
chartInfo.idleDayCount = this.idleDayCount;
chartInfo.templateId = this.templateId;
chartInfo.testTemplate = this.testTemplate;
Map<String, Object> chartConsumingMap = new HashMap<>();
for (Map.Entry<String, Object> entry : this.chartConsumingMap.entrySet()) {
chartConsumingMap.put(entry.getKey(), entry.getValue());
}
chartInfo.chartConsumingMap = chartConsumingMap;
return chartInfo;
}
}

333
designer-base/src/main/java/com/fr/design/mainframe/chart/info/ChartInfoCollector.java

@ -0,0 +1,333 @@
package com.fr.design.mainframe.chart.info;
import com.fr.base.FRContext;
import com.fr.base.io.BaseBook;
import com.fr.base.io.XMLReadHelper;
import com.fr.design.DesignerEnvManager;
import com.fr.design.mainframe.template.info.TemplateProcessInfo;
import com.fr.general.ComparatorUtils;
import com.fr.log.FineLoggerFactory;
import com.fr.stable.ProductConstants;
import com.fr.stable.StableUtils;
import com.fr.stable.StringUtils;
import com.fr.stable.xml.XMLPrintWriter;
import com.fr.stable.xml.XMLReadable;
import com.fr.stable.xml.XMLTools;
import com.fr.stable.xml.XMLWriter;
import com.fr.stable.xml.XMLableReader;
import com.fr.third.javax.xml.stream.XMLStreamException;
import com.fr.third.org.apache.commons.io.FileUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Bjorn
* @version 10.0
* Created by Bjorn on 2020-02-18
*/
public class ChartInfoCollector implements XMLReadable, XMLWriter {
private static final String XML_TAG = "ChartInfoCollector";
private static final String XML_LAST_EDIT_DAY = "lastEditDay";
private static final String XML_CHART_INFO_LIST = "ChartInfoList";
private static final String XML_FILE_NAME = "chart.info";
private static final int VALID_CELL_COUNT = 5; // 有效报表模板的格子数
private static final int VALID_WIDGET_COUNT = 5; // 有效报表模板的控件数
private static ChartInfoCollector instance;
private static final int MAX_SIZE = 512 * 1024 * 1024;
private Map<String, ChartInfo> chartInfoMap;
private Map<String, ChartInfo> chartInfoCacheMap;
private String lastEditDay = StringUtils.EMPTY;
private ChartInfoCollector() {
init();
}
private void init() {
chartInfoMap = new ConcurrentHashMap<>();
chartInfoCacheMap = new HashMap<>();
loadFromFile();
}
public static ChartInfoCollector getInstance() {
if (instance == null) {
instance = new ChartInfoCollector();
}
return instance;
}
/**
* 新建图表保存状态
*/
public void collection(String chartId, String chartType, String createTime) {
if (!shouldCollectInfo()) {
return;
}
ChartInfo chartInfo = ChartInfo.newInstance(chartId, chartType, createTime);
chartInfoCacheMap.put(chartId, chartInfo);
}
/**
* 图表编辑更新编辑时间
*/
public void updateChartPropertyTime(String chartId, String chartType) {
if (!shouldCollectInfo()) {
return;
}
ChartInfo chartInfo = getOrCreateChartInfo(chartId, chartType);
//更新编辑时间
chartInfo.updatePropertyTime();
//重置计数
chartInfo.resetIdleDayCount();
}
/**
* 图表类型变化更新类型和类型确认时间
*/
public void updateChartTypeTime(String chartId, String chartType) {
if (!shouldCollectInfo()) {
return;
}
ChartInfo chartInfo = getOrCreateChartInfo(chartId, chartType);
//更新类型确认时间和类型
chartInfo.updateChartType(chartType);
//重置计数
chartInfo.resetIdleDayCount();
}
private ChartInfo getOrCreateChartInfo(String chartId, String chartType) {
//缓存中有从缓存中拿
if (chartInfoCacheMap.containsKey(chartId)) {
return chartInfoCacheMap.get(chartId);
}
//缓存中没有从文件中读取的信息中拷贝到缓存
if (chartInfoMap.containsKey(chartId)) {
ChartInfo chartInfo = chartInfoMap.get(chartId).clone();
chartInfoCacheMap.put(chartId, chartInfo);
return chartInfo;
}
//都有的话创建一个并加入到缓存中
ChartInfo chartInfo = ChartInfo.newInstance(chartId, chartType);
chartInfoCacheMap.put(chartId, chartInfo);
return chartInfo;
}
/**
* 保存模板的时候将该模板中的图表埋点信息保存
*/
public void collectInfo(String templateId, String originID, TemplateProcessInfo processInfo) {
if (!shouldCollectInfo()) {
return;
}
if (StringUtils.isEmpty(originID)) {
originID = templateId;
}
boolean testTemplate = isTestTemplate(processInfo);
for (ChartInfo chartInfo : chartInfoMap.values()) {
if (originID.equals(chartInfo.getTemplateId())) {
chartInfo.setTemplateId(templateId);
chartInfo.setTestTemplate(testTemplate);
}
}
for (ChartInfo chartInfo : chartInfoCacheMap.values()) {
BaseBook book = chartInfo.getBook();
if ((book != null && templateId.equals(book.getTemplateID())) ||
originID.equals(chartInfo.getTemplateId())) {
chartInfo.setTemplateId(templateId);
chartInfo.setTestTemplate(testTemplate);
chartInfoMap.put(chartInfo.getChartId(), chartInfo);
}
}
// 每次更新之后,都同步到暂存文件中
saveInfo();
}
private boolean isTestTemplate(TemplateProcessInfo processInfo) {
int reportType = processInfo.getReportType();
int cellCount = processInfo.getCellCount();
int floatCount = processInfo.getFloatCount();
int blockCount = processInfo.getBlockCount();
int widgetCount = processInfo.getWidgetCount();
boolean isTestTemplate;
if (reportType == 0) { // 普通报表
isTestTemplate = cellCount <= VALID_CELL_COUNT && floatCount <= 1 && widgetCount <= VALID_WIDGET_COUNT;
} else if (reportType == 1) { // 聚合报表
isTestTemplate = blockCount <= 1 && widgetCount <= VALID_WIDGET_COUNT;
} else { // 表单(reportType == 2)
isTestTemplate = widgetCount <= 1;
}
return isTestTemplate;
}
/**
* 发送本地图表信息到服务器并清空已发送图表的本地记录
*/
public void sendChartInfo() {
addIdleDayCount();
List<String> removeLaterList = new ArrayList<>();
for (String key : chartInfoMap.keySet()) {
ChartInfo chartInfo = chartInfoMap.get(key);
if (chartInfo.isComplete()) {
if (!chartInfo.isTestTemplate()) {
if (ChartSendHelper.sendChartInfo(chartInfo)) {
removeLaterList.add(key);
}
} else {
removeLaterList.add(key);
}
}
}
// 清空记录
for (String key : removeLaterList) {
chartInfoMap.remove(key);
}
saveInfo();
}
/**
* 更新 day_count打开设计器却未编辑图表的连续日子
*/
private void addIdleDayCount() {
// 判断今天是否第一次打开设计器,为了防止同一天内,多次 addIdleDayCount
String today = new SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().getTime());
if (ComparatorUtils.equals(today, lastEditDay)) {
return;
}
for (ChartInfo chartInfo : chartInfoMap.values()) {
chartInfo.addIdleDayCountByOne();
}
lastEditDay = today;
}
private void loadFromFile() {
if (!getInfoFile().exists()) {
return;
}
XMLableReader reader = null;
try (InputStream in = new FileInputStream(getInfoFile())) {
// XMLableReader 还是应该考虑实现 Closable 接口的,这样就能使用 try-with 语句了
reader = XMLReadHelper.createXMLableReader(in, XMLPrintWriter.XML_ENCODER);
if (reader == null) {
return;
}
reader.readXMLObject(this);
} catch (FileNotFoundException e) {
// do nothing
} catch (XMLStreamException | IOException e) {
FineLoggerFactory.getLogger().error(e.getMessage(), e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (XMLStreamException e) {
FineLoggerFactory.getLogger().error(e.getMessage(), e);
}
}
}
/**
* 获取缓存文件存放路径
*/
private static File getInfoFile() {
File file = new File(StableUtils.pathJoin(ProductConstants.getEnvHome(), XML_FILE_NAME));
try {
if (!file.exists()) {
file.createNewFile();
}
} catch (Exception ex) {
FineLoggerFactory.getLogger().error(ex.getMessage(), ex);
}
return file;
}
private boolean shouldCollectInfo() {
return FileUtils.sizeOf(getInfoFile()) <= MAX_SIZE && DesignerEnvManager.getEnvManager().isJoinProductImprove() && FRContext.isChineseEnv();
}
/**
* 将包含所有信息的对象保存到文件
*/
private void saveInfo() {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
XMLTools.writeOutputStreamXML(this, out);
out.flush();
out.close();
String fileContent = new String(out.toByteArray(), StandardCharsets.UTF_8);
FileUtils.writeStringToFile(getInfoFile(), fileContent, StandardCharsets.UTF_8);
} catch (Exception ex) {
FineLoggerFactory.getLogger().error(ex.getMessage());
}
}
@Override
public void readXML(XMLableReader reader) {
if (reader.isChildNode()) {
try {
String name = reader.getTagName();
if (ChartInfo.XML_TAG.equals(name)) {
ChartInfo chartInfo = ChartInfo.newInstanceByRead(reader);
chartInfoMap.put(chartInfo.getChartId(), chartInfo);
} else if (XML_LAST_EDIT_DAY.equals(name)) {
lastEditDay = reader.getElementValue();
}
} catch (Exception ex) {
// 什么也不做,使用默认值
}
}
}
@Override
public void writeXML(XMLPrintWriter writer) {
writer.startTAG(XML_TAG);
writer.startTAG(XML_LAST_EDIT_DAY);
writer.textNode(lastEditDay);
writer.end();
writer.startTAG(XML_CHART_INFO_LIST);
for (ChartInfo chartInfo : chartInfoMap.values()) {
chartInfo.writeXML(writer);
}
writer.end();
writer.end();
}
}

40
designer-base/src/main/java/com/fr/design/mainframe/chart/info/ChartSendHelper.java

@ -0,0 +1,40 @@
package com.fr.design.mainframe.chart.info;
import com.fr.design.mainframe.SiteCenterToken;
import com.fr.general.CloudCenter;
import com.fr.general.ComparatorUtils;
import com.fr.general.http.HttpToolbox;
import com.fr.json.JSONObject;
import com.fr.log.FineLoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* @author Bjorn
* @version 10.0
* Created by Bjorn on 2020-02-19
*/
class ChartSendHelper {
private static final String CHART_CONSUMING_URL = CloudCenter.getInstance().acquireUrlByKind("chartinfo.consuming") + "/single";
static boolean sendChartInfo(ChartInfo chartInfo) {
return sendSingleChartInfo(CHART_CONSUMING_URL, chartInfo.getChartConsumingMapJsonString());
}
private static boolean sendSingleChartInfo(String url, String content) {
Map<String, Object> para = new HashMap<>();
para.put("token", SiteCenterToken.generateToken());
para.put("content", content);
try {
String res = HttpToolbox.post(url, para);
return ComparatorUtils.equals(new JSONObject(res).get("status"), "success");
} catch (Throwable e) {
// 客户不需要关心,错误等级为 debug 就行了
FineLoggerFactory.getLogger().debug(e.getMessage(), e);
}
return false;
}
}

24
designer-chart/src/main/java/com/fr/design/chart/ChartDialog.java

@ -1,5 +1,15 @@
package com.fr.design.chart; package com.fr.design.chart;
import com.fr.base.chart.BaseChartCollection;
import com.fr.chart.chartattr.ChartCollection;
import com.fr.design.dialog.BasicDialog;
import com.fr.design.gui.chart.MiddleChartDialog;
import com.fr.design.gui.ibutton.UIButton;
import com.fr.design.utils.gui.GUICoreUtils;
import javax.swing.JPanel;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Dialog; import java.awt.Dialog;
import java.awt.FlowLayout; import java.awt.FlowLayout;
@ -7,16 +17,6 @@ import java.awt.Frame;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import javax.swing.JPanel;
import com.fr.base.chart.BaseChartCollection;
import com.fr.chart.chartattr.ChartCollection;
import com.fr.design.gui.chart.MiddleChartDialog;
import com.fr.design.gui.ibutton.UIButton;
import com.fr.design.dialog.BasicDialog;
import com.fr.design.utils.gui.GUICoreUtils;
/** /**
* 封装一层 图表新建的对话框, 配合属性表确定: 先单独只要一种图表类型的对话框. * 封装一层 图表新建的对话框, 配合属性表确定: 先单独只要一种图表类型的对话框.
* @author kunsnat E-mail:kunsnat@gmail.com * @author kunsnat E-mail:kunsnat@gmail.com
@ -40,6 +40,8 @@ public class ChartDialog extends MiddleChartDialog {
} }
private void initComponent() { private void initComponent() {
final String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance().getTime());
this.setModal(true); this.setModal(true);
this.setLayout(new BorderLayout()); this.setLayout(new BorderLayout());
final ChartTypePane chartTypePane = new ChartTypePane(); final ChartTypePane chartTypePane = new ChartTypePane();
@ -63,7 +65,7 @@ public class ChartDialog extends MiddleChartDialog {
ok.addActionListener(new ActionListener() { ok.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
chartTypePane.update((ChartCollection)cc); chartTypePane.update((ChartCollection)cc, createTime);
doOK(); doOK();
} }
}); });

11
designer-chart/src/main/java/com/fr/design/chart/ChartTypePane.java

@ -7,10 +7,12 @@ import com.fr.chart.chartattr.ChartCollection;
import com.fr.chart.charttypes.ChartTypeManager; import com.fr.chart.charttypes.ChartTypeManager;
import com.fr.chartx.attr.ChartProvider; import com.fr.chartx.attr.ChartProvider;
import com.fr.design.ChartTypeInterfaceManager; import com.fr.design.ChartTypeInterfaceManager;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.design.gui.ilable.UILabel; import com.fr.design.gui.ilable.UILabel;
import com.fr.design.layout.FRGUIPaneFactory; import com.fr.design.layout.FRGUIPaneFactory;
import com.fr.design.utils.gui.GUICoreUtils; import com.fr.design.utils.gui.GUICoreUtils;
import com.fr.log.FineLoggerFactory; import com.fr.log.FineLoggerFactory;
import com.fr.plugin.chart.vanchart.VanChart;
import com.fr.stable.StringUtils; import com.fr.stable.StringUtils;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
@ -119,6 +121,10 @@ public class ChartTypePane extends ChartCommonWizardPane {
} }
public void update(ChartCollection cc) { public void update(ChartCollection cc) {
update(cc, null);
}
public void update(ChartCollection cc, String creatTime) {
if (cc == null) { if (cc == null) {
return; return;
} }
@ -130,6 +136,11 @@ public class ChartTypePane extends ChartCommonWizardPane {
try { try {
chart4Update = (ChartProvider) chart.clone(); chart4Update = (ChartProvider) chart.clone();
cc.addChart(chart4Update); cc.addChart(chart4Update);
//记录埋点
if (chart4Update instanceof VanChart) {
VanChart vanChart = (VanChart) chart4Update;
ChartInfoCollector.getInstance().collection(vanChart.getUuid(), vanChart.getID(), creatTime);
}
} catch (CloneNotSupportedException ex) { } catch (CloneNotSupportedException ex) {
FineLoggerFactory.getLogger().error(ex.getMessage(), ex); FineLoggerFactory.getLogger().error(ex.getMessage(), ex);
} }

14
designer-chart/src/main/java/com/fr/design/mainframe/chart/ChartEditPane.java

@ -6,6 +6,7 @@ import com.fr.chart.chartattr.ChartCollection;
import com.fr.chartx.attr.ChartProvider; import com.fr.chartx.attr.ChartProvider;
import com.fr.design.ChartTypeInterfaceManager; import com.fr.design.ChartTypeInterfaceManager;
import com.fr.design.beans.FurtherBasicBeanPane; import com.fr.design.beans.FurtherBasicBeanPane;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.design.data.DesignTableDataManager; import com.fr.design.data.DesignTableDataManager;
import com.fr.design.data.tabledata.Prepare4DataSourceChange; import com.fr.design.data.tabledata.Prepare4DataSourceChange;
import com.fr.design.dialog.BasicPane; import com.fr.design.dialog.BasicPane;
@ -26,11 +27,11 @@ import com.fr.plugin.chart.vanchart.VanChart;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener; import javax.swing.event.ChangeListener;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.List; import java.util.List;
import java.awt.BorderLayout;
import java.awt.CardLayout;
public class ChartEditPane extends BasicPane implements AttributeChange, Prepare4DataSourceChange, ChartEditPaneProvider { public class ChartEditPane extends BasicPane implements AttributeChange, Prepare4DataSourceChange, ChartEditPaneProvider {
@ -107,11 +108,15 @@ public class ChartEditPane extends BasicPane implements AttributeChange,Prepare4
return; return;
} }
AbstractChartAttrPane selectedPane = paneList.get(tabsHeaderIconPane.getSelectedIndex()); AbstractChartAttrPane selectedPane = paneList.get(tabsHeaderIconPane.getSelectedIndex());
//图表配置变化,埋点记录
VanChart vanChart = collection.getSelectedChartProvider(VanChart.class);
if (vanChart != null) {
ChartInfoCollector.getInstance().updateChartPropertyTime(vanChart.getUuid(), vanChart.getID());
}
selectedPane.update(collection); selectedPane.update(collection);
if (!ComparatorUtils.equals(collection, lastCollection)) { if (!ComparatorUtils.equals(collection, lastCollection)) {
VanChart vanChart = collection.getSelectedChartProvider(VanChart.class);
if (vanChart != null) { if (vanChart != null) {
//此处画图 //此处画图
vanChart.demoImgEvent(true); vanChart.demoImgEvent(true);
@ -138,6 +143,7 @@ public class ChartEditPane extends BasicPane implements AttributeChange,Prepare4
/** /**
* 重新构造面板 * 重新构造面板
*
* @param currentChart 图表 * @param currentChart 图表
*/ */
public void reLayout(ChartProvider currentChart) { public void reLayout(ChartProvider currentChart) {
@ -292,6 +298,7 @@ public class ChartEditPane extends BasicPane implements AttributeChange,Prepare4
/** /**
* 当前界面是否是默认的界面 * 当前界面是否是默认的界面
*
* @return 是否是默认的界面 * @return 是否是默认的界面
*/ */
public boolean isDefaultPane() { public boolean isDefaultPane() {
@ -332,6 +339,7 @@ public class ChartEditPane extends BasicPane implements AttributeChange,Prepare4
/** /**
* 主要用于图表设计器判断样式改变是否来自工具栏的全局样式按钮 * 主要用于图表设计器判断样式改变是否来自工具栏的全局样式按钮
*
* @param isFromToolBar 是否来自工具栏 * @param isFromToolBar 是否来自工具栏
*/ */
public void styleChange(boolean isFromToolBar) { public void styleChange(boolean isFromToolBar) {

11
designer-chart/src/main/java/com/fr/design/mainframe/chart/gui/ChartTypeButtonPane.java

@ -6,6 +6,7 @@ import com.fr.chart.chartattr.ChartCollection;
import com.fr.chart.charttypes.ChartTypeManager; import com.fr.chart.charttypes.ChartTypeManager;
import com.fr.chartx.attr.ChartProvider; import com.fr.chartx.attr.ChartProvider;
import com.fr.design.beans.BasicBeanPane; import com.fr.design.beans.BasicBeanPane;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.design.dialog.DialogActionListener; import com.fr.design.dialog.DialogActionListener;
import com.fr.design.dialog.UIDialog; import com.fr.design.dialog.UIDialog;
import com.fr.design.event.UIObserver; import com.fr.design.event.UIObserver;
@ -25,6 +26,9 @@ import javax.swing.BorderFactory;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.FlowLayout; import java.awt.FlowLayout;
@ -41,9 +45,6 @@ import java.awt.event.MouseEvent;
import java.awt.event.MouseListener; import java.awt.event.MouseListener;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/** /**
* 图表 类型 增删 控制按钮界面. * 图表 类型 增删 控制按钮界面.
@ -151,6 +152,10 @@ public class ChartTypeButtonPane extends BasicBeanPane<ChartCollection> implemen
try { try {
ChartProvider newChart = (ChartProvider) chart.clone(); ChartProvider newChart = (ChartProvider) chart.clone();
editingCollection.addNamedChart(name, newChart); editingCollection.addNamedChart(name, newChart);
if (newChart instanceof VanChart) {
VanChart vanchart = (VanChart) newChart;
ChartInfoCollector.getInstance().collection(vanchart.getUuid(), vanchart.getID(), null);
}
} catch (CloneNotSupportedException e1) { } catch (CloneNotSupportedException e1) {
FineLoggerFactory.getLogger().error("Error in Clone"); FineLoggerFactory.getLogger().error("Error in Clone");
} }

5
designer-chart/src/main/java/com/fr/van/chart/designer/type/AbstractVanChartTypePane.java

@ -12,6 +12,7 @@ import com.fr.design.gui.icheckbox.UICheckBox;
import com.fr.design.gui.ilable.MultilineLabel; import com.fr.design.gui.ilable.MultilineLabel;
import com.fr.design.mainframe.chart.gui.type.AbstractChartTypePane; import com.fr.design.mainframe.chart.gui.type.AbstractChartTypePane;
import com.fr.design.mainframe.chart.gui.type.ChartImagePane; import com.fr.design.mainframe.chart.gui.type.ChartImagePane;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.general.Background; import com.fr.general.Background;
import com.fr.js.NameJavaScriptGroup; import com.fr.js.NameJavaScriptGroup;
import com.fr.log.FineLoggerFactory; import com.fr.log.FineLoggerFactory;
@ -105,6 +106,10 @@ public abstract class AbstractVanChartTypePane extends AbstractChartTypePane<Cha
resetChartAttr(chart, newPlot); resetChartAttr(chart, newPlot);
//切换图表时,数据配置不变,分类个数也不变 //切换图表时,数据配置不变,分类个数也不变
newPlot.setCategoryNum(oldPlot.getCategoryNum()); newPlot.setCategoryNum(oldPlot.getCategoryNum());
if(chart instanceof VanChart){
VanChart vanChart = (VanChart) chart;
ChartInfoCollector.getInstance().updateChartTypeTime(vanChart.getUuid(), vanChart.getID());
}
} }
} }

36
designer-form/src/main/java/com/fr/design/mainframe/FormCreatorDropTarget.java

@ -1,6 +1,8 @@
package com.fr.design.mainframe; package com.fr.design.mainframe;
import com.fr.base.BaseUtils; import com.fr.base.BaseUtils;
import com.fr.base.chart.BaseChartCollection;
import com.fr.chart.chartattr.ChartCollection;
import com.fr.design.DesignModelAdapter; import com.fr.design.DesignModelAdapter;
import com.fr.design.data.datapane.TableDataTreePane; import com.fr.design.data.datapane.TableDataTreePane;
import com.fr.design.designer.beans.AdapterBus; import com.fr.design.designer.beans.AdapterBus;
@ -8,25 +10,37 @@ import com.fr.design.designer.beans.HoverPainter;
import com.fr.design.designer.beans.Painter; import com.fr.design.designer.beans.Painter;
import com.fr.design.designer.beans.events.DesignerEvent; import com.fr.design.designer.beans.events.DesignerEvent;
import com.fr.design.designer.beans.models.AddingModel; import com.fr.design.designer.beans.models.AddingModel;
import com.fr.design.designer.creator.*; import com.fr.design.designer.creator.XCreator;
import com.fr.design.designer.creator.XCreatorUtils;
import com.fr.design.designer.creator.XLayoutContainer;
import com.fr.design.designer.creator.XWAbsoluteLayout;
import com.fr.design.designer.creator.XWFitLayout;
import com.fr.design.designer.creator.XWParameterLayout;
import com.fr.design.form.util.XCreatorConstants; import com.fr.design.form.util.XCreatorConstants;
import com.fr.design.gui.ibutton.UIButton; import com.fr.design.gui.ibutton.UIButton;
import com.fr.design.icon.IconPathConstants; import com.fr.design.icon.IconPathConstants;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.design.utils.ComponentUtils; import com.fr.design.utils.ComponentUtils;
import com.fr.form.share.SharableEditorProvider; import com.fr.form.share.SharableEditorProvider;
import com.fr.form.share.ShareLoader; import com.fr.form.share.ShareLoader;
import com.fr.form.ui.ChartEditor;
import com.fr.form.ui.SharableWidgetBindInfo; import com.fr.form.ui.SharableWidgetBindInfo;
import com.fr.form.ui.Widget; import com.fr.form.ui.Widget;
import com.fr.plugin.chart.vanchart.VanChart;
import com.fr.stable.Constants; import com.fr.stable.Constants;
import javax.swing.*; import javax.swing.BorderFactory;
import java.awt.*; import javax.swing.JWindow;
import java.util.Map;
import java.awt.Color;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.dnd.DropTarget; import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetEvent;
import java.util.Map;
/** /**
* 添加模式下鼠标事件处理器 * 添加模式下鼠标事件处理器
@ -81,6 +95,8 @@ public class FormCreatorDropTarget extends DropTarget {
//tab布局添加的时候是初始化了XWCardLayout,实际上最顶层的布局是XWCardMainBorderLayout //tab布局添加的时候是初始化了XWCardLayout,实际上最顶层的布局是XWCardMainBorderLayout
XCreator addingXCreator = addingModel.getXCreator(); XCreator addingXCreator = addingModel.getXCreator();
Widget widget = (addingXCreator.getTopLayout() != null) ? (addingXCreator.getTopLayout().toData()) : addingXCreator.toData(); Widget widget = (addingXCreator.getTopLayout() != null) ? (addingXCreator.getTopLayout().toData()) : addingXCreator.toData();
//图表埋点
dealChartBuryingPoint(widget);
if (addingXCreator.isShared()) { if (addingXCreator.isShared()) {
String shareId = addingXCreator.getShareId(); String shareId = addingXCreator.getShareId();
SharableEditorProvider sharableEditor = ShareLoader.getLoader().getSharedElCaseEditorById(shareId); SharableEditorProvider sharableEditor = ShareLoader.getLoader().getSharedElCaseEditorById(shareId);
@ -266,4 +282,14 @@ public class FormCreatorDropTarget extends DropTarget {
//针对在表单中拖入一个控件直接ctrl+s无反应 //针对在表单中拖入一个控件直接ctrl+s无反应
designer.requestFocus(); designer.requestFocus();
} }
private void dealChartBuryingPoint(Widget widget) {
if (widget instanceof ChartEditor) {
BaseChartCollection chartCollection = ((ChartEditor) widget).getChartCollection();
VanChart vanChart = ((ChartCollection) chartCollection).getSelectedChartProvider(VanChart.class);
if (vanChart != null) {
ChartInfoCollector.getInstance().collection(vanChart.getUuid(), vanChart.getID(), null);
}
}
}
} }

2
designer-realize/src/main/java/com/fr/design/mainframe/InformationCollector.java

@ -7,6 +7,7 @@ import com.fr.base.FRContext;
import com.fr.concurrent.NamedThreadFactory; import com.fr.concurrent.NamedThreadFactory;
import com.fr.config.MarketConfig; import com.fr.config.MarketConfig;
import com.fr.design.DesignerEnvManager; import com.fr.design.DesignerEnvManager;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.design.mainframe.errorinfo.ErrorInfoUploader; import com.fr.design.mainframe.errorinfo.ErrorInfoUploader;
import com.fr.design.mainframe.messagecollect.impl.FocusPointMessageUploader; import com.fr.design.mainframe.messagecollect.impl.FocusPointMessageUploader;
import com.fr.design.mainframe.messagecollect.solid.SolidCollector; import com.fr.design.mainframe.messagecollect.solid.SolidCollector;
@ -235,6 +236,7 @@ public class InformationCollector implements XMLReadable, XMLWriter {
sendUserInfo(); sendUserInfo();
FocusPointMessageUploader.getInstance().sendToCloudCenter(); FocusPointMessageUploader.getInstance().sendToCloudCenter();
TemplateInfoCollector.getInstance().sendTemplateInfo(); TemplateInfoCollector.getInstance().sendTemplateInfo();
ChartInfoCollector.getInstance().sendChartInfo();
ErrorInfoUploader.getInstance().sendErrorInfo(); ErrorInfoUploader.getInstance().sendErrorInfo();
} }
}, SEND_DELAY, TimeUnit.MILLISECONDS); }, SEND_DELAY, TimeUnit.MILLISECONDS);

18
designer-realize/src/main/java/com/fr/poly/hanlder/PolyDesignerDropTarget.java

@ -6,13 +6,18 @@ package com.fr.poly.hanlder;
import com.fr.base.ScreenResolution; import com.fr.base.ScreenResolution;
import com.fr.base.chart.BaseChartCollection; import com.fr.base.chart.BaseChartCollection;
import com.fr.base.vcs.DesignerMode; import com.fr.base.vcs.DesignerMode;
import com.fr.chart.chartattr.ChartCollection;
import com.fr.design.mainframe.DesignerContext; import com.fr.design.mainframe.DesignerContext;
import com.fr.design.mainframe.chart.info.ChartInfoCollector;
import com.fr.grid.Grid; import com.fr.grid.Grid;
import com.fr.log.FineLoggerFactory; import com.fr.log.FineLoggerFactory;
import com.fr.plugin.chart.vanchart.VanChart;
import com.fr.poly.PolyDesigner; import com.fr.poly.PolyDesigner;
import com.fr.poly.PolyUtils; import com.fr.poly.PolyUtils;
import com.fr.poly.creator.BlockCreator; import com.fr.poly.creator.BlockCreator;
import com.fr.poly.creator.ChartBlockCreator;
import com.fr.poly.model.AddingData; import com.fr.poly.model.AddingData;
import com.fr.report.poly.PolyChartBlock;
import com.fr.stable.unit.UnitRectangle; import com.fr.stable.unit.UnitRectangle;
import javax.swing.JScrollBar; import javax.swing.JScrollBar;
@ -164,6 +169,8 @@ public class PolyDesignerDropTarget extends DropTargetAdapter {
} }
designer.addBlockCreator(creator); designer.addBlockCreator(creator);
//图表埋点
dealChartBuryingPoint(creator);
designer.stopEditing(); designer.stopEditing();
designer.setSelection(creator); designer.setSelection(creator);
//在重新设置了选择之后,要对菜单和工具进行target的重新设置 //在重新设置了选择之后,要对菜单和工具进行target的重新设置
@ -229,4 +236,15 @@ public class PolyDesignerDropTarget extends DropTargetAdapter {
forbiddenWindow.hideWindow(); forbiddenWindow.hideWindow();
} }
private void dealChartBuryingPoint(BlockCreator creator) {
if (creator instanceof ChartBlockCreator) {
PolyChartBlock value = ((ChartBlockCreator) creator).getValue();
ChartCollection chartCollection = (ChartCollection) value.getChartCollection();
VanChart vanChart = chartCollection.getSelectedChartProvider(VanChart.class);
if (vanChart != null) {
ChartInfoCollector.getInstance().collection(vanChart.getUuid(), vanChart.getID(), null);
}
}
}
} }
Loading…
Cancel
Save