Generate admin ECharts business blocks
This commit is contained in:
@@ -10,6 +10,7 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.GenProject;
|
||||
@@ -475,6 +476,10 @@ public class BusinessBlockGenerationService
|
||||
context.put("chartType", file.getDefinition().getChartType());
|
||||
context.put("chartDataset", instance.getDataset());
|
||||
context.put("chartDisplay", instance.getDisplay());
|
||||
context.put("chartDisplayJson", javascriptJson(instance.getDisplay() == null
|
||||
? Collections.<String, Object>emptyMap() : instance.getDisplay()));
|
||||
context.put("chartInstanceNameJson", javascriptJson(
|
||||
StringUtils.defaultIfEmpty(instance.getName(), file.getDefinition().getName())));
|
||||
context.put("chartMetrics", chartMetricRenderModels(chartQuery.getMetrics()));
|
||||
context.put("chartDateStartLiteral", ChartQueryParameter.toJavaLiteral(
|
||||
chartQuery.getDateRange() == null ? null : chartQuery.getDateRange().getStart()));
|
||||
@@ -537,6 +542,14 @@ public class BusinessBlockGenerationService
|
||||
return renderConfig;
|
||||
}
|
||||
|
||||
private String javascriptJson(Object value)
|
||||
{
|
||||
return JSON.toJSONString(value)
|
||||
.replace("<", "\\u003c")
|
||||
.replace(">", "\\u003e")
|
||||
.replace("&", "\\u0026");
|
||||
}
|
||||
|
||||
private List<Map<String, String>> chartMetricRenderModels(List<ChartMetricConfig> metrics)
|
||||
{
|
||||
List<Map<String, String>> result = new ArrayList<Map<String, String>>();
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<section class="admin-chart-block">
|
||||
<header class="admin-chart-block__header">
|
||||
<div>
|
||||
<span>Data insight</span>
|
||||
<h3>{{ display.title || ${chartInstanceNameJson} }}</h3>
|
||||
</div>
|
||||
<el-button type="text" icon="el-icon-refresh" :disabled="loading" @click="retry">Refresh</el-button>
|
||||
</header>
|
||||
|
||||
<el-skeleton v-if="loading" :rows="5" animated />
|
||||
|
||||
<div v-else-if="errorMessage" class="admin-chart-block__error">
|
||||
<el-alert :title="errorMessage" type="error" :closable="false" show-icon />
|
||||
<el-button size="small" type="primary" plain @click="retry">Retry</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!hasData" description="No chart data" />
|
||||
|
||||
<div v-else-if="chartType === 'metric'" class="admin-chart-block__metric">
|
||||
<strong>{{ metricValue }}</strong>
|
||||
<span>{{ display.subtitle || display.unit || "" }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else ref="chart" class="admin-chart-block__chart"></div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { load${InstanceClassName}Data } from "${apiModulePath}"
|
||||
import { buildChartOption, createChart, hasChartData } from "@/utils/chartRuntime"
|
||||
|
||||
export default {
|
||||
name: "${InstanceClassName}",
|
||||
data() {
|
||||
return {
|
||||
chartType: "${chartType}",
|
||||
display: ${chartDisplayJson},
|
||||
loading: false,
|
||||
errorMessage: "",
|
||||
payload: null,
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasData() {
|
||||
return hasChartData(this.payload)
|
||||
},
|
||||
metricValue() {
|
||||
const summary = this.payload && this.payload.summary
|
||||
return summary && summary.value !== null && summary.value !== undefined
|
||||
? summary.value
|
||||
: "-"
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadData()
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.handleResize)
|
||||
this.disposeChart()
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.disposeChart()
|
||||
this.loading = true
|
||||
this.errorMessage = ""
|
||||
return load${InstanceClassName}Data()
|
||||
.then(response => {
|
||||
this.payload = (response && response.data) || null
|
||||
this.loading = false
|
||||
if (this.chartType === "metric" || !this.hasData) {
|
||||
this.disposeChart()
|
||||
return
|
||||
}
|
||||
this.#[[$nextTick]]#(() => this.renderChart())
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false
|
||||
this.payload = null
|
||||
this.errorMessage = error && error.message ? error.message : "Failed to load chart data"
|
||||
this.disposeChart()
|
||||
})
|
||||
},
|
||||
retry() {
|
||||
return this.loadData()
|
||||
},
|
||||
renderChart() {
|
||||
if (!this.#[[$refs]]#.chart) {
|
||||
return
|
||||
}
|
||||
if (!this.chart) {
|
||||
this.chart = createChart(this.#[[$refs]]#.chart)
|
||||
}
|
||||
this.chart.setOption(buildChartOption(this.chartType, this.display, this.payload), true)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize()
|
||||
}
|
||||
},
|
||||
disposeChart() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-chart-block {
|
||||
min-height: 320px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.admin-chart-block__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.admin-chart-block__header span {
|
||||
color: #409eff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-chart-block__header h3 {
|
||||
margin: 4px 0 0;
|
||||
color: #303133;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.admin-chart-block__chart {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.admin-chart-block__metric {
|
||||
display: flex;
|
||||
min-height: 220px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-chart-block__metric strong {
|
||||
color: #303133;
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-chart-block__metric span {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.admin-chart-block__error {
|
||||
display: grid;
|
||||
min-height: 220px;
|
||||
align-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import request from "@/api/request"
|
||||
|
||||
export function load${InstanceClassName}Data() {
|
||||
return request({
|
||||
url: "${apiBasePath}/data",
|
||||
method: "get"
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as echarts from "echarts"
|
||||
|
||||
export function createChart(element) {
|
||||
return echarts.init(element)
|
||||
}
|
||||
|
||||
export function buildChartOption(chartType, display, payload) {
|
||||
const safeDisplay = display || {}
|
||||
const dimensions = (payload && payload.dimensions) || []
|
||||
const sourceSeries = (payload && payload.series) || []
|
||||
const labels = dimensions.map(item => item.label)
|
||||
|
||||
if (chartType === "pie") {
|
||||
const values = sourceSeries.length ? sourceSeries[0].values : []
|
||||
return {
|
||||
tooltip: { trigger: "item" },
|
||||
legend: { show: safeDisplay.showLegend !== false },
|
||||
series: [{
|
||||
type: "pie",
|
||||
radius: safeDisplay.donut ? ["45%", "70%"] : "70%",
|
||||
label: { show: safeDisplay.showLabel !== false },
|
||||
data: labels.map((name, index) => ({
|
||||
name,
|
||||
value: values[index] == null ? 0 : values[index]
|
||||
}))
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const horizontal = chartType === "bar" && safeDisplay.horizontal
|
||||
const series = sourceSeries.map(item => ({
|
||||
name: item.name,
|
||||
type: chartType === "bar" ? "bar" : "line",
|
||||
smooth: chartType === "line" && Boolean(safeDisplay.smooth),
|
||||
stack: chartType === "bar" && safeDisplay.stacked ? "total" : undefined,
|
||||
areaStyle: chartType === "line" && safeDisplay.area ? {} : undefined,
|
||||
data: item.values || []
|
||||
}))
|
||||
|
||||
return {
|
||||
tooltip: { trigger: "axis" },
|
||||
legend: { show: safeDisplay.showLegend !== false },
|
||||
xAxis: horizontal
|
||||
? { type: "value" }
|
||||
: { type: "category", data: labels },
|
||||
yAxis: horizontal
|
||||
? { type: "category", data: labels }
|
||||
: { type: "value" },
|
||||
series
|
||||
}
|
||||
}
|
||||
|
||||
export function hasChartData(payload) {
|
||||
return Boolean(payload && (
|
||||
(payload.summary && payload.summary.value !== null && payload.summary.value !== undefined)
|
||||
|| (payload.dimensions && payload.dimensions.length)
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<section class="business-block-page">
|
||||
#if($businessBlocks && $businessBlocks.size() > 0)
|
||||
#foreach($block in $businessBlocks)
|
||||
<div class="business-block-page__item business-block-page__item--span-${block.span}">
|
||||
<${block.componentName} />
|
||||
</div>
|
||||
#end
|
||||
#else
|
||||
<div class="business-block-empty">No business blocks configured.</div>
|
||||
#end
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
#foreach($block in $businessBlocks)
|
||||
import ${block.componentName} from "${block.relativeImportPath}"
|
||||
#end
|
||||
|
||||
export default {
|
||||
name: "${pageComponentName}",
|
||||
#if($businessBlocks && $businessBlocks.size() > 0)
|
||||
components: {
|
||||
#foreach($block in $businessBlocks)
|
||||
${block.componentName}#if($foreach.hasNext),#end
|
||||
#end
|
||||
}
|
||||
#else
|
||||
components: {}
|
||||
#end
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.business-block-page {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.business-block-page__item--span-12 { grid-column: span 12; }
|
||||
.business-block-page__item--span-6 { grid-column: span 6; }
|
||||
.business-block-page__item--span-4 { grid-column: span 4; }
|
||||
|
||||
.business-block-empty {
|
||||
grid-column: span 12;
|
||||
padding: 32px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.business-block-page__item {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,8 @@
|
||||
#set($showForm = true)
|
||||
#set($renderForm = true)
|
||||
#set($currentUserOnly = false)
|
||||
#set($isDetailPage = $pageDesignIsDetail)
|
||||
#set($detailColumns = $columns)
|
||||
#set($toolbarBusinessActions = $pageDesignToolbarBusinessActions)
|
||||
#set($rowBusinessActions = $pageDesignRowBusinessActions)
|
||||
#set($allPageBusinessActions = $pageDesignBusinessActions)
|
||||
@@ -54,11 +56,26 @@
|
||||
#set($showToolbar = $pageDesignShowToolbar)
|
||||
#set($showForm = $pageDesignShowForm)
|
||||
#set($currentUserOnly = $pageDesignIsCurrentUserList)
|
||||
#set($detailColumns = $pageDesignFormColumns)
|
||||
#end
|
||||
#if(!$showToolbar)
|
||||
#set($showAdd = false)
|
||||
#set($showBatchDelete = false)
|
||||
#end
|
||||
#if($isDetailPage)
|
||||
#set($showQuery = false)
|
||||
#set($showToolbar = false)
|
||||
#set($showAdd = false)
|
||||
#set($showBatchDelete = false)
|
||||
#set($showView = false)
|
||||
#set($showEdit = false)
|
||||
#set($showDelete = false)
|
||||
#set($showForm = false)
|
||||
#set($renderForm = false)
|
||||
#if($detailColumns.size() == 0)
|
||||
#set($detailColumns = $columns)
|
||||
#end
|
||||
#end
|
||||
#set($renderForm = $showForm)
|
||||
#if($showAdd || $showEdit)
|
||||
#set($renderForm = true)
|
||||
@@ -67,7 +84,9 @@
|
||||
#if($hasPageDesignTopBusinessBlocks)
|
||||
<section class="embedded-business-blocks top-business-blocks">
|
||||
#foreach($block in $topBusinessBlocks)
|
||||
<${block.componentName} />
|
||||
<div class="embedded-business-block__item embedded-business-block__item--span-${block.span}">
|
||||
<${block.componentName} />
|
||||
</div>
|
||||
#end
|
||||
</section>
|
||||
#end
|
||||
@@ -130,12 +149,70 @@
|
||||
</el-popconfirm>
|
||||
#end
|
||||
#foreach($action in $toolbarBusinessActions)
|
||||
<el-button type="primary" plain icon="el-icon-position" @click="handleBusinessAction('${action.code}')">${action.name}</el-button>
|
||||
#set($buttonLabel = $action.buttonLabel)
|
||||
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
|
||||
#set($buttonType = $action.buttonType)
|
||||
#if(!$buttonType)#set($buttonType = "primary")#end
|
||||
#set($buttonIcon = $action.buttonIcon)
|
||||
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
|
||||
<el-button type="${buttonType}" plain icon="${buttonIcon}" @click="handleBusinessAction('${action.code}')">${buttonLabel}</el-button>
|
||||
#end
|
||||
#end
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#if($isDetailPage)
|
||||
<div class="detail-view" v-loading="detailLoading">
|
||||
<div v-if="detailLoaded && !detailRecord.${pkColumn.javaField}" class="empty-detail">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>暂无详情数据</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<dl class="detail-grid">
|
||||
#foreach ($column in $detailColumns)
|
||||
#set($javaField=$column.javaField)
|
||||
#set($columnComment=$column.columnComment)
|
||||
#set($remoteSelect=$qingRemoteSelectFields.contains($javaField))
|
||||
<div class="detail-field#if($column.htmlType == "textarea") wide#end">
|
||||
<dt>${columnComment}</dt>
|
||||
<dd>
|
||||
#if($column.htmlType == "imageUpload")
|
||||
<el-image
|
||||
v-if="detailRecord.${javaField}"
|
||||
class="detail-image"
|
||||
:src="resolveResourceUrl(detailRecord.${javaField})"
|
||||
:preview-src-list="[resolveResourceUrl(detailRecord.${javaField})]" />
|
||||
<span v-else>-</span>
|
||||
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
|
||||
#if($remoteSelect)
|
||||
{{ getOptionLabel('${javaField}', detailRecord.${javaField}) }}
|
||||
#else
|
||||
{{ _dict.getDictLabel('${column.dictType}', detailRecord.${javaField}) || '-' }}
|
||||
#end
|
||||
#else
|
||||
{{ formatDetailValue(detailRecord.${javaField}) }}
|
||||
#end
|
||||
</dd>
|
||||
</div>
|
||||
#end
|
||||
</dl>
|
||||
#if($hasRowBusinessActions)
|
||||
<div class="detail-actions">
|
||||
#foreach($action in $rowBusinessActions)
|
||||
#set($buttonLabel = $action.buttonLabel)
|
||||
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
|
||||
#set($buttonType = $action.buttonType)
|
||||
#if(!$buttonType)#set($buttonType = "primary")#end
|
||||
#set($buttonIcon = $action.buttonIcon)
|
||||
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
|
||||
<el-button type="${buttonType}" icon="${buttonIcon}" @click="handleBusinessAction('${action.code}', detailRecord)">${buttonLabel}</el-button>
|
||||
#end
|
||||
</div>
|
||||
#end
|
||||
</template>
|
||||
</div>
|
||||
#else
|
||||
|
||||
#if($showQuery)
|
||||
<div class="filter-bar">
|
||||
#foreach ($column in $queryColumns)
|
||||
@@ -188,8 +265,8 @@
|
||||
<el-image
|
||||
v-if="item.${cardImageColumn.javaField}"
|
||||
class="resource-card-image"
|
||||
:src="item.${cardImageColumn.javaField}"
|
||||
:preview-src-list="[item.${cardImageColumn.javaField}]" />
|
||||
:src="resolveResourceUrl(item.${cardImageColumn.javaField})"
|
||||
:preview-src-list="[resolveResourceUrl(item.${cardImageColumn.javaField})]" />
|
||||
#end
|
||||
#if($cardTitleColumn)
|
||||
<h3 class="resource-card-title">{{ item.${cardTitleColumn.javaField} || '-' }}</h3>
|
||||
@@ -209,8 +286,8 @@
|
||||
<el-image
|
||||
v-if="item.${javaField}"
|
||||
class="resource-card-image"
|
||||
:src="item.${javaField}"
|
||||
:preview-src-list="[item.${javaField}]" />
|
||||
:src="resolveResourceUrl(item.${javaField})"
|
||||
:preview-src-list="[resolveResourceUrl(item.${javaField})]" />
|
||||
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
|
||||
<div class="resource-card-field">
|
||||
<span>${columnComment}</span>
|
||||
@@ -249,7 +326,13 @@
|
||||
</el-popconfirm>
|
||||
#end
|
||||
#foreach($action in $rowBusinessActions)
|
||||
<el-button type="text" icon="el-icon-position" @click="handleBusinessAction('${action.code}', item)">${action.name}</el-button>
|
||||
#set($buttonLabel = $action.buttonLabel)
|
||||
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
|
||||
#set($buttonType = $action.buttonType)
|
||||
#if(!$buttonType)#set($buttonType = "text")#end
|
||||
#set($buttonIcon = $action.buttonIcon)
|
||||
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
|
||||
<el-button type="${buttonType}" icon="${buttonIcon}" @click="handleBusinessAction('${action.code}', item)">${buttonLabel}</el-button>
|
||||
#end
|
||||
</div>
|
||||
#end
|
||||
@@ -280,8 +363,8 @@
|
||||
<el-image
|
||||
v-if="scope.row.${javaField}"
|
||||
class="table-image"
|
||||
:src="scope.row.${javaField}"
|
||||
:preview-src-list="[scope.row.${javaField}]" />
|
||||
:src="resolveResourceUrl(scope.row.${javaField})"
|
||||
:preview-src-list="[resolveResourceUrl(scope.row.${javaField})]" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -321,7 +404,13 @@
|
||||
</el-popconfirm>
|
||||
#end
|
||||
#foreach($action in $rowBusinessActions)
|
||||
<el-button type="text" icon="el-icon-position" @click="handleBusinessAction('${action.code}', scope.row)">${action.name}</el-button>
|
||||
#set($buttonLabel = $action.buttonLabel)
|
||||
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
|
||||
#set($buttonType = $action.buttonType)
|
||||
#if(!$buttonType)#set($buttonType = "text")#end
|
||||
#set($buttonIcon = $action.buttonIcon)
|
||||
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
|
||||
<el-button type="${buttonType}" icon="${buttonIcon}" @click="handleBusinessAction('${action.code}', scope.row)">${buttonLabel}</el-button>
|
||||
#end
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -338,12 +427,15 @@
|
||||
:page-size="queryParams.pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total" />
|
||||
#end
|
||||
</section>
|
||||
|
||||
#if($hasPageDesignBottomBusinessBlocks)
|
||||
<section class="embedded-business-blocks bottom-business-blocks">
|
||||
#foreach($block in $bottomBusinessBlocks)
|
||||
<${block.componentName} />
|
||||
<div class="embedded-business-block__item embedded-business-block__item--span-${block.span}">
|
||||
<${block.componentName} />
|
||||
</div>
|
||||
#end
|
||||
</section>
|
||||
#end
|
||||
@@ -362,9 +454,10 @@
|
||||
#set($javaField=$column.javaField)
|
||||
#set($columnComment=$column.columnComment)
|
||||
#set($remoteSelect=$qingRemoteSelectFields.contains($javaField))
|
||||
<el-form-item label="${columnComment}" prop="${javaField}"#if($column.isRequired == "1") :rules="[{ required: true, message: '${columnComment}不能为空', trigger: #if($column.htmlType == "select")'change'#else'blur'#end }]"#end>
|
||||
#set($autoIncrementField=$column.isIncrement == "1")
|
||||
<el-form-item#if($autoIncrementField) v-if="form.${javaField}"#end label="${columnComment}" prop="${javaField}"#if($column.isRequired == "1" && !$autoIncrementField) :rules="[{ required: true, message: '${columnComment}不能为空', trigger: #if($column.htmlType == "select")'change'#else'blur'#end }]"#end>
|
||||
#if($column.htmlType == "input")
|
||||
<el-input v-model="form.${javaField}" placeholder="请输入${columnComment}" />
|
||||
<el-input v-model="form.${javaField}"#if($autoIncrementField) disabled#end placeholder="请输入${columnComment}" />
|
||||
#elseif($column.htmlType == "textarea")
|
||||
<el-input v-model="form.${javaField}" type="textarea" placeholder="请输入${columnComment}" />
|
||||
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
|
||||
@@ -391,7 +484,7 @@
|
||||
action="/api/files/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="(res) => uploadSuccess('${javaField}', res)">
|
||||
<img v-if="form.${javaField}" :src="form.${javaField}" class="avatar" />
|
||||
<img v-if="form.${javaField}" :src="resolveResourceUrl(form.${javaField})" class="avatar" />
|
||||
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
|
||||
</el-upload>
|
||||
#elseif($column.htmlType == "fileUpload")
|
||||
@@ -412,6 +505,73 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
#end
|
||||
|
||||
<el-dialog
|
||||
:title="businessActionDialogTitle"
|
||||
:visible.sync="businessActionDialogVisible"
|
||||
width="520px"
|
||||
append-to-body
|
||||
:show-close="!businessActionSubmitting"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!businessActionSubmitting"
|
||||
@closed="resetBusinessActionDialog">
|
||||
<p v-if="activeBusinessAction && activeBusinessAction.confirmMessage" class="business-action-hint">
|
||||
{{ activeBusinessAction.confirmMessage }}
|
||||
</p>
|
||||
<el-form
|
||||
ref="businessActionForm"
|
||||
:model="businessActionForm"
|
||||
label-width="110px"
|
||||
size="small">
|
||||
<el-form-item
|
||||
v-for="field in businessActionInputFields"
|
||||
:key="field.field"
|
||||
:label="field.label"
|
||||
:prop="field.field"
|
||||
:rules="[{ required: true, message: field.label + '不能为空', trigger: field.htmlType === 'select' ? 'change' : 'blur' }]">
|
||||
<el-date-picker
|
||||
v-if="field.htmlType === 'datetime'"
|
||||
v-model="businessActionForm[field.field]"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
:placeholder="'请选择' + field.label" />
|
||||
<el-select
|
||||
v-else-if="field.htmlType === 'select' && field.dictType"
|
||||
v-model="businessActionForm[field.field]"
|
||||
clearable
|
||||
:placeholder="'请选择' + field.label">
|
||||
<el-option
|
||||
v-for="option in getBusinessActionFieldOptions(field)"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else-if="field.htmlType === 'textarea'"
|
||||
v-model="businessActionForm[field.field]"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="'请输入' + field.label" />
|
||||
<el-input
|
||||
v-else
|
||||
v-model="businessActionForm[field.field]"
|
||||
:placeholder="'请输入' + field.label" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button
|
||||
:disabled="businessActionSubmitting"
|
||||
@click="businessActionDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="businessActionSubmitting"
|
||||
@click="submitBusinessAction">
|
||||
{{ activeBusinessAction && activeBusinessAction.buttonLabel ? activeBusinessAction.buttonLabel : '确认执行' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -453,9 +613,20 @@ export default {
|
||||
#end
|
||||
#end
|
||||
},
|
||||
#if($isDetailPage)
|
||||
detailRecord: {},
|
||||
detailLoading: false,
|
||||
detailLoaded: false,
|
||||
#end
|
||||
form: {},
|
||||
viewing: false,
|
||||
dialogFormVisible: false,
|
||||
businessActionDialogVisible: false,
|
||||
businessActionSubmitting: false,
|
||||
activeBusinessAction: null,
|
||||
businessActionBasePayload: {},
|
||||
businessActionForm: {},
|
||||
businessActionInputFields: [],
|
||||
multipleSelection: [],
|
||||
remoteOptionMap: {},
|
||||
remoteSelectConfig: {
|
||||
@@ -478,17 +649,53 @@ export default {
|
||||
businessActionMap: {
|
||||
#foreach($action in $allPageBusinessActions)
|
||||
"${action.code}": {
|
||||
name: "${action.name}",
|
||||
buttonLabel: "${action.buttonLabel}",
|
||||
method: "${action.method}",
|
||||
path: "${action.path}",
|
||||
requestFields: [#foreach($field in $action.requestFields)"${field}"#if($foreach.count != $action.requestFields.size()), #end#end]
|
||||
successMessage: "${action.successMessage}",
|
||||
refreshTarget: "${action.refreshTarget}",
|
||||
confirmMessage: "${action.confirmMessage}",
|
||||
requestFields: [#foreach($field in $action.requestFields)"${field}"#if($foreach.hasNext), #end#end],
|
||||
currentUserFields: [#foreach($field in $action.currentUserFields)"${field}"#if($foreach.hasNext), #end#end],
|
||||
inputFields: [
|
||||
#foreach($field in $action.inputFields)
|
||||
{
|
||||
field: "${field.field}",
|
||||
label: "${field.label}",
|
||||
htmlType: "${field.htmlType}",
|
||||
dictType: "${field.dictType}"
|
||||
}#if($foreach.hasNext),#end
|
||||
#end
|
||||
]
|
||||
}#if($foreach.count != $allPageBusinessActions.size()),#end
|
||||
#end
|
||||
},
|
||||
routeActionMap: {
|
||||
#foreach($routeAction in $pageDesignRouteActionEntries)
|
||||
"${routeAction.key}": {
|
||||
path: "${routeAction.path}",
|
||||
paramMap: {#set($paramMap = $routeAction.paramMap)#foreach($param in $paramMap.entrySet())"${param.key}":"${param.value}"#if($foreach.count != $paramMap.size()),#end#end}
|
||||
}#if($foreach.count != $pageDesignRouteActionEntries.size()),#end
|
||||
#end
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
businessActionDialogTitle() {
|
||||
if (!this.activeBusinessAction) {
|
||||
return "业务操作"
|
||||
}
|
||||
return this.activeBusinessAction.buttonLabel || this.activeBusinessAction.name || "业务操作"
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadRemoteOptions()
|
||||
#if($isDetailPage)
|
||||
this.loadDetail()
|
||||
#else
|
||||
this.load()
|
||||
#end
|
||||
},
|
||||
methods: {
|
||||
loadRemoteOptions() {
|
||||
@@ -517,6 +724,7 @@ export default {
|
||||
}
|
||||
return value === undefined || value === null || value === "" ? "-" : value
|
||||
},
|
||||
#if(!$isDetailPage)
|
||||
load() {
|
||||
this.request.get("/${moduleName}/${businessName}/page", {
|
||||
params: this.queryParams
|
||||
@@ -526,6 +734,33 @@ export default {
|
||||
this.total = page.total || 0
|
||||
})
|
||||
},
|
||||
#end
|
||||
#if($isDetailPage)
|
||||
resolveDetailId() {
|
||||
const params = this.#[[$route]]# && this.#[[$route]]#.params ? this.#[[$route]]#.params : {}
|
||||
return params.id || params.${pkColumn.javaField} || ""
|
||||
},
|
||||
loadDetail() {
|
||||
const detailId = this.resolveDetailId()
|
||||
if (!detailId) {
|
||||
this.detailRecord = {}
|
||||
this.form = {}
|
||||
this.detailLoaded = true
|
||||
return
|
||||
}
|
||||
this.detailLoading = true
|
||||
this.request.get("/${moduleName}/${businessName}/" + detailId).then(res => {
|
||||
this.detailRecord = res.data || {}
|
||||
this.form = JSON.parse(JSON.stringify(this.detailRecord))
|
||||
this.detailLoaded = true
|
||||
}).finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
},
|
||||
formatDetailValue(value) {
|
||||
return value === undefined || value === null || value === "" ? "-" : value
|
||||
},
|
||||
#end
|
||||
save() {
|
||||
this.#[[$refs]]#.form.validate(valid => {
|
||||
if (!valid) {
|
||||
@@ -543,17 +778,32 @@ export default {
|
||||
})
|
||||
},
|
||||
handleAdd() {
|
||||
if (this.handleConfiguredRoute("toolbar:create")) {
|
||||
return
|
||||
}
|
||||
this.form = {}
|
||||
this.viewing = false
|
||||
this.dialogFormVisible = true
|
||||
this.#[[$nextTick]]#(() => this.#[[$refs]]#.form && this.#[[$refs]]#.form.clearValidate())
|
||||
},
|
||||
handleView(row) {
|
||||
if (this.handleConfiguredRoute("table:view", row)) {
|
||||
return
|
||||
}
|
||||
if (this.handleConfiguredRoute("cardActions:view", row)) {
|
||||
return
|
||||
}
|
||||
this.form = JSON.parse(JSON.stringify(row))
|
||||
this.viewing = true
|
||||
this.dialogFormVisible = true
|
||||
},
|
||||
handleEdit(row) {
|
||||
if (this.handleConfiguredRoute("table:edit", row)) {
|
||||
return
|
||||
}
|
||||
if (this.handleConfiguredRoute("cardActions:edit", row)) {
|
||||
return
|
||||
}
|
||||
this.form = JSON.parse(JSON.stringify(row))
|
||||
this.viewing = false
|
||||
this.dialogFormVisible = true
|
||||
@@ -575,6 +825,32 @@ export default {
|
||||
handleCardSelectionChange() {
|
||||
this.multipleSelection = this.${businessName}List.filter(item => item.__checked)
|
||||
},
|
||||
handleConfiguredRoute(actionKey, row) {
|
||||
const action = this.routeActionMap[actionKey]
|
||||
if (!action || !action.path) {
|
||||
return false
|
||||
}
|
||||
const path = this.buildConfiguredRoutePath(action.path, action.paramMap || {}, row)
|
||||
if (!path) {
|
||||
return false
|
||||
}
|
||||
this.#[[$router]]#.push(path)
|
||||
return true
|
||||
},
|
||||
buildConfiguredRoutePath(routePath, paramMap, row) {
|
||||
let path = routePath
|
||||
Object.keys(paramMap || {}).forEach(param => {
|
||||
const field = paramMap[param]
|
||||
const value = row && row[field]
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
path = path.replace(new RegExp(":" + param + "(?=/|$)", "g"), encodeURIComponent(value))
|
||||
}
|
||||
})
|
||||
return this.hasUnresolvedRouteParam(path) ? "" : path
|
||||
},
|
||||
hasUnresolvedRouteParam(path) {
|
||||
return String(path || "").split("/").some(segment => segment.indexOf(":") === 0)
|
||||
},
|
||||
delBatch() {
|
||||
const ids = this.multipleSelection.map(item => item.${pkColumn.javaField})
|
||||
if (!ids.length) {
|
||||
@@ -599,28 +875,130 @@ export default {
|
||||
this.load()
|
||||
},
|
||||
handleBusinessAction(code, row) {
|
||||
if (this.handleConfiguredRoute("business:" + code, row) || this.handleConfiguredRoute("toolbar:" + code, row) || this.handleConfiguredRoute("table:" + code, row)) {
|
||||
return
|
||||
}
|
||||
const action = this.businessActionMap[code]
|
||||
if (!action || !action.path) {
|
||||
this.#[[$message]]#.warning("动作未配置")
|
||||
return
|
||||
}
|
||||
const payload = this.buildBusinessActionPayload(action, row)
|
||||
const missingFields = (action.inputFields || []).filter(field => !this.hasBusinessActionValue(payload[field.field]))
|
||||
if (missingFields.length > 0) {
|
||||
this.openBusinessActionDialog(action, payload, missingFields)
|
||||
return
|
||||
}
|
||||
if (action.confirmMessage) {
|
||||
this.#[[$confirm]]#(action.confirmMessage, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => this.executeBusinessAction(action, payload)).catch(() => {})
|
||||
return
|
||||
}
|
||||
this.executeBusinessAction(action, payload).catch(() => {})
|
||||
},
|
||||
openBusinessActionDialog(action, payload, fields) {
|
||||
const form = {}
|
||||
fields.forEach(field => {
|
||||
form[field.field] = this.hasBusinessActionValue(payload[field.field]) ? payload[field.field] : undefined
|
||||
})
|
||||
this.activeBusinessAction = action
|
||||
this.businessActionBasePayload = Object.assign({}, payload)
|
||||
this.businessActionInputFields = fields.slice()
|
||||
this.businessActionForm = form
|
||||
this.businessActionDialogVisible = true
|
||||
this.#[[$nextTick]]#(() => {
|
||||
const formRef = this.#[[$refs]]#.businessActionForm
|
||||
if (formRef) {
|
||||
formRef.clearValidate()
|
||||
}
|
||||
})
|
||||
},
|
||||
submitBusinessAction() {
|
||||
const formRef = this.#[[$refs]]#.businessActionForm
|
||||
if (!formRef || !this.activeBusinessAction) {
|
||||
return
|
||||
}
|
||||
formRef.validate(valid => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
const action = this.activeBusinessAction
|
||||
const payload = Object.assign({}, this.businessActionBasePayload, this.businessActionForm)
|
||||
this.businessActionSubmitting = true
|
||||
this.executeBusinessAction(action, payload)
|
||||
.then(() => {
|
||||
this.businessActionDialogVisible = false
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
this.businessActionSubmitting = false
|
||||
})
|
||||
})
|
||||
},
|
||||
resetBusinessActionDialog() {
|
||||
this.activeBusinessAction = null
|
||||
this.businessActionBasePayload = {}
|
||||
this.businessActionForm = {}
|
||||
this.businessActionInputFields = []
|
||||
},
|
||||
hasBusinessActionValue(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return false
|
||||
}
|
||||
return typeof value !== "string" || value.trim() !== ""
|
||||
},
|
||||
getBusinessActionFieldOptions(field) {
|
||||
if (!field || !field.dictType) {
|
||||
return []
|
||||
}
|
||||
return this._dict.getDictDataByType(field.dictType).map(option => ({
|
||||
label: option.name,
|
||||
value: option.value
|
||||
}))
|
||||
},
|
||||
executeBusinessAction(action, payload) {
|
||||
const method = String(action.method || "POST").toLowerCase()
|
||||
const request = method === "get"
|
||||
? this.request.get(action.path, { params: payload })
|
||||
: this.request[method] ? this.request[method](action.path, payload) : this.request.post(action.path, payload)
|
||||
request.then(res => {
|
||||
if (res.code === "200") {
|
||||
this.#[[$message]]#.success("操作成功")
|
||||
this.load()
|
||||
} else {
|
||||
this.#[[$message]]#.error(res.msg || "操作失败")
|
||||
return request.then(res => {
|
||||
if (res.code !== "200") {
|
||||
return Promise.reject(new Error(res.msg || "操作失败"))
|
||||
}
|
||||
this.#[[$message]]#.success(action.successMessage || "操作成功")
|
||||
this.refreshBusinessActionTarget(action.refreshTarget)
|
||||
return res
|
||||
}).catch(error => {
|
||||
const data = error && error.response ? error.response.data : null
|
||||
this.#[[$message]]#.error((data && data.msg) || (error && error.message) || "操作失败")
|
||||
return Promise.reject(error)
|
||||
})
|
||||
},
|
||||
refreshBusinessActionTarget(refreshTarget) {
|
||||
const target = refreshTarget || "list"
|
||||
if (target === "none") {
|
||||
return
|
||||
}
|
||||
if (target === "page") {
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
#if($isDetailPage)
|
||||
this.loadDetail()
|
||||
#else
|
||||
this.load()
|
||||
#end
|
||||
},
|
||||
buildBusinessActionPayload(action, row) {
|
||||
const payload = {}
|
||||
const currentUserFields = new Set(action.currentUserFields || [])
|
||||
;(action.requestFields || []).forEach(field => {
|
||||
if (currentUserFields.has(field)) {
|
||||
return
|
||||
}
|
||||
const value = this.resolvePayloadValue(field, row)
|
||||
if (value !== undefined) {
|
||||
payload[field] = value
|
||||
@@ -630,7 +1008,7 @@ export default {
|
||||
},
|
||||
resolvePayloadValue(field, row) {
|
||||
const camelField = this.toCamelCase(field)
|
||||
const sources = [row, this.form, this.queryParams]
|
||||
const sources = [row, this.detailRecord, this.form, this.queryParams]
|
||||
for (const source of sources) {
|
||||
if (!source) {
|
||||
continue
|
||||
@@ -655,6 +1033,21 @@ export default {
|
||||
this.queryParams.pageNum = pageNum
|
||||
this.load()
|
||||
},
|
||||
resolveResourceUrl(url) {
|
||||
if (!url) {
|
||||
return ""
|
||||
}
|
||||
if (url.indexOf("http://") === 0 || url.indexOf("https://") === 0 || url.indexOf("//") === 0 || url.indexOf("data:") === 0 || url.indexOf("blob:") === 0) {
|
||||
return url
|
||||
}
|
||||
if (url.indexOf("/api/") === 0) {
|
||||
return url
|
||||
}
|
||||
if (url.indexOf("/files/") === 0) {
|
||||
return "/api" + url
|
||||
}
|
||||
return url
|
||||
},
|
||||
uploadSuccess(field, res) {
|
||||
if (res && res.code === "200" && res.data && res.data.url) {
|
||||
this.#[[$set]]#(this.form, field, res.data.url)
|
||||
@@ -674,9 +1067,14 @@ export default {
|
||||
|
||||
.embedded-business-blocks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.embedded-business-block__item--span-12 { grid-column: span 12; }
|
||||
.embedded-business-block__item--span-6 { grid-column: span 6; }
|
||||
.embedded-business-block__item--span-4 { grid-column: span 4; }
|
||||
|
||||
.content-board {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.36fr) minmax(320px, 0.64fr);
|
||||
@@ -691,6 +1089,12 @@ export default {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.embedded-business-block__item {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-illustration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -999,6 +1403,66 @@ export default {
|
||||
border-top: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.detail-view {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 14px 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.detail-field.wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-field dt {
|
||||
color: #7b8493;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-field dd {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-image {
|
||||
width: 180px;
|
||||
height: 128px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.empty-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 120px;
|
||||
color: #7b8493;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 14px;
|
||||
text-align: right;
|
||||
@@ -1037,6 +1501,23 @@ export default {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.business-action-hint {
|
||||
margin: 0 0 18px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
color: #606266;
|
||||
background: #f5f7fa;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.business-action-hint + .el-form .el-date-editor,
|
||||
.business-action-hint + .el-form .el-select,
|
||||
.business-action-hint + .el-form .el-input,
|
||||
.el-dialog .el-form .el-date-editor,
|
||||
.el-dialog .el-form .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.content-board {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "${businessName}-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve --host 0.0.0.0",
|
||||
"serve": "vue-cli-service serve --host 0.0.0.0",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"core-js": "^3.8.3",
|
||||
"echarts": "5.4.0",
|
||||
"element-ui": "^2.15.14",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,7 @@ public class BusinessBlockGenerationServiceTest
|
||||
|
||||
assertTrue(pageContent.contains("portal-page"));
|
||||
assertTrue(pageContent.contains("import Notice001Block from \"@/views/pages/book_catalog/blocks/notice_001/NoticeBlock.vue\""));
|
||||
assertTrue(pageContent.contains("embedded-business-block__item--span-12"));
|
||||
assertTrue(pageContent.contains("<Notice001Block />"));
|
||||
}
|
||||
|
||||
@@ -367,6 +368,41 @@ public class BusinessBlockGenerationServiceTest
|
||||
assertTrue(rendered.get("controller").contains("@GetMapping(\"/data\")"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedChartComponentHasBlockLevelStatesAndCleanup()
|
||||
{
|
||||
GenProject project = chartProject();
|
||||
String component = renderChartFrontendFile(project, "frontendComponent");
|
||||
String api = renderChartFrontendFile(project, "frontendApi");
|
||||
String runtime = service.renderFile(project,
|
||||
findPath(service.listFiles(project, "admin_frontend"), "src/utils/chartRuntime.js"));
|
||||
|
||||
assertTrue(component.contains("el-skeleton"));
|
||||
assertTrue(component.contains("el-empty"));
|
||||
assertTrue(component.contains("retry"));
|
||||
assertTrue(component.contains("disposeChart"));
|
||||
assertTrue(component.contains("window.removeEventListener"));
|
||||
assertTrue(component.contains("chartType: \"line\""));
|
||||
assertTrue(api.contains("url: \"/admin/business-blocks/admin_line_chart_001/data\""));
|
||||
assertTrue(runtime.contains("import * as echarts from \"echarts\""));
|
||||
assertTrue(runtime.contains("buildChartOption"));
|
||||
assertTrue(runtime.contains("hasChartData"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlockPageUsesTwelveColumnSpanClasses()
|
||||
{
|
||||
GenProject project = chartProject();
|
||||
GeneratedBusinessBlockFile pageFile = findPath(service.listFiles(project, "admin_frontend"),
|
||||
"src/views/pages/sales_dashboard/index.vue");
|
||||
|
||||
String page = service.renderFile(project, pageFile);
|
||||
|
||||
assertTrue(page.contains("business-block-page__item--span-6"));
|
||||
assertTrue(page.contains("business-block-page__item--span-4"));
|
||||
assertTrue(page.contains("grid-template-columns: repeat(12, minmax(0, 1fr))"));
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
{
|
||||
Field field = BusinessBlockGenerationService.class.getDeclaredField(name);
|
||||
@@ -442,6 +478,13 @@ public class BusinessBlockGenerationServiceTest
|
||||
return rendered;
|
||||
}
|
||||
|
||||
private String renderChartFrontendFile(GenProject project, String templateKey)
|
||||
{
|
||||
GeneratedBusinessBlockFile file = findBlockFile(service.listFiles(project, "admin_frontend"),
|
||||
"admin_line_chart_001", templateKey);
|
||||
return service.renderFile(project, file);
|
||||
}
|
||||
|
||||
private void assertRenderedFiles(GenProject project, List<GeneratedBusinessBlockFile> files)
|
||||
{
|
||||
for (GeneratedBusinessBlockFile file : files)
|
||||
|
||||
@@ -10,6 +10,10 @@ import com.ruoyi.generator.domain.GenProject;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppMenuDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionInputFieldDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionRuleDesign;
|
||||
import com.ruoyi.generator.service.GenTableServiceImpl;
|
||||
import com.ruoyi.generator.service.GenProjectServiceImpl;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -20,7 +24,9 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.velocity.Template;
|
||||
@@ -62,6 +68,15 @@ public class QingTemplateSupportTest
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingPackageIncludesEcharts()
|
||||
{
|
||||
VelocityInitializer.initVelocity();
|
||||
String content = render("qing/vue-package.json.vm", VelocityUtils.prepareContextProject(project()));
|
||||
|
||||
assertTrue(content.contains("\"echarts\": \"5.4.0\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingTemplatesAllResolveRunnableOutputFileNames()
|
||||
{
|
||||
@@ -128,6 +143,145 @@ public class QingTemplateSupportTest
|
||||
assertTrue(controllerTemplate.contains("${className}Service.saveOrUpdate(${className})"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingEntityImportsDateTypesForStandaloneAuditColumns()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
GenTableColumn createTime = column("create_time", "创建时间", "Date", "createTime",
|
||||
"0", "1", "0", "1", "EQ", "datetime");
|
||||
GenTableColumn updateTime = column("update_time", "更新时间", "Date", "updateTime",
|
||||
"0", "1", "0", "1", "EQ", "datetime");
|
||||
List<GenTableColumn> columns = new ArrayList<GenTableColumn>(table.getColumns());
|
||||
columns.add(createTime);
|
||||
columns.add(updateTime);
|
||||
table.setColumns(columns);
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
String rendered = render("qing/entity.java.vm", VelocityUtils.prepareQingContext(table));
|
||||
|
||||
assertTrue(rendered.contains("import java.util.Date;"));
|
||||
assertTrue(rendered.contains("import com.fasterxml.jackson.annotation.JsonFormat;"));
|
||||
assertTrue(rendered.contains("private Date createTime;"));
|
||||
assertTrue(rendered.contains("private Date updateTime;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingTemplatesRenderExecutableBusinessActions()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
BusinessActionDesign action = businessAction();
|
||||
action.setCurrentUserFields(Arrays.asList("user_id", "username"));
|
||||
BusinessActionRuleDesign rule = new BusinessActionRuleDesign();
|
||||
rule.setType("NUMBER_GTE");
|
||||
rule.setTargetTable("demo_product");
|
||||
rule.setTargetField("product_stock");
|
||||
rule.setCompareValue("1");
|
||||
rule.setConditionFields(Arrays.asList("id"));
|
||||
rule.setMessage("Stock is not enough");
|
||||
action.setRuleChecks(Arrays.asList(rule));
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
context.put("tableBusinessActions", Arrays.asList(action));
|
||||
context.put("hasTableBusinessActions", true);
|
||||
|
||||
String controller = render("qing/controller.java.vm", context);
|
||||
String service = render("qing/service.java.vm", context);
|
||||
String implementation = render("qing/serviceImpl.java.vm", context);
|
||||
|
||||
assertTrue(controller.contains("@PostMapping(\"/library/borrow/borrow\")"));
|
||||
assertTrue(controller.contains("actionParams.put(\"__currentUserId\", currentUser.getUserId())"));
|
||||
assertTrue(controller.contains(
|
||||
"prepareBusinessActionParams(params, request, new String[] {\"user_id\", \"username\"})"));
|
||||
assertTrue(controller.contains("\"username\".equals(field)"));
|
||||
assertTrue(controller.contains("currentUser.getUsername()"));
|
||||
assertTrue(controller.contains("actionParams.put(field, currentUser.getUserId())"));
|
||||
assertTrue(service.contains("int borrow_book(Map<String, Object> params)"));
|
||||
assertTrue(implementation.contains("@Transactional(rollbackFor = Exception.class)"));
|
||||
assertTrue(implementation.contains("resolveBusinessValue"));
|
||||
assertTrue(implementation.contains("\"${current_user.id}\""));
|
||||
assertTrue(implementation.contains(
|
||||
"assertNumberGte(params, \"demo_product\", \"product_stock\", \"1\", new String[] {\"id\"}"));
|
||||
assertTrue(implementation.contains("where.append(safeField).append(\" = ?\")"));
|
||||
assertFalse(implementation.contains("conditionColumns"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingBackendResolvesLegacyCurrentUserIdPlaceholder()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
BusinessActionDesign action = businessAction();
|
||||
action.getEffects().get(0).getValues().put("user_id", "${current_user_id}");
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
context.put("tableBusinessActions", Arrays.asList(action));
|
||||
context.put("hasTableBusinessActions", true);
|
||||
|
||||
String implementation = render("qing/serviceImpl.java.vm", context);
|
||||
|
||||
assertTrue(implementation.contains(
|
||||
"insertValues0.put(\"user_id\", resolveBusinessValue(params, \"${current_user_id}\"))"));
|
||||
assertTrue(implementation.contains("\"${current_user_id}\".equals(valueExpression)"));
|
||||
assertTrue(implementation.contains("return params.get(\"__currentUserId\")"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingFrontendUsesOneDialogForMissingBusinessActionFields()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
BusinessActionDesign action = businessAction();
|
||||
action.setButtonLabel("Borrow");
|
||||
action.setCurrentUserFields(Arrays.asList("user_id"));
|
||||
BusinessActionInputFieldDesign dueDate = new BusinessActionInputFieldDesign();
|
||||
dueDate.setField("due_date");
|
||||
dueDate.setLabel("Due date");
|
||||
dueDate.setHtmlType("datetime");
|
||||
dueDate.setDictType("");
|
||||
action.setInputFields(Arrays.asList(dueDate));
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
context.put("pageDesignBusinessActions", Arrays.asList(action));
|
||||
|
||||
String page = render("qing/index.vue.vm", context);
|
||||
|
||||
assertTrue(page.contains("businessActionDialogVisible"));
|
||||
assertTrue(page.contains("businessActionInputFields"));
|
||||
assertTrue(page.contains("submitBusinessAction"));
|
||||
assertTrue(page.contains("openBusinessActionDialog"));
|
||||
assertTrue(page.contains("currentUserFields: [\"user_id\"]"));
|
||||
assertTrue(page.contains("field: \"due_date\""));
|
||||
assertTrue(page.contains("label: \"Due date\""));
|
||||
assertTrue(page.contains("htmlType: \"datetime\""));
|
||||
assertTrue(page.contains("v-for=\"field in businessActionInputFields\""));
|
||||
assertTrue(page.contains("field.htmlType === 'datetime'"));
|
||||
assertTrue(page.contains("field.htmlType === 'textarea'"));
|
||||
assertTrue(page.contains("getBusinessActionFieldOptions(field)"));
|
||||
assertFalse(page.contains("this.$prompt"));
|
||||
assertFalse(page.contains("collectBusinessActionPayload"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingCrudFormHidesAutoIncrementIdOnCreateAndShowsItReadOnlyOnEdit()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
GenTableColumn id = column("id", "ID", "Long", "id",
|
||||
"1", "1", "1", "1", "EQ", "input");
|
||||
id.setIsRequired("1");
|
||||
GenTableColumn name = column("name", "Name", "String", "name",
|
||||
"0", "1", "1", "1", "LIKE", "input");
|
||||
name.setIsRequired("1");
|
||||
table.setPkColumn(id);
|
||||
table.setColumns(Arrays.asList(id, name));
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
String rendered = render("qing/index.vue.vm", VelocityUtils.prepareContext(table));
|
||||
|
||||
assertTrue(rendered.contains("<el-form-item v-if=\"form.id\" label=\"ID\" prop=\"id\">"));
|
||||
assertTrue(rendered.contains("<el-input v-model=\"form.id\" disabled"));
|
||||
assertFalse(rendered.contains("prop=\"id\" :rules="));
|
||||
assertTrue(rendered.contains("<el-form-item label=\"Name\" prop=\"name\" :rules="));
|
||||
assertTrue(rendered.contains("<el-input v-model=\"form.name\" placeholder="));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingTemplatesRenderWithSampleCrudTable()
|
||||
{
|
||||
@@ -222,6 +376,7 @@ public class QingTemplateSupportTest
|
||||
assertTrue(rendered.contains("PortalAuthInterceptor"));
|
||||
assertTrue(rendered.contains("excludePathPatterns(\"/auth/login\""));
|
||||
assertTrue(rendered.contains("\"/auth/admin/login\""));
|
||||
assertTrue(rendered.contains("\"/business-blocks/**\""));
|
||||
}
|
||||
if ("qing/sql.vm".equals(templateName))
|
||||
{
|
||||
@@ -269,6 +424,37 @@ public class QingTemplateSupportTest
|
||||
assertTrue(rendered.contains("{{ getOptionLabel('categoryId', scope.row.categoryId) }}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingIndexTemplateResolvesUploadedResourceUrlsForPreviewProxy()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
GenTableColumn cover = column("cover_url", "Cover", "String", "coverUrl",
|
||||
"0", "1", "1", "1", "EQ", "imageUpload");
|
||||
List<GenTableColumn> columns = new ArrayList<GenTableColumn>(table.getColumns());
|
||||
columns.add(cover);
|
||||
table.setColumns(columns);
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
String rendered = render("qing/index.vue.vm", VelocityUtils.prepareContext(table));
|
||||
|
||||
assertTrue(rendered.contains(":src=\"resolveResourceUrl(scope.row.coverUrl)\""));
|
||||
assertTrue(rendered.contains(":preview-src-list=\"[resolveResourceUrl(scope.row.coverUrl)]\""));
|
||||
assertTrue(rendered.contains(":src=\"resolveResourceUrl(form.coverUrl)\""));
|
||||
assertTrue(rendered.contains("resolveResourceUrl(url)"));
|
||||
assertTrue(rendered.contains("return \"/api\" + url"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingIndexTemplateBuildsConfiguredRoutesWithoutSqlFragileRegexLiteral()
|
||||
{
|
||||
VelocityInitializer.initVelocity();
|
||||
String rendered = render("qing/index.vue.vm", VelocityUtils.prepareContext(renderTable()));
|
||||
|
||||
assertTrue(rendered.contains("hasUnresolvedRouteParam(path)"));
|
||||
assertFalse(rendered.contains("return /(^|\\/):[^/]+/.test(path) ? \"\" : path"));
|
||||
assertFalse(rendered.contains("return /(^|/):[^/]+/.test(path) ? \"\" : path"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingProjectTemplatesRenderWithSampleProject()
|
||||
{
|
||||
@@ -489,6 +675,229 @@ public class QingTemplateSupportTest
|
||||
assertTrue(router.contains("component: BookCatalogPage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingPortalAppRendersPageDesignerMenuHierarchy()
|
||||
{
|
||||
GenProject project = project();
|
||||
project.setProjectName("Library Portal");
|
||||
GenTable book = table();
|
||||
book.setTableName("book_info");
|
||||
book.setModuleName("library");
|
||||
book.setBusinessName("book");
|
||||
book.setClassName("Book");
|
||||
book.setFunctionName("Books");
|
||||
project.setTables(Arrays.asList(book));
|
||||
|
||||
FrontProjectPageDesign home = new FrontProjectPageDesign();
|
||||
home.setPageCode("home_page");
|
||||
home.setMenuCode("home_page");
|
||||
home.setPageName("Home");
|
||||
home.setRoutePath("/home");
|
||||
home.setPageScope("frontend");
|
||||
home.setPageType("list");
|
||||
home.setTableName("book_info");
|
||||
home.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"home_page\",\"menuName\":\"Home\",\"menuOrder\":1},\"regions\":[]}");
|
||||
|
||||
FrontProjectPageDesign directory = new FrontProjectPageDesign();
|
||||
directory.setPageCode("library_directory");
|
||||
directory.setMenuCode("library_directory");
|
||||
directory.setPageName("Library");
|
||||
directory.setPageScope("frontend");
|
||||
directory.setPageType("menu_directory");
|
||||
directory.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"nodeType\":\"directory\",\"menuCode\":\"library_directory\",\"menuName\":\"Library\",\"menuOrder\":2}}");
|
||||
|
||||
FrontProjectPageDesign books = new FrontProjectPageDesign();
|
||||
books.setPageCode("books_page");
|
||||
books.setMenuCode("library_directory");
|
||||
books.setPageName("Books");
|
||||
books.setRoutePath("/books");
|
||||
books.setPageScope("frontend");
|
||||
books.setPageType("list");
|
||||
books.setTableName("book_info");
|
||||
books.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"secondary\",\"parentMenuCode\":\"library_directory\",\"parentMenuName\":\"Library\",\"menuOrder\":1},\"regions\":[]}");
|
||||
|
||||
FrontProjectPageDesign notices = new FrontProjectPageDesign();
|
||||
notices.setPageCode("notices_page");
|
||||
notices.setMenuCode("notices_page");
|
||||
notices.setPageName("Notices");
|
||||
notices.setRoutePath("/notices");
|
||||
notices.setPageScope("frontend");
|
||||
notices.setPageType("list");
|
||||
notices.setTableName("book_info");
|
||||
notices.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"notices_page\",\"menuName\":\"Notices\",\"menuOrder\":3},\"regions\":[]}");
|
||||
project.setPageDesigns(Arrays.asList(home, directory, books, notices));
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContextProject(project);
|
||||
|
||||
assertEquals(true, context.get("hasFrontendNavGroups"));
|
||||
String app = render("qing/vue-app.vue.vm", context);
|
||||
assertTrue(app.contains("class=\"portal-nav-group has-children\""));
|
||||
assertTrue(app.contains(":class='{ active: isNavGroupActive([\"/home\"]) }'"));
|
||||
assertFalse(app.contains(":class=\"{ active: isNavGroupActive([\""));
|
||||
assertTrue(app.contains("portal-nav-directory"));
|
||||
assertTrue(app.contains("class=\"portal-nav-dropdown\""));
|
||||
assertTrue(app.contains(">Library<"));
|
||||
assertTrue(app.contains(">Books<"));
|
||||
assertTrue(app.indexOf(">Home<") < app.indexOf(">Library<"));
|
||||
assertTrue(app.indexOf(">Library<") < app.indexOf(">Notices<"));
|
||||
|
||||
String router = render("qing/vue-router.js.vm", context);
|
||||
assertFalse(router.contains("LibraryDirectory"));
|
||||
assertTrue(router.contains("path: \"/books\""));
|
||||
assertTrue(router.contains("path: \"/notices\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingPortalTreatsLegacyMenuCodeAsPrimaryMenu()
|
||||
{
|
||||
GenProject project = project();
|
||||
project.setProjectName("Library Portal");
|
||||
GenTable announcement = table();
|
||||
announcement.setTableName("announcement");
|
||||
announcement.setModuleName("library");
|
||||
announcement.setBusinessName("announcement");
|
||||
announcement.setClassName("Announcement");
|
||||
announcement.setFunctionName("Announcements");
|
||||
project.setTables(Arrays.asList(announcement));
|
||||
|
||||
FrontProjectPageDesign page = new FrontProjectPageDesign();
|
||||
page.setPageCode("announcement_list_page");
|
||||
page.setMenuCode("announcements");
|
||||
page.setPageName("\u516c\u544a\u5217\u8868");
|
||||
page.setRoutePath("/announcements");
|
||||
page.setPageScope("frontend");
|
||||
page.setPageType("list");
|
||||
page.setTableName("announcement");
|
||||
page.setLayoutJson("{\"canvas\":\"frontend-list-v1\",\"regions\":[]}");
|
||||
project.setPageDesigns(Arrays.asList(page));
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContextProject(project);
|
||||
String app = render("qing/vue-app.vue.vm", context);
|
||||
|
||||
assertTrue(app.contains("class=\"portal-nav-parent\" to=\"/announcements\" exact>\u516c\u544a\u5217\u8868</router-link>"));
|
||||
assertFalse(app.contains("portal-nav-directory\">announcements</span>"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void qingAdminContextUsesPageDesignerMenuHierarchy()
|
||||
{
|
||||
GenProject project = project();
|
||||
GenTable book = table();
|
||||
book.setTableName("book_info");
|
||||
book.setModuleName("library");
|
||||
book.setBusinessName("book");
|
||||
book.setClassName("Book");
|
||||
book.setFunctionName("Books");
|
||||
GenTable user = table();
|
||||
user.setTableName("user_info");
|
||||
user.setModuleName("system");
|
||||
user.setBusinessName("user");
|
||||
user.setClassName("User");
|
||||
user.setFunctionName("Users");
|
||||
project.setTables(Arrays.asList(book, user));
|
||||
|
||||
FrontProjectPageDesign directory = new FrontProjectPageDesign();
|
||||
directory.setPageCode("admin_library");
|
||||
directory.setMenuCode("admin_library");
|
||||
directory.setPageName("Library");
|
||||
directory.setPageScope("admin");
|
||||
directory.setPageType("menu_directory");
|
||||
directory.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"nodeType\":\"directory\",\"menuCode\":\"admin_library\",\"menuName\":\"Library\",\"menuOrder\":1}}");
|
||||
|
||||
FrontProjectPageDesign books = new FrontProjectPageDesign();
|
||||
books.setPageCode("admin_books");
|
||||
books.setMenuCode("admin_library");
|
||||
books.setPageName("Books");
|
||||
books.setRoutePath("/admin/books");
|
||||
books.setPageScope("admin");
|
||||
books.setPageType("list");
|
||||
books.setTableName("book_info");
|
||||
books.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"secondary\",\"parentMenuCode\":\"admin_library\",\"parentMenuName\":\"Library\",\"menuOrder\":1}}");
|
||||
|
||||
FrontProjectPageDesign users = new FrontProjectPageDesign();
|
||||
users.setPageCode("admin_users");
|
||||
users.setMenuCode("admin_users");
|
||||
users.setPageName("Users");
|
||||
users.setRoutePath("/admin/users");
|
||||
users.setPageScope("admin");
|
||||
users.setPageType("list");
|
||||
users.setTableName("user_info");
|
||||
users.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"admin_users\",\"menuName\":\"Users\",\"menuOrder\":2}}");
|
||||
project.setPageDesigns(Arrays.asList(directory, books, users));
|
||||
|
||||
VelocityContext context = VelocityUtils.prepareContextProject(project);
|
||||
|
||||
assertEquals(true, context.get("hasAdminNavGroups"));
|
||||
assertEquals("/admin/books", context.get("defaultAdminPath"));
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) context.get("adminNavGroups");
|
||||
assertEquals(2, groups.size());
|
||||
assertEquals("Library", groups.get(0).get("name"));
|
||||
assertEquals(true, groups.get(0).get("directory"));
|
||||
assertEquals(true, groups.get(0).get("hasChildren"));
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) groups.get(0).get("children");
|
||||
assertEquals("Books", children.get(0).get("name"));
|
||||
assertEquals("/admin/books", children.get(0).get("path"));
|
||||
|
||||
List<Map<String, Object>> routeItems = (List<Map<String, Object>>) context.get("adminRouteItems");
|
||||
assertEquals(2, routeItems.size());
|
||||
assertEquals("/admin/books", routeItems.get(0).get("path"));
|
||||
assertEquals("Book", routeItems.get(0).get("componentName"));
|
||||
assertEquals("/admin/users", routeItems.get(1).get("path"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void qingAdminContextRoutesBusinessBlockHomePageDesign()
|
||||
{
|
||||
GenProject project = project();
|
||||
FrontProjectPageDesign home = new FrontProjectPageDesign();
|
||||
home.setPageCode("admin_home");
|
||||
home.setMenuCode("admin_home");
|
||||
home.setPageName("Home");
|
||||
home.setRoutePath("/admin/home");
|
||||
home.setPageScope("admin");
|
||||
home.setPageType("business_block");
|
||||
home.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":[],\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"admin_home\",\"menuName\":\"Home\",\"menuOrder\":1}}");
|
||||
project.setPageDesigns(Arrays.asList(home));
|
||||
|
||||
VelocityContext context = VelocityUtils.prepareContextProject(project);
|
||||
|
||||
assertEquals("/admin/home", context.get("defaultAdminPath"));
|
||||
List<Map<String, Object>> routeItems = (List<Map<String, Object>>) context.get("adminRouteItems");
|
||||
assertEquals(1, routeItems.size());
|
||||
assertEquals("/admin/home", routeItems.get(0).get("path"));
|
||||
assertEquals("AdminHome", routeItems.get(0).get("componentName"));
|
||||
assertEquals("@/views/pages/admin_home/index.vue", routeItems.get(0).get("componentPath"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingPortalSkipsMenuDirectoryPageDesignsAsRoutes()
|
||||
{
|
||||
GenProject project = project();
|
||||
FrontProjectPageDesign directory = new FrontProjectPageDesign();
|
||||
directory.setPageCode("library_directory");
|
||||
directory.setMenuCode("library_directory");
|
||||
directory.setPageName("Library");
|
||||
directory.setPageScope("frontend");
|
||||
directory.setPageType("menu_directory");
|
||||
directory.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"nodeType\":\"directory\",\"menuCode\":\"library_directory\",\"menuName\":\"Library\"}}");
|
||||
project.setPageDesigns(Arrays.asList(directory));
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContextProject(project);
|
||||
|
||||
String app = render("qing/vue-app.vue.vm", context);
|
||||
assertTrue(app.contains("Library"));
|
||||
assertTrue(app.contains("portal-nav-directory"));
|
||||
|
||||
String router = render("qing/vue-router.js.vm", context);
|
||||
assertFalse(router.contains("LibraryDirectory"));
|
||||
assertFalse(router.contains("library_directory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingIndexTemplateUsesFrontendPageDesignFields()
|
||||
{
|
||||
@@ -546,6 +955,99 @@ public class QingTemplateSupportTest
|
||||
assertTrue(rendered.contains(":disabled=\"viewing\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingIndexTemplateRoutesConfiguredViewActionToAssociatedDetailPage()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
GenProject project = project();
|
||||
project.setTables(Arrays.asList(table));
|
||||
FrontProjectPageDesign listDesign = new FrontProjectPageDesign();
|
||||
listDesign.setPageCode("product_page");
|
||||
listDesign.setTableName("demo_product");
|
||||
listDesign.setRoutePath("/products");
|
||||
listDesign.setLayoutJson("{\"regions\":["
|
||||
+ "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"view\"]},"
|
||||
+ "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"productName\"]}"
|
||||
+ "],\"interactions\":{\"actions\":{\"table:view\":{\"type\":\"route\",\"targetPageCode\":\"product_detail_page\",\"paramMap\":{\"id\":\"productId\"},\"fallback\":\"dialog\"}}}}");
|
||||
FrontProjectPageDesign detailDesign = new FrontProjectPageDesign();
|
||||
detailDesign.setPageCode("product_detail_page");
|
||||
detailDesign.setTableName("demo_product");
|
||||
detailDesign.setPageType("detail");
|
||||
detailDesign.setRoutePath("/products/:id");
|
||||
detailDesign.setLayoutJson("{\"navigation\":{\"showInMenu\":false,\"parentPageCode\":\"product_page\"}}");
|
||||
project.setPageDesigns(Arrays.asList(listDesign, detailDesign));
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", "product_page");
|
||||
|
||||
String rendered = render("qing/index.vue.vm", context);
|
||||
|
||||
assertTrue(rendered.contains("\"table:view\": {"));
|
||||
assertTrue(rendered.contains("path: \"/products/:id\""));
|
||||
assertTrue(rendered.contains("paramMap: {\"id\":\"productId\"}"));
|
||||
assertTrue(rendered.contains("if (this.handleConfiguredRoute(\"table:view\", row))"));
|
||||
assertTrue(rendered.contains("this.$router.push(path)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingIndexTemplateRepairsLegacyDetailRouteAndInfersViewParamMap()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
GenProject project = project();
|
||||
project.setTables(Arrays.asList(table));
|
||||
FrontProjectPageDesign listDesign = new FrontProjectPageDesign();
|
||||
listDesign.setPageCode("product_page");
|
||||
listDesign.setTableName("demo_product");
|
||||
listDesign.setRoutePath("/products");
|
||||
listDesign.setLayoutJson("{\"regions\":["
|
||||
+ "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"view\"]}"
|
||||
+ "],\"interactions\":{\"actions\":{\"table:view\":{\"type\":\"route\",\"targetPageCode\":\"product_detail_page\",\"paramMap\":{}}}}}");
|
||||
FrontProjectPageDesign detailDesign = new FrontProjectPageDesign();
|
||||
detailDesign.setPageCode("product_detail_page");
|
||||
detailDesign.setTableName("demo_product");
|
||||
detailDesign.setPageType("detail");
|
||||
detailDesign.setRoutePath("/products/-id");
|
||||
detailDesign.setLayoutJson("{\"navigation\":{\"showInMenu\":false,\"parentPageCode\":\"product_page\"}}");
|
||||
project.setPageDesigns(Arrays.asList(listDesign, detailDesign));
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", "product_page");
|
||||
|
||||
String rendered = render("qing/index.vue.vm", context);
|
||||
|
||||
assertTrue(rendered.contains("path: \"/products/:id\""));
|
||||
assertTrue(rendered.contains("paramMap: {\"id\":\"productId\"}"));
|
||||
assertFalse(rendered.contains("path: \"/products/-id\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingIndexTemplateRendersDetailPageAsSingleRecord()
|
||||
{
|
||||
GenTable table = renderTable();
|
||||
GenProject project = project();
|
||||
project.setTables(Arrays.asList(table));
|
||||
FrontProjectPageDesign detailDesign = new FrontProjectPageDesign();
|
||||
detailDesign.setPageCode("product_detail_page");
|
||||
detailDesign.setTableName("demo_product");
|
||||
detailDesign.setPageType("detail");
|
||||
detailDesign.setRoutePath("/products/:id");
|
||||
detailDesign.setLayoutJson("{\"navigation\":{\"showInMenu\":false,\"parentPageCode\":\"product_page\"}}");
|
||||
project.setPageDesigns(Arrays.asList(detailDesign));
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", "product_detail_page");
|
||||
|
||||
String rendered = render("qing/index.vue.vm", context);
|
||||
|
||||
assertTrue(rendered.contains("detailRecord: {}"));
|
||||
assertTrue(rendered.contains("loadDetail()"));
|
||||
assertTrue(rendered.contains("const detailId = this.resolveDetailId()"));
|
||||
assertTrue(rendered.contains("this.request.get(\"/demo/product/\" + detailId)"));
|
||||
assertTrue(rendered.contains("{{ formatDetailValue(detailRecord.productName) }}"));
|
||||
assertFalse(rendered.contains("<el-table :data=\"productList\""));
|
||||
assertFalse(rendered.contains("this.request.get(\"/demo/product/page\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingCurrentUserListAddsUserFilterFlagAndControllerFilter()
|
||||
{
|
||||
@@ -643,7 +1145,7 @@ public class QingTemplateSupportTest
|
||||
assertEquals(false, context.get("pageDesignShowQuery"));
|
||||
assertEquals(cover, context.get("pageDesignCardImageColumn"));
|
||||
assertTrue(rendered.contains("class=\"resource-card-image\""));
|
||||
assertTrue(rendered.contains(":src=\"item.coverUrl\""));
|
||||
assertTrue(rendered.contains(":src=\"resolveResourceUrl(item.coverUrl)\""));
|
||||
assertFalse(rendered.contains("class=\"filter-bar\""));
|
||||
assertFalse(rendered.contains("productName: undefined"));
|
||||
assertTrue(rendered.contains("@click=\"handleEdit(item)\""));
|
||||
@@ -739,6 +1241,7 @@ public class QingTemplateSupportTest
|
||||
assertTrue(sql.contains("'PortalAuthInterceptor.java.vm'"));
|
||||
assertTrue(sql.contains("'AdminAuthController.java.vm'"));
|
||||
assertTrue(sql.contains("'PortalAuthController.java.vm'"));
|
||||
assertTrue(sql.contains("\"/business-blocks/**\""));
|
||||
assertTrue(sql.contains("create table if not exists t_admin"));
|
||||
assertTrue(sql.contains("insert into t_admin"));
|
||||
assertTrue(sql.contains("select ''admin''"));
|
||||
@@ -781,8 +1284,47 @@ public class QingTemplateSupportTest
|
||||
sql.contains("handleBusinessAction"));
|
||||
assertTrue("Frontend SQL template should serialize page-designed business actions",
|
||||
sql.contains("businessActionMap"));
|
||||
assertTrue("Frontend SQL template should serialize business button labels",
|
||||
sql.contains("$action.buttonLabel"));
|
||||
assertTrue("Frontend SQL template should serialize business button styles",
|
||||
sql.contains("type=\"${buttonType}\""));
|
||||
assertTrue("Frontend SQL template should serialize business button icons",
|
||||
sql.contains("icon=\"${buttonIcon}\""));
|
||||
assertTrue("Frontend SQL template should execute configured business actions",
|
||||
sql.contains("executeBusinessAction"));
|
||||
assertTrue("Frontend SQL template should refresh configured business action target",
|
||||
sql.contains("refreshBusinessActionTarget"));
|
||||
assertTrue("Frontend SQL template should support business action confirmation",
|
||||
sql.contains("confirmMessage"));
|
||||
assertTrue("Frontend SQL template should support business action success messages",
|
||||
sql.contains("successMessage"));
|
||||
assertTrue("Frontend SQL template should support business action refresh targets",
|
||||
sql.contains("refreshTarget"));
|
||||
assertTrue("Frontend SQL template should render detail pages as a single record",
|
||||
sql.contains("detailRecord: {}"));
|
||||
assertTrue("Frontend SQL template should load detail pages by route id",
|
||||
sql.contains("loadDetail()"));
|
||||
assertTrue("Frontend SQL template should avoid SQL-fragile route param regex literals",
|
||||
sql.contains("hasUnresolvedRouteParam(path)"));
|
||||
assertFalse("Frontend SQL template should not keep escaped regex literals that MySQL can unescape",
|
||||
sql.contains("return /(^|\\/):[^/]+/.test(path) ? \"\" : path"));
|
||||
assertFalse("Frontend SQL template should not contain already-broken route regex literals",
|
||||
sql.contains("return /(^|/):[^/]+/.test(path) ? \"\" : path"));
|
||||
assertTrue("Frontend SQL template should resolve uploaded image resources through the preview proxy",
|
||||
sql.contains("resolveResourceUrl(url)"));
|
||||
assertTrue(sql.contains(":src=\"resolveResourceUrl(scope.row.${javaField})\""));
|
||||
assertTrue(sql.contains(":src=\"resolveResourceUrl(form.${javaField})\""));
|
||||
assertTrue(sql.contains(":src=\"resolveResourceUrl(item.${cardImageColumn.javaField})\""));
|
||||
assertFalse(sql.contains(":src=\"scope.row.${javaField}\""));
|
||||
assertFalse(sql.contains(":src=\"form.${javaField}\""));
|
||||
assertFalse(sql.contains(":src=\"item.${cardImageColumn.javaField}\""));
|
||||
assertTrue(sql.contains("${projectName}"));
|
||||
assertTrue(sql.contains("#foreach($item in $frontendNavItems)"));
|
||||
assertTrue(sql.contains("#foreach($group in $frontendNavGroups)"));
|
||||
assertTrue(sql.contains("portal-nav-group#if($group.hasChildren) has-children#end"));
|
||||
assertTrue(sql.contains(":class=''{ active: isNavGroupActive(${group.activePathsLiteral}) }''"));
|
||||
assertTrue(sql.contains(":class=''{ active: isSidebarGroupActive(${group.activePathsLiteral}) }''"));
|
||||
assertFalse(sql.contains(":class=\"{ active: isNavGroupActive"));
|
||||
assertFalse(sql.contains(":class=\"{ active: isSidebarGroupActive"));
|
||||
assertTrue(sql.contains("frontendRouteItems"));
|
||||
assertFalse(sql.contains("游戏资源平台"));
|
||||
assertFalse(sql.contains("游戏攻略"));
|
||||
@@ -833,8 +1375,44 @@ public class QingTemplateSupportTest
|
||||
dbSql.contains("resource-card-grid"));
|
||||
assertTrue("db.sql should include page-designed business buttons",
|
||||
dbSql.contains("handleBusinessAction"));
|
||||
assertTrue("db.sql should serialize business button labels",
|
||||
dbSql.contains("$action.buttonLabel"));
|
||||
assertTrue("db.sql should serialize business button styles",
|
||||
dbSql.contains("type=\"${buttonType}\""));
|
||||
assertTrue("db.sql should serialize business button icons",
|
||||
dbSql.contains("icon=\"${buttonIcon}\""));
|
||||
assertTrue("db.sql should execute configured business actions",
|
||||
dbSql.contains("executeBusinessAction"));
|
||||
assertTrue("db.sql should refresh configured business action target",
|
||||
dbSql.contains("refreshBusinessActionTarget"));
|
||||
assertTrue("db.sql should support business action confirmation",
|
||||
dbSql.contains("confirmMessage"));
|
||||
assertTrue("db.sql should support business action success messages",
|
||||
dbSql.contains("successMessage"));
|
||||
assertTrue("db.sql should support business action refresh targets",
|
||||
dbSql.contains("refreshTarget"));
|
||||
assertTrue("db.sql should render detail pages as a single record",
|
||||
dbSql.contains("detailRecord: {}"));
|
||||
assertTrue("db.sql should load detail pages by route id",
|
||||
dbSql.contains("loadDetail()"));
|
||||
assertTrue("db.sql should avoid SQL-fragile route param regex literals",
|
||||
dbSql.contains("hasUnresolvedRouteParam(path)"));
|
||||
assertFalse("db.sql should not keep escaped regex literals that MySQL can unescape",
|
||||
dbSql.contains("return /(^|\\/):[^/]+/.test(path) ? \"\" : path"));
|
||||
assertFalse("db.sql should not contain already-broken route regex literals",
|
||||
dbSql.contains("return /(^|/):[^/]+/.test(path) ? \"\" : path"));
|
||||
assertTrue("db.sql should include runnable portal auth backend",
|
||||
dbSql.contains("'PortalAuthController.java.vm'"));
|
||||
assertTrue("db.sql should allow public business block APIs",
|
||||
dbSql.contains("\"/business-blocks/**\""));
|
||||
assertTrue("db.sql should resolve uploaded image resources through the preview proxy",
|
||||
dbSql.contains("resolveResourceUrl(url)"));
|
||||
assertTrue(dbSql.contains(":src=\"resolveResourceUrl(scope.row.${javaField})\""));
|
||||
assertTrue(dbSql.contains(":src=\"resolveResourceUrl(form.${javaField})\""));
|
||||
assertTrue(dbSql.contains(":src=\"resolveResourceUrl(item.${cardImageColumn.javaField})\""));
|
||||
assertFalse(dbSql.contains(":src=\"scope.row.${javaField}\""));
|
||||
assertFalse(dbSql.contains(":src=\"form.${javaField}\""));
|
||||
assertFalse(dbSql.contains(":src=\"item.${cardImageColumn.javaField}\""));
|
||||
assertTrue("db.sql should include fixed admin table",
|
||||
dbSql.contains("create table if not exists t_admin"));
|
||||
assertTrue("db.sql should include default admin account",
|
||||
@@ -853,6 +1431,10 @@ public class QingTemplateSupportTest
|
||||
dbSql.contains("'admin-login.vue.vm'"));
|
||||
assertTrue("db.sql should include uploaded-image-like admin shell layout",
|
||||
dbSql.contains("class=\"admin-shell\""));
|
||||
assertTrue("db.sql should render admin menu groups from page designer",
|
||||
dbSql.contains("#foreach($group in $adminNavGroups)"));
|
||||
assertTrue("db.sql should include admin route items from page designer",
|
||||
dbSql.contains("#foreach($item in $adminRouteItems)"));
|
||||
assertTrue("db.sql should route to admin login",
|
||||
dbSql.contains("path: \"/login\""));
|
||||
assertTrue("db.sql should include real admin login call",
|
||||
@@ -867,6 +1449,26 @@ public class QingTemplateSupportTest
|
||||
assertRunnableTemplatePorts(dbSql, "sql/db.sql");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingDatabaseSeedsContainCurrentBusinessActionTemplates() throws Exception
|
||||
{
|
||||
Path resourceRoot = repoRoot().resolve("ruoyi-generator/src/main/resources/qing");
|
||||
String qingSql = normalizeNewlines(read(repoRoot().resolve("sql/qing_templates.sql")));
|
||||
String databaseSql = normalizeNewlines(read(repoRoot().resolve("sql/db.sql")));
|
||||
assertFalse("db.sql should not fall back from effect condition fields to action request fields",
|
||||
databaseSql.contains("#set($conditionFields = $action.requestFields)"));
|
||||
|
||||
assertSeedContainsBundledTemplate(qingSql, resourceRoot, "service.java.vm", "service.java.vm");
|
||||
assertSeedContainsBundledTemplate(qingSql, resourceRoot, "serviceImpl.java.vm", "serviceImpl.java.vm");
|
||||
assertSeedContainsBundledTemplate(qingSql, resourceRoot, "controller.java.vm", "controller.java.vm");
|
||||
assertSeedContainsBundledTemplate(qingSql, resourceRoot, "index.vue.vm", "frontend-index.vue.vm");
|
||||
|
||||
assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "service.java.vm", "service.java.vm");
|
||||
assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "serviceImpl.java.vm", "serviceImpl.java.vm");
|
||||
assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "controller.java.vm", "controller.java.vm");
|
||||
assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "index.vue.vm", "frontend-index.vue.vm");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectStructurePackagePathPlaceholderBuildsJavaDirectories() throws Exception
|
||||
{
|
||||
@@ -951,6 +1553,29 @@ public class QingTemplateSupportTest
|
||||
return column;
|
||||
}
|
||||
|
||||
private BusinessActionDesign businessAction()
|
||||
{
|
||||
BusinessActionDesign action = new BusinessActionDesign();
|
||||
action.setCode("borrow_book");
|
||||
action.setName("借书");
|
||||
action.setOwnerTable("demo_product");
|
||||
action.setMethod("POST");
|
||||
action.setPath("/library/borrow/borrow");
|
||||
action.setTransaction(true);
|
||||
action.setRequestFields(Arrays.asList("product_id", "due_date"));
|
||||
|
||||
BusinessActionEffectDesign effect = new BusinessActionEffectDesign();
|
||||
effect.setType("INSERT_ROW");
|
||||
effect.setTargetTable("borrow_record");
|
||||
Map<String, String> values = new LinkedHashMap<String, String>();
|
||||
values.put("product_id", "${param.product_id}");
|
||||
values.put("user_id", "${current_user.id}");
|
||||
values.put("due_date", "${param.due_date}");
|
||||
effect.setValues(values);
|
||||
action.setEffects(Arrays.asList(effect));
|
||||
return action;
|
||||
}
|
||||
|
||||
private Path repoRoot()
|
||||
{
|
||||
Path cwd = Paths.get("").toAbsolutePath();
|
||||
@@ -979,6 +1604,15 @@ public class QingTemplateSupportTest
|
||||
return text.replace("\r\n", "\n");
|
||||
}
|
||||
|
||||
private void assertSeedContainsBundledTemplate(String sql, Path resourceRoot, String resourceName,
|
||||
String databaseFileName) throws Exception
|
||||
{
|
||||
String content = normalizeNewlines(read(resourceRoot.resolve(resourceName))).replace("'", "''");
|
||||
String recordFragment = "'" + databaseFileName + "', NULL, '" + databaseFileName + "', '" + content
|
||||
+ "', 'admin', sysdate())";
|
||||
assertTrue(databaseFileName + " should match the bundled Qing template", sql.contains(recordFragment));
|
||||
}
|
||||
|
||||
private void assertProjectStructureNodeNamesFitSchema(String sql)
|
||||
{
|
||||
Matcher matcher = Pattern.compile("^\\(\\d+, \\d+, '([^']*)', '", Pattern.MULTILINE).matcher(sql);
|
||||
|
||||
5926
RuoYi-Vue/sql/db.sql
5926
RuoYi-Vue/sql/db.sql
File diff suppressed because it is too large
Load Diff
5336
RuoYi-Vue/sql/qing_templates.sql
Normal file
5336
RuoYi-Vue/sql/qing_templates.sql
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user