From e12fb4e6e8e0a408e81ce05a269a15cc535d8c78 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期三, 01 四月 2026 16:27:17 +0800
Subject: [PATCH] #生成器
---
rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt | 136 ++++
rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt | 96 ++
rsf-framework/src/main/resources/templates/rsf-design/Index.txt | 240 +++++++
rsf-framework/src/main/resources/templates/rsf-design/Controller.txt | 140 ++++
rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt | 245 +++++++
rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java | 946 ++++++++++++++++++++++++++++
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java | 25
rsf-framework/src/main/resources/templates/rsf-design/Api.txt | 84 ++
rsf-framework/src/main/resources/templates/rsf-design/Search.txt | 66 ++
9 files changed, 1,978 insertions(+), 0 deletions(-)
diff --git a/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java b/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
new file mode 100644
index 0000000..c805452
--- /dev/null
+++ b/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
@@ -0,0 +1,946 @@
+package com.vincent.rsf.framework.generators;
+
+import com.vincent.rsf.framework.common.Cools;
+import com.vincent.rsf.framework.generators.constant.SqlOsType;
+import com.vincent.rsf.framework.generators.domain.Column;
+import com.vincent.rsf.framework.generators.utils.GeneratorUtils;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class RsfDesignGenerator {
+
+ private static final String BASE_DIR = "src/main/";
+ private static final String JAVA_DIR = BASE_DIR + "java/";
+ private static final String MODULES_DIR = "modules/";
+ private static final List<String> MANAGED_FIELDS = Arrays.asList(
+ "deleted",
+ "tenantId",
+ "createBy",
+ "createTime",
+ "updateBy",
+ "updateTime",
+ "version"
+ );
+ private static final List<String> SEARCH_EXCLUDED_FIELDS = Arrays.asList(
+ "deleted",
+ "tenantId",
+ "createBy",
+ "createTime",
+ "updateBy",
+ "updateTime",
+ "version"
+ );
+
+ public String url;
+ public String username;
+ public String password;
+ public String table;
+ public String tableDesc;
+ public String packagePath;
+ public boolean controller = true;
+ public boolean service = true;
+ public boolean mapper = true;
+ public boolean entity = true;
+ public boolean xml = true;
+ public boolean frontend = true;
+ public boolean sql = true;
+ public SqlOsType sqlOsType;
+ public String backendPrefixPath;
+ public String frontendPrefixPath;
+ public String frontendViewPath;
+ public String frontendApiModule;
+
+ private List<Column> columns = new ArrayList<>();
+ private String fullEntityName;
+ private String simpleEntityName;
+ private String kebabEntityName;
+ private String constantPrefix;
+ private String primaryKeyColumn;
+ private String majorColumn;
+ private String itemName;
+ private String normalizedFrontendViewPath;
+ private String normalizedFrontendApiModule;
+
+ public void build() throws Exception {
+ init();
+ buildBackendArtifacts();
+ if (controller) {
+ writeTemplate("Controller", resolveControllerDirectory(), fullEntityName + "Controller.java");
+ }
+ if (frontend) {
+ buildFrontendArtifacts();
+ }
+ }
+
+ private void init() throws Exception {
+ validateBaseConfig();
+ gainDbInfo();
+ fullEntityName = GeneratorUtils.getNameSpace(table);
+ simpleEntityName = GeneratorUtils.firstCharConvert(fullEntityName, true);
+ kebabEntityName = humpToKebab(simpleEntityName);
+ constantPrefix = GeneratorUtils.humpToLine(simpleEntityName).toUpperCase();
+ primaryKeyColumn = resolvePrimaryKeyColumn();
+ majorColumn = resolveMajorColumn();
+
+ String[] packagePathSplit = packagePath.split("\\.");
+ itemName = packagePathSplit[packagePathSplit.length - 1];
+ normalizedFrontendViewPath = normalizePath(frontendViewPath);
+ normalizedFrontendApiModule = normalizeApiModule(frontendApiModule);
+ }
+
+ private void validateBaseConfig() {
+ if (this.sqlOsType == null) {
+ throw new RuntimeException("璇烽�夋嫨sqlOsType锛�");
+ }
+ if (Cools.isEmpty(this.table)) {
+ throw new RuntimeException("璇疯緭鍏able锛�");
+ }
+ if (Cools.isEmpty(this.tableDesc)) {
+ throw new RuntimeException("璇疯緭鍏ableDesc锛�");
+ }
+ if (Cools.isEmpty(this.packagePath)) {
+ throw new RuntimeException("璇疯緭鍏ackagePath锛�");
+ }
+ if (frontend) {
+ if (Cools.isEmpty(frontendPrefixPath)) {
+ throw new RuntimeException("璇疯緭鍏rontendPrefixPath锛�");
+ }
+ if (Cools.isEmpty(frontendViewPath)) {
+ throw new RuntimeException("璇疯緭鍏rontendViewPath锛�");
+ }
+ if (Cools.isEmpty(frontendApiModule)) {
+ throw new RuntimeException("璇疯緭鍏rontendApiModule锛�");
+ }
+ }
+ }
+
+ private void gainDbInfo() throws Exception {
+ Connection connection = null;
+ try {
+ switch (this.sqlOsType) {
+ case MYSQL:
+ Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
+ connection = DriverManager.getConnection("jdbc:mysql://" + url, username, password);
+ this.columns = ReactGenerator.getMysqlColumns(connection, table, true, sqlOsType);
+ break;
+ case SQL_SERVER:
+ Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver").newInstance();
+ connection = DriverManager.getConnection("jdbc:sqlserver://" + url, username, password);
+ this.columns = ReactGenerator.getSqlServerColumns(connection, table, true, sqlOsType);
+ break;
+ default:
+ throw new RuntimeException("璇锋寚瀹氭暟鎹簱绫诲瀷锛�");
+ }
+ } finally {
+ if (connection != null) {
+ try {
+ connection.close();
+ } catch (Exception ignore) {
+ }
+ }
+ }
+ }
+
+ private void buildBackendArtifacts() throws Exception {
+ ReactGenerator generator = new ReactGenerator();
+ generator.url = url;
+ generator.username = username;
+ generator.password = password;
+ generator.table = table;
+ generator.tableDesc = tableDesc;
+ generator.packagePath = packagePath;
+ generator.controller = false;
+ generator.service = service;
+ generator.mapper = mapper;
+ generator.entity = entity;
+ generator.xml = xml;
+ generator.react = false;
+ generator.sql = sql;
+ generator.sqlOsType = sqlOsType;
+ generator.backendPrefixPath = backendPrefixPath;
+ generator.frontendPrefixPath = frontendPrefixPath;
+ generator.build();
+ }
+
+ private void buildFrontendArtifacts() throws IOException {
+ String pageDirectory = resolveFrontendPageDirectory();
+ String modulesDirectory = pageDirectory + MODULES_DIR;
+ writeTemplate("Index", pageDirectory, "index.vue");
+ writeTemplate("PageHelpers", pageDirectory, simpleEntityName + "Page.helpers.js");
+ writeTemplate("TableColumns", pageDirectory, simpleEntityName + "Table.columns.js");
+ writeTemplate("Search", modulesDirectory, kebabEntityName + "-search.vue");
+ writeTemplate("EditDialog", modulesDirectory, kebabEntityName + "-edit-dialog.vue");
+ writeTemplate("Api", resolveFrontendApiDirectory(), normalizedFrontendApiModule + ".js");
+ }
+
+ private String resolveControllerDirectory() {
+ return ensureTrailingSlash(backendPrefixPath)
+ + JAVA_DIR
+ + packagePath.replace(".", "/")
+ + "/controller/";
+ }
+
+ private String resolveFrontendPageDirectory() {
+ return ensureTrailingSlash(frontendPrefixPath) + "src/views/" + normalizedFrontendViewPath + "/";
+ }
+
+ private String resolveFrontendApiDirectory() {
+ String directory = ensureTrailingSlash(frontendPrefixPath) + "src/api/";
+ int index = normalizedFrontendApiModule.lastIndexOf('/');
+ if (index < 0) {
+ return directory;
+ }
+ return directory + normalizedFrontendApiModule.substring(0, index + 1);
+ }
+
+ private void writeTemplate(String templateName, String directory, String fileName) throws IOException {
+ String content = readTemplate(templateName);
+ writeFile(applyReplacements(content), directory, fileName, templateName);
+ }
+
+ private String readTemplate(String templateName) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ ClassPathResource classPath = new ClassPathResource("templates/rsf-design/" + templateName + ".txt");
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(classPath.getInputStream(), StandardCharsets.UTF_8))) {
+ String lineContent;
+ while ((lineContent = reader.readLine()) != null) {
+ builder.append(lineContent).append("\n");
+ }
+ }
+ return builder.toString();
+ }
+
+ private String applyReplacements(String content) {
+ Map<String, String> replacements = buildReplacements();
+ String resolved = content;
+ for (Map.Entry<String, String> entry : replacements.entrySet()) {
+ resolved = resolved.replace("@{" + entry.getKey() + "}", Objects.toString(entry.getValue(), ""));
+ }
+ return resolved;
+ }
+
+ private Map<String, String> buildReplacements() {
+ Map<String, String> replacements = new LinkedHashMap<>();
+ replacements.put("TABLENAME", table);
+ replacements.put("TABLEDESC", safeText(tableDesc));
+ replacements.put("ENTITYNAME", fullEntityName);
+ replacements.put("SIMPLEENTITYNAME", simpleEntityName);
+ replacements.put("UENTITYNAME", GeneratorUtils.firstCharConvert(simpleEntityName, false));
+ replacements.put("KEBABENTITYNAME", kebabEntityName);
+ replacements.put("COMPANYNAME", packagePath);
+ replacements.put("ITEMNAME", itemName);
+ replacements.put("PRIMARYKEYCOLUMN", GeneratorUtils.firstCharConvert(primaryKeyColumn, false));
+ replacements.put("PRIMARYKEYCOLUMN0", GeneratorUtils.firstCharConvert(primaryKeyColumn, true));
+ replacements.put("MAJORCOLUMN", GeneratorUtils.firstCharConvert(majorColumn, false));
+ replacements.put("MAJORCOLUMN0", GeneratorUtils.firstCharConvert(majorColumn, true));
+ replacements.put("ENTITYPREFIX", constantPrefix);
+ replacements.put("APIMODULE", normalizedFrontendApiModule);
+ replacements.put("SEARCHSTATECONTENT", buildSearchStateContent());
+ replacements.put("FORMSTATECONTENT", buildFormStateContent());
+ replacements.put("FIELDOPTIONSCONTENT", buildFieldOptionsContent());
+ replacements.put("SEARCHPARAMSCONTENT", buildSearchParamsContent());
+ replacements.put("SAVEPAYLOADCONTENT", buildSavePayloadContent());
+ replacements.put("DIALOGMODELCONTENT", buildDialogModelContent());
+ replacements.put("NORMALIZEROWCONTENT", buildNormalizeRowContent());
+ replacements.put("REPORTCOLUMNSCONTENT", buildReportColumnsContent());
+ replacements.put("REPORTSOURCEALIASCONTENT", buildReportSourceAliasContent());
+ replacements.put("SEARCHITEMSCONTENT", buildSearchItemsContent());
+ replacements.put("FORMITEMSCONTENT", buildFormItemsContent());
+ replacements.put("RULESCONTENT", buildRulesContent());
+ replacements.put("TABLECOLUMNSCONTENT", buildTableColumnsContent());
+ replacements.put("EXPORTROWCONTENT", buildExportRowContent());
+ replacements.put("SAVEINITCONTENT", buildSaveInitContent());
+ replacements.put("UPDATEINITCONTENT", buildUpdateInitContent());
+ return replacements;
+ }
+
+ private void writeFile(String content, String directory, String fileName, String templateName) throws IOException {
+ File codeDirectory = new File(directory);
+ if (!codeDirectory.exists()) {
+ codeDirectory.mkdirs();
+ }
+
+ File writerFile = new File(directory + fileName);
+ if (writerFile.exists()) {
+ System.out.println(fullEntityName + templateName + " 婧愭枃浠跺凡缁忓瓨鍦ㄥ垱寤哄け璐ワ紒");
+ return;
+ }
+
+ writerFile.createNewFile();
+ try (BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(writerFile), StandardCharsets.UTF_8))) {
+ writer.write(content);
+ writer.flush();
+ }
+ System.out.println(fullEntityName + templateName + " 婧愭枃浠跺垱寤烘垚鍔燂紒");
+ }
+
+ private String resolvePrimaryKeyColumn() {
+ for (Column column : columns) {
+ if (column.isPrimaryKey() || column.isMainKey()) {
+ return column.getHumpName();
+ }
+ }
+ return "id";
+ }
+
+ private String resolveMajorColumn() {
+ for (Column column : columns) {
+ if (column.isMajor()) {
+ return column.getHumpName();
+ }
+ }
+ for (String preferred : Arrays.asList("name", "code", "title", "uuid")) {
+ Column column = findColumn(preferred);
+ if (column != null) {
+ return column.getHumpName();
+ }
+ }
+ for (Column column : columns) {
+ if (!MANAGED_FIELDS.contains(column.getHumpName()) && !column.isPrimaryKey()) {
+ return column.getHumpName();
+ }
+ }
+ return primaryKeyColumn;
+ }
+
+ private Column findColumn(String humpName) {
+ for (Column column : columns) {
+ if (humpName.equals(column.getHumpName())) {
+ return column;
+ }
+ }
+ return null;
+ }
+
+ private boolean hasColumn(String humpName) {
+ return findColumn(humpName) != null;
+ }
+
+ private boolean isManagedColumn(Column column) {
+ return MANAGED_FIELDS.contains(column.getHumpName());
+ }
+
+ private boolean isSearchExcludedColumn(Column column) {
+ return SEARCH_EXCLUDED_FIELDS.contains(column.getHumpName());
+ }
+
+ private boolean isBooleanColumn(Column column) {
+ return "Boolean".equals(column.getType());
+ }
+
+ private boolean isNumericColumn(Column column) {
+ return "Short".equals(column.getType())
+ || "Integer".equals(column.getType())
+ || "Long".equals(column.getType())
+ || "Double".equals(column.getType());
+ }
+
+ private boolean isDateColumn(Column column) {
+ return "Date".equals(column.getType());
+ }
+
+ private boolean hasEnumOptions(Column column) {
+ return !Cools.isEmpty(column.getEnums());
+ }
+
+ private boolean hasForeignDisplay(Column column) {
+ return !Cools.isEmpty(column.getForeignKeyMajor());
+ }
+
+ private boolean isStatusColumn(Column column) {
+ return "status".equals(column.getHumpName());
+ }
+
+ private boolean isMemoColumn(Column column) {
+ return "memo".equals(column.getHumpName());
+ }
+
+ private boolean isSelectableColumn(Column column) {
+ return isBooleanColumn(column) || hasEnumOptions(column);
+ }
+
+ private boolean isDisplayTextColumn(Column column) {
+ return isBooleanColumn(column) || isDateColumn(column) || hasEnumOptions(column) || hasForeignDisplay(column);
+ }
+
+ private List<Column> getSearchColumns() {
+ List<Column> result = new ArrayList<>();
+ for (Column column : columns) {
+ if (column.isPrimaryKey() || isSearchExcludedColumn(column) || isDateColumn(column)) {
+ continue;
+ }
+ result.add(column);
+ }
+ return result;
+ }
+
+ private List<Column> getFormColumns() {
+ List<Column> result = new ArrayList<>();
+ for (Column column : columns) {
+ if (column.isPrimaryKey() || isManagedColumn(column)) {
+ continue;
+ }
+ result.add(column);
+ }
+ return result;
+ }
+
+ private List<Column> getListColumns() {
+ List<Column> result = new ArrayList<>();
+ for (Column column : columns) {
+ if (column.isPrimaryKey()) {
+ continue;
+ }
+ if ("deleted".equals(column.getHumpName()) || "tenantId".equals(column.getHumpName())) {
+ continue;
+ }
+ result.add(column);
+ }
+ return result;
+ }
+
+ private List<Column> getReportColumns() {
+ return getListColumns();
+ }
+
+ private List<Column> getPayloadColumns() {
+ List<Column> result = new ArrayList<>();
+ Column primaryColumn = findColumn(primaryKeyColumn);
+ if (primaryColumn != null) {
+ result.add(primaryColumn);
+ }
+ Column versionColumn = findColumn("version");
+ if (versionColumn != null) {
+ result.add(versionColumn);
+ }
+ result.addAll(getFormColumns());
+ return result;
+ }
+
+ private String buildSearchStateContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getSearchColumns()) {
+ sb.append(" ").append(column.getHumpName()).append(": '',\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildFormStateContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getPayloadColumns()) {
+ sb.append(" ")
+ .append(column.getHumpName())
+ .append(": ")
+ .append(resolveFormDefaultValue(column))
+ .append(",\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildFieldOptionsContent() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{\n");
+ boolean hasOptions = false;
+ for (Column column : columns) {
+ if (!isSelectableColumn(column)) {
+ continue;
+ }
+ hasOptions = true;
+ sb.append(" ").append(column.getHumpName()).append(": [\n");
+ for (Map<String, Object> option : buildColumnOptions(column)) {
+ sb.append(" { label: '")
+ .append(escapeJs(String.valueOf(option.get("label"))))
+ .append("', value: ")
+ .append(option.get("value"))
+ .append(" },\n");
+ }
+ sb.append(" ],\n");
+ }
+ if (!hasOptions) {
+ return "{}";
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ private String buildSearchParamsContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getSearchColumns()) {
+ sb.append(" ")
+ .append(column.getHumpName())
+ .append(": ")
+ .append(resolveParamNormalizer("params", column))
+ .append(",\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildSavePayloadContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getPayloadColumns()) {
+ sb.append(resolvePayloadLine("formData", column));
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildDialogModelContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getPayloadColumns()) {
+ sb.append(" ")
+ .append(column.getHumpName())
+ .append(": ")
+ .append(resolveDialogValue(column))
+ .append(",\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildNormalizeRowContent() {
+ StringBuilder sb = new StringBuilder();
+ Column statusColumn = findColumn("status");
+ if (statusColumn != null) {
+ sb.append(" statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,\n")
+ .append(" statusText: normalizeText(record.statusText || record.status$ || statusMeta.text),\n")
+ .append(" statusType: statusMeta.type,\n");
+ }
+ for (Column column : getReportColumns()) {
+ if (isStatusColumn(column)) {
+ continue;
+ }
+ if (isBooleanColumn(column)) {
+ sb.append(" ")
+ .append(column.getHumpName())
+ .append("Text: formatBooleanText(toOptionalBoolean(record.")
+ .append(column.getHumpName())
+ .append(")),\n");
+ continue;
+ }
+ if (isDisplayTextColumn(column)) {
+ sb.append(" ")
+ .append(column.getHumpName())
+ .append("Text: normalizeText(record.")
+ .append(column.getHumpName())
+ .append("$ || record.")
+ .append(column.getHumpName())
+ .append("Text || record.")
+ .append(column.getHumpName())
+ .append("),\n");
+ }
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildReportColumnsContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getReportColumns()) {
+ sb.append(" { source: '")
+ .append(resolveReportSource(column))
+ .append("', label: '")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("' },\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildReportSourceAliasContent() {
+ Column statusColumn = findColumn("status");
+ if (statusColumn == null) {
+ return "";
+ }
+ return " status: 'statusText'";
+ }
+
+ private String buildSearchItemsContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getSearchColumns()) {
+ if (isSelectableColumn(column)) {
+ sb.append(" createSelectSearchItem('")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("', '")
+ .append(column.getHumpName())
+ .append("', '璇烽�夋嫨")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("', get")
+ .append(fullEntityName)
+ .append("FieldOptions('")
+ .append(column.getHumpName())
+ .append("')),\n");
+ } else {
+ sb.append(" createInputSearchItem('")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("', '")
+ .append(column.getHumpName())
+ .append("', '璇疯緭鍏�")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("'),\n");
+ }
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildFormItemsContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getFormColumns()) {
+ String label = escapeJs(resolveFieldLabel(column));
+ String key = column.getHumpName();
+ if (isSelectableColumn(column)) {
+ sb.append(" createSelectFormItem('")
+ .append(label)
+ .append("', '")
+ .append(key)
+ .append("', '璇烽�夋嫨")
+ .append(label)
+ .append("', get")
+ .append(fullEntityName)
+ .append("FieldOptions('")
+ .append(key)
+ .append("')),\n");
+ continue;
+ }
+ if (isMemoColumn(column) || (column.getLength() != null && column.getLength() > 255)) {
+ sb.append(" createInputFormItem('")
+ .append(label)
+ .append("', '")
+ .append(key)
+ .append("', '璇疯緭鍏�")
+ .append(label)
+ .append("', { type: 'textarea', rows: 3 }, { span: 24 }),\n");
+ continue;
+ }
+ if (isNumericColumn(column)) {
+ sb.append(" createInputFormItem('")
+ .append(label)
+ .append("', '")
+ .append(key)
+ .append("', '璇疯緭鍏�")
+ .append(label)
+ .append("', { type: 'number' }),\n");
+ continue;
+ }
+ sb.append(" createInputFormItem('")
+ .append(label)
+ .append("', '")
+ .append(key)
+ .append("', '璇疯緭鍏�")
+ .append(label)
+ .append("'),\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildRulesContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getFormColumns()) {
+ if (!column.isNotNull()) {
+ continue;
+ }
+ String label = escapeJs(resolveFieldLabel(column));
+ String message = (isSelectableColumn(column) ? "璇烽�夋嫨" : "璇疯緭鍏�") + label;
+ String trigger = isSelectableColumn(column) ? "change" : "blur";
+ sb.append(" ")
+ .append(column.getHumpName())
+ .append(": [{ required: true, message: '")
+ .append(message)
+ .append("', trigger: '")
+ .append(trigger)
+ .append("' }],\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildTableColumnsContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getListColumns()) {
+ if (isStatusColumn(column)) {
+ sb.append(" createTagColumn('status', '")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("', 120, (row) => get")
+ .append(fullEntityName)
+ .append("StatusMeta(row.statusBool ?? row.status)),\n");
+ continue;
+ }
+ if (isNumericColumn(column)) {
+ sb.append(" createNumberColumn('")
+ .append(column.getHumpName())
+ .append("', '")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("', 120),\n");
+ continue;
+ }
+ if (isDisplayTextColumn(column)) {
+ sb.append(" createTextColumn('")
+ .append(column.getHumpName())
+ .append("Text', '")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("', ")
+ .append(resolveTextColumnWidth(column))
+ .append("),\n");
+ continue;
+ }
+ sb.append(" createTextColumn('")
+ .append(column.getHumpName())
+ .append("', '")
+ .append(escapeJs(resolveFieldLabel(column)))
+ .append("', ")
+ .append(resolveTextColumnWidth(column))
+ .append("),\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildExportRowContent() {
+ StringBuilder sb = new StringBuilder();
+ for (Column column : getReportColumns()) {
+ sb.append(" row.put(\"")
+ .append(resolveReportSource(column))
+ .append("\", ")
+ .append(resolveExportExpression(column))
+ .append(");\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildSaveInitContent() {
+ StringBuilder sb = new StringBuilder();
+ if (hasColumn("createBy")) {
+ sb.append(" ").append(simpleEntityName).append(".setCreateBy(getLoginUserId());\n");
+ }
+ if (hasColumn("createTime")) {
+ sb.append(" ").append(simpleEntityName).append(".setCreateTime(new Date());\n");
+ }
+ if (hasColumn("updateBy")) {
+ sb.append(" ").append(simpleEntityName).append(".setUpdateBy(getLoginUserId());\n");
+ }
+ if (hasColumn("updateTime")) {
+ sb.append(" ").append(simpleEntityName).append(".setUpdateTime(new Date());\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private String buildUpdateInitContent() {
+ StringBuilder sb = new StringBuilder();
+ if (hasColumn("updateBy")) {
+ sb.append(" ").append(simpleEntityName).append(".setUpdateBy(getLoginUserId());\n");
+ }
+ if (hasColumn("updateTime")) {
+ sb.append(" ").append(simpleEntityName).append(".setUpdateTime(new Date());\n");
+ }
+ return trimTrailingLineBreak(sb);
+ }
+
+ private List<Map<String, Object>> buildColumnOptions(Column column) {
+ List<Map<String, Object>> options = new ArrayList<>();
+ if (isBooleanColumn(column) && Cools.isEmpty(column.getEnums())) {
+ options.add(buildOption("鏄�", "true"));
+ options.add(buildOption("鍚�", "false"));
+ return options;
+ }
+ if (Cools.isEmpty(column.getEnums())) {
+ return options;
+ }
+ for (Map<String, Object> item : column.getEnums()) {
+ for (Map.Entry<String, Object> entry : item.entrySet()) {
+ options.add(buildOption(String.valueOf(entry.getValue()), toJsValue(column, String.valueOf(entry.getKey()))));
+ }
+ }
+ return options;
+ }
+
+ private Map<String, Object> buildOption(String label, String jsValue) {
+ Map<String, Object> option = new LinkedHashMap<>();
+ option.put("label", label);
+ option.put("value", jsValue);
+ return option;
+ }
+
+ private String resolveFormDefaultValue(Column column) {
+ if (column == null) {
+ return "void 0";
+ }
+ if (isStatusColumn(column) && isSelectableColumn(column)) {
+ List<Map<String, Object>> options = buildColumnOptions(column);
+ if (!options.isEmpty()) {
+ return String.valueOf(options.get(0).get("value"));
+ }
+ }
+ if (isBooleanColumn(column) || isNumericColumn(column)) {
+ return "void 0";
+ }
+ return "''";
+ }
+
+ private String resolveParamNormalizer(String sourceName, Column column) {
+ String field = sourceName + "." + column.getHumpName();
+ if (isBooleanColumn(column)) {
+ return "toOptionalBoolean(" + field + ")";
+ }
+ if (isNumericColumn(column)) {
+ return "toOptionalNumber(" + field + ")";
+ }
+ return "normalizeText(" + field + ")";
+ }
+
+ private String resolvePayloadLine(String sourceName, Column column) {
+ String field = sourceName + "." + column.getHumpName();
+ if (isBooleanColumn(column)) {
+ return " ...(hasValue(" + field + ") ? { " + column.getHumpName() + ": toOptionalBoolean(" + field + ") } : {}),\n";
+ }
+ if (isNumericColumn(column)) {
+ return " ...buildNumberField('" + column.getHumpName() + "', " + field + "),\n";
+ }
+ return " " + column.getHumpName() + ": normalizeText(" + field + ") || '',\n";
+ }
+
+ private String resolveDialogValue(Column column) {
+ String field = "record." + column.getHumpName();
+ if (isStatusColumn(column) && isSelectableColumn(column)) {
+ List<Map<String, Object>> options = buildColumnOptions(column);
+ String fallback = options.isEmpty() ? "void 0" : String.valueOf(options.get(0).get("value"));
+ if (isBooleanColumn(column)) {
+ return "hasValue(" + field + ") ? toOptionalBoolean(" + field + ") : " + fallback;
+ }
+ if (isNumericColumn(column)) {
+ return "hasValue(" + field + ") ? toOptionalNumber(" + field + ") : " + fallback;
+ }
+ return "normalizeText(" + field + ") || " + fallback;
+ }
+ if (isBooleanColumn(column)) {
+ return "toOptionalBoolean(" + field + ")";
+ }
+ if (isNumericColumn(column)) {
+ return "toOptionalNumber(" + field + ")";
+ }
+ return "normalizeText(" + field + " || '')";
+ }
+
+ private String resolveFieldLabel(Column column) {
+ if (column == null || Cools.isEmpty(column.getComment())) {
+ return column == null ? "" : column.getHumpName();
+ }
+ return column.getComment().trim();
+ }
+
+ private int resolveTextColumnWidth(Column column) {
+ if (isMemoColumn(column)) {
+ return 220;
+ }
+ if (isDateColumn(column)) {
+ return 180;
+ }
+ return 160;
+ }
+
+ private String resolveReportSource(Column column) {
+ if (isStatusColumn(column) || isDisplayTextColumn(column)) {
+ return column.getHumpName() + "Text";
+ }
+ return column.getHumpName();
+ }
+
+ private String resolveExportExpression(Column column) {
+ String getter = "record." + getterName(column);
+ if (isBooleanColumn(column)) {
+ return "Boolean.TRUE.equals(" + getter + "()) ? \"鏄痋" : Boolean.FALSE.equals(" + getter + "()) ? \"鍚" : \"\"";
+ }
+ if (isStatusColumn(column) || isDisplayTextColumn(column)) {
+ return getter + "$()";
+ }
+ return getter + "()";
+ }
+
+ private String getterName(Column column) {
+ return "get" + GeneratorUtils.firstCharConvert(column.getHumpName(), false);
+ }
+
+ private String toJsValue(Column column, String rawValue) {
+ if (isBooleanColumn(column)) {
+ if ("1".equals(rawValue) || "true".equalsIgnoreCase(rawValue)) {
+ return "true";
+ }
+ if ("0".equals(rawValue) || "false".equalsIgnoreCase(rawValue)) {
+ return "false";
+ }
+ }
+ if (isNumericColumn(column)) {
+ return rawValue;
+ }
+ return "'" + escapeJs(rawValue) + "'";
+ }
+
+ private String trimTrailingLineBreak(StringBuilder sb) {
+ if (sb.length() == 0) {
+ return "";
+ }
+ while (sb.length() > 0 && (sb.charAt(sb.length() - 1) == '\n' || sb.charAt(sb.length() - 1) == '\r')) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ if (sb.length() > 0 && sb.charAt(sb.length() - 1) == ',') {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ private String safeText(String value) {
+ return value == null ? "" : value.trim();
+ }
+
+ private String escapeJs(String value) {
+ return value
+ .replace("\\", "\\\\")
+ .replace("'", "\\'");
+ }
+
+ private String humpToKebab(String value) {
+ if (Cools.isEmpty(value)) {
+ return "";
+ }
+ return value.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase();
+ }
+
+ private String ensureTrailingSlash(String prefix) {
+ if (Cools.isEmpty(prefix)) {
+ return "";
+ }
+ return prefix.endsWith("/") ? prefix : prefix + "/";
+ }
+
+ private String normalizePath(String value) {
+ if (Cools.isEmpty(value)) {
+ return "";
+ }
+ String normalized = value.replace("\\", "/");
+ while (normalized.startsWith("/")) {
+ normalized = normalized.substring(1);
+ }
+ while (normalized.endsWith("/")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+ return normalized;
+ }
+
+ private String normalizeApiModule(String value) {
+ String normalized = normalizePath(value);
+ if (normalized.endsWith(".js")) {
+ return normalized.substring(0, normalized.length() - 3);
+ }
+ return normalized;
+ }
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Api.txt b/rsf-framework/src/main/resources/templates/rsf-design/Api.txt
new file mode 100644
index 0000000..7dfb175
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Api.txt
@@ -0,0 +1,84 @@
+import request from '@/utils/http'
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+export function fetch@{ENTITYNAME}Page(params = {}) {
+ return request.post({
+ url: '/@{SIMPLEENTITYNAME}/page',
+ params
+ })
+}
+
+export function fetch@{ENTITYNAME}List(params = {}) {
+ return request.post({
+ url: '/@{SIMPLEENTITYNAME}/list',
+ params
+ })
+}
+
+export function fetchGet@{ENTITYNAME}Detail(id) {
+ return request.get({
+ url: `/@{SIMPLEENTITYNAME}/${id}`
+ })
+}
+
+export function fetchGet@{ENTITYNAME}Many(ids) {
+ return request.post({
+ url: `/@{SIMPLEENTITYNAME}/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSave@{ENTITYNAME}(params = {}) {
+ return request.post({
+ url: '/@{SIMPLEENTITYNAME}/save',
+ params
+ })
+}
+
+export function fetchUpdate@{ENTITYNAME}(params = {}) {
+ return request.post({
+ url: '/@{SIMPLEENTITYNAME}/update',
+ params
+ })
+}
+
+export function fetchDelete@{ENTITYNAME}(ids) {
+ return request.post({
+ url: `/@{SIMPLEENTITYNAME}/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetch@{ENTITYNAME}Query(condition = '') {
+ return request.post({
+ url: '/@{SIMPLEENTITYNAME}/query',
+ params: {
+ condition: normalizeText(condition)
+ }
+ })
+}
+
+export async function fetchExport@{ENTITYNAME}Report(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/@{SIMPLEENTITYNAME}/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Controller.txt b/rsf-framework/src/main/resources/templates/rsf-design/Controller.txt
new file mode 100644
index 0000000..24cad1e
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Controller.txt
@@ -0,0 +1,140 @@
+package @{COMPANYNAME}.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.vincent.rsf.framework.common.Cools;
+import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.server.common.annotation.OperationLog;
+import com.vincent.rsf.server.common.domain.BaseParam;
+import com.vincent.rsf.server.common.domain.KeyValVo;
+import com.vincent.rsf.server.common.domain.PageParam;
+import com.vincent.rsf.server.common.service.ListExportHandler;
+import com.vincent.rsf.server.common.service.ListExportService;
+import com.vincent.rsf.server.common.utils.ExcelUtil;
+import @{COMPANYNAME}.entity.@{ENTITYNAME};
+import @{COMPANYNAME}.service.@{ENTITYNAME}Service;
+import com.vincent.rsf.server.system.controller.BaseController;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+public class @{ENTITYNAME}Controller extends BaseController {
+
+ @Autowired
+ private @{ENTITYNAME}Service @{SIMPLEENTITYNAME}Service;
+ @Autowired
+ private ListExportService listExportService;
+
+ private final ListExportHandler<@{ENTITYNAME}, BaseParam> @{SIMPLEENTITYNAME}ExportHandler =
+ new ListExportHandler<@{ENTITYNAME}, BaseParam>() {
+ @Override
+ public List<@{ENTITYNAME}> listByIds(List<Long> ids) {
+ return @{SIMPLEENTITYNAME}Service.listByIds(ids);
+ }
+
+ @Override
+ public List<@{ENTITYNAME}> listByFilter(Map<String, Object> sanitizedMap, BaseParam baseParam) {
+ PageParam<@{ENTITYNAME}, BaseParam> pageParam = new PageParam<>(baseParam, @{ENTITYNAME}.class);
+ return @{SIMPLEENTITYNAME}Service.list(pageParam.buildWrapper(true));
+ }
+
+ @Override
+ public Map<String, Object> toExportRow(@{ENTITYNAME} record, List<ExcelUtil.ExportColumn> columns) {
+ Map<String, Object> row = new LinkedHashMap<>();
+@{EXPORTROWCONTENT}
+ return row;
+ }
+
+ @Override
+ public String defaultReportTitle() {
+ return "@{TABLEDESC}鎶ヨ〃";
+ }
+ };
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
+ @PostMapping("/@{SIMPLEENTITYNAME}/page")
+ public R page(@RequestBody Map<String, Object> map) {
+ BaseParam baseParam = buildParam(map, BaseParam.class);
+ PageParam<@{ENTITYNAME}, BaseParam> pageParam = new PageParam<>(baseParam, @{ENTITYNAME}.class);
+ return R.ok().add(@{SIMPLEENTITYNAME}Service.page(pageParam, pageParam.buildWrapper(true)));
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
+ @PostMapping("/@{SIMPLEENTITYNAME}/list")
+ public R list(@RequestBody Map<String, Object> map) {
+ return R.ok().add(@{SIMPLEENTITYNAME}Service.list());
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
+ @PostMapping({"/@{SIMPLEENTITYNAME}/many/{ids}", "/@{SIMPLEENTITYNAME}s/many/{ids}"})
+ public R many(@PathVariable Long[] ids) {
+ return R.ok().add(@{SIMPLEENTITYNAME}Service.listByIds(Arrays.asList(ids)));
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
+ @GetMapping("/@{SIMPLEENTITYNAME}/{id}")
+ public R get(@PathVariable("id") Long id) {
+ return R.ok().add(@{SIMPLEENTITYNAME}Service.getById(id));
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:save')")
+ @OperationLog("Create @{TABLEDESC}")
+ @PostMapping("/@{SIMPLEENTITYNAME}/save")
+ public R save(@RequestBody @{ENTITYNAME} @{SIMPLEENTITYNAME}) {
+@{SAVEINITCONTENT}
+ if (!@{SIMPLEENTITYNAME}Service.save(@{SIMPLEENTITYNAME})) {
+ return R.error("Save Fail");
+ }
+ return R.ok("Save Success").add(@{SIMPLEENTITYNAME});
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:update')")
+ @OperationLog("Update @{TABLEDESC}")
+ @PostMapping("/@{SIMPLEENTITYNAME}/update")
+ public R update(@RequestBody @{ENTITYNAME} @{SIMPLEENTITYNAME}) {
+@{UPDATEINITCONTENT}
+ if (!@{SIMPLEENTITYNAME}Service.updateById(@{SIMPLEENTITYNAME})) {
+ return R.error("Update Fail");
+ }
+ return R.ok("Update Success").add(@{SIMPLEENTITYNAME});
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:remove')")
+ @OperationLog("Delete @{TABLEDESC}")
+ @PostMapping("/@{SIMPLEENTITYNAME}/remove/{ids}")
+ public R remove(@PathVariable Long[] ids) {
+ if (!@{SIMPLEENTITYNAME}Service.removeByIds(Arrays.asList(ids))) {
+ return R.error("Delete Fail");
+ }
+ return R.ok("Delete Success").add(ids);
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
+ @PostMapping("/@{SIMPLEENTITYNAME}/query")
+ public R query(@RequestParam(required = false) String condition) {
+ List<KeyValVo> vos = new ArrayList<>();
+ LambdaQueryWrapper<@{ENTITYNAME}> wrapper = new LambdaQueryWrapper<>();
+ if (!Cools.isEmpty(condition)) {
+ wrapper.like(@{ENTITYNAME}::get@{MAJORCOLUMN}, condition);
+ }
+ @{SIMPLEENTITYNAME}Service.page(new Page<>(1, 30), wrapper).getRecords().forEach(
+ item -> vos.add(new KeyValVo(item.get@{PRIMARYKEYCOLUMN}(), item.get@{MAJORCOLUMN}()))
+ );
+ return R.ok().add(vos);
+ }
+
+ @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
+ @PostMapping("/@{SIMPLEENTITYNAME}/export")
+ public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
+ listExportService.export(map, exportMap -> buildParam(exportMap, BaseParam.class), @{SIMPLEENTITYNAME}ExportHandler, response);
+ }
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt b/rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt
new file mode 100644
index 0000000..393e645
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt
@@ -0,0 +1,136 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ build@{ENTITYNAME}DialogModel,
+ create@{ENTITYNAME}FormState,
+ get@{ENTITYNAME}FieldOptions
+ } from '../@{SIMPLEENTITYNAME}Page.helpers'
+
+ const props = defineProps({
+ visible: { required: false, default: false },
+ dialogType: { required: false, default: 'add' },
+ record: { required: false, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(create@{ENTITYNAME}FormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫@{TABLEDESC}' : '鏂板@{TABLEDESC}'))
+
+ const rules = computed(() => ({
+@{RULESCONTENT}
+ }))
+
+ function createInputFormItem(label, key, placeholder, extraProps = {}, extraConfig = {}) {
+ return {
+ label,
+ key,
+ type: 'input',
+ props: {
+ placeholder,
+ clearable: true,
+ ...extraProps
+ },
+ ...extraConfig
+ }
+ }
+
+ function createSelectFormItem(label, key, placeholder, options, extraProps = {}, extraConfig = {}) {
+ return {
+ label,
+ key,
+ type: 'select',
+ props: {
+ placeholder,
+ clearable: true,
+ options,
+ ...extraProps
+ },
+ ...extraConfig
+ }
+ }
+
+ const formItems = computed(() => [
+@{FORMITEMSCONTENT}
+ ])
+
+ function resetForm() {
+ Object.assign(form, create@{ENTITYNAME}FormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, build@{ENTITYNAME}DialogModel(props.record))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.record,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Index.txt b/rsf-framework/src/main/resources/templates/rsf-design/Index.txt
new file mode 100644
index 0000000..539cdb7
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Index.txt
@@ -0,0 +1,240 @@
+<template>
+ <div class="art-full-height">
+ <@{ENTITYNAME}Search
+ v-show="showSearchBar"
+ v-model="searchForm"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
+ <ArtTableHeader
+ v-model:columns="columnChecks"
+ v-model:showSearchBar="showSearchBar"
+ :loading="loading"
+ @refresh="refreshData"
+ >
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板@{TABLEDESC}</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <span v-auth="'query'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <@{ENTITYNAME}EditDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :record="currentRecordData"
+ @submit="handleDialogSubmit"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { useUserStore } from '@/store/modules/user'
+ import {
+ fetchDelete@{ENTITYNAME},
+ fetchExport@{ENTITYNAME}Report,
+ fetchGet@{ENTITYNAME}Many,
+ fetch@{ENTITYNAME}Page,
+ fetchSave@{ENTITYNAME},
+ fetchUpdate@{ENTITYNAME}
+ } from '@/api/@{APIMODULE}'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import @{ENTITYNAME}Search from './modules/@{KEBABENTITYNAME}-search.vue'
+ import @{ENTITYNAME}EditDialog from './modules/@{KEBABENTITYNAME}-edit-dialog.vue'
+ import { create@{ENTITYNAME}TableColumns } from './@{SIMPLEENTITYNAME}Table.columns'
+ import {
+ build@{ENTITYNAME}DialogModel,
+ build@{ENTITYNAME}PageQueryParams,
+ build@{ENTITYNAME}PrintRows,
+ build@{ENTITYNAME}ReportMeta,
+ build@{ENTITYNAME}SavePayload,
+ build@{ENTITYNAME}SearchParams,
+ create@{ENTITYNAME}SearchState,
+ get@{ENTITYNAME}PaginationKey,
+ normalize@{ENTITYNAME}ListRow,
+ @{ENTITYPREFIX}_REPORT_STYLE,
+ @{ENTITYPREFIX}_REPORT_TITLE,
+ resolve@{ENTITYNAME}ReportColumns
+ } from './@{SIMPLEENTITYNAME}Page.helpers'
+
+ defineOptions({ name: '@{ENTITYNAME}' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(create@{ENTITYNAME}SearchState())
+ const showSearchBar = ref(false)
+ const reportTitle = @{ENTITYPREFIX}_REPORT_TITLE
+ const reportQueryParams = computed(() => build@{ENTITYNAME}SearchParams(searchForm.value))
+ let handleEditAction = null
+ let handleDeleteAction = null
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetch@{ENTITYNAME}Page,
+ apiParams: build@{ENTITYNAME}PageQueryParams(searchForm.value),
+ paginationKey: get@{ENTITYNAME}PaginationKey(),
+ columnsFactory: () =>
+ create@{ENTITYNAME}TableColumns({
+ handleEdit: (row) => handleEditAction?.(row),
+ handleDelete: (row) => handleDeleteAction?.(row)
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalize@{ENTITYNAME}ListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentRecordData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => build@{ENTITYNAME}DialogModel(),
+ buildEditModel: (record) => build@{ENTITYNAME}DialogModel(record),
+ buildSavePayload: (formData) => build@{ENTITYNAME}SavePayload(formData),
+ saveRequest: fetchSave@{ENTITYNAME},
+ updateRequest: fetchUpdate@{ENTITYNAME},
+ deleteRequest: fetchDelete@{ENTITYNAME},
+ entityName: '@{TABLEDESC}',
+ resolveRecordLabel: (record) => record?.@{MAJORCOLUMN0} || record?.@{PRIMARYKEYCOLUMN0},
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+
+ handleEditAction = (row) => showDialog('edit', row)
+ handleDeleteAction = handleDelete
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response =
+ Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchGet@{ENTITYNAME}Many(payload.ids)
+ : await fetch@{ENTITYNAME}Page({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0
+ ? Number(pagination.total)
+ : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: '@{KEBABENTITYNAME}.xlsx',
+ requestExport: (payload) =>
+ fetchExport@{ENTITYNAME}Report(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => build@{ENTITYNAME}PrintRows(records),
+ buildPreviewMeta: (rows) => buildPreviewDialogMeta(rows)
+ })
+
+ const reportColumns = computed(() => resolve@{ENTITYNAME}ReportColumns(columns.value))
+ const resolvedPreviewMeta = computed(() =>
+ build@{ENTITYNAME}ReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ titleAlign: @{ENTITYPREFIX}_REPORT_STYLE.titleAlign,
+ titleLevel: @{ENTITYPREFIX}_REPORT_STYLE.titleLevel
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(build@{ENTITYNAME}SearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, create@{ENTITYNAME}SearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt b/rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt
new file mode 100644
index 0000000..4bb0d1e
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt
@@ -0,0 +1,245 @@
+const FIELD_OPTIONS = @{FIELDOPTIONSCONTENT}
+
+export const @{ENTITYPREFIX}_REPORT_TITLE = '@{TABLEDESC}鎶ヨ〃'
+export const @{ENTITYPREFIX}_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function hasValue(value) {
+ return value !== '' && value !== null && value !== undefined
+}
+
+function toOptionalNumber(value) {
+ if (!hasValue(value)) {
+ return void 0
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? void 0 : parsed
+}
+
+function toOptionalBoolean(value) {
+ if (!hasValue(value)) {
+ return void 0
+ }
+ if (value === true || value === false) {
+ return value
+ }
+ if (value === 1 || value === '1' || String(value).toLowerCase() === 'true') {
+ return true
+ }
+ if (value === 0 || value === '0' || String(value).toLowerCase() === 'false') {
+ return false
+ }
+ return Boolean(value)
+}
+
+function buildNumberField(key, value) {
+ return hasValue(value) ? { [key]: Number(value) } : {}
+}
+
+function cloneOptions(options = []) {
+ return options.map((option) => ({ ...option }))
+}
+
+function normalizeStatusValue(status) {
+ if (!hasValue(status)) {
+ return ''
+ }
+ return String(status)
+}
+
+function formatBooleanText(value) {
+ if (value === true) {
+ return '鏄�'
+ }
+ if (value === false) {
+ return '鍚�'
+ }
+ return ''
+}
+
+function resolveStatusType(option = {}) {
+ const label = String(option.label || '')
+ const value = normalizeStatusValue(option.value)
+ if (value === '1' || value === 'true' || /姝e父|鍚敤|鏈夋晥|寮�鍚瘄鏄�/.test(label)) {
+ return 'success'
+ }
+ if (value === '0' || value === 'false' || /绂佺敤|鍐荤粨|鍋滅敤|鍏抽棴|鍚�/.test(label)) {
+ return 'danger'
+ }
+ return 'info'
+}
+
+function resolveStatusBool(option = {}) {
+ const value = normalizeStatusValue(option.value)
+ if (value === '1' || value === 'true') {
+ return true
+ }
+ if (value === '0' || value === 'false') {
+ return false
+ }
+ return false
+}
+
+export function create@{ENTITYNAME}SearchState() {
+ return {
+ condition: '',
+@{SEARCHSTATECONTENT}
+ }
+}
+
+export function create@{ENTITYNAME}FormState() {
+ return {
+@{FORMSTATECONTENT}
+ }
+}
+
+export function get@{ENTITYNAME}PaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function get@{ENTITYNAME}FieldOptions(field) {
+ return cloneOptions(FIELD_OPTIONS[field] || [])
+}
+
+export function get@{ENTITYNAME}StatusOptions() {
+ return get@{ENTITYNAME}FieldOptions('status')
+}
+
+export function get@{ENTITYNAME}StatusMeta(status) {
+ const options = FIELD_OPTIONS.status || []
+ const normalizedStatus = normalizeStatusValue(status)
+ const matchedOption = options.find((option) => normalizeStatusValue(option.value) === normalizedStatus)
+ if (!matchedOption) {
+ return {
+ type: 'info',
+ text: normalizeText(status) || '鏈煡',
+ bool: false
+ }
+ }
+ return {
+ type: resolveStatusType(matchedOption),
+ text: matchedOption.label,
+ bool: resolveStatusBool(matchedOption)
+ }
+}
+
+export function build@{ENTITYNAME}SearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+@{SEARCHPARAMSCONTENT}
+ }
+
+ return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => hasValue(value)))
+}
+
+export function build@{ENTITYNAME}PageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...build@{ENTITYNAME}SearchParams(params)
+ }
+}
+
+export const @{ENTITYPREFIX}_REPORT_COLUMNS = [
+@{REPORTCOLUMNSCONTENT}
+]
+
+const @{ENTITYPREFIX}_REPORT_SOURCE_ALIAS = {
+@{REPORTSOURCEALIASCONTENT}
+}
+
+export function get@{ENTITYNAME}ReportColumns() {
+ return @{ENTITYPREFIX}_REPORT_COLUMNS.map((column) => ({ ...column }))
+}
+
+export function resolve@{ENTITYNAME}ReportColumns(columns = []) {
+ if (!Array.isArray(columns)) {
+ return []
+ }
+
+ const allowedColumns = new Map(@{ENTITYPREFIX}_REPORT_COLUMNS.map((column) => [column.source, column]))
+ const seenSources = new Set()
+
+ return columns
+ .map((column) => {
+ if (!column || typeof column !== 'object') {
+ return null
+ }
+
+ const source =
+ @{ENTITYPREFIX}_REPORT_SOURCE_ALIAS[column.source ?? column.prop] ??
+ column.source ??
+ column.prop
+ if (!source || !allowedColumns.has(source) || seenSources.has(source)) {
+ return null
+ }
+
+ seenSources.add(source)
+ const allowedColumn = allowedColumns.get(source)
+ return {
+ source,
+ label: column.label || allowedColumn.label
+ }
+ })
+ .filter(Boolean)
+}
+
+export function build@{ENTITYNAME}ReportMeta({
+ previewMeta = {},
+ count = 0,
+ titleAlign = @{ENTITYPREFIX}_REPORT_STYLE.titleAlign,
+ titleLevel = @{ENTITYPREFIX}_REPORT_STYLE.titleLevel
+} = {}) {
+ return {
+ reportTitle: @{ENTITYPREFIX}_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...@{ENTITYPREFIX}_REPORT_STYLE,
+ titleAlign,
+ titleLevel
+ }
+ }
+}
+
+export function build@{ENTITYNAME}DialogModel(record = {}) {
+ return {
+ ...create@{ENTITYNAME}FormState(),
+@{DIALOGMODELCONTENT}
+ }
+}
+
+export function build@{ENTITYNAME}SavePayload(formData = {}) {
+ return {
+@{SAVEPAYLOADCONTENT}
+ }
+}
+
+export function normalize@{ENTITYNAME}ListRow(record = {}) {
+ const statusMeta = get@{ENTITYNAME}StatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+@{NORMALIZEROWCONTENT}
+ }
+}
+
+export function build@{ENTITYNAME}PrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalize@{ENTITYNAME}ListRow(record))
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Search.txt b/rsf-framework/src/main/resources/templates/rsf-design/Search.txt
new file mode 100644
index 0000000..13daed5
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Search.txt
@@ -0,0 +1,66 @@
+<template>
+ <ArtSearchBar
+ ref="searchBarRef"
+ v-model="formData"
+ :items="formItems"
+ :showExpand="false"
+ @reset="handleReset"
+ @search="handleSearch"
+ />
+</template>
+
+<script setup>
+ import { create@{ENTITYNAME}SearchState, get@{ENTITYNAME}FieldOptions } from '../@{SIMPLEENTITYNAME}Page.helpers'
+
+ const props = defineProps({
+ modelValue: { required: true }
+ })
+
+ const emit = defineEmits(['update:modelValue', 'search', 'reset'])
+ const searchBarRef = ref()
+
+ const formData = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+ })
+
+ function createInputSearchItem(label, key, placeholder) {
+ return {
+ label,
+ key,
+ type: 'input',
+ props: {
+ placeholder,
+ clearable: true
+ }
+ }
+ }
+
+ function createSelectSearchItem(label, key, placeholder, options) {
+ return {
+ label,
+ key,
+ type: 'select',
+ props: {
+ placeholder,
+ clearable: true,
+ options
+ }
+ }
+ }
+
+ const formItems = computed(() => [
+ createInputSearchItem('鍏抽敭瀛�', 'condition', '璇疯緭鍏{TABLEDESC}鍏抽敭瀛�'),
+@{SEARCHITEMSCONTENT}
+ ])
+
+ function handleReset() {
+ emit('update:modelValue', create@{ENTITYNAME}SearchState())
+ emit('reset')
+ }
+
+ async function handleSearch(params) {
+ await searchBarRef.value.validate()
+ emit('search', params)
+ }
+</script>
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt b/rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt
new file mode 100644
index 0000000..187ed92
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt
@@ -0,0 +1,96 @@
+import { h } from 'vue'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { ElTag } from 'element-plus'
+import { get@{ENTITYNAME}StatusMeta } from './@{SIMPLEENTITYNAME}Page.helpers'
+
+const MORE_ACTIONS = [
+ {
+ key: 'edit',
+ label: '缂栬緫',
+ icon: 'ri:edit-2-line',
+ auth: 'edit'
+ },
+ {
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-4-line',
+ color: '#f56c6c',
+ auth: 'delete'
+ }
+]
+
+function resolveCellValue(row, prop) {
+ const value = row?.[prop]
+ return value === '' || value === void 0 || value === null ? '-' : value
+}
+
+function createTextColumn(prop, label, minWidth, extra = {}) {
+ return {
+ prop,
+ label,
+ minWidth,
+ showOverflowTooltip: true,
+ formatter: (row) => resolveCellValue(row, prop),
+ ...extra
+ }
+}
+
+function createNumberColumn(prop, label, width, extra = {}) {
+ return {
+ prop,
+ label,
+ width,
+ formatter: (row) => resolveCellValue(row, prop),
+ ...extra
+ }
+}
+
+function createTagColumn(prop, label, width, resolveMeta) {
+ return {
+ prop,
+ label,
+ width,
+ formatter: (row) => {
+ const statusMeta = resolveMeta(row)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ }
+}
+
+function buildMoreActions(handleEdit, handleDelete) {
+ return MORE_ACTIONS.filter((item) => {
+ if (item.key === 'edit') {
+ return typeof handleEdit === 'function'
+ }
+ if (item.key === 'delete') {
+ return typeof handleDelete === 'function'
+ }
+ return true
+ })
+}
+
+export function create@{ENTITYNAME}TableColumns({ handleEdit, handleDelete } = {}) {
+ return [
+ { type: 'selection', width: 52, fixed: 'left' },
+@{TABLECOLUMNSCONTENT}
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: buildMoreActions(handleEdit, handleDelete),
+ onClick: (item) => {
+ if (item.key === 'edit') {
+ handleEdit?.(row)
+ }
+ if (item.key === 'delete') {
+ handleDelete?.(row)
+ }
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
index 1b5dba8..830935a 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
@@ -2,6 +2,7 @@
import com.vincent.rsf.framework.generators.ReactGenerator;
+import com.vincent.rsf.framework.generators.RsfDesignGenerator;
import com.vincent.rsf.framework.generators.constant.SqlOsType;
/**
@@ -10,6 +11,10 @@
public class CodeBuilder {
public static void main(String[] args) throws Exception {
+ buildRsfDesign();
+ }
+
+ public static void buildReactAdmin() throws Exception {
ReactGenerator generator = new ReactGenerator();
generator.backendPrefixPath = "rsf-server/";
generator.frontendPrefixPath = "rsf-admin/";
@@ -25,7 +30,27 @@
generator.table = "man_matnr_restriction_warehouse";
generator.tableDesc = "鐗╂枡闄愬埗";
generator.packagePath = "com.vincent.rsf.server.manager";
+ generator.build();
+ }
+ public static void buildRsfDesign() throws Exception {
+ RsfDesignGenerator generator = new RsfDesignGenerator();
+ generator.backendPrefixPath = "rsf-server/";
+ generator.frontendPrefixPath = "rsf-design/";
+
+ generator.sqlOsType = SqlOsType.MYSQL;
+ generator.url = "127.0.0.1:3306/rsf";
+ generator.username = "root";
+ generator.password = "root";
+// generator.url="47.97.1.152:51433;databasename=jkasrs";
+// generator.username="sa";
+// generator.password="Zoneyung@zy56$";
+
+ generator.table = "man_matnr_restriction_warehouse";
+ generator.tableDesc = "鐗╂枡闄愬埗";
+ generator.packagePath = "com.vincent.rsf.server.manager";
+ generator.frontendViewPath = "manager/matnr-restriction-warehouse";
+ generator.frontendApiModule = "matnr-restriction-warehouse";
generator.build();
}
/*
--
Gitblit v1.9.1