feat(CRUD):使用 Phinx 创建表和修改表结构

This commit is contained in:
妙码生花 2023-06-29 14:37:04 +08:00
parent 0d3af06da7
commit d6feb33b29
8 changed files with 518 additions and 80 deletions

View File

@ -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);

View File

@ -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) {

View File

@ -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>

View File

@ -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));
}
/**

View File

@ -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',
}

View File

@ -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': '数据表设计变更预览',
}

View File

@ -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>

View File

@ -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,
},
{