forked from github/dataease
Merge pull request #6741 from dataease/pr@dev-v2@refactor_template
refactor: 优化模版样式应用逻辑等
This commit is contained in:
commit
08cc003980
@ -64,7 +64,7 @@ public class SysParameterManage {
|
||||
|
||||
public Map<String,String> groupVal(String groupKey) {
|
||||
QueryWrapper<CoreSysSetting> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.likeLeft("pkey", groupKey);
|
||||
queryWrapper.like("pkey", groupKey);
|
||||
List<CoreSysSetting> sysSettings = coreSysSettingMapper.selectList(queryWrapper);
|
||||
if (!CollectionUtils.isEmpty(sysSettings)) {
|
||||
return sysSettings.stream().collect(Collectors.toMap(CoreSysSetting::getPkey, CoreSysSetting::getPval));
|
||||
|
@ -5,12 +5,15 @@ 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.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;
|
||||
@ -57,14 +60,13 @@ public class TemplateMarketManage {
|
||||
return HttpClientUtil.get(url, config);
|
||||
}
|
||||
|
||||
public MarketBaseResponse searchTemplate(TemplateMarketSearchRequest request) {
|
||||
public MarketBaseResponse searchTemplate() {
|
||||
try {
|
||||
Map<String,String> templateParams = sysParameterManage.groupVal("template.");
|
||||
String result = marketGet(templateParams.get("template.url") + POSTS_API, templateParams.get("template.accessKey"));
|
||||
TypeReference<List<TemplateMarketDTO>> market = new TypeReference<>() {
|
||||
};
|
||||
List<TemplateMarketDTO> postsResult = JsonUtil.parseList(result,market);
|
||||
return new MarketBaseResponse(templateParams.get("template.url"), postsResult);
|
||||
MarketTemplateBaseResponse postsResult = JsonUtil.parseObject(result, MarketTemplateBaseResponse.class);
|
||||
MarketBaseResponse response = new MarketBaseResponse(templateParams.get("template.url"), postsResult.getData().getContent());
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
DEException.throwException(e);
|
||||
}
|
||||
@ -74,9 +76,8 @@ public class TemplateMarketManage {
|
||||
public List<String> getCategories() {
|
||||
Map<String,String> templateParams = sysParameterManage.groupVal("template.");
|
||||
String resultStr = marketGet(templateParams.get("template.url") + CATEGORIES_API, templateParams.get("template.accessKey"));
|
||||
TypeReference<List<TemplateCategoryVO>> market = new TypeReference<>() {
|
||||
};
|
||||
List<TemplateCategoryVO> categories = JsonUtil.parseList(resultStr,market);
|
||||
MarketCategoryBaseResponse categoryBaseResponse = JsonUtil.parseObject(resultStr, MarketCategoryBaseResponse.class);
|
||||
List<TemplateCategoryVO> categories = categoryBaseResponse.getData();
|
||||
if (CollectionUtils.isNotEmpty(categories)) {
|
||||
return categories.stream().filter(item -> !"应用系列".equals(item.getName())).sorted(Comparator.comparing(TemplateCategoryVO::getPriority)).map(TemplateCategoryVO::getName).collect(Collectors.toList());
|
||||
} else {
|
||||
|
@ -0,0 +1,31 @@
|
||||
package io.dataease.template.service;
|
||||
|
||||
import io.dataease.api.template.TemplateMarketApi;
|
||||
import io.dataease.api.template.response.MarketBaseResponse;
|
||||
import io.dataease.template.manage.TemplateMarketManage;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author : WangJiaHao
|
||||
* @date : 2023/11/17 13:20
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/templateMarket")
|
||||
public class TemplateMarketService implements TemplateMarketApi {
|
||||
|
||||
@Resource
|
||||
private TemplateMarketManage templateMarketManage;
|
||||
@Override
|
||||
public MarketBaseResponse searchTemplate() {
|
||||
return templateMarketManage.searchTemplate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> categories() {
|
||||
return templateMarketManage.getCategories();
|
||||
}
|
||||
}
|
13
core/core-frontend/src/api/templateMarket.ts
Normal file
13
core/core-frontend/src/api/templateMarket.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export function searchMarket() {
|
||||
return request.get({
|
||||
url: '/templateMarket/search'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCategories() {
|
||||
return request.get({
|
||||
url: '/templateMarket/categories'
|
||||
})
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export function searchMarket(data) {
|
||||
return request.post({
|
||||
url: '/template/market/search',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getCategories() {
|
||||
return request.post({
|
||||
url: '/template/market/categories'
|
||||
})
|
||||
}
|
@ -0,0 +1,483 @@
|
||||
<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>
|
||||
<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' : ''"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
</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'
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
previewId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['templateApply', 'closeDialog', 'closePreview'])
|
||||
|
||||
const state = reactive({
|
||||
hasResult: true,
|
||||
extFilterActive: false,
|
||||
asideActive: true,
|
||||
previewVisible: false,
|
||||
templatePreviewUrl: null,
|
||||
marketTabs: null,
|
||||
marketActiveTab: null,
|
||||
searchText: null,
|
||||
panelGroupList: [],
|
||||
curApplyTemplate: null,
|
||||
folderSelectShow: false,
|
||||
baseUrl: 'https://dataease.io/templates',
|
||||
currentMarketTemplateShowList: [],
|
||||
networkStatus: true,
|
||||
curTemplate: null
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.marketActiveTab,
|
||||
value => {
|
||||
initTemplateShow()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.searchText,
|
||||
value => {
|
||||
state.currentMarketTemplateShowList.forEach(template => {
|
||||
if (value === template.id) {
|
||||
previewTemplate(template)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const initMarketTemplate = () => {
|
||||
searchMarket()
|
||||
.then(rsp => {
|
||||
state.baseUrl = rsp.data.baseUrl
|
||||
state.currentMarketTemplateShowList = rsp.data.contents
|
||||
state.hasResult = true
|
||||
})
|
||||
.catch(() => {
|
||||
state.networkStatus = false
|
||||
})
|
||||
getCategories()
|
||||
.then(rsp => {
|
||||
state.marketTabs = rsp.data
|
||||
state.marketActiveTab = state.marketTabs[0]
|
||||
})
|
||||
.catch(() => {
|
||||
state.networkStatus = false
|
||||
})
|
||||
if (props.previewId) {
|
||||
state.currentMarketTemplateShowList.forEach(template => {
|
||||
if (props.previewId === template.id) {
|
||||
previewTemplate(template)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getGroupTree = () => {
|
||||
// do getGroupTree
|
||||
}
|
||||
|
||||
const normalizer = node => {
|
||||
// 去掉children=null的属性
|
||||
if (node.children === null || node.children === 'null') {
|
||||
delete node.children
|
||||
}
|
||||
}
|
||||
|
||||
const templateApply = template => {
|
||||
emits('templateApply', template)
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
emits('closeDialog')
|
||||
}
|
||||
|
||||
const handleClick = item => {
|
||||
//handleClick
|
||||
}
|
||||
|
||||
const initTemplateShow = () => {
|
||||
state.hasResult = false
|
||||
state.currentMarketTemplateShowList.forEach(template => {
|
||||
template.showFlag = templateShow(template)
|
||||
if (template.showFlag) {
|
||||
state.hasResult = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 previewTemplate = template => {
|
||||
state.curTemplate = template
|
||||
if (template.thumbnail.indexOf('http') > -1) {
|
||||
state.templatePreviewUrl = template.thumbnail
|
||||
} else {
|
||||
state.templatePreviewUrl = state.baseUrl + template.thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
const asideActiveChange = prop => {
|
||||
state.asideActive = prop
|
||||
}
|
||||
|
||||
const extFilterActiveChange = () => {
|
||||
state.extFilterActive = !state.extFilterActive
|
||||
state.marketActiveTab = state.marketTabs[0]
|
||||
}
|
||||
const closePreview = () => {
|
||||
emits('closePreview')
|
||||
}
|
||||
|
||||
const active = template => {
|
||||
return state.curTemplate && state.curTemplate.id === template.id
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initMarketTemplate()
|
||||
getGroupTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.aside-list {
|
||||
padding: 0px 12px 12px 12px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.aside-list-filter-active {
|
||||
height: calc(100vh - 250px);
|
||||
}
|
||||
|
||||
.template-main {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 2px 0 rgba(31, 31, 31, 0.15), 0 1px 2px 0 rgba(31, 31, 31, 0.15);
|
||||
border: solid 2px #fff;
|
||||
padding-bottom: 24px;
|
||||
min-height: calc(100vh - 190px);
|
||||
}
|
||||
|
||||
.market-main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.title-left {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
float: right;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.dialog-footer-self {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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: 70vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
flex-flow: row nowrap;
|
||||
color: #646a73;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.aside-active {
|
||||
width: 206px;
|
||||
height: calc(100vh - 56px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: var(--ContentBG, #ffffff);
|
||||
}
|
||||
|
||||
.aside-inActive {
|
||||
position: relative;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.main-area-active {
|
||||
width: calc(100% - 206px) !important;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
height: calc(100vh - 56px);
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.title-name-search {
|
||||
width: 140px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.icon20 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: var(--TextPrimary, #1f2329);
|
||||
}
|
||||
|
||||
.insert-filter {
|
||||
display: inline-block;
|
||||
font-weight: 400 !important;
|
||||
font-family: PingFang SC;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
color: var(--TextPrimary, #1f2329);
|
||||
-webkit-appearance: none;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
margin: 0;
|
||||
transition: 0.1s;
|
||||
border-radius: 3px;
|
||||
|
||||
&:active {
|
||||
color: #000;
|
||||
border-color: #3a8ee6;
|
||||
background-color: red;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(31, 35, 41, 0.1);
|
||||
color: #3a8ee6;
|
||||
}
|
||||
}
|
||||
|
||||
.insert {
|
||||
display: inline-block;
|
||||
font-weight: 400 !important;
|
||||
font-family: PingFang SC;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
color: #646a73;
|
||||
-webkit-appearance: none;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
margin: 0;
|
||||
transition: 0.1s;
|
||||
border-radius: 3px;
|
||||
|
||||
&:active {
|
||||
color: #000;
|
||||
border-color: #3a8ee6;
|
||||
background-color: red;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(31, 35, 41, 0.1);
|
||||
color: #3a8ee6;
|
||||
}
|
||||
}
|
||||
|
||||
.template-title {
|
||||
float: left;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
margin-bottom: 24px;
|
||||
color: var(--TextPrimary, #1f2329);
|
||||
}
|
||||
|
||||
.margin-top16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.img-main {
|
||||
border-radius: 4px;
|
||||
background: #0f1114;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: calc(100% - 50px) !important;
|
||||
}
|
||||
.open-button {
|
||||
cursor: pointer;
|
||||
font-size: 30px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 16px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
//.open-button:hover{
|
||||
// transition: 0.5s;
|
||||
// width: 50px;
|
||||
//}
|
||||
.open-button:hover {
|
||||
color: #3a8ee6;
|
||||
}
|
||||
.filter-icon-span {
|
||||
float: left;
|
||||
border: 1px solid #dcdfe6;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
padding: 7px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.filter-icon-active {
|
||||
border: 1px solid #3370ff;
|
||||
color: #3370ff;
|
||||
}
|
||||
|
||||
.filter-icon-active {
|
||||
border: 1px solid #3370ff;
|
||||
color: #3370ff;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="testcase-template">
|
||||
<div class="template-img" :style="classBackground" @click.stop="templatePreview" />
|
||||
<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">{{
|
||||
t('visualization.preview')
|
||||
}}</el-button>
|
||||
<el-button size="mini" style="width: 141px" type="primary" @click="apply">{{
|
||||
t('visualization.apply')
|
||||
}}</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { computed } from 'vue'
|
||||
import { imgUrlTrans } from '@/utils/imgUtils'
|
||||
const { t } = useI18n()
|
||||
|
||||
const emits = defineEmits(['templateApply', 'templatePreview'])
|
||||
|
||||
const props = defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
baseUrl: {
|
||||
type: String
|
||||
},
|
||||
width: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
|
||||
const classBackground = computed(() => {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
height: props.width * 0.58 + 'px',
|
||||
background: `url(${imgUrlTrans(thumbnailUrl.value)}) no-repeat`,
|
||||
'background-size': `100% 100%`
|
||||
}
|
||||
})
|
||||
|
||||
const thumbnailUrl = computed(() => {
|
||||
if (props.template.thumbnail.indexOf('http') > -1) {
|
||||
return props.template.thumbnail
|
||||
} else {
|
||||
return props.baseUrl + props.template.thumbnail
|
||||
}
|
||||
})
|
||||
|
||||
const apply = () => {
|
||||
emits('templateApply', props.template)
|
||||
}
|
||||
|
||||
const templatePreview = () => {
|
||||
emits('templatePreview', props.template.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.testcase-template {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
box-shadow: 0 0 2px 0 rgba(31, 31, 31, 0.15), 0 1px 2px 0 rgba(31, 31, 31, 0.15);
|
||||
border: solid 2px #fff;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.demonstration {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
margin-left: 12px;
|
||||
margin-top: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--TextPrimary, #1f2329);
|
||||
}
|
||||
|
||||
.template-img {
|
||||
background-size: 100% 100%;
|
||||
margin: 0 auto;
|
||||
border: solid 2px #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.template-img:hover {
|
||||
border: solid 1px #4b8fdf;
|
||||
border-radius: 4px;
|
||||
color: deepskyblue;
|
||||
cursor: pointer;
|
||||
}
|
||||
.testcase-template:hover ::v-deep .template-button {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.template-button {
|
||||
display: none;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bottom-area {
|
||||
height: 75px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
{
|
||||
['template-item-main-active']: active
|
||||
},
|
||||
'template-item-main'
|
||||
]"
|
||||
@click.stop="previewTemplate"
|
||||
>
|
||||
<div class="template-item-img" :style="classBackground" />
|
||||
<span class="demonstration">{{ template.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { imgUrlTrans } from '@/utils/imgUtils'
|
||||
import { computed } from 'vue'
|
||||
const emits = defineEmits(['previewTemplate'])
|
||||
|
||||
const props = defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
baseUrl: {
|
||||
type: String
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const classBackground = computed(() => {
|
||||
return {
|
||||
background: `url(${imgUrlTrans(thumbnailUrl.value)}) no-repeat`,
|
||||
'background-size': `100% 100%`
|
||||
}
|
||||
})
|
||||
|
||||
const thumbnailUrl = computed(() => {
|
||||
if (props.template.thumbnail.indexOf('http') > -1) {
|
||||
return props.template.thumbnail
|
||||
} else {
|
||||
return props.baseUrl + props.template.thumbnail
|
||||
}
|
||||
})
|
||||
|
||||
const previewTemplate = () => {
|
||||
emits('previewTemplate', props.template)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.template-item-main {
|
||||
margin: 0 0 12px 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 182px;
|
||||
height: 116px;
|
||||
background-color: var(--ContentBG, #ffffff);
|
||||
border: 1px solid #dee0e3;
|
||||
border-radius: 4px;
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-item-main-active {
|
||||
border: 2px solid #3370ff !important;
|
||||
}
|
||||
.template-item-img {
|
||||
position: absolute;
|
||||
width: 182px;
|
||||
height: 86px;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.demonstration {
|
||||
position: absolute;
|
||||
width: 166px;
|
||||
height: 20px;
|
||||
left: 8px;
|
||||
top: 91px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.template-item-main:hover {
|
||||
border: solid 1px #3370ff;
|
||||
}
|
||||
</style>
|
414
core/core-frontend/src/views/template-market/market.vue
Normal file
414
core/core-frontend/src/views/template-market/market.vue
Normal file
@ -0,0 +1,414 @@
|
||||
<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>
|
@ -14,8 +14,8 @@ import java.util.List;
|
||||
*/
|
||||
public interface TemplateMarketApi {
|
||||
|
||||
@PostMapping("/search")
|
||||
MarketBaseResponse searchTemplate(@RequestBody TemplateMarketSearchRequest request);
|
||||
@GetMapping("/search")
|
||||
MarketBaseResponse searchTemplate();
|
||||
|
||||
@GetMapping("/categories")
|
||||
List<String> categories();
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.dataease.api.template.response;
|
||||
|
||||
import io.dataease.api.template.dto.TemplateMarketDTO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -8,6 +9,7 @@ import java.util.List;
|
||||
* @author : WangJiaHao
|
||||
* @date : 2023/11/6 17:43
|
||||
*/
|
||||
@Data
|
||||
public class MarketBaseResponse {
|
||||
private String baseUrl;
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
package io.dataease.api.template.response;
|
||||
|
||||
import io.dataease.api.template.vo.TemplateCategoryVO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Author: wangjiahao
|
||||
* Date: 2022/7/15
|
||||
* Description:
|
||||
*/
|
||||
@Data
|
||||
public class MarketCategoryBaseResponse {
|
||||
private List<TemplateCategoryVO> data;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package io.dataease.api.template.response;
|
||||
|
||||
import io.dataease.api.template.dto.TemplateMarketDTO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author : WangJiaHao
|
||||
* @date : 2023/11/17 13:41
|
||||
*/
|
||||
@Data
|
||||
public class MarketTemplateBaseResponse {
|
||||
|
||||
private MarketTemplateInnerResult data;
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package io.dataease.api.template.response;
|
||||
|
||||
import io.dataease.api.template.dto.TemplateMarketDTO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author : WangJiaHao
|
||||
* @date : 2023/11/17 13:41
|
||||
*/
|
||||
@Data
|
||||
public class MarketTemplateInnerResult {
|
||||
|
||||
private List<TemplateMarketDTO> content;
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user