项目初始化

This commit is contained in:
2026-03-18 15:54:43 +08:00
commit dfcd762e23
601 changed files with 57883 additions and 0 deletions

View File

@@ -0,0 +1,2094 @@
<template>
<div class="default-main">
<div class="header-config-box">
<el-row class="header-box">
<div class="header">
<div class="header-item-box">
<FormItem
class="mr-20 table-name-item"
:label="t('crud.log.table_name')"
v-model="state.table.name"
type="string"
:placeholder="t('crud.crud.Name of the data table')"
:input-attr="{
onChange: onTableNameChange,
}"
:error="state.error.tableName"
/>
<FormItem
class="table-comment-item"
:label="t('crud.crud.Data Table Notes')"
v-model="state.table.comment"
type="string"
:placeholder="t('crud.crud.For example: `user table` will be generated into `user management`')"
/>
</div>
<div class="header-right">
<el-link v-if="crudState.type != 'create'" @click="state.showDesignChangeLog = true" class="design-change-log" type="primary">
{{ 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>
<el-button @click="onAbandonDesign" type="danger" v-blur>{{ t('crud.crud.give up') }}</el-button>
</div>
</div>
</el-row>
<transition :name="state.showHeaderSeniorConfig ? 'el-zoom-in-top' : 'el-zoom-in-bottom'">
<div v-if="state.showHeaderSeniorConfig" class="header-senior-config-box">
<div class="header-senior-config-form">
<el-form-item :label-width="140" :label="t('crud.crud.Table Quick Search Fields')">
<el-select :clearable="true" :multiple="true" class="w100" v-model="state.table.quickSearchField" placement="bottom">
<el-option
v-for="(item, idx) in state.fields"
:key="idx + item.uuid!"
:label="item.name + (item.comment ? '-' + item.comment : item.title)"
:value="item.uuid!"
/>
</el-select>
</el-form-item>
<div class="default-sort-field-box">
<el-form-item :label-width="140" class="default-sort-field mr-20" :label="t('crud.crud.Table Default Sort Fields')">
<el-select :clearable="true" v-model="state.table.defaultSortField" placement="bottom">
<el-option
v-for="(item, idx) in state.fields"
:key="idx + item.uuid!"
:label="item.name + (item.comment ? '-' + item.comment : item.title)"
:value="item.uuid!"
/>
</el-select>
</el-form-item>
<FormItem
class="default-sort-field-type"
:label="t('crud.crud.sort order')"
v-model="state.table.defaultSortType"
type="select"
:input-attr="{
content: { desc: t('crud.crud.sort order desc'), asc: t('crud.crud.sort order asc') },
}"
/>
</div>
<el-form-item :label-width="140" :label="t('crud.crud.Fields as Table Columns')">
<el-select :clearable="true" :multiple="true" class="w100" v-model="state.table.columnFields" placement="bottom">
<el-option
v-for="(item, idx) in state.fields"
:key="idx + item.uuid!"
:label="item.name + (item.comment ? '-' + item.comment : item.title)"
:value="item.uuid!"
/>
</el-select>
</el-form-item>
<el-form-item :label-width="140" :label="t('crud.crud.Fields as form items')">
<el-select :clearable="true" :multiple="true" class="w100" v-model="state.table.formFields" placement="bottom">
<el-option
v-for="(item, idx) in state.fields"
:key="idx + item.uuid!"
:label="item.name + (item.comment ? '-' + item.comment : item.title)"
:value="item.uuid!"
/>
</el-select>
</el-form-item>
<FormItem
:label="t('crud.crud.The relative path to the generated code')"
v-model="state.table.generateRelativePath"
type="string"
:label-width="140"
:block-help="t('crud.crud.For quick combination code generation location, please fill in the relative path')"
:input-attr="{
onChange: onTableChange,
onInput: debouncedOnRelativePathInput,
}"
/>
<FormItem
:label="t('crud.crud.Generated Controller Location')"
v-model="state.table.controllerFile"
type="string"
:label-width="140"
/>
<el-form-item :label="t('crud.crud.Generated Data Model Location')" :label-width="140">
<el-input v-model="state.table.modelFile" type="string">
<template #append>
<el-checkbox
@change="onChangeCommonModel"
v-model="state.table.isCommonModel"
:label="t('crud.crud.Common model')"
size="small"
:true-value="1"
:false-value="0"
/>
</template>
</el-input>
</el-form-item>
<FormItem
:label="t('crud.crud.Generated Validator Location')"
v-model="state.table.validateFile"
type="string"
:label-width="140"
/>
<FormItem :label="t('crud.crud.WEB end view directory')" v-model="state.table.webViewsDir" type="string" :label-width="140" />
<FormItem
:label="t('Database connection')"
v-model="state.table.databaseConnection"
type="remoteSelect"
:label-width="140"
:block-help="t('Database connection help')"
:input-attr="{
pk: 'key',
field: 'key',
remoteUrl: getDatabaseConnectionListUrl,
}"
/>
</div>
</div>
</transition>
<div @click="state.showHeaderSeniorConfig = !state.showHeaderSeniorConfig" class="header-senior-config">
<span>{{ t('crud.crud.Advanced Configuration') }}</span>
<Icon
class="senior-config-arrow-icon"
size="14"
color="var(--el-text-color-primary)"
:name="state.showHeaderSeniorConfig ? 'el-icon-ArrowUp' : 'el-icon-ArrowDown'"
/>
</div>
</div>
<el-row v-loading="state.loading.init" class="fields-box" :gutter="20">
<el-col :xs="24" :span="6">
<el-collapse class="field-collapse" v-model="state.fieldCollapseName">
<el-collapse-item :title="t('crud.crud.Common Fields')" name="common">
<div class="field-box" :ref="tabsRefs.set">
<div v-for="(field, index) in fieldItem.common" :key="index" class="field-item">
<span>{{ field.title }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item :title="t('crud.crud.Base Fields')" name="base">
<div class="field-box" :ref="tabsRefs.set">
<div v-for="(field, index) in fieldItem.base" :key="index" class="field-item">
<span>{{ field.title }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item :title="t('crud.crud.Advanced Fields')" name="senior">
<div class="field-box" :ref="tabsRefs.set">
<div v-for="(field, index) in fieldItem.senior" :key="index" class="field-item">
<span>{{ field.title }}</span>
</div>
</div>
</el-collapse-item>
</el-collapse>
</el-col>
<el-col :xs="24" :span="12">
<div ref="designWindowRef" class="design-window ba-scroll-style">
<div
v-for="(field, index) in state.fields"
:key="index"
:class="index === state.activateField ? 'activate' : ''"
@click="onActivateField(index)"
class="design-field-box"
:data-id="index"
>
<div class="design-field">
<span>{{ t('crud.crud.Field Name') }}</span>
<BaInput
@pointerdown.stop
class="design-field-name-input"
:model-value="field.name"
type="string"
:attr="{
size: 'small',
onInput: ($event: string) => onFieldNameChange($event, index),
}"
/>
</div>
<div class="design-field">
<span>{{ t('crud.crud.field comment') }}</span>
<BaInput
@pointerdown.stop
class="design-field-name-comment"
v-model="field.comment"
type="string"
:attr="{
size: 'small',
onChange: onFieldCommentChange,
}"
/>
</div>
<div class="design-field-right">
<el-button
v-if="['remoteSelect', 'remoteSelects'].includes(field.designType)"
@click.stop="onEditField(index, field)"
type="primary"
size="small"
v-blur
circle
>
<Icon color="var(--el-color-white)" size="15" name="fa fa-pencil icon" />
</el-button>
<el-button @click.stop="onDelField(index)" type="danger" size="small" v-blur circle>
<Icon color="var(--el-color-white)" size="15" name="fa fa-trash" />
</el-button>
</div>
</div>
<div class="design-field-empty" v-if="!state.fields.length && !state.draggingField">
{{ t('crud.crud.Drag the left element here to start designing CRUD') }}
</div>
</div>
</el-col>
<el-col :xs="24" :span="6">
<div class="field-config ba-scroll-style">
<div v-if="state.activateField === -1" class="design-field-empty">
{{ t('crud.crud.Please select a field from the left first') }}
</div>
<div v-else :key="'activate-field-' + state.activateField">
<el-form label-position="top">
<el-divider content-position="left">{{ t('crud.crud.Common') }}</el-divider>
<el-form-item :label="t('crud.crud.Generate type')">
<el-select
@change="onFieldDesignTypeChange($event)"
class="w100"
:model-value="state.fields[state.activateField].designType"
placement="bottom"
>
<el-option v-for="(item, idx) in designTypes" :key="idx" :label="item.name" :value="idx" />
</el-select>
</el-form-item>
<FormItem
:label="t('crud.crud.Field comments (CRUD dictionary)')"
type="textarea"
:input-attr="{
rows: 2,
onChange: onFieldCommentChange,
}"
:placeholder="
t(
'crud.crud.The field comment will be used as the CRUD dictionary, and will be identified as the field title before the colon, and as the data dictionary after the colon'
)
"
v-model="state.fields[state.activateField].comment"
/>
<el-divider content-position="left">{{ t('crud.crud.Field Properties') }}</el-divider>
<FormItem
:label="t('crud.crud.Field Name')"
type="string"
:model-value="state.fields[state.activateField].name"
:input-attr="{
onInput: ($event: string) => onFieldNameChange($event, state.activateField),
}"
/>
<template v-if="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')"
: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="state.fields[state.activateField].length"
:input-attr="{
onChange: onFieldAttrChange,
}"
/>
<FormItem
:label="t('crud.crud.decimal point')"
type="number"
v-model="state.fields[state.activateField].precision"
:input-attr="{
onChange: onFieldAttrChange,
}"
/>
</div>
</template>
<el-form-item :label="t('crud.crud.Field Defaults')">
<el-select v-model="state.fields[state.activateField].defaultType">
<el-option label="手动输入" value="INPUT" />
<el-option label="EMPTY STRING空字符串" value="EMPTY STRING" />
<el-option label="NULL" value="NULL" />
<el-option label="不设默认值" value="NONE" />
</el-select>
<el-input
v-if="state.fields[state.activateField].defaultType == 'INPUT'"
:placeholder="t('crud.crud.Please input the default value')"
type="text"
v-model="state.fields[state.activateField].default"
@change="onFieldAttrChange"
class="default-input"
/>
</el-form-item>
<div class="field-inline">
<FormItem
class="form-item-position-right"
: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">
<FormItem
class="form-item-position-right"
: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)">
<el-divider content-position="left">{{ t('crud.crud.Field Table Properties') }}</el-divider>
<template v-for="(item, idx) in state.fields[state.activateField].table" :key="idx">
<FormItem
:label="$t('crud.crud.' + idx)"
:type="item.type"
v-model="state.fields[state.activateField].table[idx].value"
:placeholder="state.fields[state.activateField].table[idx].placeholder ?? ''"
:input-attr="{
content: state.fields[state.activateField].table[idx].options ?? {},
...(state.fields[state.activateField].table[idx].attr ?? {}),
}"
/>
</template>
</template>
<template v-if="!isEmpty(state.fields[state.activateField].form)">
<el-divider content-position="left">{{ t('crud.crud.Field Form Properties') }}</el-divider>
<template v-for="(item, idx) in state.fields[state.activateField].form" :key="idx">
<FormItem
v-if="item.type != 'hidden'"
:label="$t('crud.crud.' + idx)"
:type="item.type"
v-model="state.fields[state.activateField].form[idx].value"
:placeholder="state.fields[state.activateField].form[idx].placeholder ?? ''"
:input-attr="{
content: state.fields[state.activateField].form[idx].options ?? {},
...(state.fields[state.activateField].form[idx].attr ?? {}),
}"
/>
</template>
</template>
</el-form>
</div>
</div>
</el-col>
</el-row>
<el-dialog
@close="onCancelRemoteSelect"
class="ba-operate-dialog"
:model-value="state.remoteSelectPre.show"
:title="t('crud.crud.Remote drop-down association information')"
:close-on-click-modal="false"
:destroy-on-close="true"
@keyup.enter="onSaveRemoteSelect"
>
<el-scrollbar max-height="60vh">
<div class="ba-operate-form" :style="'width: calc(100% - 80px)'">
<el-form
ref="formRef"
:model="state.remoteSelectPre.form"
:rules="remoteSelectPreFormRules"
v-loading="state.remoteSelectPre.loading"
label-position="right"
label-width="160px"
v-if="state.remoteSelectPre.index != -1 && state.fields[state.remoteSelectPre.index]"
>
<FormItem
:label="t('crud.crud.Associated Data Table')"
v-model="state.remoteSelectPre.form.table"
type="remoteSelect"
:key="state.table.databaseConnection"
:input-attr="{
pk: 'table',
field: 'comment',
params: {
connection: state.table.databaseConnection,
samePrefix: 1,
excludeTable: [
'area',
'token',
'captcha',
'admin_group_access',
'config',
'admin_log',
'user_money_log',
'user_score_log',
],
},
remoteUrl: getTableListUrl,
onChange: onJoinTableChange,
}"
prop="table"
/>
<div v-loading="state.loading.remoteSelect">
<FormItem
prop="pk"
type="select"
:label="t('crud.crud.Drop down value field')"
v-model="state.remoteSelectPre.form.pk"
:placeholder="t('crud.crud.Please select the value field of the select component')"
:key="'select-value' + JSON.stringify(state.remoteSelectPre.fieldList)"
:input-attr="{
content: state.remoteSelectPre.fieldList,
}"
/>
<FormItem
prop="label"
type="select"
:label="t('crud.crud.Drop down label field')"
v-model="state.remoteSelectPre.form.label"
:placeholder="t('crud.crud.Please select the label field of the select component')"
:key="'select-label' + JSON.stringify(state.remoteSelectPre.fieldList)"
:input-attr="{
content: state.remoteSelectPre.fieldList,
}"
/>
<FormItem
v-if="state.fields[state.remoteSelectPre.index].designType == 'remoteSelect'"
prop="joinField"
type="selects"
:label="t('crud.crud.Fields displayed in the table')"
v-model="state.remoteSelectPre.form.joinField"
:placeholder="t('crud.crud.Please select the fields displayed in the table')"
:key="'join-field' + JSON.stringify(state.remoteSelectPre.fieldList)"
:input-attr="{
content: state.remoteSelectPre.fieldList,
}"
/>
<FormItem
:label="t('crud.crud.Data source configuration type')"
v-model="state.remoteSelectPre.form.sourceConfigType"
type="radio"
:input-attr="{
border: true,
content: {
crud: t('crud.crud.Fast configuration with generated controllers and models'),
custom: t('crud.crud.Custom configuration'),
},
}"
/>
<FormItem
v-if="state.remoteSelectPre.form.sourceConfigType == 'crud'"
prop="controllerFile"
type="select"
:label="t('crud.crud.Controller position')"
v-model="state.remoteSelectPre.form.controllerFile"
:placeholder="t('crud.crud.Please select the controller of the data table')"
:key="'controller-file' + JSON.stringify(state.remoteSelectPre.controllerFileList)"
:input-attr="{
content: state.remoteSelectPre.controllerFileList,
}"
:block-help="
t(
'crud.crud.The remote pull-down will request the corresponding controller to obtain data, so it is recommended that you create the CRUD of the associated table'
)
"
/>
<!-- 数据源配置类型为CRUD时模型位置必填 -->
<FormItem
:prop="state.remoteSelectPre.form.sourceConfigType == 'crud' ? 'modelFile' : ''"
type="select"
:label="t('crud.crud.Data Model Location')"
v-model="state.remoteSelectPre.form.modelFile"
:placeholder="t('crud.crud.Please select the data model location of the data table')"
:key="'model-file' + JSON.stringify(state.remoteSelectPre.modelFileList)"
:input-attr="{
content: state.remoteSelectPre.modelFileList,
}"
:block-help="
state.remoteSelectPre.form.sourceConfigType == 'crud'
? ''
: t(
'crud.crud.If it is left blank, the model of the associated table will be generated automatically If the table already has a model, it is recommended to select it to avoid repeated generation'
)
"
/>
<el-form-item
v-if="state.table.databaseConnection && state.remoteSelectPre.form.modelFile"
:label="t('Database connection')"
>
<el-text size="large" type="danger">{{ state.table.databaseConnection }}</el-text>
<div class="block-help">
<div>{{ t('crud.crud.Check model class', { connection: state.table.databaseConnection }) }}</div>
<div>{{ t('crud.crud.There is no connection attribute in model class') }}</div>
</div>
</el-form-item>
<FormItem
v-if="state.remoteSelectPre.form.sourceConfigType == 'custom'"
prop="remoteUrl"
:label="t('crud.crud.api url')"
type="string"
v-model="state.remoteSelectPre.form.remoteUrl"
:placeholder="t('crud.crud.api url example')"
/>
<FormItem
v-if="state.remoteSelectPre.form.sourceConfigType == 'custom'"
:label="t('crud.crud.remote-primary-table-alias')"
type="string"
v-model="state.remoteSelectPre.form.primaryTableAlias"
:block-help="
t(
'crud.crud.If the remote interface query involves associated query of multiple tables, enter the alias of the primary data table here'
)
"
>
<template #append>.{{ state.remoteSelectPre.form.pk }}</template>
</FormItem>
<el-form-item :label="t('Reminder')">
<div class="block-help">
{{ t('crud.crud.Design remote select tips') }}
</div>
</el-form-item>
</div>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - 88px)'">
<el-button @click="onCancelRemoteSelect">{{ $t('Cancel') }}</el-button>
<el-button v-blur @click="onSaveRemoteSelect" type="primary">
{{ $t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
@close="closeConfirmGenerate"
class="ba-operate-dialog confirm-generate-dialog"
:model-value="state.confirmGenerate.show"
:title="t('crud.crud.Confirm CRUD code generation')"
>
<div class="confirm-generate-dialog-body">
<el-alert
v-if="state.confirmGenerate.controller"
:title="t('crud.crud.The controller already exists Continuing to generate will automatically overwrite the existing code!')"
center
type="error"
/>
<el-alert
v-if="showTableConflictConfirmGenerate()"
:title="
t(
'crud.crud.The data table already exists Continuing to generate will automatically delete the original table and create a new one!'
)
"
class="mt-10"
center
type="error"
/>
<el-alert
v-if="state.confirmGenerate.menu"
:title="
t(
'crud.crud.The menu rule with the same name already exists The menu and permission node will not be created in this generation'
)
"
class="mt-10"
center
type="error"
/>
</div>
<template #footer>
<div class="confirm-generate-dialog-footer">
<el-button @click="closeConfirmGenerate">{{ $t('Cancel') }}</el-button>
<el-button :loading="state.loading.generate" v-blur @click="startGenerate" type="primary">
{{ t('crud.crud.Continue building') }}
</el-button>
</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">
<template v-if="state.table.designChange.length">
<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"
>
<div class="design-timeline-box">
<el-checkbox v-model="item.sync" :label="getTableDesignChangeContent(item)" size="small" />
</div>
</el-timeline-item>
</el-timeline>
<span class="design-change-tips">{{ t('crud.crud.designChangeTips') }}</span>
</template>
<div class="design-change-tips" v-else>暂无表设计变更</div>
<FormItem
:label="t('crud.crud.tableReBuild')"
class="rebuild-form-item"
v-model="state.table.rebuild"
type="radio"
:input-attr="{
border: true,
content: { No: t('crud.crud.No'), Yes: t('crud.crud.Yes') },
}"
:block-help="t('crud.crud.tableReBuildBlockHelp')"
/>
</el-scrollbar>
<template #footer>
<div class="confirm-generate-dialog-footer">
<el-button @click="state.showDesignChangeLog = false">
{{ t('Confirm') }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { useTemplateRefsList } from '@vueuse/core'
import type { FormItemRule, MessageHandler, TimelineItemProps } from 'element-plus'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { cloneDeep, debounce, isEmpty, range } from 'lodash-es'
import type { SortableEvent } from 'sortablejs'
import Sortable from 'sortablejs'
import { nextTick, onMounted, reactive, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { generate, generateCheck, getFileData, parseFieldData, postLogStart, uploadCompleted, uploadLog } from '/@/api/backend/crud'
import { getDatabaseConnectionListUrl, getTableFieldList, getTableListUrl } from '/@/api/common'
import BaInput from '/@/components/baInput/index.vue'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import { useTerminal } from '/@/stores/terminal'
import { getArrayKey } from '/@/utils/common'
import { uuid } from '/@/utils/random'
import { buildValidatorData, regularVarName } from '/@/utils/validate'
import { reloadServer } from '/@/utils/vite'
import type { FieldItem, TableDesignChange, TableDesignChangeType } from '/@/views/backend/crud/index'
import { changeStep, state as crudState, designTypes, fieldItem, getTableAttr, tableFieldsKey } from '/@/views/backend/crud/index'
let nameRepeatCount = 1
const { t } = useI18n()
const config = useConfig()
const terminal = useTerminal()
const formRef = useTemplateRef('formRef')
const tabsRefs = useTemplateRefsList<HTMLElement>()
const designWindowRef = useTemplateRef('designWindowRef')
const state: {
loading: {
init: boolean
generate: boolean
remoteSelect: boolean
}
sync: number
table: {
name: string
comment: string
quickSearchField: string[]
defaultSortField: string
formFields: string[]
columnFields: string[]
defaultSortType: string
generateRelativePath: string
isCommonModel: number
modelFile: string
controllerFile: string
validateFile: string
webViewsDir: string
databaseConnection: string
designChange: TableDesignChange[]
rebuild: string
}
fields: FieldItem[]
activateField: number
fieldCollapseName: string[]
remoteSelectPre: {
show: boolean
index: number
fieldList: anyObj
modelFileList: anyObj
controllerFileList: anyObj
loading: boolean
hideDelField: boolean
form: {
table: string
pk: string
label: string
joinField: string[]
sourceConfigType: 'crud' | 'custom'
remoteUrl: string
modelFile: string
controllerFile: string
primaryTableAlias: string
}
}
showHeaderSeniorConfig: boolean
confirmGenerate: {
show: boolean
menu: boolean
table: boolean
controller: boolean
}
draggingField: boolean
showDesignChangeLog: boolean
error: {
tableName: string
fieldName: MessageHandler | null
fieldNameDuplication: MessageHandler | null
}
} = reactive({
loading: {
init: false,
generate: false,
remoteSelect: false,
},
sync: 0,
table: {
name: '',
comment: '',
quickSearchField: [],
defaultSortField: '',
formFields: [],
columnFields: [],
defaultSortType: 'desc',
generateRelativePath: '',
isCommonModel: 0,
modelFile: '',
controllerFile: '',
validateFile: '',
webViewsDir: '',
databaseConnection: '',
designChange: [],
rebuild: 'No',
},
fields: [],
activateField: -1,
fieldCollapseName: ['common', 'base', 'senior'],
remoteSelectPre: {
show: false,
index: -1,
fieldList: [],
modelFileList: [],
controllerFileList: [],
loading: false,
hideDelField: false,
form: {
table: '',
pk: '',
label: '',
joinField: [],
sourceConfigType: 'crud',
remoteUrl: '',
modelFile: '',
controllerFile: '',
primaryTableAlias: '',
},
},
showHeaderSeniorConfig: true,
confirmGenerate: {
show: false,
menu: false,
table: false,
controller: false,
},
draggingField: false,
showDesignChangeLog: false,
error: {
tableName: '',
fieldName: null,
fieldNameDuplication: null,
},
})
type TableKey = keyof typeof state.table
const onActivateField = (idx: number) => {
state.activateField = idx
}
const onFieldDesignTypeChange = (designType: string) => {
// 获取新的类型的数据
let fieldDesignData: FieldItem | null = null
for (const key in fieldItem) {
const fieldItemIndex = getArrayKey(fieldItem[key as keyof typeof fieldItem], 'designType', designType)
if (fieldItemIndex !== false) {
fieldDesignData = cloneDeep(fieldItem[key as keyof typeof fieldItem][fieldItemIndex])
break
}
}
if (!fieldDesignData) return false
// 主键重复检查
if (!primaryKeyRepeatCheck(fieldDesignData, state.activateField)) {
return false
}
// 选中字段数据
const field = cloneDeep(state.fields[state.activateField])
// 赋值新类型
field.designType = designType
// 保留字段的 table 和 form 数据,此处额外处理以便交付给 handleFieldAttr 函数
for (const tKey in field.table) {
field.table[tKey] = field.table[tKey].value
}
for (const tKey in field.form) {
field.form[tKey] = field.form[tKey].value
}
state.fields[state.activateField] = handleFieldAttr(field)
// 保留字段的 uuid
state.fields[state.activateField].uuid = field.uuid
// 询问是否切换至预设方案(除了字段名的属性全部重置)
ElMessageBox.confirm(t('crud.crud.Reset generate type attr'), t('Reminder'), {
confirmButtonText: t('Confirm') + t('Reset'),
cancelButtonText: t('crud.crud.Design efficiency'),
type: 'warning',
closeOnClickModal: false,
})
.then(() => {
// 记录字段属性更新
onFieldAttrChange()
// 删除快速搜索和排序,根据新类型重新赋值
clearFieldTableData(state.fields[state.activateField].uuid!)
// 重置属性,除了 name
const oldName = state.fields[state.activateField].name
state.fields[state.activateField] = handleFieldAttr(fieldDesignData)
state.fields[state.activateField].name = oldName
if (fieldDesignData.primaryKey) {
// 设置为默认排序字段、快速搜索字段
state.table.quickSearchField.push(state.fields[state.activateField].uuid!)
if (!state.table.defaultSortField) {
state.table.defaultSortField = state.fields[state.activateField].uuid!
}
}
if (fieldDesignData.designType == 'weigh') {
state.table.defaultSortField = state.fields[state.activateField].uuid!
}
// 远程下拉参数预填
if (['remoteSelect', 'remoteSelects'].includes(fieldDesignData.designType)) {
showRemoteSelectPre(state.activateField, true)
}
// 表单表格字段预定义
if (!fieldDesignData.formBuildExclude) {
state.table.formFields.push(state.fields[state.activateField].uuid!)
}
if (!fieldDesignData.tableBuildExclude) {
state.table.columnFields.push(state.fields[state.activateField].uuid!)
}
})
.catch(() => {})
}
/**
* 字段名修改
*/
const onFieldNameChange = (val: string, index: number) => {
const oldName = state.fields[index].name
state.fields[index].name = val
logTableDesignChange({
type: 'change-field-name',
index: state.activateField,
oldName: oldName,
newName: val,
})
}
/**
* 主键字段重复检测
*/
const primaryKeyRepeatCheck = (field: FieldItem, excludeIndex: number = -1) => {
if (field.primaryKey === true) {
const primaryKeyField = state.fields.find((item, index) => {
if (excludeIndex > -1 && index == excludeIndex) {
return false
}
return item.primaryKey
})
if (primaryKeyField) {
ElNotification({
type: 'error',
message: t('crud.crud.There can only be one primary key field'),
})
return false
}
}
return true
}
/**
* 全部字段的名称命名规则检测
*/
const fieldNameCheck = (showErrorType: 'ElNotification' | 'ElMessage') => {
if (state.error.fieldName) {
state.error.fieldName.close()
state.error.fieldName = null
}
for (const key in state.fields) {
if (!regularVarName(state.fields[key].name)) {
let msg = t(
'crud.crud.Field name is invalid It starts with a letter or underscore and cannot contain any character other than letters, digits, or underscores',
{ field: state.fields[key].name }
)
if (showErrorType == 'ElMessage') {
state.error.fieldName = ElMessage({
message: msg,
type: 'error',
duration: 0,
})
} else {
ElNotification({
type: 'error',
message: msg,
})
}
return false
}
}
return true
}
/**
* 全部字段的名称重复检测
*/
const fieldNameDuplicationCheck = (showErrorType: 'ElNotification' | 'ElMessage') => {
if (state.error.fieldNameDuplication) {
state.error.fieldNameDuplication.close()
state.error.fieldNameDuplication = null
}
for (const key in state.fields) {
let count = 0
for (const checkKey in state.fields) {
if (state.fields[key].name == state.fields[checkKey].name) {
count++
}
if (count > 1) {
let msg = t('crud.crud.Field name duplication', { field: state.fields[key].name })
if (showErrorType == 'ElMessage') {
state.error.fieldNameDuplication = ElMessage({
message: msg,
type: 'error',
duration: 0,
})
} else {
ElNotification({
type: 'error',
message: msg,
})
}
return false
}
}
}
return true
}
const onFieldAttrChange = () => {
logTableDesignChange({
type: 'change-field-attr',
index: state.activateField,
oldName: state.fields[state.activateField].name,
newName: '',
})
}
/**
* 从 state.table.* 清理某个字段的数据
*/
const clearFieldTableData = (uuid: string) => {
if (uuid == state.table.defaultSortField) {
state.table.defaultSortField = ''
}
for (const key in tableFieldsKey) {
const delIdx = (state.table[tableFieldsKey[key] as TableKey] as string[]).findIndex((item) => {
return item == uuid
})
if (delIdx != -1) {
;(state.table[tableFieldsKey[key] as TableKey] as string[]).splice(delIdx, 1)
}
}
}
const onDelField = (index: number) => {
if (!state.fields[index]) return
state.activateField = -1
clearFieldTableData(state.fields[index].uuid!)
logTableDesignChange({
type: 'del-field',
oldName: state.fields[index].name,
newName: '',
})
// 删除权重字段时,重设默认排序字段
if (state.fields[index].designType == 'weigh') {
const pkField = state.fields.find((item) => {
return ['pk', 'spk'].includes(item.designType)
})
if (pkField) {
state.table.defaultSortField = pkField.uuid!
}
}
state.fields.splice(index, 1)
}
const showRemoteSelectPre = (index: number, hideDelField = false) => {
state.remoteSelectPre.show = true
state.remoteSelectPre.loading = true
state.remoteSelectPre.index = index
state.remoteSelectPre.hideDelField = hideDelField
if (state.fields[index] && state.fields[index].form['remote-table'].value) {
state.remoteSelectPre.form.table = state.fields[index].form['remote-table'].value
state.remoteSelectPre.form.pk = state.fields[index].form['remote-pk'].value
state.remoteSelectPre.form.label = state.fields[index].form['remote-field'].value
state.remoteSelectPre.form.controllerFile = state.fields[index].form['remote-controller'].value
state.remoteSelectPre.form.modelFile = state.fields[index].form['remote-model'].value
state.remoteSelectPre.form.remoteUrl = state.fields[index].form['remote-url'].value
state.remoteSelectPre.form.sourceConfigType = state.fields[index].form['remote-source-config-type'].value
state.remoteSelectPre.form.primaryTableAlias = state.fields[index].form['remote-primary-table-alias'].value
state.remoteSelectPre.form.joinField = state.fields[index].form['relation-fields'].value.split(',')
getTableFieldList(state.fields[index].form['remote-table'].value, true, state.table.databaseConnection).then((res) => {
const fieldSelect: anyObj = {}
for (const key in res.data.fieldList) {
fieldSelect[key] = (key ? key + ' - ' : '') + res.data.fieldList[key]
}
state.remoteSelectPre.fieldList = fieldSelect
})
if (isEmpty(state.remoteSelectPre.modelFileList) || isEmpty(state.remoteSelectPre.controllerFileList)) {
getFileData(state.fields[index].form['remote-table'].value).then((res) => {
state.remoteSelectPre.modelFileList = res.data.modelFileList
state.remoteSelectPre.controllerFileList = res.data.controllerFileList
})
}
}
state.remoteSelectPre.loading = false
}
const onEditField = (index: number, field: FieldItem) => {
if (['remoteSelect', 'remoteSelects'].includes(field.designType)) return showRemoteSelectPre(index)
}
const closeConfirmGenerate = () => {
state.confirmGenerate.show = false
}
const startGenerate = () => {
state.loading.generate = true
// 简化设计字段数据
const fields = cloneDeep(state.fields)
for (const key in fields) {
for (const tKey in fields[key].table) {
fields[key].table[tKey] = fields[key].table[tKey].value
}
for (const tKey in fields[key].form) {
fields[key].form[tKey] = fields[key].form[tKey].value
}
}
// 通过 uuid 获取字段 name
const table = cloneDeep(state.table)
if (table.defaultSortField) {
const defaultSortFieldIndex = getArrayKey(state.fields, 'uuid', table.defaultSortField)
if (defaultSortFieldIndex !== false) {
table.defaultSortField = state.fields[defaultSortFieldIndex].name
}
}
for (const key in tableFieldsKey) {
const names: string[] = []
const uuids = table[tableFieldsKey[key] as TableKey] as string[]
for (const uKey in uuids) {
const uuidFieldIndex = getArrayKey(state.fields, 'uuid', uuids[uKey])
if (uuidFieldIndex !== false) {
names.push(state.fields[uuidFieldIndex].name)
}
}
;(table[tableFieldsKey[key] as TableKey] as string[]) = names
}
generate({
type: crudState.type,
table,
fields,
})
.then((res) => {
const callback = () => {
const webViewsDir = state.table.webViewsDir.replace(/^web/, '.')
terminal.toggle(true)
terminal.addTask('npx.prettier', false, webViewsDir, () => {
terminal.toggle(false)
terminal.toggleDot(true)
nextTick(() => {
// 要求 Vite 服务端重启
if (import.meta.hot) {
reloadServer('crud')
} else {
ElNotification({
type: 'error',
message: t('crud.crud.Vite hot warning'),
})
}
})
})
}
if ((state.sync > 0 && config.crud.syncedUpdate === 'yes') || (state.sync == 0 && config.crud.syncType == 'automatic')) {
uploadLog({
logs: [
{
...res.data.crudLog,
public: config.crud.syncAutoPublic === 'yes' ? 1 : 0,
newLog: 1,
},
],
save: 1,
})
.then((res) => {
uploadCompleted({ syncIds: res.data.syncIds }).finally(() => {
callback()
})
})
.catch(() => {
callback()
})
} else {
callback()
}
})
.finally(() => {
state.loading.generate = false
closeConfirmGenerate()
})
}
const onGenerate = () => {
// 字段名称检查
if (!fieldNameCheck('ElNotification')) return
if (!fieldNameDuplicationCheck('ElNotification')) return
let msg = ''
// 主键检查
const pkIndex = state.fields.findIndex((item) => {
return item.primaryKey
})
if (pkIndex === -1) {
msg = t('crud.crud.Please design the primary key field!')
}
// 表名检查
if (!state.table.name) msg = t('crud.crud.Please enter the data table name!')
if (state.error.tableName) msg = t('crud.crud.Please enter the correct table name!')
if (msg) {
ElNotification({
type: 'error',
message: msg,
})
return
}
state.loading.generate = true
generateCheck({
table: state.table.name,
connection: state.table.databaseConnection,
webViewsDir: state.table.webViewsDir,
controllerFile: state.table.controllerFile,
})
.then(() => {
startGenerate()
})
.catch((res) => {
state.loading.generate = false
if (res.code == -1) {
state.confirmGenerate.menu = res.data.menu
state.confirmGenerate.table = res.data.table
state.confirmGenerate.controller = res.data.controller
if (showTableConflictConfirmGenerate() || state.confirmGenerate.controller || state.confirmGenerate.menu) {
state.confirmGenerate.show = true
} else {
startGenerate()
}
} else {
ElNotification({
type: 'error',
message: res.msg,
})
}
})
}
const showTableConflictConfirmGenerate = () => state.confirmGenerate.table && (crudState.type == 'create' || state.table.rebuild == 'Yes')
const onAbandonDesign = () => {
if (!state.table.name && !state.table.comment && !state.fields.length) {
return changeStep('start')
}
ElMessageBox.confirm(t('crud.crud.It is irreversible to give up the design Are you sure you want to give up?'), t('Reminder'), {
confirmButtonText: t('crud.crud.give up'),
cancelButtonText: t('Cancel'),
type: 'warning',
})
.then(() => {
changeStep('start')
})
.catch(() => {})
}
interface SortableEvt extends SortableEvent {
originalEvent?: DragEvent
}
/**
* 处理字段的属性
*/
const handleFieldAttr = (field: FieldItem) => {
field = cloneDeep(field)
const designTypeAttr = cloneDeep(designTypes[field.designType])
for (const tKey in field.form) {
if (designTypeAttr.form[tKey]) designTypeAttr.form[tKey].value = field.form[tKey]
if (tKey == 'image-multi' && field.form[tKey]) {
designTypeAttr.table['render'] = getTableAttr('render', 'images')
}
}
for (const tKey in field.table) {
if (designTypeAttr.table[tKey]) designTypeAttr.table[tKey].value = field.table[tKey]
}
field.form = designTypeAttr.form
field.table = designTypeAttr.table
field.uuid = uuid()
return field
}
/**
* 根据字段字典重新生成字段的数据类型
*/
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}()`
return
}
comment = comment.replaceAll('', ':')
comment = comment.replaceAll('', ',')
let comments = comment.split(':')
if (comments[1]) {
comments = comments[1].split(',')
comments = comments
.map((value) => {
if (!value) return ''
let temp = value.split('=')
if (temp[0] && temp[1]) {
return `'${temp[0]}'`
}
return ''
})
.filter((str: string) => str != '')
// 字段数据类型
state.fields[state.activateField].dataType = `${state.fields[state.activateField].type}(${comments.join(',')})`
}
}
}
const loadData = () => {
tableDesignChangeInit()
if (!['db', 'sql', 'log'].includes(crudState.type)) return
state.loading.init = true
// 从历史记录开始
if (crudState.type == 'log') {
postLogStart(crudState.startData.logId, crudState.startData.logType)
.then((res) => {
// 字段数据
const fields = res.data.fields
for (const key in fields) {
const field = handleFieldAttr(fields[key])
// 默认值和默认值类型分析
if (typeof field.defaultType == 'undefined') {
if (field.default && ['none', 'null', 'empty string'].includes(field.default)) {
field.defaultType = field.default.toUpperCase() as 'EMPTY STRING' | 'NULL' | 'NONE'
field.default = ''
} else {
field.defaultType = 'INPUT'
}
}
state.fields.push(field)
}
// 表数据
if (res.data.table.defaultSortField) {
const defaultSortFieldNameIndex = getArrayKey(state.fields, 'name', res.data.table.defaultSortField)
if (defaultSortFieldNameIndex !== false) {
res.data.table.defaultSortField = state.fields[defaultSortFieldNameIndex].uuid!
}
}
for (const key in tableFieldsKey) {
const uuids: string[] = []
const names = res.data.table[tableFieldsKey[key] as TableKey] as string[]
for (const nKey in names) {
const nameFieldIndex = getArrayKey(state.fields, 'name', names[nKey])
if (nameFieldIndex !== false) {
uuids.push(state.fields[nameFieldIndex].uuid!)
}
}
;(res.data.table[tableFieldsKey[key] as TableKey] as string[]) = uuids
}
state.sync = res.data.sync
state.table = res.data.table
tableDesignChangeInit()
if (res.data.table.empty) {
state.table.rebuild = 'Yes'
}
state.table.isCommonModel = parseInt(res.data.table.isCommonModel)
state.table.databaseConnection = res.data.table.databaseConnection ? res.data.table.databaseConnection : ''
})
.finally(() => {
state.loading.init = false
})
return
}
// 从数据表或sql开始
parseFieldData({
type: crudState.type,
table: crudState.startData.table,
sql: crudState.startData.sql,
connection: crudState.startData.databaseConnection,
})
.then((res) => {
let fields = []
for (const key in res.data.columns) {
const field = handleFieldAttr(res.data.columns[key])
if (!['id', 'update_time', 'create_time', 'updatetime', 'createtime'].includes(field.name)) {
state.table.formFields.push(field.uuid!)
}
if (!['textarea', 'file', 'files', 'editor', 'password', 'array'].includes(field.designType)) {
state.table.columnFields.push(field.uuid!)
}
if (field.designType == 'pk') {
state.table.defaultSortField = field.uuid!
state.table.quickSearchField.push(field.uuid!)
}
if (field.designType == 'weigh') {
state.table.defaultSortField = field.uuid!
}
fields.push(field)
}
state.fields = fields
state.table.comment = res.data.comment
state.table.databaseConnection = crudState.startData.databaseConnection
if (res.data.empty) {
state.table.rebuild = 'Yes'
}
if (crudState.type == 'db' && crudState.startData.table) {
state.table.name = crudState.startData.table
onTableChange(crudState.startData.table)
}
})
.finally(() => {
state.loading.init = false
})
}
/**
* 字段名称重复时自动重命名
*/
const autoRenameRepeatField = (fieldName: string) => {
const nameRepeatKey = getArrayKey(state.fields, 'name', fieldName)
if (nameRepeatKey !== false) {
fieldName += nameRepeatCount
nameRepeatCount++
return autoRenameRepeatField(fieldName)
} else {
return fieldName
}
}
onMounted(() => {
loadData()
const sortable = Sortable.create(designWindowRef.value!, {
group: 'design-field',
animation: 200,
filter: '.design-field-empty',
onAdd: (evt: SortableEvt) => {
const name = evt.originalEvent?.dataTransfer?.getData('name')
const field = fieldItem[name as keyof typeof fieldItem]
if (field && field[evt.oldIndex!]) {
const data = handleFieldAttr(field[evt.oldIndex!])
// 主键重复检测
if (data.primaryKey) {
if (primaryKeyRepeatCheck(data)) {
// 设置为默认排序字段、快速搜索字段
state.table.quickSearchField.push(data.uuid!)
if (!state.table.defaultSortField) {
state.table.defaultSortField = data.uuid!
}
} else {
return evt.item.remove()
}
}
// 出现权重字段则以其排序
if (data.designType == 'weigh') {
state.table.defaultSortField = data.uuid!
}
// name 重复时,自动重命名
data.name = autoRenameRepeatField(data.name)
// 插入字段
state.fields.splice(evt.newIndex!, 0, data)
logTableDesignChange({
type: 'add-field',
index: evt.newIndex!,
newName: data.name,
oldName: '',
after: evt.newIndex === 0 ? 'FIRST FIELD' : state.fields[evt.newIndex! - 1].name,
})
// 远程下拉参数预填
if (['remoteSelect', 'remoteSelects'].includes(data.designType)) {
showRemoteSelectPre(evt.newIndex!, true)
}
// 表单表格字段预定义
if (!data.formBuildExclude) {
state.table.formFields.push(data.uuid!)
}
if (!data.tableBuildExclude) {
state.table.columnFields.push(data.uuid!)
}
}
evt.item.remove()
nextTick(() => {
sortable.sort(range(state.fields.length).map((value) => value.toString()))
})
},
onEnd: (evt) => {
const temp = state.fields[evt.oldIndex!]
state.fields.splice(evt.oldIndex!, 1)
state.fields.splice(evt.newIndex!, 0, temp)
logTableDesignChange({
type: 'change-field-order',
index: evt.newIndex!,
newName: '',
oldName: temp.name,
after: evt.newIndex === 0 ? 'FIRST FIELD' : state.fields[evt.newIndex! - 1].name,
})
nextTick(() => {
sortable.sort(range(state.fields.length).map((value) => value.toString()))
})
},
})
tabsRefs.value.forEach((item, index) => {
Sortable.create(item, {
sort: false,
group: {
name: 'design-field',
pull: 'clone',
put: false,
},
animation: 200,
setData: (dataTransfer) => {
dataTransfer.setData('name', Object.keys(fieldItem)[index])
},
onStart: () => {
state.draggingField = true
},
onEnd: () => {
state.draggingField = false
},
})
})
})
/**
* 修改表名(失焦时校验,路径填充由 watch 统一触发避免重复请求)
*/
const onTableNameChange = (val: string) => {
if (!val) return (state.error.tableName = '')
if (/^[a-z_][a-z0-9_]*$/.test(val)) {
state.error.tableName = ''
} else {
state.error.tableName = t('crud.crud.Use lower case underlined for table names')
}
tableDesignChangeInit()
}
/** 相对路径输入时防抖触发路径填充 */
const debouncedOnRelativePathInput = debounce((val: string) => {
if (val) onTableChange(val)
}, 400)
/** 监听表名变化,统一防抖触发路径填充(唯一入口,避免 watch + onInput 重复请求) */
watch(
() => state.table.name,
debounce((val: string) => {
if (val && /^[a-z_][a-z0-9_]*$/.test(val)) {
onTableChange(val)
}
}, 400)
)
const tableDesignChangeInit = () => {
state.table.rebuild = 'No'
state.table.designChange = []
}
/**
* 预获取一个表的生成数据
* @param val 新表名
*/
const onTableChange = (val: string) => {
if (!val) return
getFileData(val, state.table.isCommonModel)
.then((res) => {
state.table.modelFile = res.data.modelFile
state.table.controllerFile = res.data.controllerFile
state.table.validateFile = res.data.validateFile
state.table.webViewsDir = res.data.webViewsDir
state.table.generateRelativePath = val.replaceAll('/', '\\')
})
.catch(() => {
// 接口失败时静默处理避免重复弹窗axios 已统一处理错误提示)
})
}
const onChangeCommonModel = () => {
const table = state.table.name || state.table.generateRelativePath?.replace(/\\/g, '/')
if (table) onTableChange(table)
}
const onJoinTableChange = () => {
if (!state.remoteSelectPre.form.table) return
// 重置远程下拉信息表单
resetRemoteSelectForm(['table'])
state.loading.remoteSelect = true
getTableFieldList(state.remoteSelectPre.form.table, true, state.table.databaseConnection)
.then((res) => {
state.remoteSelectPre.form.pk = res.data.pk
const preLabel = ['name', 'title', 'username', 'nickname']
for (const key in res.data.fieldList) {
if (preLabel.includes(key)) {
state.remoteSelectPre.form.label = key
state.remoteSelectPre.form.joinField.push(key)
break
}
}
const fieldSelect: anyObj = {}
for (const key in res.data.fieldList) {
fieldSelect[key] = (key ? key + ' - ' : '') + res.data.fieldList[key]
}
state.remoteSelectPre.fieldList = fieldSelect
})
.finally(() => {
state.loading.remoteSelect = false
})
getFileData(state.remoteSelectPre.form.table).then((res) => {
state.remoteSelectPre.modelFileList = res.data.modelFileList
state.remoteSelectPre.controllerFileList = res.data.controllerFileList
if (Object.keys(res.data.modelFileList).includes(res.data.modelFile)) {
state.remoteSelectPre.form.modelFile = res.data.modelFile
}
if (Object.keys(res.data.controllerFileList).includes(res.data.controllerFile)) {
state.remoteSelectPre.form.controllerFile = res.data.controllerFile
}
})
}
const onSaveRemoteSelect = () => {
const submitCallback = () => {
// 修改字段名
if (state.fields[state.remoteSelectPre.index].name == 'remote_select') {
const newName =
state.remoteSelectPre.form.table + (state.fields[state.remoteSelectPre.index].designType == 'remoteSelect' ? '_id' : '_ids')
onFieldNameChange(newName, state.remoteSelectPre.index)
}
state.fields[state.remoteSelectPre.index].form['remote-table'].value = state.remoteSelectPre.form.table
state.fields[state.remoteSelectPre.index].form['remote-pk'].value = state.remoteSelectPre.form.pk
state.fields[state.remoteSelectPre.index].form['remote-field'].value = state.remoteSelectPre.form.label
state.fields[state.remoteSelectPre.index].form['remote-controller'].value = state.remoteSelectPre.form.controllerFile
state.fields[state.remoteSelectPre.index].form['remote-model'].value = state.remoteSelectPre.form.modelFile
state.fields[state.remoteSelectPre.index].form['remote-url'].value = state.remoteSelectPre.form.remoteUrl
state.fields[state.remoteSelectPre.index].form['remote-source-config-type'].value = state.remoteSelectPre.form.sourceConfigType
state.fields[state.remoteSelectPre.index].form['remote-primary-table-alias'].value = state.remoteSelectPre.form.primaryTableAlias
state.fields[state.remoteSelectPre.index].form['relation-fields'].value =
state.fields[state.remoteSelectPre.index].designType == 'remoteSelect'
? state.remoteSelectPre.form.joinField.join(',')
: state.remoteSelectPre.form.label
state.remoteSelectPre.index = -1
state.remoteSelectPre.show = false
resetRemoteSelectForm()
}
if (formRef.value) {
formRef.value.validate((valid) => {
if (valid) {
submitCallback()
}
})
}
}
const onCancelRemoteSelect = () => {
state.remoteSelectPre.show = false
resetRemoteSelectForm()
if (state.remoteSelectPre.index !== -1 && state.remoteSelectPre.hideDelField) {
onDelField(state.remoteSelectPre.index)
}
}
const resetRemoteSelectForm = (excludes: string[] = []) => {
for (const key in state.remoteSelectPre.form) {
if (excludes.includes(key)) continue
if (key == 'joinField') {
state.remoteSelectPre.form[key] = []
} else if (key == 'sourceConfigType') {
state.remoteSelectPre.form[key] = 'crud'
} else {
;(state.remoteSelectPre.form[key as keyof typeof state.remoteSelectPre.form] as string) = ''
}
}
}
const remoteSelectPreFormRules: Partial<Record<string, FormItemRule[]>> = reactive({
table: [buildValidatorData({ name: 'required', title: t('crud.crud.remote-table') })],
pk: [buildValidatorData({ name: 'required', title: t('crud.crud.Drop down value field') })],
label: [buildValidatorData({ name: 'required', title: t('crud.crud.Drop down label field') })],
joinField: [buildValidatorData({ name: 'required', title: t('crud.crud.Fields displayed in the table') })],
controllerFile: [buildValidatorData({ name: 'required', title: t('crud.crud.Controller position') })],
modelFile: [buildValidatorData({ name: 'required', title: t('crud.crud.Data Model Location') })],
remoteUrl: [buildValidatorData({ name: 'required', title: t('crud.crud.remote-url') })],
})
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
}
// 有排序记录的字段被改名
if (state.table.designChange[key].type == 'change-field-order' && data.oldName == state.table.designChange[key].oldName) {
state.table.designChange[key].oldName = data.newName
}
if (state.table.designChange[key].after == data.oldName) {
state.table.designChange[key].after = data.newName
}
}
for (const key in state.table.designChange) {
// 新增字段改名
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
}
// 字段再次改名
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
// 取消字段改名
if (state.table.designChange[key].newName == state.table.designChange[key].oldName) {
state.table.designChange.splice(key as any, 1)
}
push = false
break
}
}
} else if (data.type == 'del-field') {
let add = false
state.table.designChange = state.table.designChange.filter((item) => {
// 新增的字段被删除
add = item.type == 'add-field' && item.newName == data.oldName
// 有属性修改记录的字段被删除
const attr = item.type == 'change-field-attr' && item.oldName == data.oldName
// 有排序记录的字段被删除
const order = item.type == 'change-field-order' && item.oldName == data.oldName
return !add && !attr && !order
})
// 有改名记录的字段被删除(延后单独处理避免先改名再改属性的情况)
state.table.designChange = state.table.designChange.filter((item) => {
const name = item.type == 'change-field-name' && item.newName == data.oldName
if (name) data.oldName = item.oldName
return !name
})
// 添加的字段需要过滤掉记录同时不记录删除操作
if (add) push = false
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
}
}
} 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
}
}
} else if (data.type == 'change-field-order') {
for (const key in state.table.designChange) {
// 新增的字段
if (state.table.designChange[key].type == 'add-field' && state.table.designChange[key].newName == data.oldName) {
// 更新排序设定
state.table.designChange[key].after = data.after
push = false
break
}
// 重复的排序记录
if (state.table.designChange[key].type == 'change-field-order' && state.table.designChange[key].oldName == data.oldName) {
state.table.designChange[key] = data
push = false
break
}
}
}
data.sync = true
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
case 'change-field-order':
return (
t('crud.crud.Modify field order') +
' ' +
data.oldName +
' => ' +
(data.after == 'FIRST FIELD' ? t('crud.crud.First field') : data.after + ' ' + t('crud.crud.After'))
)
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
case 'change-field-order':
timeline = 'info'
break
default:
timeline = 'success'
break
}
return timeline as TimelineItemProps['type']
}
</script>
<style scoped lang="scss">
.form-item-position-right {
display: flex !important;
align-items: center;
:deep(.el-form-item__label) {
margin-bottom: 0 !important;
}
}
.default-main {
margin-bottom: 0;
}
.mt-10 {
margin-top: 10px;
}
.mr-20 {
margin-right: 20px;
}
.field-collapse :deep(.el-collapse-item__header) {
padding-left: 10px;
user-select: none;
}
.field-box {
padding: 10px;
}
.field-item {
display: inline-block;
padding: 3px 16px;
border: 1px dashed var(--el-border-color);
border-radius: var(--el-border-radius-base);
margin: 6px;
cursor: pointer;
user-select: none;
&:hover {
border-color: var(--el-color-primary);
}
}
.header-config-box {
position: relative;
.header-senior-config {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
height: 24px;
bottom: -24px;
padding: 4px 20px;
padding-top: 0;
left: calc(50% - 10px);
font-size: var(--el-font-size-small);
border-bottom-left-radius: 50px;
border-bottom-right-radius: 50px;
background-color: var(--ba-bg-color-overlay);
color: var(--el-text-color-primary);
cursor: pointer;
user-select: none;
.senior-config-arrow-icon {
margin-left: 4px;
}
}
}
.header-senior-config-box {
width: 100%;
padding: 10px;
background-color: var(--ba-bg-color-overlay);
}
.header-senior-config-form {
width: 50%;
:deep(.el-form-item__label) {
justify-content: flex-start;
}
}
.header-box {
display: flex;
align-items: center;
height: v-bind("state.error.tableName ? '70px':'60px'");
padding: 10px;
background-color: var(--ba-bg-color-overlay);
border-radius: var(--el-border-radius-base);
transition: 0.1s;
.header,
.header-item-box {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
white-space: nowrap;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.header-item-box {
width: 50%;
}
.table-name-item {
flex: 3;
}
.table-comment-item {
flex: 4;
}
.header-right {
margin-left: auto;
.design-change-log {
margin-right: 10px;
}
}
}
.default-sort-field-box {
display: flex;
.default-sort-field {
flex: 6;
}
.default-sort-field-type {
flex: 3;
}
}
.fields-box {
margin-top: 36px;
}
.design-field-empty {
display: flex;
height: 100%;
color: var(--el-color-info);
font-size: var(--el-font-size-medium);
align-items: center;
justify-content: center;
}
.design-window {
overflow-x: auto;
height: calc(100vh - 200px);
border-radius: var(--el-border-radius-base);
background-color: var(--ba-bg-color-overlay);
border: v-bind('state.draggingField ? "1px dashed var(--el-color-primary)":(state.fields.length ? "none":"1px dashed var(--el-border-color)")');
.design-field-box {
display: flex;
padding: 10px;
align-items: center;
border: 1px dashed var(--el-border-color);
border-radius: var(--el-border-radius-base);
margin-bottom: 2px;
cursor: pointer;
user-select: none;
.design-field {
padding-right: 10px;
}
.design-field-name-input {
width: 200px;
}
.design-field-name-comment {
width: 100px;
}
.design-field-right {
margin-left: auto;
}
&:hover {
border-color: var(--el-color-primary);
}
}
.design-field-box.activate {
border-color: var(--el-color-primary);
}
}
.field-inline {
display: flex;
:deep(.el-form-item) {
width: 46%;
margin-right: 2%;
}
}
.default-input {
margin-top: 10px;
}
.field-config {
overflow-x: auto;
height: calc(100vh - 200px);
padding: 20px;
background-color: var(--ba-bg-color-overlay);
}
:deep(.confirm-generate-dialog) .el-dialog__body {
height: unset;
}
.confirm-generate-dialog-body {
padding: 30px;
}
.confirm-generate-dialog-footer {
display: flex;
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;
.el-timeline-item .el-timeline-item__node {
top: 3px;
}
}
.design-change-tips {
display: block;
margin-bottom: 20px;
color: var(--el-color-info);
font-size: var(--el-font-size-small);
}
.rebuild-form-item {
padding-top: 20px;
border-top: 1px solid var(--el-border-color-lighter);
}
}
</style>