refactor: 对接V2模版市场正式环境优化

This commit is contained in:
wangjiahao 2023-11-20 19:51:24 +08:00
parent 4b14332163
commit 372b747bd2
21 changed files with 719 additions and 534 deletions

View File

@ -1,24 +1,22 @@
package io.dataease.template.manage;
import com.fasterxml.jackson.core.type.TypeReference;
import io.dataease.api.template.dto.TemplateManageFileDTO;
import io.dataease.api.template.dto.TemplateMarketDTO;
import io.dataease.api.template.request.TemplateMarketSearchRequest;
import io.dataease.api.template.response.MarketBaseResponse;
import io.dataease.api.template.response.MarketCategoryBaseResponse;
import io.dataease.api.template.response.MarketTemplateBaseResponse;
import io.dataease.api.template.response.*;
import io.dataease.api.template.vo.MarketApplicationSpecVO;
import io.dataease.api.template.vo.MarketMetaDataVO;
import io.dataease.api.template.vo.TemplateCategoryVO;
import io.dataease.exception.DEException;
import io.dataease.system.manage.SysParameterManage;
import io.dataease.utils.HttpClientConfig;
import io.dataease.utils.HttpClientUtil;
import io.dataease.utils.JsonUtil;
import io.swagger.v3.core.util.Json;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@ -31,8 +29,12 @@ import java.util.stream.Collectors;
public class TemplateMarketManage {
private final static String POSTS_API = "/api/content/posts?page=0&size=2000";
private final static String POSTS_API_V2 = "/apis/api.store.halo.run/v1alpha1/applications?keyword=&priceMode=&sort=latestReleaseTimestamp%2Cdesc&type=THEME&deVersion=V2&templateType=&label=&page=1&size=2000";
private final static String CATEGORIES_API = "/api/content/categories";
private final static String TEMPLATE_META_DATA_URL = "/upload/meta_data.json";
@Resource
private SysParameterManage sysParameterManage;
@ -44,7 +46,7 @@ public class TemplateMarketManage {
if (StringUtils.isNotEmpty(templateUrl)) {
String sufUrl = sysParameterManage.groupVal("template.").get("template.url");
String templateInfo = HttpClientUtil.get(sufUrl + templateUrl, null);
return JsonUtil.parseObject(templateInfo,TemplateManageFileDTO.class);
return JsonUtil.parseObject(templateInfo, TemplateManageFileDTO.class);
} else {
return null;
}
@ -62,7 +64,29 @@ public class TemplateMarketManage {
public MarketBaseResponse searchTemplate() {
try {
Map<String,String> templateParams = sysParameterManage.groupVal("template.");
Map<String, String> templateParams = sysParameterManage.groupVal("template.");
String result = marketGet(templateParams.get("template.url") + POSTS_API_V2, null);
MarketTemplateV2BaseResponse postsResult = JsonUtil.parseObject(result, MarketTemplateV2BaseResponse.class);
return baseResponseV2Trans(postsResult, templateParams.get("template.url"));
} catch (Exception e) {
DEException.throwException(e);
}
return null;
}
private MarketBaseResponse baseResponseV2Trans(MarketTemplateV2BaseResponse v2BaseResponse, String url) {
Map<String, String> categoriesMap = getCategoriesBaseV2();
List<TemplateMarketDTO> contents = new ArrayList<>();
v2BaseResponse.getItems().stream().forEach(marketTemplateV2ItemResult -> {
MarketApplicationSpecVO spec = marketTemplateV2ItemResult.getApplication().getSpec();
contents.add(new TemplateMarketDTO(spec.getReadmeName(), spec.getDisplayName(), spec.getScreenshots().get(0).getUrl(), spec.getLinks().get(0).getUrl(), categoriesMap.get(spec.getLabel())));
});
return new MarketBaseResponse(url, contents);
}
public MarketBaseResponse searchTemplateV1() {
try {
Map<String, String> templateParams = sysParameterManage.groupVal("template.");
String result = marketGet(templateParams.get("template.url") + POSTS_API, templateParams.get("template.accessKey"));
MarketTemplateBaseResponse postsResult = JsonUtil.parseObject(result, MarketTemplateBaseResponse.class);
MarketBaseResponse response = new MarketBaseResponse(templateParams.get("template.url"), postsResult.getData().getContent());
@ -73,8 +97,8 @@ public class TemplateMarketManage {
return null;
}
public List<String> getCategories() {
Map<String,String> templateParams = sysParameterManage.groupVal("template.");
public List<String> getCategoriesV1() {
Map<String, String> templateParams = sysParameterManage.groupVal("template.");
String resultStr = marketGet(templateParams.get("template.url") + CATEGORIES_API, templateParams.get("template.accessKey"));
MarketCategoryBaseResponse categoryBaseResponse = JsonUtil.parseObject(resultStr, MarketCategoryBaseResponse.class);
List<TemplateCategoryVO> categories = categoryBaseResponse.getData();
@ -85,4 +109,23 @@ public class TemplateMarketManage {
}
}
public List<String> getCategories() {
return getCategoriesV2().stream().map(MarketMetaDataVO::getLabel)
.collect(Collectors.toList());
}
public Map<String, String> getCategoriesBaseV2() {
Map<String, String> categories = getCategoriesV2().stream()
.collect(Collectors.toMap(MarketMetaDataVO::getSlug, MarketMetaDataVO::getLabel));
return categories;
}
public List<MarketMetaDataVO> getCategoriesV2() {
Map<String, String> templateParams = sysParameterManage.groupVal("template.");
String resultStr = marketGet(templateParams.get("template.url") + TEMPLATE_META_DATA_URL, null);
MarketMetaDataBaseResponse metaData = JsonUtil.parseObject(resultStr, MarketMetaDataBaseResponse.class);
List<MarketMetaDataVO> categories = metaData.getLabels();
return categories;
}
}

View File

@ -255,8 +255,8 @@ public class DataVisualizationServer implements DataVisualizationApi {
templateData = templateFileInfo.getComponentData();
dynamicData = templateFileInfo.getDynamicData();
staticResource = templateFileInfo.getStaticResource();
name = request.getName();
dvType = request.getType();
name = templateFileInfo.getName();
dvType = templateFileInfo.getDvType();
}
// 解析动态数据
Map<String, String> dynamicDataMap = JsonUtil.parseObject(dynamicData, Map.class);

View File

@ -1,6 +1,6 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/dataease?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false
url: jdbc:mysql://localhost:3306/dataease?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
messages:

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0855 1.00006H4C3.73478 1.00006 3.48043 1.10542 3.29289 1.29295C3.10536 1.48049 3 1.73484 3 2.00006V22.0001C3 22.2653 3.10536 22.5196 3.29289 22.7072C3.48043 22.8947 3.73478 23.0001 4 23.0001H20C20.2652 23.0001 20.5196 22.8947 20.7071 22.7072C20.8946 22.5196 21 22.2653 21 22.0001V4.91556C21.0001 4.65044 20.8949 4.39614 20.7075 4.20856L17.793 1.29306C17.7001 1.20014 17.5898 1.12643 17.4684 1.07616C17.347 1.02588 17.2169 1.00002 17.0855 1.00006ZM7 8.5C7 8.22386 7.22386 8 7.5 8H16.5C16.7761 8 17 8.22386 17 8.5V9.5C17 9.77614 16.7761 10 16.5 10H7.5C7.22386 10 7 9.77614 7 9.5V8.5ZM11 12.5C11 12.2239 11.2239 12 11.5 12H16.5C16.7761 12 17 12.2239 17 12.5V17.5C17 17.7761 16.7761 18 16.5 18H11.5C11.2239 18 11 17.7761 11 17.5V12.5ZM7 12.5C7 12.2239 7.22386 12 7.5 12H8.5C8.77614 12 9 12.2239 9 12.5V17.5C9 17.7761 8.77614 18 8.5 18H7.5C7.22386 18 7 17.7761 7 17.5V12.5Z" fill="#FF8800"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -101,15 +101,19 @@ const state = reactive({
templateCreatePid: 0
})
const dvSvgType = computed(() =>
curCanvasType.value === 'dashboard' ? 'dv-dashboard-spine' : 'dv-screen-spine'
)
state.resourceTypeList = [
{
label: newResourceLabel,
svgName: curCanvasType.value === 'dashboard' ? 'dv-dashboard-spine' : 'dv-screen-spine',
label: '空白新建',
svgName: dvSvgType.value,
command: 'newLeaf'
},
{
label: '模版新建',
svgName: curCanvasType.value === 'dashboard' ? 'dv-dashboard-spine' : 'dv-screen-spine',
label: '使用模版新建',
svgName: 'dv-use-template',
command: 'newFromTemplate'
},
{
@ -247,7 +251,7 @@ const addOperation = (
window.open(baseUrl, '_blank')
}
} else if (cmd === 'newFromTemplate') {
state.templateCreatePid = data.id
state.templateCreatePid = data?.id
// newFromTemplate
resourceCreateOpt.value.optInit()
} else {
@ -337,11 +341,28 @@ defineExpose({
<Icon name="dv-new-folder" />
</el-icon>
</el-tooltip>
<el-tooltip :content="newResourceLabel" placement="top" effect="dark">
<el-icon class="custom-icon btn" @click="addOperation('newLeaf', null, 'leaf', true)">
<el-dropdown popper-class="menu-outer-dv_popper" trigger="click">
<el-icon class="custom-icon btn" @click.stop>
<Icon name="icon_file-add_outlined" />
</el-icon>
</el-tooltip>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="addOperation('newLeaf', null, 'leaf', true)">
<el-icon class="handle-icon">
<Icon :name="dvSvgType"></Icon>
</el-icon>
空白新建
</el-dropdown-item>
<el-dropdown-item @click="addOperation('newFromTemplate', null, 'leaf', true)">
<el-icon class="handle-icon">
<Icon name="dv-use-template"></Icon>
</el-icon>
使用模版新建
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<el-input
@ -459,6 +480,7 @@ defineExpose({
line-height: 24px;
}
.custom-icon {
font-size: 20px;
&.btn {
color: #3370ff;
}
@ -539,3 +561,10 @@ defineExpose({
}
}
</style>
<style lang="less">
.menu-outer-dv_popper {
width: 140px;
margin-top: -2px !important;
}
</style>

View File

@ -1,96 +1,98 @@
<template>
<el-row style="display: inherit">
<el-col :class="state.asideActive ? 'aside-active' : 'aside-inActive'">
<svg-icon
v-show="!state.asideActive"
icon-class="button_right"
class="open-button"
@click="asideActiveChange(true)"
/>
<el-row v-show="state.asideActive" style="padding: 12px 12px 0">
<el-row>
<span class="icon iconfont icon-close icon20 insert" @click="closePreview()" />
<span class="main-title">{{ t('visualization.template_preview') }}</span>
<span
style="float: right"
class="icon iconfont icon-icon_up-left_outlined insert icon20"
@click="asideActiveChange(false)"
/>
<el-row style="width: 100%">
<el-row style="display: table; width: 100%">
<el-col style="float: left" :class="state.asideActive ? 'aside-active' : 'aside-inActive'">
<el-icon v-show="!state.asideActive" class="insert" @click="asideActiveChange(true)"
><DArrowRight
/></el-icon>
<el-row v-show="state.asideActive" style="padding: 12px 12px 0">
<el-row style="align-items: center">
<el-icon class="insert" @click="closePreview()"><Close /></el-icon>
<span class="main-title">{{ t('visualization.template_preview') }}</span>
<el-icon class="insert" @click="asideActiveChange(false)"><DArrowLeft /></el-icon>
</el-row>
<el-row class="margin-top16 search-area">
<el-input
v-model="state.searchText"
size="small"
prefix-icon="Search"
class="title-name-search"
:placeholder="t('visualization.enter_template_name_tips')"
clearable="true"
/>
<el-icon
class="insert-filter filter-icon-span"
:class="state.extFilterActive ? 'filter-icon-active' : ''"
@click="extFilterActiveChange()"
><Filter
/></el-icon>
</el-row>
<el-row v-show="state.extFilterActive">
<el-select
v-model="state.marketActiveTab"
class="margin-top16"
size="small"
placeholder="请选择"
>
<el-option v-for="item in state.marketTabs" :key="item" :label="item" :value="item" />
</el-select>
</el-row>
<el-divider />
</el-row>
<el-row class="margin-top16 search-area">
<el-input
v-model="state.searchText"
size="small"
prefix-icon="el-icon-search"
class="title-name-search"
:placeholder="t('visualization.enter_template_name_tips')"
clearable="true"
/>
<span
class="icon iconfont icon-icon-filter insert-filter filter-icon-span"
:class="state.extFilterActive ? 'filter-icon-active' : ''"
@click="extFilterActiveChange()"
/>
</el-row>
<el-row v-show="state.extFilterActive">
<el-select
v-model="state.marketActiveTab"
class="margin-top16"
size="small"
placeholder="请选择"
>
<el-option v-for="item in state.marketTabs" :key="item" :label="item" :value="item" />
</el-select>
</el-row>
<el-divider />
</el-row>
<el-row
v-show="state.asideActive"
class="aside-list"
:class="state.extFilterActive ? 'aside-list-filter-active' : ''"
<el-row
v-show="state.asideActive"
class="aside-list"
:class="state.extFilterActive ? 'aside-list-filter-active' : ''"
>
<template-market-preview-item
v-for="templateItem in state.currentMarketTemplateShowList"
v-show="templateItem.showFlag"
:key="templateItem.id"
:template="templateItem"
:base-url="state.baseUrl"
:active="active(templateItem)"
@previewTemplate="previewTemplate"
/>
<el-row v-show="!state.hasResult" class="custom-position">
<div style="text-align: center">
<Icon name="no_result" style="margin-bottom: 16px; font-size: 75px"></Icon>
<br />
<span>{{ t('commons.no_result') }}</span>
</div>
</el-row>
</el-row>
</el-col>
<el-col
style="float: left"
class="main-area"
:class="state.asideActive ? 'main-area-active' : ''"
>
<template-market-preview-item
v-for="templateItem in state.currentMarketTemplateShowList"
v-show="templateItem.showFlag"
:key="templateItem.id"
:template="templateItem"
:base-url="state.baseUrl"
:active="active(templateItem)"
@previewTemplate="previewTemplate"
/>
<el-row v-show="!state.hasResult" class="custom-position">
<div style="text-align: center">
<svg-icon icon-class="no_result" style="margin-bottom: 16px; font-size: 75px" />
<br />
<span>{{ t('commons.no_result') }}</span>
<el-row>
<span v-if="state.curTemplate" class="template-title">{{ state.curTemplate.title }}</span>
<div style="flex: 1; text-align: right">
<el-button
style="float: right"
type="primary"
size="small"
@click="templateApply(state.curTemplate)"
>{{ t('visualization.apply_this_template') }}</el-button
>
</div>
</el-row>
</el-row>
</el-col>
<el-col class="main-area" :class="state.asideActive ? 'main-area-active' : ''">
<el-row>
<span v-if="state.curTemplate" class="template-title">{{ state.curTemplate.title }}</span>
<el-button
style="float: right"
type="primary"
size="small"
@click="templateApply(state.curTemplate)"
>{{ t('visualization.apply_this_template') }}</el-button
>
</el-row>
<el-row class="img-main">
<img height="100%" :src="state.templatePreviewUrl" alt="" />
</el-row>
</el-col>
<el-row class="img-main">
<img style="height: 100%" :src="state.templatePreviewUrl" alt="" />
</el-row>
</el-col>
</el-row>
</el-row>
</template>
<script setup lang="ts">
import { searchMarket, getCategories } from '@/api/templateMarket'
import TemplateMarketPreviewItem from '@/views/template-market/component/TemplateMarketPreviewItem.vue'
import { onMounted, reactive, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import TemplateMarketPreviewItem from '@/views/template-market/component/TemplateMarketPreviewItem.vue'
const { t } = useI18n()
const props = defineProps({
@ -129,6 +131,13 @@ watch(
watch(
() => state.searchText,
value => {
initTemplateShow()
}
)
watch(
() => props.previewId,
value => {
state.currentMarketTemplateShowList.forEach(template => {
if (value === template.id) {
@ -314,7 +323,7 @@ onMounted(() => {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
font-size: 14px;
flex-flow: row nowrap;
color: #646a73;
@ -356,6 +365,7 @@ onMounted(() => {
}
.main-title {
width: 135px;
margin-left: 8px;
font-weight: 500;
font-size: 16px;
@ -434,6 +444,7 @@ onMounted(() => {
margin-top: 16px;
}
.img-main {
display: inherit;
border-radius: 4px;
background: #0f1114;
overflow-x: auto;

View File

@ -1,13 +1,13 @@
<template>
<div class="testcase-template">
<div class="template-img" :style="classBackground" @click.stop="templatePreview" />
<div class="template-img" :style="classBackground" @click.stop="templateInnerPreview" />
<el-row class="bottom-area">
<el-row>
<span class="demonstration">{{ template.title }}</span>
</el-row>
</el-row>
<el-row class="template-button">
<el-button size="mini" style="width: 141px" @click="templatePreview">{{
<el-button size="mini" style="width: 141px" @click="templateInnerPreview">{{
t('visualization.preview')
}}</el-button>
<el-button size="mini" style="width: 141px" type="primary" @click="apply">{{
@ -61,7 +61,7 @@ const apply = () => {
emits('templateApply', props.template)
}
const templatePreview = () => {
const templateInnerPreview = e => {
emits('templatePreview', props.template.id)
}
</script>

View File

@ -1,35 +1,382 @@
<template>
<el-row class="custom-position template-main">
{{ t('visualization.market_network_tips') }}
<el-row class="outer-body" v-loading="state.loading">
<!--预览模式-->
<market-preview
v-show="previewModel"
:preview-id="state.templatePreviewId"
@closePreview="closePreview"
@templateApply="templateApply"
/>
<!--列表模式-->
<el-row v-show="!previewModel" class="market-main">
<el-row>
<el-col :span="12">
<span class="title-left">{{ t('visualization.template_market') }}</span>
</el-col>
<el-col :span="12">
<el-input
v-model="state.searchText"
prefix-icon="el-icon-search"
size="small"
class="title-right"
:placeholder="t('visualization.enter_template_name_tips')"
:clearable="true"
/>
</el-col>
</el-row>
<el-row class="custom-tabs-head">
<el-tabs v-model="state.marketActiveTab" @tab-click="handleClick">
<el-tab-pane
v-for="tabItem in state.marketTabs"
:key="tabItem"
:label="tabItem"
:name="tabItem"
/>
</el-tabs>
</el-row>
<el-row
v-show="state.networkStatus && state.hasResult"
id="template-main"
class="template-main"
>
<el-col
v-for="templateItem in state.currentMarketTemplateShowList"
v-show="templateItem.showFlag"
:key="templateItem.id"
style="padding: 24px 12px 0; text-align: center; flex: 0"
:style="{ width: state.templateSpan }"
>
<template-market-item
:key="'outer-' + templateItem.id"
:template="templateItem"
:base-url="state.baseUrl"
:width="state.templateCurWidth"
@templateApply="templateApply"
@templatePreview="templatePreview"
/>
</el-col>
</el-row>
<el-row
v-show="state.networkStatus && !state.hasResult"
class="custom-position template-main"
>
<div style="text-align: center">
<Icon name="no_result" style="margin-bottom: 16px; font-size: 75px"></Icon>
<br />
<span>{{ t('commons.no_result') }}</span>
</div>
</el-row>
<el-row v-show="!state.networkStatus" class="custom-position template-main">
{{ t('visualization.market_network_tips') }}
</el-row>
</el-row>
</el-row>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'
<script setup lang="ts">
import { getCategories, searchMarket } from '@/api/templateMarket'
import elementResizeDetectorMaker from 'element-resize-detector'
import { nextTick, reactive, watch, onMounted, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { ElMessage } from 'element-plus-secondary'
import MarketPreview from '@/views/template-market/component/MarketPreview.vue'
import TemplateMarketItem from '@/views/template-market/component/TemplateMarketItem.vue'
import { decompression } from '@/api/visualization/dataVisualization'
import { useCache } from '@/hooks/web/useCache'
const { t } = useI18n()
const { wsCache } = useCache()
const previewModel = ref(false)
const state = reactive({
loading: false,
hasResult: true,
templateMiniWidth: 330,
templateCurWidth: 310,
templateSpan: '25%',
previewVisible: false,
templatePreviewId: '',
marketTabs: null,
marketActiveTab: null,
searchText: null,
dvCreateForm: {
name: null,
pid: null,
nodeType: 'panel',
templateUrl: null,
newFrom: 'new_market_template',
panelType: 'self',
panelStyle: {},
panelData: '[]'
},
panelGroupList: [],
curApplyTemplate: null,
folderSelectShow: false,
baseUrl: 'https://dataease.io/templates',
currentMarketTemplateShowList: [],
networkStatus: true,
rule: {
name: [
{
required: true,
message: t('visualization.template_name_tips'),
trigger: 'blur'
}
],
pid: [
{
required: true,
message: '',
trigger: 'blur'
}
]
}
})
watch(
() => state.marketActiveTab,
value => {
initTemplateShow()
}
)
watch(
() => state.searchText,
value => {
initTemplateShow()
}
)
const closePreview = () => {
previewModel.value = false
}
const initMarketTemplate = () => {
searchMarket()
.then(rsp => {
state.baseUrl = rsp.data.baseUrl
state.currentMarketTemplateShowList = rsp.data.contents
})
.catch(() => {
state.networkStatus = false
})
getCategories()
.then(rsp => {
state.marketTabs = rsp.data
state.marketActiveTab = state.marketTabs[0]
})
.catch(() => {
state.networkStatus = false
})
}
const getGroupTree = () => {
// do getGroupTree
// groupTree({ nodeType: 'folder' }).then(res => {
// state.panelGroupList = res.data
// })
}
const normalizer = node => {
// children=null
if (node.children === null || node.children === 'null') {
delete node.children
}
}
const templateApply = template => {
state.curApplyTemplate = template
state.dvCreateForm.name = template.title
state.dvCreateForm.templateUrl = template.metas.theme_repo
apply()
}
const apply = () => {
if (!state.dvCreateForm.templateUrl) {
ElMessage.warning('未获取模板下载链接请联系模板市场官方')
return false
}
state.loading = true
decompression(state.dvCreateForm)
.then(response => {
state.loading = false
const templateData = response.data
// do create
wsCache.set(`de-template-data`, JSON.stringify(templateData))
const baseUrl =
templateData.type === 'dataV'
? '#/dvCanvas?opt=create&createType=template'
: '#/dashboard?opt=create&createType=template'
window.open(baseUrl, '_blank')
})
.catch(() => {
state.loading = false
})
}
const handleClick = item => {
// do handleClick
}
const initTemplateShow = () => {
let tempHasResult = false
state.currentMarketTemplateShowList.forEach(template => {
template.showFlag = templateShow(template)
if (template.showFlag) {
tempHasResult = true
}
})
if (state.currentMarketTemplateShowList.length > 0) {
state.hasResult = tempHasResult
}
}
const templateShow = templateItem => {
let categoryMarch = false
let searchMarch = false
templateItem.categories.forEach(category => {
if (category.name === state.marketActiveTab) {
categoryMarch = true
}
})
if (!state.searchText || templateItem.title.indexOf(state.searchText) > -1) {
searchMarch = true
}
return categoryMarch && searchMarch
}
const templatePreview = previewId => {
state.templatePreviewId = previewId
previewModel.value = true
}
const newPanel = () => {
// do newPanel
}
onMounted(() => {
initMarketTemplate()
getGroupTree()
const erd = elementResizeDetectorMaker()
const templateMainDom = document.getElementById('template-main')
// div
if (templateMainDom) {
erd.listenTo(templateMainDom, element => {
nextTick(() => {
const curSeparator = Math.trunc(templateMainDom.offsetWidth / state.templateMiniWidth)
state.templateSpan =
100 / Math.trunc(templateMainDom.offsetWidth / state.templateMiniWidth) + '%'
state.templateCurWidth = Math.trunc(templateMainDom.offsetWidth / curSeparator) - 33
})
})
}
})
</script>
<style lang="less" scoped>
.custom-position {
height: 80vh;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
flex-flow: row nowrap;
color: #646a73;
font-weight: 400;
.custom-tabs-head {
display: inherit;
padding-bottom: 15px;
::v-deep(.ed-tabs__item) {
font-weight: 400;
font-size: 14px;
}
}
.template-main {
text-align: center;
border-radius: 4px;
padding: 0 12px 24px 12px;
height: calc(100vh - 56px) !important;
height: calc(100vh - 190px) !important;
overflow-x: hidden;
overflow-y: auto;
background-color: var(--ContentBG, #ffffff);
}
.market-main {
padding: 24px;
display: inherit;
}
.title-left {
float: left;
font-size: 20px;
font-weight: 500;
line-height: 28px;
color: var(--TextPrimary, #1f2329);
}
.title-right {
float: right;
width: 320px;
}
.dialog-footer-self {
text-align: right;
}
.search-button-self {
text-align: left;
padding-left: 10px;
}
.topbar-icon-active {
cursor: pointer;
transition: 0.1s;
border-radius: 3px;
font-size: 22px;
background-color: rgb(245, 245, 245);
&:active {
color: #000;
border-color: #3a8ee6;
background-color: red;
outline: 0;
}
&:hover {
background-color: rgba(31, 35, 41, 0.1);
color: #3a8ee6;
}
}
.custom-position {
height: 80vh;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-flow: row nowrap;
color: #646a73;
font-weight: 400;
}
.outer-body {
display: inherit;
width: 100%;
height: calc(100vh - 56px);
background-color: var(--MainBG, #f5f6f7);
}
.market-dialog-css {
::v-deep(.ed-form-item__label) {
width: 100% !important;
text-align: left;
}
::v-deep(.ed-form-item.is-required:not(.is-no-asterisk) > .ed-form-item__label:before) {
display: none;
}
::v-deep(.ed-form-item.is-required:not(.is-no-asterisk) > .ed-form-item__label::after) {
content: '*';
color: #f54a45;
margin-left: 2px;
}
::v-deep(.ed-form-item__content) {
margin-left: 0 !important;
}
::v-deep(.vue-treeselect__input) {
vertical-align: middle;
}
}
</style>

View File

@ -1,414 +0,0 @@
<template>
<el-row class="outer-body">
<!--预览模式-->
<market-preview
v-show="state.previewModel"
:preview-id="state.templatePreviewId"
@closePreview="state.previewModel = false"
@templateApply="templateApply"
/>
<!--列表模式-->
<el-row v-show="!state.previewModel" class="market-main">
<el-row>
<el-col :span="12">
<span class="title-left">{{ t('visualization.template_market') }}</span>
</el-col>
<el-col :span="12">
<el-input
v-model="state.searchText"
prefix-icon="el-icon-search"
size="small"
class="title-right"
:placeholder="t('visualization.enter_template_name_tips')"
:clearable="true"
/>
</el-col>
</el-row>
<el-row style="display: inherit">
<el-tabs v-model="state.marketActiveTab" @tab-click="handleClick">
<el-tab-pane
v-for="tabItem in state.marketTabs"
:key="tabItem"
:label="tabItem"
:name="tabItem"
/>
</el-tabs>
</el-row>
<el-row
v-show="state.networkStatus && state.hasResult"
id="template-main"
class="template-main"
>
<el-col
v-for="templateItem in state.currentMarketTemplateShowList"
v-show="templateItem.showFlag"
:key="templateItem.id"
style="padding: 24px 12px 0; text-align: center"
:style="{ width: state.templateSpan }"
>
<template-market-item
:template="templateItem"
:base-url="state.baseUrl"
:width="state.templateCurWidth"
@templateApply="templateApply"
@templatePreview="templatePreview"
/>
</el-col>
</el-row>
<el-row
v-show="state.networkStatus && !state.hasResult"
class="custom-position template-main"
>
<div style="text-align: center">
<svg-icon icon-class="no_result" style="margin-bottom: 16px; font-size: 75px" />
<br />
<span>{{ t('commons.no_result') }}</span>
</div>
</el-row>
<el-row v-show="!state.networkStatus" class="custom-position template-main">
{{ t('visualization.market_network_tips') }}
</el-row>
</el-row>
<el-dialog
:title="t('visualization.apply_template')"
v-model="state.folderSelectShow"
width="600px"
class="market-dialog-css"
:append-to-body="true"
:destroy-on-close="true"
>
<el-form ref="panelForm" :model="state.panelForm" :rules="rule" label-width="80px">
<el-form-item :label="t('visualization.name')" prop="name">
<el-input
v-model="state.panelForm.name"
:clearable="true"
:placeholder="t('visualization.enter_name_tips')"
/>
</el-form-item>
<el-form-item :label="t('commons.folder')" prop="pid">
<treeselect
v-model="state.panelForm.pid"
:clearable="false"
:options="state.panelGroupList"
:normalizer="normalizer"
:placeholder="t('chart.select_group')"
:no-children-text="t('commons.treeselect.no_children_text')"
:no-options-text="t('commons.treeselect.no_options_text')"
:no-results-text="t('commons.treeselect.no_results_text')"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer dialog-footer-self">
<el-button size="mini" @click="state.folderSelectShow = false"
>{{ t('commons.cancel') }}
</el-button>
<el-button
size="mini"
type="primary"
:disabled="!state.panelForm.name || !state.panelForm.pid"
@click="apply"
>{{ t('commons.confirm') }}
</el-button>
</div>
</template>
</el-dialog>
</el-row>
</template>
<script setup lang="ts">
import { getCategories, searchMarket } from '@/api/templateMarket'
import elementResizeDetectorMaker from 'element-resize-detector'
import { nextTick, reactive, watch, onMounted } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { ElMessage } from 'element-plus-secondary'
import MarketPreview from '@/views/template-market/component/MarketPreview.vue'
import TemplateMarketItem from '@/views/template-market/component/TemplateMarketItem.vue'
const { t } = useI18n()
const emits = defineEmits(['closeDialog'])
const state = reactive({
hasResult: true,
templateMiniWidth: 330,
templateCurWidth: 310,
templateSpan: '25%',
previewModel: false,
previewVisible: false,
templatePreviewId: '',
marketTabs: null,
marketActiveTab: null,
searchText: null,
panelForm: {
name: null,
pid: null,
nodeType: 'panel',
templateUrl: null,
newFrom: 'new_market_template',
panelType: 'self',
panelStyle: {},
panelData: '[]'
},
panelGroupList: [],
curApplyTemplate: null,
folderSelectShow: false,
baseUrl: 'https://dataease.io/templates',
currentMarketTemplateShowList: [],
networkStatus: true,
rule: {
name: [
{
required: true,
message: t('visualization.template_name_tips'),
trigger: 'blur'
}
],
pid: [
{
required: true,
message: '',
trigger: 'blur'
}
]
}
})
watch(
() => state.marketActiveTab,
value => {
initTemplateShow()
}
)
watch(
() => state.searchText,
value => {
initTemplateShow()
}
)
const initMarketTemplate = () => {
searchMarket({})
.then(rsp => {
state.baseUrl = rsp.data.baseUrl
state.currentMarketTemplateShowList = rsp.data.contents
})
.catch(() => {
state.networkStatus = false
})
getCategories()
.then(rsp => {
state.marketTabs = rsp.data
state.marketActiveTab = state.marketTabs[0]
})
.catch(() => {
state.networkStatus = false
})
}
const getGroupTree = () => {
// do getGroupTree
// groupTree({ nodeType: 'folder' }).then(res => {
// state.panelGroupList = res.data
// })
}
const normalizer = node => {
// children=null
if (node.children === null || node.children === 'null') {
delete node.children
}
}
const templateApply = template => {
state.curApplyTemplate = template
state.panelForm.name = template.title
state.panelForm.templateUrl = state.baseUrl + template.metas.theme_repo
state.folderSelectShow = true
}
const apply = () => {
if (state.panelForm.name.length > 50) {
ElMessage.warning(t('commons.char_can_not_more_50'))
return false
}
if (!state.panelForm.templateUrl) {
ElMessage.warning('未获取模板下载链接请联系模板市场官方')
return false
}
// panelSave(state.panelForm)
// .then(response => {
// state.$message({
// message: state.t('commons.save_success'),
// type: 'success',
// showClose: true
// })
// state.folderSelectShow = false
// state.$router.push({ name: 'panel', params: response.data })
// })
// .catch(() => {
// state.loading = false
// })
}
const closeDialog = () => {
emits('closeDialog')
}
const handleClick = item => {
// do handleClick
}
const initTemplateShow = () => {
let tempHasResult = false
state.currentMarketTemplateShowList.forEach(template => {
template.showFlag = templateShow(template)
if (template.showFlag) {
tempHasResult = true
}
})
if (state.currentMarketTemplateShowList.length > 0) {
state.hasResult = tempHasResult
}
}
const templateShow = templateItem => {
let categoryMarch = false
let searchMarch = false
templateItem.categories.forEach(category => {
if (category.name === state.marketActiveTab) {
categoryMarch = true
}
})
if (!state.searchText || templateItem.title.indexOf(state.searchText) > -1) {
searchMarch = true
}
return categoryMarch && searchMarch
}
const templatePreview = previewId => {
state.templatePreviewId = previewId
state.previewModel = true
}
const newPanel = () => {
// do newPanel
}
onMounted(() => {
initMarketTemplate()
getGroupTree()
const erd = elementResizeDetectorMaker()
const templateMainDom = document.getElementById('template-main')
// div
if (templateMainDom) {
erd.listenTo(templateMainDom, element => {
nextTick(() => {
const curSeparator = Math.trunc(templateMainDom.offsetWidth / state.templateMiniWidth)
state.templateSpan =
100 / Math.trunc(templateMainDom.offsetWidth / state.templateMiniWidth) + '%'
state.templateCurWidth = Math.trunc(templateMainDom.offsetWidth / curSeparator) - 33
})
})
}
})
</script>
<style lang="less" scoped>
.template-main {
text-align: center;
border-radius: 4px;
padding: 0 12px 24px 12px;
height: calc(100vh - 190px) !important;
overflow-x: hidden;
overflow-y: auto;
background-color: var(--ContentBG, #ffffff);
}
.market-main {
padding: 24px;
display: inherit;
}
.title-left {
float: left;
font-size: 20px;
font-weight: 500;
line-height: 28px;
color: var(--TextPrimary, #1f2329);
}
.title-right {
float: right;
width: 320px;
}
.dialog-footer-self {
text-align: right;
}
.search-button-self {
text-align: left;
padding-left: 10px;
}
.topbar-icon-active {
cursor: pointer;
transition: 0.1s;
border-radius: 3px;
font-size: 22px;
background-color: rgb(245, 245, 245);
&:active {
color: #000;
border-color: #3a8ee6;
background-color: red;
outline: 0;
}
&:hover {
background-color: rgba(31, 35, 41, 0.1);
color: #3a8ee6;
}
}
.custom-position {
height: 80vh;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
flex-flow: row nowrap;
color: #646a73;
font-weight: 400;
}
.outer-body {
display: inherit;
width: 100%;
height: calc(100vh - 56px);
background-color: var(--MainBG, #f5f6f7);
}
.market-dialog-css {
::v-deep(.ed-form-item__label) {
width: 100% !important;
text-align: left;
}
::v-deep(.ed-form-item.is-required:not(.is-no-asterisk) > .ed-form-item__label:before) {
display: none;
}
::v-deep(.ed-form-item.is-required:not(.is-no-asterisk) > .ed-form-item__label::after) {
content: '*';
color: #f54a45;
margin-left: 2px;
}
::v-deep(.ed-form-item__content) {
margin-left: 0 !important;
}
::v-deep(.vue-treeselect__input) {
vertical-align: middle;
}
}
</style>

View File

@ -3,10 +3,15 @@ package io.dataease.api.template.dto;
import io.dataease.api.template.vo.MarketCategoryVO;
import io.dataease.api.template.vo.MarketMetasVO;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Data
@NoArgsConstructor
public class TemplateMarketDTO {
private String id;
private String title;
@ -18,4 +23,12 @@ public class TemplateMarketDTO {
private Boolean showFlag = true;
private List<MarketCategoryVO> categories;
private MarketMetasVO metas;
public TemplateMarketDTO(String id, String title,String themeRepo,String templateUrl,String categoryName) {
this.id = id;
this.title = title;
this.categories = Arrays.asList(new MarketCategoryVO(categoryName),new MarketCategoryVO("全部"));
this.metas = new MarketMetasVO(templateUrl);
this.thumbnail = themeRepo;
}
}

View File

@ -23,4 +23,6 @@ public class MarketBaseResponse {
this.contents = contents;
}
}

View File

@ -0,0 +1,21 @@
package io.dataease.api.template.response;
import io.dataease.api.template.vo.MarketMetaDataVO;
import lombok.Data;
import java.util.List;
/**
* Author: wangjiahao
* Date: 2022/7/15
* Description:
*/
@Data
public class MarketMetaDataBaseResponse {
private List<MarketMetaDataVO> deVersion;
private List<MarketMetaDataVO> templateTypes;
private List<MarketMetaDataVO> labels;
}

View File

@ -0,0 +1,16 @@
package io.dataease.api.template.response;
import lombok.Data;
import java.util.List;
/**
* @author : WangJiaHao
* @date : 2023/11/17 13:41
*/
@Data
public class MarketTemplateV2BaseResponse {
private List<MarketTemplateV2ItemResult> items;
}

View File

@ -0,0 +1,15 @@
package io.dataease.api.template.response;
import io.dataease.api.template.vo.MarketApplicationVO;
import lombok.Data;
/**
* @author : WangJiaHao
* @date : 2023/11/17 13:41
*/
@Data
public class MarketTemplateV2ItemResult {
private MarketApplicationVO application;
}

View File

@ -0,0 +1,16 @@
package io.dataease.api.template.vo;
import lombok.Data;
/**
* Author: wangjiahao
* Date: 2022/7/15
* Description:
*/
@Data
public class MarketApplicationSpecLinkVO {
private String name;
private String url;
}

View File

@ -0,0 +1,13 @@
package io.dataease.api.template.vo;
import lombok.Data;
/**
* Author: wangjiahao
* Date: 2022/7/15
* Description:
*/
@Data
public class MarketApplicationSpecScreenshotBaseVO {
private String url;
}

View File

@ -0,0 +1,30 @@
package io.dataease.api.template.vo;
import lombok.Data;
import java.util.List;
/**
* Author: wangjiahao
* Date: 2022/7/15
* Description:
*/
@Data
public class MarketApplicationSpecVO {
private String displayName;
private String type;
private String deVersion;
private String templateType;
private String label;
private String readmeName;
private List<MarketApplicationSpecScreenshotBaseVO> screenshots;
private List<MarketApplicationSpecLinkVO> links;
}

View File

@ -0,0 +1,13 @@
package io.dataease.api.template.vo;
import lombok.Data;
/**
* Author: wangjiahao
* Date: 2022/7/15
* Description:
*/
@Data
public class MarketApplicationVO {
private MarketApplicationSpecVO spec;
}

View File

@ -1,6 +1,7 @@
package io.dataease.api.template.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Author: wangjiahao
@ -8,8 +9,13 @@ import lombok.Data;
* Description:
*/
@Data
@NoArgsConstructor
public class MarketCategoryVO {
private String id;
private String name;
private String slug;
public MarketCategoryVO(String name) {
this.name = name;
}
}

View File

@ -0,0 +1,15 @@
package io.dataease.api.template.vo;
import lombok.Data;
/**
* Author: wangjiahao
* Date: 2022/7/15
* Description:
*/
@Data
public class MarketMetaDataVO {
private String slug;
private String value;
private String label;
}

View File

@ -1,6 +1,7 @@
package io.dataease.api.template.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Author: wangjiahao
@ -8,6 +9,11 @@ import lombok.Data;
* Description:
*/
@Data
@NoArgsConstructor
public class MarketMetasVO {
private String theme_repo;
public MarketMetasVO(String theme_repo) {
this.theme_repo = theme_repo;
}
}