mirror of
https://github.com/dataease/dataease.git
synced 2025-02-24 11:32:57 +08:00
feat: 系统模版管理
This commit is contained in:
parent
da62aebb89
commit
cc525bd69b
@ -92,7 +92,20 @@
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dataease</groupId>
|
||||
<artifactId>xpack-permissions</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dataease</groupId>
|
||||
<artifactId>xpack-base</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -14,18 +14,18 @@ public class MybatisPlusGenerator {
|
||||
* 第一 我嫌麻烦
|
||||
* 第二 后面配置会放到nacos读起来更麻烦了
|
||||
*/
|
||||
private static final String url = "jdbc:mysql://127.0.0.1:3306/de_standalone?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false";
|
||||
private static final String url = "jdbc:mysql://39.98.78.97:3306/dataease?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false";
|
||||
private static final String username = "root";
|
||||
private static final String password = "Password123@mysql";
|
||||
|
||||
/**
|
||||
* 业务模块例如datasource,dataset,panel等
|
||||
*/
|
||||
private static final String busi = "system";
|
||||
private static final String busi = "template";
|
||||
/**
|
||||
* 这是要生成代码的表名称
|
||||
*/
|
||||
private static final String TABLE_NAME = "core_sys_setting";
|
||||
private static final String TABLE_NAME = "visualization_template";
|
||||
|
||||
/**
|
||||
* 下面两个配置基本上不用动
|
||||
|
@ -1,6 +1,6 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://127.0.0.1:3306/de_standalone?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
|
||||
url: jdbc:mysql://39.98.78.97:3306/dataease?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false
|
||||
username: root
|
||||
password: Password123@mysql
|
||||
messages:
|
||||
|
42
core/core-frontend/src/api/template.ts
Normal file
42
core/core-frontend/src/api/template.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export function save(data) {
|
||||
return request.post({
|
||||
url: '/template/save',
|
||||
data: data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
export function templateDelete(id) {
|
||||
return request.post({
|
||||
url: '/template/delete/' + id
|
||||
})
|
||||
}
|
||||
|
||||
export function showTemplateList(data) {
|
||||
return request.post({
|
||||
url: '/template/templateList',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function findOne(id) {
|
||||
return request.get({
|
||||
url: '/template/findOne/' + id
|
||||
})
|
||||
}
|
||||
|
||||
export function find(data) {
|
||||
return request.post({
|
||||
url: '/template/find',
|
||||
data: data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
export function nameCheck(data) {
|
||||
return request.post({
|
||||
url: '/template/nameCheck',
|
||||
data: data
|
||||
})
|
||||
}
|
14
core/core-frontend/src/api/templateMarket/index.js
Normal file
14
core/core-frontend/src/api/templateMarket/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
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,209 @@
|
||||
<template>
|
||||
<div class="template-import">
|
||||
<el-form
|
||||
ref="templateImportForm"
|
||||
class="de-form-item"
|
||||
:model="state.templateInfo"
|
||||
:rules="state.templateInfoRules"
|
||||
>
|
||||
<el-form-item :label="t('system_parameter_setting.template_name')" prop="name">
|
||||
<div class="flex-template">
|
||||
<el-input v-model="state.templateInfo.name" clearable size="small" />
|
||||
<el-button style="margin-left: 10px" class="el-icon-upload2" secondary @click="goFile">{{
|
||||
t('panel.upload_template')
|
||||
}}</el-button>
|
||||
<input
|
||||
id="input"
|
||||
ref="filesRef"
|
||||
type="file"
|
||||
accept=".DET"
|
||||
hidden
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row class="preview" :style="classBackground" />
|
||||
<el-row class="de-root-class">
|
||||
<deBtn secondary @click="cancel()">{{ t('commons.cancel') }}</deBtn>
|
||||
<deBtn type="primary" @click="saveTemplate()">{{ t('commons.confirm') }}</deBtn>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { save, nameCheck, find } from '@/api/template'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { imgUrlTrans } from '@/utils/imgUtils'
|
||||
import { ElMessage } from 'element-plus-secondary'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
const emits = defineEmits(['closeEditTemplateDialog', 'refresh'])
|
||||
const { t } = useI18n()
|
||||
const filesRef = ref(null)
|
||||
const props = defineProps({
|
||||
pid: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
nameList: [],
|
||||
importTemplateInfo: {
|
||||
snapshot: ''
|
||||
},
|
||||
templateInfoRules: {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: t('commons.input_content'),
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
},
|
||||
recover: false,
|
||||
templateInfo: {
|
||||
level: '1',
|
||||
pid: props.pid,
|
||||
name: '',
|
||||
templateStyle: null,
|
||||
templateData: null,
|
||||
dynamicData: null,
|
||||
staticResource: null,
|
||||
snapshot: ''
|
||||
}
|
||||
})
|
||||
|
||||
const classBackground = computed(() => {
|
||||
if (state.importTemplateInfo.snapshot) {
|
||||
return {
|
||||
background: `url(${imgUrlTrans(state.importTemplateInfo.snapshot)}) no-repeat`
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
const showCurrentTemplate = pid => {
|
||||
find({ pid }).then(response => {
|
||||
state.nameList = response.data
|
||||
})
|
||||
}
|
||||
const cancel = () => {
|
||||
emits('closeEditTemplateDialog')
|
||||
}
|
||||
|
||||
const saveTemplate = () => {
|
||||
if (!state.templateInfo.name) {
|
||||
ElMessage.warning(t('chart.name_can_not_empty'))
|
||||
return false
|
||||
}
|
||||
if (!state.templateInfo.templateData) {
|
||||
ElMessage.warning(t('chart.template_can_not_empty'))
|
||||
return false
|
||||
}
|
||||
const nameCheckRequest = {
|
||||
pid: state.templateInfo.pid,
|
||||
name: state.templateInfo.name,
|
||||
optType: 'insert'
|
||||
}
|
||||
nameCheck(nameCheckRequest).then(response => {
|
||||
if (response.data.indexOf('exist') > -1) {
|
||||
const options = {
|
||||
title: 'commons.prompt',
|
||||
content: 'system_parameter_setting.to_overwrite_them',
|
||||
type: 'primary',
|
||||
cb: () =>
|
||||
save(state.templateInfo).then(response => {
|
||||
ElMessage.success(t('system_parameter_setting.import_succeeded'))
|
||||
emits('refresh')
|
||||
emits('closeEditTemplateDialog')
|
||||
}),
|
||||
confirmButtonText: t('template.override')
|
||||
}
|
||||
handlerConfirm(options)
|
||||
} else {
|
||||
save(state.templateInfo).then(response => {
|
||||
ElMessage.success(t('system_parameter_setting.import_succeeded'))
|
||||
emits('refresh')
|
||||
emits('closeEditTemplateDialog')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handlerConfirm = option => {
|
||||
// do handlerConfirm
|
||||
}
|
||||
|
||||
const handleFileChange = e => {
|
||||
const file = e.target.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.onload = res => {
|
||||
const result = res.target.result as string
|
||||
state.importTemplateInfo = JSON.parse(result)
|
||||
state.templateInfo.name = state.importTemplateInfo['name']
|
||||
state.templateInfo.templateStyle = state.importTemplateInfo['panelStyle']
|
||||
state.templateInfo.templateData = state.importTemplateInfo['panelData']
|
||||
state.templateInfo.snapshot = state.importTemplateInfo.snapshot
|
||||
state.templateInfo.dynamicData = state.importTemplateInfo['dynamicData']
|
||||
state.templateInfo.staticResource = state.importTemplateInfo['staticResource']
|
||||
state.templateInfo['nodeType'] = 'template'
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
const goFile = () => {
|
||||
filesRef.value.click()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
showCurrentTemplate(props.pid)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my_table :deep(.el-table__row > td) {
|
||||
/* 去除表格线 */
|
||||
border: none;
|
||||
padding: 0 0;
|
||||
}
|
||||
.my_table :deep(.el-table th.is-leaf) {
|
||||
/* 去除上边框 */
|
||||
border: none;
|
||||
}
|
||||
.my_table :deep(.el-table::before) {
|
||||
/* 去除下边框 */
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.de-root-class {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
.preview {
|
||||
margin-top: -12px;
|
||||
border: 1px solid #e6e6e6;
|
||||
height: 300px !important;
|
||||
overflow: auto;
|
||||
background-size: 100% 100% !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.preview-show {
|
||||
border-left: 1px solid #e6e6e6;
|
||||
height: 300px;
|
||||
background-size: 100% 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.flex-template {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.el-input {
|
||||
margin-right: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
140
core/core-frontend/src/views/template/component/TemplateItem.vue
Normal file
140
core/core-frontend/src/views/template/component/TemplateItem.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div :style="classBackground" class="de-card-model">
|
||||
<div class="card-img-model" :style="classImg">
|
||||
<img :src="model.snapshot" alt="" />
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<el-tooltip class="item" effect="dark" :content="model.name" placement="top">
|
||||
<span class="de-model-text">{{ model.name }}</span>
|
||||
</el-tooltip>
|
||||
<el-dropdown size="medium" trigger="click" @command="handleCommand">
|
||||
<i class="el-icon-more" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="de-card-dropdown">
|
||||
<slot>
|
||||
<el-dropdown-item command="rename">
|
||||
<i class="el-icon-edit" />
|
||||
{{ $t('chart.rename') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete">
|
||||
<i class="el-icon-delete" />
|
||||
{{ $t('chart.delete') }}
|
||||
</el-dropdown-item>
|
||||
</slot>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
const { t } = useI18n()
|
||||
const emits = defineEmits(['command'])
|
||||
|
||||
const props = defineProps({
|
||||
model: {
|
||||
type: Object
|
||||
},
|
||||
width: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
|
||||
const classBackground = computed(() => {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
height: props.width * 0.714 + 'px'
|
||||
}
|
||||
})
|
||||
const classImg = computed(() => {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
height: props.width * 0.576 + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
const handleCommand = key => {
|
||||
emits('command', key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.de-card-model {
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--deCardStrokeColor, #dee0e3);
|
||||
border-radius: 4px;
|
||||
margin: 0 24px 25px 0;
|
||||
.card-img-model {
|
||||
border-bottom: 1px solid var(--deCardStrokeColor, #dee0e3);
|
||||
height: 144px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px 9px 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.el-icon-more {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-icon-more:hover {
|
||||
background: rgba(31, 35, 41, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.el-icon-more:active {
|
||||
background: rgba(31, 35, 41, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.de-model-text {
|
||||
font-family: 'PingFang SC';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #1f2329;
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.de-card-model:hover {
|
||||
box-shadow: 0px 6px 24px rgba(31, 35, 41, 0.08);
|
||||
}
|
||||
|
||||
.de-card-dropdown {
|
||||
margin-top: 0 !important;
|
||||
.popper__arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
247
core/core-frontend/src/views/template/component/TemplateList.vue
Normal file
247
core/core-frontend/src/views/template/component/TemplateList.vue
Normal file
@ -0,0 +1,247 @@
|
||||
<template xmlns:el-col="http://www.w3.org/1999/html">
|
||||
<div class="de-template-list">
|
||||
<el-input
|
||||
v-model="state.templateFilterText"
|
||||
:placeholder="t('system_parameter_setting.search_keywords')"
|
||||
size="small"
|
||||
class="de-input-search"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="de-search" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-empty
|
||||
v-if="!templateListComputed.length && state.templateFilterText === ''"
|
||||
:image="state.noneImg"
|
||||
:description="t('components.no_classification')"
|
||||
/>
|
||||
<el-empty
|
||||
v-if="!templateListComputed.length && state.templateFilterText !== ''"
|
||||
:image="state.nothingImg"
|
||||
:description="t('components.relevant_content_found')"
|
||||
/>
|
||||
<ul>
|
||||
<li
|
||||
v-for="ele in templateListComputed"
|
||||
:key="ele.name"
|
||||
:class="[{ select: state.activeTemplate === ele.id }]"
|
||||
@click="nodeClick(ele)"
|
||||
>
|
||||
<svg-icon icon-class="scene" class="de-icon-sense" />
|
||||
<span class="text-template-overflow" :title="ele.name">{{ ele.name }}</span>
|
||||
<span class="more" @click.stop>
|
||||
<el-dropdown trigger="click" size="small" @command="type => clickMore(type, ele)">
|
||||
<span class="el-dropdown-link">
|
||||
<i class="el-icon-more" />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="de-template-dropdown">
|
||||
<el-dropdown-item icon="el-icon-upload2" command="import">
|
||||
{{ t('panel.import') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-edit" command="edit">
|
||||
{{ t('panel.rename') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-delete" command="delete">
|
||||
{{ t('panel.delete') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<el-button
|
||||
v-if="state.templateFilterText === ''"
|
||||
style="width: 100%"
|
||||
icon="el-icon-plus"
|
||||
secondary
|
||||
@click="add()"
|
||||
>
|
||||
{{ t('panel.add_category') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { computed, reactive } from 'vue'
|
||||
const { t } = useI18n()
|
||||
|
||||
const emits = defineEmits([
|
||||
'showCurrentTemplate',
|
||||
'showTemplateEditDialog',
|
||||
'templateDelete',
|
||||
'templateEdit',
|
||||
'templateImport'
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
templateType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
templateList: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
templateFilterText: '',
|
||||
activeTemplate: '',
|
||||
noneImg: '@/assets/None.png',
|
||||
nothingImg: '@/assets/nothing.png'
|
||||
})
|
||||
|
||||
const templateListComputed = computed(() => {
|
||||
if (!state.templateFilterText) return [...props.templateList]
|
||||
return props.templateList.filter(ele => ele['name']?.includes(state.templateFilterText))
|
||||
})
|
||||
|
||||
const clickMore = (type, data) => {
|
||||
switch (type) {
|
||||
case 'edit':
|
||||
templateEdit(data)
|
||||
break
|
||||
case 'delete':
|
||||
templateDelete(data)
|
||||
break
|
||||
case 'import':
|
||||
templateImport(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
const nodeClick = ({ id, label }) => {
|
||||
state.activeTemplate = id
|
||||
emits('showCurrentTemplate', id, label)
|
||||
}
|
||||
const add = () => {
|
||||
emits('showTemplateEditDialog', 'new')
|
||||
}
|
||||
const templateDelete = template => {
|
||||
const options = {
|
||||
title: 'system_parameter_setting.delete_this_category',
|
||||
content: 'system_parameter_setting.also_be_deleted',
|
||||
type: 'primary',
|
||||
cb: () => emits('templateDelete', template.id)
|
||||
}
|
||||
handlerConfirm(options)
|
||||
}
|
||||
const templateEdit = template => {
|
||||
emits('templateEdit', template)
|
||||
}
|
||||
const templateImport = template => {
|
||||
emits('templateImport', template.id)
|
||||
}
|
||||
|
||||
const handlerConfirm = options => {
|
||||
// do handlerConfirm
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.de-template-list {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
ul {
|
||||
margin: 16px 0 20px 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100% - 90px);
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
padding: 0 30px 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: var(--deTextPrimary, #1f2329);
|
||||
font-family: 'PingFang SC';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.text-template-overflow {
|
||||
display: inline-block;
|
||||
max-width: 87%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.folder {
|
||||
color: #8f959e;
|
||||
margin-right: 9px;
|
||||
}
|
||||
|
||||
.more {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
display: none;
|
||||
|
||||
.el-icon-more {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-icon-more:hover {
|
||||
background: rgba(31, 35, 41, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.el-icon-more:active {
|
||||
background: rgba(31, 35, 41, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(31, 35, 41, 0.1);
|
||||
|
||||
.more {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li.select {
|
||||
background: var(--deWhiteHover, #e0eaff) !important;
|
||||
color: var(--TextActive, #3370ff) !important;
|
||||
}
|
||||
|
||||
.de-btn-fix {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.de-template-dropdown {
|
||||
margin-top: 0 !important;
|
||||
|
||||
.popper__arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,12 +1,412 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const wizard = ref('wizard')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ wizard }}
|
||||
<div class="de-template">
|
||||
<el-tabs v-model="state.currentTemplateType" class="de-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane name="self">
|
||||
<template #label>
|
||||
<span>{{ t('panel.user_template') }}</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="system">
|
||||
<template #label>
|
||||
<span>{{ t('panel.sys_template') }}</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="tabs-container flex-tabs">
|
||||
<div class="de-tabs-left">
|
||||
<template-list
|
||||
ref="templateListRef"
|
||||
:template-type="state.currentTemplateType"
|
||||
:template-list="state.templateList"
|
||||
@templateDelete="templateFolderDelete"
|
||||
@templateEdit="templateEdit"
|
||||
@showCurrentTemplate="showCurrentTemplate"
|
||||
@templateImport="templateImport"
|
||||
@showTemplateEditDialog="showTemplateEditDialog"
|
||||
/>
|
||||
</div>
|
||||
<div class="de-tabs-right">
|
||||
<div v-if="state.currentTemplateLabel" class="active-template">
|
||||
{{ state.currentTemplateLabel }} ({{ state.currentTemplateShowList.length }})
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-upload2"
|
||||
@click="templateImport(state.currentTemplateId)"
|
||||
>
|
||||
{{ t('panel.import') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!state.currentTemplateShowList.length"
|
||||
:image="state.noneImg"
|
||||
:description="t('components.no_template')"
|
||||
/>
|
||||
<div v-show="state.currentTemplateId !== ''" id="template-box" class="template-box">
|
||||
<template-item
|
||||
v-for="item in state.currentTemplateShowList"
|
||||
:key="item.id"
|
||||
:width="state.templateCurWidth"
|
||||
:model="item"
|
||||
@command="key => handleCommand(key, item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
:title="state.dialogTitle"
|
||||
v-model:visible="state.editTemplate"
|
||||
append-to-body
|
||||
class="de-dialog-form"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="templateEditFormRef"
|
||||
class="de-form-item"
|
||||
:model="state.templateEditForm"
|
||||
:rules="state.templateEditFormRules"
|
||||
>
|
||||
<el-form-item :label="state.dialogTitleLabel" prop="name">
|
||||
<el-input v-model="state.templateEditForm.name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button secondary @click="close()">{{ t('commons.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="saveTemplateEdit(state.templateEditForm)"
|
||||
>{{ t('commons.confirm') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!--导入templatedialog-->
|
||||
<el-dialog
|
||||
:title="state.templateDialog.title"
|
||||
v-model:visible="state.templateDialog.visible"
|
||||
:show-close="true"
|
||||
class="de-dialog-form"
|
||||
width="600px"
|
||||
>
|
||||
<template-import
|
||||
v-if="state.templateDialog.visible"
|
||||
:pid="state.templateDialog.pid"
|
||||
@refresh="showCurrentTemplate(state.currentTemplateId, state.currentTemplateLabel)"
|
||||
@closeEditTemplateDialog="closeEditTemplateDialog"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<script lang="ts" setup>
|
||||
import { save, templateDelete, find } from '@/api/template'
|
||||
import elementResizeDetectorMaker from 'element-resize-detector'
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ElMessage } from 'element-plus-secondary'
|
||||
import TemplateList from '@/views/template/component/TemplateList.vue'
|
||||
const { t } = useI18n()
|
||||
const templateEditFormRef = ref(null)
|
||||
const templateListRef = ref(null)
|
||||
|
||||
const roleValidator = (rule, value, callback) => {
|
||||
if (nameRepeat(value)) {
|
||||
const { nodeType } = state.templateEditForm || {}
|
||||
callback(
|
||||
new Error(
|
||||
t(
|
||||
`system_parameter_setting.${
|
||||
nodeType === 'folder' ? 'name_already_exists_type' : 'the_same_category'
|
||||
}`
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
showShare: false,
|
||||
currentTemplateShowList: [],
|
||||
noneImg: '@/assets/None.png',
|
||||
currentPid: '',
|
||||
currentTemplateType: 'self',
|
||||
templateEditFormRules: {
|
||||
name: [
|
||||
{ required: true, trigger: 'blur', validator: roleValidator },
|
||||
{
|
||||
required: true,
|
||||
message: t('commons.input_content'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
max: 50,
|
||||
message: t('commons.char_can_not_more_50'),
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
},
|
||||
templateEditForm: {},
|
||||
editTemplate: false,
|
||||
dialogTitle: '',
|
||||
dialogTitleLabel: '',
|
||||
currentTemplateLabel: '',
|
||||
currentTemplateId: '',
|
||||
templateList: [],
|
||||
templateMiniWidth: 286,
|
||||
templateCurWidth: 286,
|
||||
formType: '',
|
||||
originName: '',
|
||||
templateDialog: {
|
||||
title: t('panel.import_template'),
|
||||
visible: false,
|
||||
pid: ''
|
||||
}
|
||||
})
|
||||
|
||||
const nameList = computed(() => {
|
||||
const { nodeType } = state.templateEditForm || {}
|
||||
if (nodeType === 'template') {
|
||||
return state.currentTemplateShowList.map(ele => ele.label)
|
||||
}
|
||||
|
||||
if (nodeType === 'folder') {
|
||||
return state.templateList.map(ele => ele.label)
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const nameRepeat = value => {
|
||||
if (!nameList.value) {
|
||||
return false
|
||||
}
|
||||
// 编辑场景 不能 因为名称重复而报错
|
||||
if (state.formType === 'edit' && state.originName === value) {
|
||||
return false
|
||||
}
|
||||
return nameList.value.some(name => name === value)
|
||||
}
|
||||
|
||||
const handleCommand = (key, data) => {
|
||||
switch (key) {
|
||||
case 'rename':
|
||||
templateEdit(data)
|
||||
break
|
||||
case 'delete':
|
||||
templateDeleteConfirm(data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handlerConfirm = option => {
|
||||
//do handlerConfirm
|
||||
}
|
||||
|
||||
const templateDeleteConfirm = template => {
|
||||
const options = {
|
||||
title: 'system_parameter_setting.delete_this_template',
|
||||
type: 'primary',
|
||||
cb: () => templateDelete(template.id)
|
||||
}
|
||||
handlerConfirm(options)
|
||||
}
|
||||
|
||||
const handleClick = (tab, event) => {
|
||||
getTree()
|
||||
}
|
||||
|
||||
const showCurrentTemplate = (pid, label) => {
|
||||
state.currentTemplateId = pid
|
||||
state.currentTemplateLabel = label
|
||||
if (state.currentTemplateId) {
|
||||
find({ pid: state.currentTemplateId }).then(response => {
|
||||
state.currentTemplateShowList = response.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const templateFolderDelete = id => {
|
||||
if (id) {
|
||||
templateDelete(id).then(response => {
|
||||
ElMessage.info(t('commons.delete_success'))
|
||||
getTree()
|
||||
})
|
||||
}
|
||||
}
|
||||
const templateDelete = id => {
|
||||
if (id) {
|
||||
templateDelete(id).then(response => {
|
||||
ElMessage.info(t('commons.delete_success'))
|
||||
showCurrentTemplate(state.currentTemplateId, state.currentTemplateLabel)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const showTemplateEditDialog = (type, templateInfo) => {
|
||||
state.templateEditForm = null
|
||||
state.formType = type
|
||||
if (type === 'edit') {
|
||||
state.templateEditForm = JSON.parse(JSON.stringify(templateInfo))
|
||||
state.dialogTitle = t(
|
||||
`system_parameter_setting.${
|
||||
state.templateEditForm['nodeType'] === 'folder' ? 'edit_classification' : 'edit_template'
|
||||
}`
|
||||
)
|
||||
state.originName = state.templateEditForm['label']
|
||||
} else {
|
||||
state.dialogTitle = t('panel.add_category')
|
||||
state.templateEditForm = {
|
||||
name: '',
|
||||
nodeType: 'folder',
|
||||
templateType: state.currentTemplateType,
|
||||
level: 0
|
||||
}
|
||||
}
|
||||
state.dialogTitleLabel = t(
|
||||
`system_parameter_setting.${
|
||||
state.templateEditForm['nodeType'] === 'folder' ? 'classification_name' : 'template_name'
|
||||
}`
|
||||
)
|
||||
state.editTemplate = true
|
||||
}
|
||||
|
||||
const templateEdit = templateInfo => {
|
||||
showTemplateEditDialog('edit', templateInfo)
|
||||
}
|
||||
|
||||
const saveTemplateEdit = templateEditForm => {
|
||||
templateEditFormRef.value.validate(valid => {
|
||||
if (valid) {
|
||||
save(templateEditForm).then(response => {
|
||||
close()
|
||||
// openMessageSuccess(
|
||||
// `system_parameter_setting.${
|
||||
// this.templateEditForm.id ? 'rename_succeeded' : 'added_successfully'
|
||||
// }`
|
||||
// )
|
||||
getTree()
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
const close = () => {
|
||||
templateEditFormRef.value.resetFields()
|
||||
state.editTemplate = false
|
||||
}
|
||||
const getTree = () => {
|
||||
const request = {
|
||||
templateType: state.currentTemplateType,
|
||||
level: '0'
|
||||
}
|
||||
find(request).then(res => {
|
||||
state.templateList = res.data
|
||||
showFirst()
|
||||
})
|
||||
}
|
||||
const showFirst = () => {
|
||||
// 判断是否默认点击第一条
|
||||
if (state.templateList && state.templateList.length > 0) {
|
||||
let showFirst = true
|
||||
state.templateList.forEach(template => {
|
||||
if (template.id === state.currentTemplateId) {
|
||||
showFirst = false
|
||||
}
|
||||
})
|
||||
if (showFirst) {
|
||||
nextTick().then(() => {
|
||||
const [obj = {}] = state.templateList
|
||||
templateListRef.value.nodeClick(obj)
|
||||
})
|
||||
} else {
|
||||
showCurrentTemplate(state.currentTemplateId, state.currentTemplateLabel)
|
||||
}
|
||||
} else {
|
||||
state.currentTemplateShowList = []
|
||||
}
|
||||
}
|
||||
|
||||
const closeEditTemplateDialog = () => {
|
||||
state.templateDialog.visible = false
|
||||
}
|
||||
|
||||
const templateImport = pid => {
|
||||
state.templateDialog.visible = true
|
||||
state.templateDialog.pid = pid
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTree()
|
||||
const erd = elementResizeDetectorMaker()
|
||||
const templateMainDom = document.getElementById('template-box')
|
||||
// 监听div变动事件
|
||||
erd.listenTo(templateMainDom, element => {
|
||||
nextTick(() => {
|
||||
const curSeparator = Math.trunc(templateMainDom.offsetWidth / state.templateMiniWidth)
|
||||
state.templateCurWidth =
|
||||
Math.trunc(templateMainDom.offsetWidth / curSeparator) - 24 - curSeparator
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.de-template {
|
||||
height: 100%;
|
||||
background-color: var(--MainBG, #f5f6f7);
|
||||
|
||||
.tabs-container {
|
||||
height: calc(100% - 48px);
|
||||
background: var(--ContentBG, #ffffff);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.flex-tabs {
|
||||
display: flex;
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.de-tabs-left {
|
||||
background: #fff;
|
||||
width: 269px;
|
||||
border-right: 1px solid rgba(31, 35, 41, 0.15);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.de-tabs-right {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
padding: 24px 0 24px 24px;
|
||||
overflow: hidden;
|
||||
|
||||
.template-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
align-content: flex-start;
|
||||
height: calc(100% - 10px);
|
||||
width: 100%;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.active-template {
|
||||
margin: 4px 0 20px 0;
|
||||
padding-right: 24px;
|
||||
font-family: 'PingFang SC';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--deTextPrimary, #1f2329);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
2
de-xpack
2
de-xpack
@ -1 +1 @@
|
||||
Subproject commit 912a648808e87bdf1e5a11f7cdc6467b89fd9347
|
||||
Subproject commit e3143e3176ca189c35729fa7bb195264c061a824
|
@ -0,0 +1,13 @@
|
||||
package io.dataease.api.template;
|
||||
|
||||
import io.dataease.api.template.dto.TemplateManageDTO;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TemplateManageApi {
|
||||
|
||||
@PostMapping("/templateList")
|
||||
List<TemplateManageDTO> templateList();
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package io.dataease.api.template;
|
||||
|
||||
import io.dataease.api.template.request.TemplateMarketSearchRequest;
|
||||
import io.dataease.api.template.response.MarketBaseResponse;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author : WangJiaHao
|
||||
* @date : 2023/11/6 17:23
|
||||
*/
|
||||
public interface TemplateMarketApi {
|
||||
|
||||
@PostMapping("/search")
|
||||
MarketBaseResponse searchTemplate(@RequestBody TemplateMarketSearchRequest request);
|
||||
|
||||
@GetMapping("/categories")
|
||||
List<String> categories();
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package io.dataease.api.template.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
|
||||
@Data
|
||||
public class TemplateManageDTO {
|
||||
|
||||
private String label;
|
||||
private Integer childrenCount;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package io.dataease.api.template.dto;
|
||||
|
||||
import io.dataease.api.template.vo.MarketCategoryVO;
|
||||
import io.dataease.api.template.vo.MarketMetasVO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TemplateMarketDTO {
|
||||
private String id;
|
||||
private String title;
|
||||
private String status;
|
||||
private String slug;
|
||||
private String editorType;
|
||||
private String summary;
|
||||
private String thumbnail;
|
||||
private Boolean showFlag = true;
|
||||
private List<MarketCategoryVO> categories;
|
||||
private MarketMetasVO metas;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package io.dataease.api.template.request;
|
||||
|
||||
import io.dataease.api.template.dto.TemplateMarketDTO;
|
||||
|
||||
public class TemplateMarketSearchRequest extends TemplateMarketDTO {
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package io.dataease.api.template.response;
|
||||
|
||||
import io.dataease.api.template.dto.TemplateMarketDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author : WangJiaHao
|
||||
* @date : 2023/11/6 17:43
|
||||
*/
|
||||
public class MarketBaseResponse {
|
||||
private String baseUrl;
|
||||
|
||||
private List<TemplateMarketDTO> contents;
|
||||
|
||||
public MarketBaseResponse() {
|
||||
}
|
||||
|
||||
public MarketBaseResponse(String baseUrl, List<TemplateMarketDTO> contents) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package io.dataease.api.template.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Author: wangjiahao
|
||||
* Date: 2022/7/15
|
||||
* Description:
|
||||
*/
|
||||
@Data
|
||||
public class MarketCategoryVO {
|
||||
private String id;
|
||||
private String name;
|
||||
private String slug;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package io.dataease.api.template.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Author: wangjiahao
|
||||
* Date: 2022/7/15
|
||||
* Description:
|
||||
*/
|
||||
@Data
|
||||
public class MarketMetasVO {
|
||||
private String theme_repo;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package io.dataease.api.template.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Author: wangjiahao
|
||||
* Date: 2022/7/18
|
||||
* Description:
|
||||
*/
|
||||
@Data
|
||||
public class TemplateCategoryVO {
|
||||
private Integer id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String slug;
|
||||
|
||||
private Integer priority;
|
||||
}
|
Loading…
Reference in New Issue
Block a user