mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-25 13:51:04 +00:00
feat: 重构前端项目结构并优化代码
refactor: 迁移前端资源文件至web目录 feat: 新增多种图标资源 style: 统一代码风格和格式化配置 docs: 更新README和文档说明 chore: 更新依赖和配置文件 fix: 修复部分类型定义和枚举 perf: 优化路由和组件加载逻辑
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 将「表结构」表单状态编译为可执行的 CREATE TABLE SQL(MySQL / PostgreSQL)。
|
||||
*/
|
||||
|
||||
export type SqlDialect = 'mysql' | 'postgres';
|
||||
|
||||
export interface ColDef {
|
||||
name: string;
|
||||
type: string;
|
||||
nullable: boolean;
|
||||
isPk: boolean;
|
||||
comment: string;
|
||||
/** 仅 MySQL:数字主键自增 */
|
||||
autoIncrement?: boolean;
|
||||
}
|
||||
|
||||
export interface VisualBuildState {
|
||||
dialect: SqlDialect;
|
||||
mainTableName: string;
|
||||
mainComment: string;
|
||||
mainColumns: ColDef[];
|
||||
subEnabled: boolean;
|
||||
subTableName: string;
|
||||
subComment: string;
|
||||
/** 子表上指向主表的外键列名 */
|
||||
fkColumn: string;
|
||||
/** 主表被引用列,一般为 id */
|
||||
fkRefColumn: string;
|
||||
subColumns: ColDef[];
|
||||
}
|
||||
|
||||
function qMysql(name: string): string {
|
||||
return `\`${name.replace(/`/g, '')}\``;
|
||||
}
|
||||
|
||||
function buildMysqlTable(name: string, comment: string, columns: ColDef[]): string {
|
||||
const lines: string[] = [];
|
||||
const pkCols = columns.filter((c) => c.isPk).map((c) => qMysql(c.name));
|
||||
for (const c of columns) {
|
||||
let line = ` ${qMysql(c.name)} ${c.type}`;
|
||||
if (c.isPk && c.autoIncrement) {
|
||||
line += ' NOT NULL AUTO_INCREMENT';
|
||||
} else if (c.isPk) {
|
||||
line += ' NOT NULL';
|
||||
} else if (!c.nullable) {
|
||||
line += ' NOT NULL';
|
||||
} else {
|
||||
line += ' DEFAULT NULL';
|
||||
}
|
||||
if (c.comment) line += ` COMMENT '${c.comment.replace(/'/g, "''")}'`;
|
||||
lines.push(line);
|
||||
}
|
||||
const tail: string[] = [];
|
||||
if (pkCols.length) {
|
||||
tail.push(` PRIMARY KEY (${pkCols.join(', ')})`);
|
||||
}
|
||||
const uuidCol = columns.find((c) => c.name === 'uuid');
|
||||
if (uuidCol) {
|
||||
tail.push(` UNIQUE KEY ${qMysql(`uk_${name.replace(/`/g, '')}_uuid`)} (${qMysql('uuid')})`);
|
||||
}
|
||||
const allLines = [...lines, ...tail];
|
||||
return (
|
||||
`CREATE TABLE ${qMysql(name)} (\n${allLines.join(',\n')}\n)` +
|
||||
` ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='${comment.replace(/'/g, "''")}';`
|
||||
);
|
||||
}
|
||||
|
||||
function buildMysqlFk(subName: string, fkCol: string, mainName: string, refCol: string): string {
|
||||
return (
|
||||
`ALTER TABLE ${qMysql(subName)} ADD CONSTRAINT ${qMysql(`fk_${subName}_${fkCol}`)} ` +
|
||||
`FOREIGN KEY (${qMysql(fkCol)}) REFERENCES ${qMysql(mainName)} (${qMysql(refCol)}) ON DELETE CASCADE;`
|
||||
);
|
||||
}
|
||||
|
||||
function buildPostgresTable(name: string, comment: string, columns: ColDef[]): string {
|
||||
const lines: string[] = [];
|
||||
const pkCols = columns.filter((c) => c.isPk).map((c) => c.name);
|
||||
for (const c of columns) {
|
||||
let line = ` ${c.name} ${c.type}`;
|
||||
const isSerial = c.type.toUpperCase().includes('SERIAL');
|
||||
if (!isSerial && !c.nullable) line += ' NOT NULL';
|
||||
lines.push(line);
|
||||
}
|
||||
const tail: string[] = [];
|
||||
if (pkCols.length) {
|
||||
tail.push(` PRIMARY KEY (${pkCols.join(', ')})`);
|
||||
}
|
||||
const body = [...lines, ...tail].join(',\n');
|
||||
return (
|
||||
`CREATE TABLE ${name} (\n${body}\n);\n` +
|
||||
`COMMENT ON TABLE ${name} IS '${comment.replace(/'/g, "''")}';`
|
||||
);
|
||||
}
|
||||
|
||||
function buildPostgresColumnComments(table: string, columns: ColDef[]): string {
|
||||
return columns
|
||||
.filter((c) => c.comment)
|
||||
.map((c) => `COMMENT ON COLUMN ${table}.${c.name} IS '${c.comment.replace(/'/g, "''")}';`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function buildPostgresFk(subName: string, fkCol: string, mainName: string, refCol: string): string {
|
||||
return (
|
||||
`ALTER TABLE ${subName} ADD CONSTRAINT fk_${subName}_${fkCol} ` +
|
||||
`FOREIGN KEY (${fkCol}) REFERENCES ${mainName}(${refCol}) ON DELETE CASCADE;`
|
||||
);
|
||||
}
|
||||
|
||||
/** 由表结构状态生成完整 SQL(单表或主子表) */
|
||||
export function buildSqlFromVisual(state: VisualBuildState): string {
|
||||
const m = state.mainTableName.trim();
|
||||
if (!m) return '';
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (state.dialect === 'mysql') {
|
||||
parts.push(buildMysqlTable(m, state.mainComment || m, state.mainColumns));
|
||||
if (state.subEnabled && state.subTableName.trim()) {
|
||||
const s = state.subTableName.trim();
|
||||
parts.push(buildMysqlTable(s, state.subComment || s, state.subColumns));
|
||||
parts.push(buildMysqlFk(s, state.fkColumn, m, state.fkRefColumn));
|
||||
}
|
||||
} else {
|
||||
parts.push(buildPostgresTable(m, state.mainComment || m, state.mainColumns));
|
||||
parts.push(buildPostgresColumnComments(m, state.mainColumns));
|
||||
if (state.subEnabled && state.subTableName.trim()) {
|
||||
const s = state.subTableName.trim();
|
||||
parts.push(buildPostgresTable(s, state.subComment || s, state.subColumns));
|
||||
parts.push(buildPostgresColumnComments(s, state.subColumns));
|
||||
parts.push(buildPostgresFk(s, state.fkColumn, m, state.fkRefColumn));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.filter(Boolean).join('\n\n');
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { buildSqlFromVisual } from './buildCreateTableSql';
|
||||
import {
|
||||
applySubColumns,
|
||||
visualPresetMasterSub,
|
||||
visualPresetSingle,
|
||||
} from './createTableVisualPresets';
|
||||
|
||||
/** 与「表结构」单表模板一致的 SQL(便于两种模式对齐) */
|
||||
export function getExampleFromPresetSingle(dialect: 'mysql' | 'postgres'): string {
|
||||
return buildSqlFromVisual(visualPresetSingle(dialect));
|
||||
}
|
||||
|
||||
/** 与「表结构」主子表模板一致的 SQL */
|
||||
export function getExampleFromPresetMasterSub(dialect: 'mysql' | 'postgres'): string {
|
||||
return buildSqlFromVisual(applySubColumns(visualPresetMasterSub(dialect)));
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import type { ColDef, SqlDialect, VisualBuildState } from './buildCreateTableSql';
|
||||
|
||||
/** 与代码生成第三步「基本信息」联动时的输入(主表名、子表名、外键列名) */
|
||||
export interface GenTableCreateLink {
|
||||
table_name?: string;
|
||||
table_comment?: string;
|
||||
sub_table_name?: string;
|
||||
sub_table_fk_name?: string;
|
||||
}
|
||||
|
||||
function mixinMysql(): ColDef[] {
|
||||
return [
|
||||
{
|
||||
name: 'status',
|
||||
type: 'varchar(10)',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '是否启用(0:启用 1:禁用)',
|
||||
},
|
||||
{ name: 'description', type: 'text', nullable: true, isPk: false, comment: '备注/描述' },
|
||||
{
|
||||
name: 'created_time',
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '创建时间',
|
||||
},
|
||||
{
|
||||
name: 'updated_time',
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '更新时间',
|
||||
},
|
||||
{ name: 'created_id', type: 'int', nullable: true, isPk: false, comment: '创建人ID' },
|
||||
{ name: 'updated_id', type: 'int', nullable: true, isPk: false, comment: '更新人ID' },
|
||||
{
|
||||
name: 'is_deleted',
|
||||
type: 'tinyint(1)',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '是否已删除(0:未删除 1:已删除)',
|
||||
},
|
||||
{ name: 'deleted_time', type: 'datetime', nullable: true, isPk: false, comment: '删除时间' },
|
||||
{ name: 'deleted_id', type: 'int', nullable: true, isPk: false, comment: '删除人ID' },
|
||||
];
|
||||
}
|
||||
|
||||
function mixinPostgres(): ColDef[] {
|
||||
return [
|
||||
{
|
||||
name: 'status',
|
||||
type: 'varchar(10)',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '是否启用(0:启用 1:禁用)',
|
||||
},
|
||||
{ name: 'description', type: 'text', nullable: true, isPk: false, comment: '备注/描述' },
|
||||
{
|
||||
name: 'created_time',
|
||||
type: 'timestamp without time zone',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '创建时间',
|
||||
},
|
||||
{
|
||||
name: 'updated_time',
|
||||
type: 'timestamp without time zone',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '更新时间',
|
||||
},
|
||||
{ name: 'created_id', type: 'integer', nullable: true, isPk: false, comment: '创建人ID' },
|
||||
{ name: 'updated_id', type: 'integer', nullable: true, isPk: false, comment: '更新人ID' },
|
||||
{
|
||||
name: 'is_deleted',
|
||||
type: 'boolean',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '是否已删除(0:未删除 1:已删除)',
|
||||
},
|
||||
{
|
||||
name: 'deleted_time',
|
||||
type: 'timestamp without time zone',
|
||||
nullable: true,
|
||||
isPk: false,
|
||||
comment: '删除时间',
|
||||
},
|
||||
{ name: 'deleted_id', type: 'integer', nullable: true, isPk: false, comment: '删除人ID' },
|
||||
];
|
||||
}
|
||||
|
||||
function pkMysql(): ColDef[] {
|
||||
return [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigint',
|
||||
nullable: false,
|
||||
isPk: true,
|
||||
comment: '主键ID',
|
||||
autoIncrement: true,
|
||||
},
|
||||
{
|
||||
name: 'uuid',
|
||||
type: 'varchar(64)',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: 'UUID全局唯一标识',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function pkPostgres(): ColDef[] {
|
||||
return [
|
||||
{ name: 'id', type: 'SERIAL', nullable: false, isPk: true, comment: '主键ID' },
|
||||
{
|
||||
name: 'uuid',
|
||||
type: 'varchar(64)',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: 'UUID全局唯一标识',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildSubColumns(dialect: SqlDialect, mainTableName: string, fkColumn: string): ColDef[] {
|
||||
const isMysql = dialect === 'mysql';
|
||||
const mixin = isMysql ? mixinMysql() : mixinPostgres();
|
||||
const subPk = isMysql ? pkMysql() : pkPostgres();
|
||||
const fkColDef: ColDef = {
|
||||
name: fkColumn,
|
||||
type: 'bigint',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: `关联 ${mainTableName}.${'id'}`,
|
||||
};
|
||||
const subBusiness: ColDef[] = [
|
||||
{
|
||||
name: 'line_name',
|
||||
type: 'varchar(128)',
|
||||
nullable: true,
|
||||
isPk: false,
|
||||
comment: '明细名称',
|
||||
},
|
||||
{
|
||||
name: 'qty',
|
||||
type: isMysql ? 'int' : 'integer',
|
||||
nullable: false,
|
||||
isPk: false,
|
||||
comment: '数量',
|
||||
},
|
||||
];
|
||||
return [...subPk, fkColDef, ...subBusiness, ...mixin];
|
||||
}
|
||||
|
||||
export function applySubColumns(state: VisualBuildState): VisualBuildState {
|
||||
if (!state.subEnabled) {
|
||||
return { ...state, subColumns: [] };
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
subColumns: buildSubColumns(state.dialect, state.mainTableName, state.fkColumn),
|
||||
};
|
||||
}
|
||||
|
||||
/** 单表:带业务字段 name */
|
||||
export function visualPresetSingle(dialect: SqlDialect): VisualBuildState {
|
||||
const isMysql = dialect === 'mysql';
|
||||
const mixin = isMysql ? mixinMysql() : mixinPostgres();
|
||||
const pk = isMysql ? pkMysql() : pkPostgres();
|
||||
const business: ColDef[] = [
|
||||
{ name: 'name', type: 'varchar(64)', nullable: true, isPk: false, comment: '名称' },
|
||||
];
|
||||
return {
|
||||
dialect,
|
||||
mainTableName: 'gen_demo_single',
|
||||
mainComment: '代码生成-单表示例',
|
||||
mainColumns: [...pk, ...business, ...mixin],
|
||||
subEnabled: false,
|
||||
subTableName: 'gen_demo_order_item',
|
||||
subComment: '子表示例',
|
||||
fkColumn: 'order_id',
|
||||
fkRefColumn: 'id',
|
||||
subColumns: [],
|
||||
};
|
||||
}
|
||||
|
||||
/** 主子表:主表订单头 + 子表明细,子表含外键列 */
|
||||
export function visualPresetMasterSub(dialect: SqlDialect): VisualBuildState {
|
||||
const isMysql = dialect === 'mysql';
|
||||
const mixin = isMysql ? mixinMysql() : mixinPostgres();
|
||||
const pk = isMysql ? pkMysql() : pkPostgres();
|
||||
const mainBusiness: ColDef[] = [
|
||||
{
|
||||
name: 'order_title',
|
||||
type: 'varchar(128)',
|
||||
nullable: true,
|
||||
isPk: false,
|
||||
comment: '订单标题',
|
||||
},
|
||||
];
|
||||
return applySubColumns({
|
||||
dialect,
|
||||
mainTableName: 'gen_demo_order_master',
|
||||
mainComment: '代码生成-主表示例(订单头)',
|
||||
mainColumns: [...pk, ...mainBusiness, ...mixin],
|
||||
subEnabled: true,
|
||||
subTableName: 'gen_demo_order_item',
|
||||
subComment: '代码生成-子表示例(订单明细)',
|
||||
fkColumn: 'order_id',
|
||||
fkRefColumn: 'id',
|
||||
subColumns: [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将第三步已填的主子表信息合并到表结构预设。
|
||||
* 规则:子表名与外键列名同时非空时使用主子表模板;否则为单表模板(仅主表名/注释覆盖)。
|
||||
*/
|
||||
export function mergeGenTableLinkIntoVisual(
|
||||
link: GenTableCreateLink,
|
||||
dialect: SqlDialect
|
||||
): VisualBuildState {
|
||||
const main = (link.table_name || '').trim();
|
||||
const subSn = (link.sub_table_name || '').trim();
|
||||
const subFk = (link.sub_table_fk_name || '').trim();
|
||||
const hasSubPair = Boolean(subSn && subFk);
|
||||
const base: VisualBuildState = hasSubPair
|
||||
? visualPresetMasterSub(dialect)
|
||||
: visualPresetSingle(dialect);
|
||||
if (main) base.mainTableName = main;
|
||||
const mc = (link.table_comment || '').trim();
|
||||
if (mc) base.mainComment = mc;
|
||||
if (hasSubPair) {
|
||||
base.subEnabled = true;
|
||||
base.subTableName = subSn;
|
||||
base.fkColumn = subFk;
|
||||
base.fkRefColumn = 'id';
|
||||
} else {
|
||||
base.subEnabled = false;
|
||||
}
|
||||
return applySubColumns(base);
|
||||
}
|
||||
Reference in New Issue
Block a user