mirror of
https://gitee.com/wonderful-code/buildadmin
synced 2024-11-21 22:55:36 +00:00
feat(CRUD):使用 Phinx 创建表和修改表结构
This commit is contained in:
parent
0d3af06da7
commit
d6feb33b29
@ -66,7 +66,7 @@ class Crud extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言包数据
|
||||
* 开始生成
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function generate()
|
||||
@ -86,11 +86,8 @@ class Crud extends Backend
|
||||
'status' => 'start',
|
||||
]);
|
||||
|
||||
// 表存在则删除
|
||||
Helper::delTable($table['name']);
|
||||
|
||||
// 创建表
|
||||
[$tablePk] = Helper::createTable($table['name'], $table['comment'] ?? '', $fields);
|
||||
// 处理表设计
|
||||
[$tablePk] = Helper::handleTableDesign($table, $fields);
|
||||
|
||||
// 表名称
|
||||
$tableName = Helper::getTableName($table['name'], false);
|
||||
|
@ -5,10 +5,13 @@ namespace app\admin\library\crud;
|
||||
use Throwable;
|
||||
use ba\Filesystem;
|
||||
use think\Exception;
|
||||
use ba\TableManager;
|
||||
use think\facade\Db;
|
||||
use app\common\library\Menu;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\admin\model\CrudLog;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
use Phinx\Db\Adapter\AdapterInterface;
|
||||
|
||||
class Helper
|
||||
{
|
||||
@ -280,56 +283,189 @@ class Helper
|
||||
return $log->id;
|
||||
}
|
||||
|
||||
public static function createTable($name, $comment, $fields): array
|
||||
/**
|
||||
* 获取 Phinx 的字段类型数据
|
||||
* @param string $type 字段类型
|
||||
* @param array $field 字段数据
|
||||
* @return array
|
||||
*/
|
||||
public static function getPhinxFieldType(string $type, array $field): array
|
||||
{
|
||||
$fieldType = [
|
||||
'variableLength' => ['blob', 'date', 'enum', 'geometry', 'geometrycollection', 'json', 'linestring', 'longblob', 'longtext', 'mediumblob', 'mediumtext', 'multilinestring', 'multipoint', 'multipolygon', 'point', 'polygon', 'set', 'text', 'tinyblob', 'tinytext', 'year'],
|
||||
'fixedLength' => ['int', 'bigint', 'binary', 'bit', 'char', 'datetime', 'mediumint', 'smallint', 'time', 'timestamp', 'tinyint', 'varbinary', 'varchar'],
|
||||
'decimal' => ['decimal', 'double', 'float'],
|
||||
'supportUnsigned' => ['int', 'tinyint', 'smallint', 'mediumint', 'integer', 'bigint', 'real', 'double', 'float', 'decimal', 'numeric'],
|
||||
];
|
||||
$name = self::getTableName($name);
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `$name` (" . PHP_EOL;
|
||||
$pk = '';
|
||||
foreach ($fields as $field) {
|
||||
$fieldConciseType = self::analyseFieldType($field);
|
||||
// 组装dateType
|
||||
if (!isset($field['dataType']) || !$field['dataType']) {
|
||||
if (!$field['type']) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($field['type'], $fieldType['fixedLength'])) {
|
||||
$field['dataType'] = "{$field['type']}({$field['length']})";
|
||||
} elseif (in_array($field['type'], $fieldType['decimal'])) {
|
||||
$field['dataType'] = "{$field['type']}({$field['length']},{$field['precision']})";
|
||||
} elseif (in_array($field['type'], $fieldType['variableLength'])) {
|
||||
$field['dataType'] = $field['type'];
|
||||
} else {
|
||||
$field['dataType'] = $field['precision'] ? "{$field['type']}({$field['length']},{$field['precision']})" : "{$field['type']}({$field['length']})";
|
||||
}
|
||||
}
|
||||
$unsigned = ($field['unsigned'] && in_array($fieldConciseType, $fieldType['supportUnsigned'])) ? ' UNSIGNED' : '';
|
||||
$null = $field['null'] ? ' NULL' : ' NOT NULL';
|
||||
$autoIncrement = $field['autoIncrement'] ? ' AUTO_INCREMENT' : '';
|
||||
$default = '';
|
||||
if (strtolower((string)$field['default']) == 'null') {
|
||||
$default = ' DEFAULT NULL';
|
||||
} elseif ($field['default'] == '0') {
|
||||
$default = " DEFAULT '0'";
|
||||
} elseif ($field['default'] == 'empty string') {
|
||||
$default = " DEFAULT ''";
|
||||
} elseif ($field['default']) {
|
||||
$default = " DEFAULT '{$field['default']}'";
|
||||
}
|
||||
$fieldComment = $field['comment'] ? " COMMENT '{$field['comment']}'" : '';
|
||||
$sql .= "`{$field['name']}` {$field['dataType']}$unsigned$null$autoIncrement$default$fieldComment ," . PHP_EOL;
|
||||
if ($field['primaryKey']) {
|
||||
$pk = $field['name'];
|
||||
if ($type == 'tinyint') {
|
||||
if ((isset($field['dataType']) && $field['dataType'] == 'tinyint(1)') || $field['default'] == '1') {
|
||||
$type = 'boolean';
|
||||
}
|
||||
}
|
||||
$sql .= "PRIMARY KEY (`$pk`)" . PHP_EOL . ") ";
|
||||
$sql .= "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='$comment'";
|
||||
Db::execute($sql);
|
||||
$phinxFieldTypeMap = [
|
||||
// 数字
|
||||
'tinyint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_TINY],
|
||||
'smallint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_SMALL],
|
||||
'mediumint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_MEDIUM],
|
||||
'int' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => null],
|
||||
'bigint' => ['type' => AdapterInterface::PHINX_TYPE_BIG_INTEGER, 'limit' => null],
|
||||
'boolean' => ['type' => AdapterInterface::PHINX_TYPE_BOOLEAN, 'limit' => null],
|
||||
// 文本
|
||||
'varchar' => ['type' => AdapterInterface::PHINX_TYPE_STRING, 'limit' => null],
|
||||
'tinytext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_TINY],
|
||||
'mediumtext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_MEDIUM],
|
||||
'longtext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_LONG],
|
||||
'tinyblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_TINY],
|
||||
'mediumblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_MEDIUM],
|
||||
'longblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_LONG],
|
||||
];
|
||||
return array_key_exists($type, $phinxFieldTypeMap) ? $phinxFieldTypeMap[$type] : ['type' => $type, 'limit' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析字段limit和精度
|
||||
* @param string $type 字段类型
|
||||
* @param array $field 字段数据
|
||||
* @return array ['limit' => 10, 'precision' => null, 'scale' => null]
|
||||
*/
|
||||
public static function analyseFieldLimit(string $type, array $field): array
|
||||
{
|
||||
$fieldType = [
|
||||
'decimal' => ['decimal', 'double', 'float'],
|
||||
'values' => ['enum', 'set'],
|
||||
];
|
||||
|
||||
$dataTypeLimit = self::dataTypeLimit($field['dataType'] ?? '');
|
||||
if (in_array($type, $fieldType['decimal'])) {
|
||||
if ($dataTypeLimit) {
|
||||
return ['precision' => $dataTypeLimit[0], 'scale' => $dataTypeLimit[1] ?? 0];
|
||||
}
|
||||
$scale = isset($field['precision']) ? intval($field['precision']) : 0;
|
||||
return ['precision' => $field['length'] ?: 10, 'scale' => $scale];
|
||||
} elseif (in_array($type, $fieldType['values'])) {
|
||||
foreach ($dataTypeLimit as &$item) {
|
||||
$item = str_replace(['"', "'"], '', $item);
|
||||
}
|
||||
return ['values' => $dataTypeLimit];
|
||||
} else {
|
||||
if ($dataTypeLimit && $dataTypeLimit[0]) {
|
||||
return ['limit' => $dataTypeLimit[0]];
|
||||
} elseif ($field['length']) {
|
||||
return ['limit' => $field['length']];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function dataTypeLimit(string $dataType): array
|
||||
{
|
||||
preg_match("/\((.*?)\)/", $dataType, $matches);
|
||||
if (isset($matches[1]) && $matches[1]) {
|
||||
return explode(',', trim($matches[1], ','));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function analyseFieldDefault(array $field): mixed
|
||||
{
|
||||
if (strtolower((string)$field['default']) == 'null') {
|
||||
return null;
|
||||
}
|
||||
return match ($field['default']) {
|
||||
'0' => 0,
|
||||
'empty string' => '',
|
||||
default => $field['default'],
|
||||
};
|
||||
}
|
||||
|
||||
public static function searchArray($fields, callable $myFunction): array|bool
|
||||
{
|
||||
foreach ($fields as $key => $field) {
|
||||
if (call_user_func($myFunction, $field, $key)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Phinx 格式的字段数据
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
public static function getPhinxFieldData(array $field): array
|
||||
{
|
||||
$conciseType = self::analyseFieldType($field);
|
||||
$phinxTypeData = self::getPhinxFieldType($conciseType, $field);
|
||||
|
||||
$phinxColumnOptions = self::analyseFieldLimit($conciseType, $field);
|
||||
if (!is_null($phinxTypeData['limit'])) {
|
||||
$phinxColumnOptions['limit'] = $phinxTypeData['limit'];
|
||||
}
|
||||
if ($field['default'] != 'none') {
|
||||
$phinxColumnOptions['default'] = self::analyseFieldDefault($field);
|
||||
}
|
||||
$phinxColumnOptions['null'] = (bool)$field['null'];
|
||||
$phinxColumnOptions['comment'] = $field['comment'];
|
||||
$phinxColumnOptions['signed'] = !$field['unsigned'];
|
||||
$phinxColumnOptions['identity'] = $field['autoIncrement'];
|
||||
return [
|
||||
'type' => $phinxTypeData['type'],
|
||||
'options' => $phinxColumnOptions,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 表设计处理
|
||||
* @param array $table 表数据
|
||||
* @param array $fields 字段数据
|
||||
* @return array
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function handleTableDesign(array $table, array $fields): array
|
||||
{
|
||||
$name = self::getTableName($table['name']);
|
||||
$comment = $table['comment'] ?? '';
|
||||
$designChange = $table['designChange'] ?? [];
|
||||
$adapter = TableManager::adapter(false);
|
||||
|
||||
$pk = self::searchArray($fields, function ($item) {
|
||||
return $item['primaryKey'];
|
||||
});
|
||||
$pk = $pk ? $pk['name'] : '';
|
||||
|
||||
if ($adapter->hasTable($name)) {
|
||||
// 更新表
|
||||
TableManager::changeComment($name, $comment);
|
||||
$table = TableManager::instance($name, [], false);
|
||||
foreach ($designChange as $item) {
|
||||
if ($item['type'] == 'change-field-name') {
|
||||
$table->renameColumn($item['oldName'], $item['newName']);
|
||||
} elseif ($item['type'] == 'del-field') {
|
||||
$table->removeColumn($item['oldName']);
|
||||
} elseif ($item['type'] == 'change-field-attr') {
|
||||
$phinxFieldData = self::getPhinxFieldData(self::searchArray($fields, function ($field) use ($item) {
|
||||
return $field['name'] == $item['oldName'];
|
||||
}));
|
||||
$table->changeColumn($item['oldName'], $phinxFieldData['type'], $phinxFieldData['options']);
|
||||
} elseif ($item['type'] == 'add-field') {
|
||||
$phinxFieldData = self::getPhinxFieldData(self::searchArray($fields, function ($field) use ($item) {
|
||||
return $field['name'] == $item['newName'];
|
||||
}));
|
||||
$table->addColumn($item['newName'], $phinxFieldData['type'], $phinxFieldData['options']);
|
||||
}
|
||||
}
|
||||
$table->update();
|
||||
} else {
|
||||
// 创建表
|
||||
$table = TableManager::instance($name, [
|
||||
'id' => false,
|
||||
'comment' => $comment,
|
||||
'row_format' => 'DYNAMIC',
|
||||
'primary_key' => $pk,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
], false);
|
||||
foreach ($fields as $field) {
|
||||
$phinxFieldData = self::getPhinxFieldData($field);
|
||||
$table->addColumn($field['name'], $phinxFieldData['type'], $phinxFieldData['options']);
|
||||
}
|
||||
$table->create();
|
||||
}
|
||||
|
||||
return [$pk];
|
||||
}
|
||||
|
||||
@ -556,7 +692,12 @@ class Helper
|
||||
// 预留
|
||||
}
|
||||
|
||||
public static function analyseFieldType($field): string
|
||||
/**
|
||||
* 分析字段类型
|
||||
* @param array $field 字段数据
|
||||
* @return string 字段类型
|
||||
*/
|
||||
public static function analyseFieldType(array $field): string
|
||||
{
|
||||
$dataType = (isset($field['dataType']) && $field['dataType']) ? $field['dataType'] : $field['type'];
|
||||
if (stripos($dataType, '(') !== false) {
|
||||
|
@ -34,7 +34,7 @@
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm">{{ t('Cancel') }}</el-button>
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||
</el-button>
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
namespace ba;
|
||||
|
||||
use Phinx\Db\Table;
|
||||
use Throwable;
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
use think\migration\db\Table;
|
||||
use Phinx\Db\Adapter\AdapterFactory;
|
||||
use Phinx\Db\Adapter\AdapterInterface;
|
||||
|
||||
@ -28,29 +29,71 @@ class TableManager
|
||||
|
||||
/**
|
||||
* 返回一个 Phinx/Db/Table 实例 用于操作数据表
|
||||
* @param string $table 表名
|
||||
* @param array $options 传递给 Phinx/Db/Table 的 options
|
||||
* @param bool $tableContainsPrefix 表名是否已经包含前缀
|
||||
* @param string $table 表名
|
||||
* @param array $options 传递给 Phinx/Db/Table 的 options
|
||||
* @param bool $prefixWrapper 是否使用表前缀包装表名
|
||||
* @return Table
|
||||
*/
|
||||
public static function instance(string $table, array $options = [], bool $tableContainsPrefix = false): Table
|
||||
public static function instance(string $table, array $options = [], bool $prefixWrapper = true): Table
|
||||
{
|
||||
if (array_key_exists($table, self::$instances)) {
|
||||
return self::$instances[$table];
|
||||
}
|
||||
|
||||
self::adapter($prefixWrapper);
|
||||
|
||||
self::$instances[$table] = new Table($table, $options, $prefixWrapper ? self::$wrapper : self::$adapter);
|
||||
return self::$instances[$table];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个 Phinx\Db\Adapter\AdapterFactory 实例
|
||||
* @param bool $prefixWrapper 是否使用表前缀包装表名
|
||||
* @return AdapterInterface
|
||||
*/
|
||||
public static function adapter(bool $prefixWrapper = true): AdapterInterface
|
||||
{
|
||||
if (is_null(self::$adapter)) {
|
||||
$config = static::getDbConfig();
|
||||
$factory = AdapterFactory::instance();
|
||||
self::$adapter = $factory->getAdapter($config['adapter'], $config);
|
||||
|
||||
if (!$tableContainsPrefix && is_null(self::$wrapper)) {
|
||||
if ($prefixWrapper && is_null(self::$wrapper)) {
|
||||
self::$wrapper = $factory->getWrapper('prefix', self::$adapter);
|
||||
}
|
||||
}
|
||||
return $prefixWrapper ? self::$wrapper : self::$adapter;
|
||||
}
|
||||
|
||||
self::$instances[$table] = new Table($table, $options, $tableContainsPrefix ? self::$adapter : self::$wrapper);
|
||||
return self::$instances[$table];
|
||||
/**
|
||||
* 修改已有数据表的注释
|
||||
* Phinx只在新增表时可以设置注释
|
||||
* @param string $name
|
||||
* @param string $comment
|
||||
* @return bool
|
||||
*/
|
||||
public static function changeComment(string $name, string $comment): bool
|
||||
{
|
||||
$name = self::tableName($name);
|
||||
try {
|
||||
$sql = "ALTER TABLE `$name` COMMENT = '$comment'";
|
||||
Db::execute($sql);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据表名
|
||||
* @param string $table 表名,带不带前缀均可
|
||||
* @param bool $fullName 是否返回带前缀的表名
|
||||
* @return string 表名
|
||||
*/
|
||||
public static function tableName(string $table, bool $fullName = true): string
|
||||
{
|
||||
$tablePrefix = config('database.connections.mysql.prefix');
|
||||
$pattern = '/^' . $tablePrefix . '/i';
|
||||
return ($fullName ? $tablePrefix : '') . (preg_replace($pattern, '', $table));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -130,4 +130,11 @@ export default {
|
||||
'The selected table has already generated records You are advised to start with historical records':
|
||||
'The selected table has already generated records. You are advised to start with historical records',
|
||||
'Start with the historical record': 'Start with the historical record',
|
||||
'Add field': 'Add field',
|
||||
'Modify field properties': 'Modify field properties',
|
||||
'Modify field name': 'Modify field name',
|
||||
'Delete field': 'Delete field',
|
||||
Close: 'Close',
|
||||
'Table design change': 'Table design change',
|
||||
'Data table design changes preview': 'Data table design changes preview',
|
||||
}
|
||||
|
@ -128,4 +128,11 @@ export default {
|
||||
'The selected table has already generated records You are advised to start with historical records':
|
||||
'选择的表已有成功生成的记录,建议从历史记录开始~',
|
||||
'Start with the historical record': '从历史记录开始',
|
||||
'Add field': '添加字段',
|
||||
'Modify field properties': '修改字段属性',
|
||||
'Modify field name': '修改字段名称',
|
||||
'Delete field': '删除字段',
|
||||
Close: '关闭',
|
||||
'Table design change': '表设计变更',
|
||||
'Data table design changes preview': '数据表设计变更预览',
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
type="string"
|
||||
:placeholder="t('crud.crud.Name of the data table')"
|
||||
:input-attr="{
|
||||
onChange: onTableCheck,
|
||||
onChange: onTableNameChange,
|
||||
}"
|
||||
:error="state.tableNameError"
|
||||
/>
|
||||
@ -24,6 +24,14 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-link
|
||||
v-if="state.table.designChange.length"
|
||||
@click="state.showDesignChangeLog = true"
|
||||
class="design-change-log"
|
||||
type="danger"
|
||||
>
|
||||
{{ t('crud.crud.Table design change') }}
|
||||
</el-link>
|
||||
<el-button type="primary" :loading="state.loading.generate" @click="onGenerate" v-blur>
|
||||
{{ t('crud.crud.Generate CRUD code') }}
|
||||
</el-button>
|
||||
@ -193,6 +201,7 @@
|
||||
type="string"
|
||||
:attr="{
|
||||
size: 'small',
|
||||
onFocus: onFieldsBackup,
|
||||
onChange: onFieldNameChange,
|
||||
}"
|
||||
/>
|
||||
@ -206,6 +215,7 @@
|
||||
type="string"
|
||||
:attr="{
|
||||
size: 'small',
|
||||
onChange: onFieldCommentChange,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@ -268,24 +278,45 @@
|
||||
type="string"
|
||||
v-model="state.fields[state.activateField].name"
|
||||
:input-attr="{
|
||||
onFocus: onFieldsBackup,
|
||||
onChange: onFieldNameChange,
|
||||
}"
|
||||
/>
|
||||
<template v-if="state.fields[state.activateField].dataType">
|
||||
<FormItem :label="t('crud.crud.Field Type')" type="textarea" v-model="state.fields[state.activateField].dataType" />
|
||||
<FormItem
|
||||
:label="t('crud.crud.Field Type')"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
type="textarea"
|
||||
v-model="state.fields[state.activateField].dataType"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FormItem :label="t('crud.crud.Field Type')" type="string" v-model="state.fields[state.activateField].type" />
|
||||
<FormItem
|
||||
:label="t('crud.crud.Field Type')"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
type="string"
|
||||
v-model="state.fields[state.activateField].type"
|
||||
/>
|
||||
<div class="field-inline">
|
||||
<FormItem
|
||||
:label="t('crud.crud.length')"
|
||||
type="number"
|
||||
v-model.number="state.fields[state.activateField].length"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('crud.crud.decimal point')"
|
||||
type="number"
|
||||
v-model.number="state.fields[state.activateField].precision"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -294,6 +325,9 @@
|
||||
:placeholder="t('crud.crud.You can directly enter null, 0, empty string')"
|
||||
type="string"
|
||||
v-model="state.fields[state.activateField].default"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
/>
|
||||
<div class="field-inline">
|
||||
<FormItem
|
||||
@ -301,12 +335,18 @@
|
||||
:label="t('crud.state.Primary key')"
|
||||
type="switch"
|
||||
v-model="state.fields[state.activateField].primaryKey"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
/>
|
||||
<FormItem
|
||||
class="form-item-position-right"
|
||||
:label="t('crud.crud.Auto increment')"
|
||||
type="switch"
|
||||
v-model="state.fields[state.activateField].autoIncrement"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div class="field-inline">
|
||||
@ -315,12 +355,18 @@
|
||||
:label="t('crud.crud.Unsigned')"
|
||||
type="switch"
|
||||
v-model="state.fields[state.activateField].unsigned"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
/>
|
||||
<FormItem
|
||||
class="form-item-position-right"
|
||||
:label="t('crud.crud.Allow NULL')"
|
||||
type="switch"
|
||||
v-model="state.fields[state.activateField].null"
|
||||
:input-attr="{
|
||||
onChange: onFieldAttrChange,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!isEmpty(state.fields[state.activateField].table)">
|
||||
@ -498,6 +544,33 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog class="ba-operate-dialog design-change-log-dialog" width="20%" v-model="state.showDesignChangeLog">
|
||||
<template #header>
|
||||
<div v-drag="['.design-change-log-dialog', '.el-dialog__header']">
|
||||
{{ t('crud.crud.Data table design changes preview') }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar max-height="400px">
|
||||
<el-timeline class="design-change-log-timeline">
|
||||
<el-timeline-item
|
||||
v-for="(item, idx) in state.table.designChange"
|
||||
:key="idx"
|
||||
:type="getTableDesignTimelineType(item.type)"
|
||||
:hollow="true"
|
||||
:hide-timestamp="true"
|
||||
>
|
||||
{{ getTableDesignChangeContent(item) }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div class="confirm-generate-dialog-footer">
|
||||
<el-button @click="state.showDesignChangeLog = false">
|
||||
{{ t('crud.crud.Close') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -505,13 +578,12 @@
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import BaInput from '/@/components/baInput/index.vue'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { fieldItem, designTypes, tableFieldsKey } from '/@/views/backend/crud/index'
|
||||
import type { FieldItem } from '/@/views/backend/crud/index'
|
||||
import type { FieldItem, TableDesignChange, TableDesignChangeType } from '/@/views/backend/crud/index'
|
||||
import { cloneDeep, range, isEmpty } from 'lodash-es'
|
||||
import Sortable, { SortableEvent } from 'sortablejs'
|
||||
import { useTemplateRefsList } from '@vueuse/core'
|
||||
import { changeStep, state as crudState, getTableAttr } from '/@/views/backend/crud/index'
|
||||
import { ElNotification, FormItemRule, FormInstance, ElMessageBox } from 'element-plus'
|
||||
import { changeStep, state as crudState, getTableAttr, fieldItem, designTypes, tableFieldsKey } from '/@/views/backend/crud/index'
|
||||
import { ElNotification, FormItemRule, FormInstance, ElMessageBox, TimelineItemProps } from 'element-plus'
|
||||
import { getDatabaseList, getFileData, generateCheck, generate, parseFieldData, postLogStart } from '/@/api/backend/crud'
|
||||
import { getTableFieldList } from '/@/api/common'
|
||||
import { buildValidatorData, regularVarName } from '/@/utils/validate'
|
||||
@ -545,6 +617,7 @@ const state: {
|
||||
controllerFile: string
|
||||
validateFile: string
|
||||
webViewsDir: string
|
||||
designChange: TableDesignChange[]
|
||||
}
|
||||
fields: FieldItem[]
|
||||
activateField: number
|
||||
@ -575,6 +648,8 @@ const state: {
|
||||
}
|
||||
draggingField: boolean
|
||||
tableNameError: string
|
||||
fieldsBackup: FieldItem[]
|
||||
showDesignChangeLog: boolean
|
||||
} = reactive({
|
||||
loading: {
|
||||
init: false,
|
||||
@ -595,6 +670,7 @@ const state: {
|
||||
controllerFile: '',
|
||||
validateFile: '',
|
||||
webViewsDir: '',
|
||||
designChange: [],
|
||||
},
|
||||
fields: [],
|
||||
activateField: -1,
|
||||
@ -625,6 +701,8 @@ const state: {
|
||||
},
|
||||
draggingField: false,
|
||||
tableNameError: '',
|
||||
fieldsBackup: [],
|
||||
showDesignChangeLog: false,
|
||||
})
|
||||
|
||||
type TableKey = keyof typeof state.table
|
||||
@ -644,19 +722,40 @@ const onFieldDesignTypeChange = () => {
|
||||
state.fields[state.activateField] = handleFieldAttr(field)
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份 state.fields 数据
|
||||
*/
|
||||
const onFieldsBackup = () => {
|
||||
state.fieldsBackup = cloneDeep(state.fields)
|
||||
}
|
||||
|
||||
const onFieldNameChange = (val: string) => {
|
||||
const oldName = state.fieldsBackup[state.activateField].name
|
||||
for (const key in tableFieldsKey) {
|
||||
for (const idx in state.table[tableFieldsKey[key] as TableKey] as string[]) {
|
||||
if (!getArrayKey(state.fields, 'name', (state.table[tableFieldsKey[key] as TableKey] as string[])[idx])) {
|
||||
if ((state.table[tableFieldsKey[key] as TableKey] as string[])[idx] == oldName) {
|
||||
;(state.table[tableFieldsKey[key] as TableKey] as string[])[idx] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.table.defaultSortField) {
|
||||
if (!getArrayKey(state.fields, 'name', state.table.defaultSortField)) {
|
||||
state.table.defaultSortField = val
|
||||
}
|
||||
if (state.table.defaultSortField && state.table.defaultSortField == oldName) {
|
||||
state.table.defaultSortField = val
|
||||
}
|
||||
logTableDesignChange({
|
||||
type: 'change-field-name',
|
||||
index: state.activateField,
|
||||
oldName: oldName,
|
||||
newName: val,
|
||||
})
|
||||
}
|
||||
|
||||
const onFieldAttrChange = () => {
|
||||
logTableDesignChange({
|
||||
type: 'change-field-attr',
|
||||
index: state.activateField,
|
||||
oldName: state.fields[state.activateField].name,
|
||||
newName: '',
|
||||
})
|
||||
}
|
||||
|
||||
const onDelField = (index: number) => {
|
||||
@ -666,6 +765,12 @@ const onDelField = (index: number) => {
|
||||
state.table.defaultSortField = ''
|
||||
}
|
||||
|
||||
logTableDesignChange({
|
||||
type: 'del-field',
|
||||
oldName: state.fields[index].name,
|
||||
newName: '',
|
||||
})
|
||||
|
||||
for (const key in tableFieldsKey) {
|
||||
const delIdx = (state.table[tableFieldsKey[key] as TableKey] as string[]).findIndex((item) => {
|
||||
return item == state.fields[index].name
|
||||
@ -846,6 +951,7 @@ const handleFieldAttr = (field: FieldItem) => {
|
||||
* 根据字段字典重新生成字段的数据类型
|
||||
*/
|
||||
const onFieldCommentChange = (comment: string) => {
|
||||
onFieldAttrChange()
|
||||
if (['enum', 'set'].includes(state.fields[state.activateField].type)) {
|
||||
if (!comment) {
|
||||
state.fields[state.activateField].dataType = `${state.fields[state.activateField].type}()`
|
||||
@ -874,6 +980,7 @@ const onFieldCommentChange = (comment: string) => {
|
||||
}
|
||||
|
||||
const loadData = () => {
|
||||
state.table.designChange = []
|
||||
if (!['db', 'sql', 'log'].includes(crudState.type)) return
|
||||
|
||||
state.loading.init = true
|
||||
@ -971,6 +1078,13 @@ onMounted(() => {
|
||||
|
||||
state.fields.splice(evt.newIndex!, 0, data)
|
||||
|
||||
logTableDesignChange({
|
||||
type: 'add-field',
|
||||
index: evt.newIndex!,
|
||||
newName: data.name,
|
||||
oldName: '',
|
||||
})
|
||||
|
||||
// 远程下拉参数预填
|
||||
if (['remoteSelect', 'remoteSelects'].includes(data.designType)) {
|
||||
showRemoteSelectPre(evt.newIndex!, true)
|
||||
@ -1021,7 +1135,11 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const onTableCheck = (val: string) => {
|
||||
/**
|
||||
* 修改表名
|
||||
* @param val 新表名
|
||||
*/
|
||||
const onTableNameChange = (val: string) => {
|
||||
if (!val) return (state.tableNameError = '')
|
||||
if (/^[a-z_][a-z0-9_]*$/.test(val)) {
|
||||
state.tableNameError = ''
|
||||
@ -1029,8 +1147,13 @@ const onTableCheck = (val: string) => {
|
||||
} else {
|
||||
state.tableNameError = t('crud.crud.Use lower case underlined for table names')
|
||||
}
|
||||
state.table.designChange = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 预获取一个表的生成数据
|
||||
* @param val 新表名
|
||||
*/
|
||||
const onTableChange = (val: string) => {
|
||||
if (!val) return
|
||||
getFileData(val, state.table.isCommonModel).then((res) => {
|
||||
@ -1134,6 +1257,107 @@ const remoteSelectPreFormRules: Partial<Record<string, FormItemRule[]>> = reacti
|
||||
joinField: [buildValidatorData({ name: 'required', title: t('crud.crud.Fields displayed in the table') })],
|
||||
controllerFile: [buildValidatorData({ name: 'required', title: t('crud.crud.Controller position') })],
|
||||
})
|
||||
|
||||
const logTableDesignChange = (data: TableDesignChange) => {
|
||||
if (crudState.type == 'create') return
|
||||
let push = true
|
||||
if (data.type == 'change-field-name') {
|
||||
for (const key in state.table.designChange) {
|
||||
// 有属性修改记录的字段被改名-单独循环防止字段再次改名后造成找不到属性修改记录
|
||||
if (state.table.designChange[key].type == 'change-field-attr' && data.oldName == state.table.designChange[key].oldName) {
|
||||
state.table.designChange[key].oldName = data.newName
|
||||
break
|
||||
}
|
||||
}
|
||||
for (const key in state.table.designChange) {
|
||||
// 字段再次改名
|
||||
if (state.table.designChange[key].type == 'change-field-name' && state.table.designChange[key].newName == data.oldName) {
|
||||
data.oldName = state.table.designChange[key].oldName
|
||||
state.table.designChange[key] = data
|
||||
push = false
|
||||
break
|
||||
}
|
||||
// 新增字段改名
|
||||
if (state.table.designChange[key].type == 'add-field' && state.table.designChange[key].newName == data.oldName) {
|
||||
state.table.designChange[key].newName = data.newName
|
||||
push = false
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (data.type == 'del-field') {
|
||||
for (const key in state.table.designChange) {
|
||||
// 同一字段名称多次删除(删除后添加再删除)
|
||||
if (state.table.designChange[key].type == 'del-field' && state.table.designChange[key].oldName == data.oldName) {
|
||||
push = false
|
||||
break
|
||||
}
|
||||
// 新增的字段被删除-删除新增记录
|
||||
if (state.table.designChange[key].type == 'add-field' && state.table.designChange[key].newName == data.oldName) {
|
||||
state.table.designChange.splice(key as any, 1)
|
||||
push = false
|
||||
break
|
||||
}
|
||||
}
|
||||
state.table.designChange = state.table.designChange.filter((item) => {
|
||||
// 有改名记录的字段被删除
|
||||
const name = item.type == 'change-field-name' && item.newName == data.oldName
|
||||
// 有属性修改记录的字段被删除
|
||||
const attr = item.type == 'change-field-attr' && data.oldName == item.oldName
|
||||
return !name && !attr
|
||||
})
|
||||
} else if (data.type == 'change-field-attr') {
|
||||
for (const key in state.table.designChange) {
|
||||
// 重复修改属性只记录一次
|
||||
if (state.table.designChange[key].type == 'change-field-attr' && state.table.designChange[key].oldName == data.oldName) {
|
||||
push = false
|
||||
break
|
||||
}
|
||||
// 新增的字段无需记录属性修改
|
||||
if (state.table.designChange[key].type == 'add-field' && state.table.designChange[key].newName == data.oldName) {
|
||||
push = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (push) state.table.designChange.push(data)
|
||||
}
|
||||
|
||||
const getTableDesignChangeContent = (data: TableDesignChange): string => {
|
||||
switch (data.type) {
|
||||
case 'add-field':
|
||||
return t('crud.crud.Add field') + ' ' + data.newName
|
||||
case 'change-field-attr':
|
||||
return t('crud.crud.Modify field properties') + ' ' + data.oldName
|
||||
case 'change-field-name':
|
||||
return t('crud.crud.Modify field name') + ' ' + data.oldName + ' => ' + data.newName
|
||||
case 'del-field':
|
||||
return t('crud.crud.Delete field') + ' ' + data.oldName
|
||||
default:
|
||||
return t('Unknown')
|
||||
}
|
||||
}
|
||||
|
||||
const getTableDesignTimelineType = (type: TableDesignChangeType): TimelineItemProps['type'] => {
|
||||
let timeline = ''
|
||||
switch (type) {
|
||||
case 'change-field-name':
|
||||
timeline = 'warning'
|
||||
break
|
||||
case 'del-field':
|
||||
timeline = 'danger'
|
||||
break
|
||||
case 'add-field':
|
||||
timeline = 'primary'
|
||||
break
|
||||
case 'change-field-attr':
|
||||
timeline = 'success'
|
||||
break
|
||||
default:
|
||||
timeline = 'success'
|
||||
break
|
||||
}
|
||||
return timeline as TimelineItemProps['type']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -1234,6 +1458,9 @@ const remoteSelectPreFormRules: Partial<Record<string, FormItemRule[]>> = reacti
|
||||
}
|
||||
.header-right {
|
||||
margin-left: auto;
|
||||
.design-change-log {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.default-sort-field-box {
|
||||
@ -1315,4 +1542,11 @@ const remoteSelectPreFormRules: Partial<Record<string, FormItemRule[]>> = reacti
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:deep(.design-change-log-dialog) .el-dialog__body {
|
||||
height: unset;
|
||||
padding-top: 20px;
|
||||
.design-change-log-timeline {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -3,6 +3,15 @@ import { i18n } from '/@/lang/index'
|
||||
import { validatorType } from '/@/utils/validate'
|
||||
import { npuaFalse, fieldData } from '/@/components/baInput/helper'
|
||||
|
||||
export type TableDesignChangeType = 'change-field-name' | 'del-field' | 'add-field' | 'change-field-attr'
|
||||
|
||||
export interface TableDesignChange {
|
||||
type: TableDesignChangeType
|
||||
index?: number
|
||||
oldName: string
|
||||
newName: string
|
||||
}
|
||||
|
||||
export const state: {
|
||||
step: 'Start' | 'Design'
|
||||
type: string
|
||||
@ -68,7 +77,7 @@ export const fieldItem: {
|
||||
table: {},
|
||||
form: {},
|
||||
...fieldData.number,
|
||||
default: '',
|
||||
default: 'none',
|
||||
primaryKey: true,
|
||||
unsigned: true,
|
||||
autoIncrement: true,
|
||||
@ -84,7 +93,7 @@ export const fieldItem: {
|
||||
...fieldData.number,
|
||||
type: 'bigint',
|
||||
length: 20,
|
||||
default: '',
|
||||
default: 'none',
|
||||
primaryKey: true,
|
||||
unsigned: true,
|
||||
},
|
||||
@ -96,7 +105,7 @@ export const fieldItem: {
|
||||
table: {},
|
||||
form: {},
|
||||
...fieldData.number,
|
||||
default: '',
|
||||
default: '0',
|
||||
null: true,
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user