项目初始化

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>

View File

@@ -0,0 +1,970 @@
import { reactive } from 'vue'
import { fieldData, npuaFalse } from '/@/components/baInput/helper'
import { i18n } from '/@/lang/index'
import { validatorType } from '/@/utils/validate'
/**
* 字段修改类型标识
* 改排序需要在表结构变更完成之后再单独处理所以标识独立
*/
export type TableDesignChangeType = 'change-field-name' | 'del-field' | 'add-field' | 'change-field-attr' | 'change-field-order'
export interface TableDesignChange {
type: TableDesignChangeType
// 字段在设计器中的数组 index
index?: number
// 字段旧名称(重命名、修改属性、删除)
oldName: string
// 字段新名称(重命名、添加)
newName: string
// 是否同步到数据表
sync?: boolean
// 此字段在 after 字段之后,值为`FIRST FIELD`表示它是第一个字段
after?: string
}
export const state: {
step: 'Start' | 'Design'
type: string
startData: {
sql: string
table: string
logId: string
logType: string
databaseConnection: string
}
} = reactive({
step: 'Start',
type: '',
startData: {
sql: '',
table: '',
logId: '',
logType: '',
databaseConnection: '',
},
})
export const changeStep = (type: string) => {
state.type = type
if (type == 'start') {
state.step = 'Start'
for (const key in state.startData) {
state.startData[key as keyof typeof state.startData] = ''
}
} else {
state.step = 'Design'
}
}
export interface FieldItem {
index?: number
title: string
name: string
type: string
dataType?: string
length: number
precision: number
default?: string
defaultType: 'INPUT' | 'EMPTY STRING' | 'NULL' | 'NONE'
null: boolean
primaryKey: boolean
unsigned: boolean
autoIncrement: boolean
comment: string
designType: string
formBuildExclude?: boolean
tableBuildExclude?: boolean
table: anyObj
form: anyObj
uuid?: string
}
export const fieldItem: {
common: FieldItem[]
base: FieldItem[]
senior: FieldItem[]
} = {
common: [
{
title: i18n.global.t('crud.state.Primary key'),
name: 'id',
comment: 'ID',
designType: 'pk',
formBuildExclude: true,
table: {},
form: {},
...fieldData.number,
defaultType: 'NONE',
null: false,
primaryKey: true,
unsigned: true,
autoIncrement: true,
},
{
title: i18n.global.t('crud.state.Primary key (Snowflake ID)'),
name: 'id',
comment: 'ID',
designType: 'spk',
formBuildExclude: true,
table: {},
form: {},
...fieldData.number,
type: 'bigint',
length: 20,
defaultType: 'NONE',
null: false,
primaryKey: true,
unsigned: true,
},
{
title: i18n.global.t('State'),
name: 'status',
comment: i18n.global.t('crud.state.Status:0=Disabled,1=Enabled'),
designType: 'switch',
table: {},
form: {},
...fieldData.switch,
default: '1',
defaultType: 'INPUT',
},
{
title: i18n.global.t('crud.state.remarks'),
name: 'remark',
comment: i18n.global.t('crud.state.remarks'),
designType: 'textarea',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.textarea,
},
{
title: i18n.global.t('crud.state.Weight (drag and drop sorting)'),
name: 'weigh',
comment: i18n.global.t('Weigh'),
designType: 'weigh',
table: {},
form: {},
...fieldData.number,
},
{
title: i18n.global.t('Update time'),
name: 'update_time',
comment: i18n.global.t('Update time'),
designType: 'timestamp',
formBuildExclude: true,
table: {},
form: {},
...fieldData.datetime,
},
{
title: i18n.global.t('Create time'),
name: 'create_time',
comment: i18n.global.t('Create time'),
designType: 'timestamp',
formBuildExclude: true,
table: {},
form: {},
...fieldData.datetime,
},
{
title: i18n.global.t('crud.state.Remote Select (association table)'),
name: 'remote_select',
comment: i18n.global.t('utils.remote select'),
designType: 'remoteSelect',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.remoteSelect,
},
],
base: [
{
title: i18n.global.t('utils.string'),
name: 'string',
comment: i18n.global.t('utils.string'),
designType: 'string',
table: {},
form: {},
...fieldData.string,
},
{
title: i18n.global.t('utils.image'),
name: 'image',
comment: i18n.global.t('utils.image'),
designType: 'image',
table: {},
form: {},
...fieldData.image,
},
{
title: i18n.global.t('utils.file'),
name: 'file',
comment: i18n.global.t('utils.file'),
designType: 'file',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.file,
},
{
title: i18n.global.t('utils.radio'),
name: 'radio',
dataType: "enum('opt0','opt1')",
comment: i18n.global.t('crud.state.Radio:opt0=Option1,opt1=Option2'),
designType: 'radio',
table: {},
form: {},
...fieldData.radio,
default: 'opt0',
defaultType: 'INPUT',
},
{
title: i18n.global.t('utils.checkbox'),
name: 'checkbox',
dataType: "set('opt0','opt1')",
comment: i18n.global.t('crud.state.Checkbox:opt0=Option1,opt1=Option2'),
designType: 'checkbox',
table: {},
form: {},
...fieldData.checkbox,
default: 'opt0,opt1',
defaultType: 'INPUT',
},
{
title: i18n.global.t('utils.select'),
name: 'select',
dataType: "enum('opt0','opt1')",
comment: i18n.global.t('crud.state.Select:opt0=Option1,opt1=Option2'),
designType: 'select',
table: {},
form: {},
...fieldData.select,
default: 'opt0',
defaultType: 'INPUT',
},
{
title: i18n.global.t('utils.switch'),
name: 'switch',
comment: i18n.global.t('crud.state.Switch:0=off,1=on'),
designType: 'switch',
table: {},
form: {},
...fieldData.switch,
default: '1',
defaultType: 'INPUT',
},
{
title: i18n.global.t('utils.rich Text'),
name: 'editor',
comment: i18n.global.t('utils.rich Text'),
designType: 'editor',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.editor,
},
{
title: i18n.global.t('utils.textarea'),
name: 'textarea',
comment: i18n.global.t('utils.textarea'),
designType: 'textarea',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.textarea,
},
{
title: i18n.global.t('utils.number'),
name: 'number',
comment: i18n.global.t('utils.number'),
designType: 'number',
table: {},
form: {},
...fieldData.number,
},
{
title: i18n.global.t('utils.float'),
name: 'float',
type: 'decimal',
length: 5,
precision: 2,
defaultType: 'NULL',
...npuaFalse(),
null: true,
comment: i18n.global.t('utils.float'),
designType: 'float',
table: {},
form: {},
},
{
title: i18n.global.t('utils.password'),
name: 'password',
comment: i18n.global.t('utils.password'),
designType: 'password',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.password,
},
{
title: i18n.global.t('utils.date'),
name: 'date',
comment: i18n.global.t('utils.date'),
designType: 'date',
table: {},
form: {},
...fieldData.date,
},
{
title: i18n.global.t('utils.time'),
name: 'time',
comment: i18n.global.t('utils.time'),
designType: 'time',
table: {},
form: {},
...fieldData.time,
},
{
title: i18n.global.t('utils.time date'),
name: 'datetime',
type: 'datetime',
length: 0,
precision: 0,
defaultType: 'NULL',
...npuaFalse(),
null: true,
comment: i18n.global.t('utils.time date'),
designType: 'datetime',
table: {},
form: {},
},
{
title: i18n.global.t('utils.year'),
name: 'year',
comment: i18n.global.t('utils.year'),
designType: 'year',
table: {},
form: {},
...fieldData.year,
},
{
title: i18n.global.t('crud.state.Time date (timestamp storage)'),
name: 'timestamp',
comment: i18n.global.t('utils.time date'),
designType: 'timestamp',
table: {},
form: {},
...fieldData.datetime,
},
],
senior: [
{
title: i18n.global.t('utils.array'),
name: 'array',
comment: i18n.global.t('utils.array'),
designType: 'array',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.array,
},
{
title: i18n.global.t('utils.city select'),
name: 'city',
comment: i18n.global.t('utils.city select'),
designType: 'city',
table: {},
form: {},
...fieldData.city,
},
{
title: i18n.global.t('utils.icon select'),
name: 'icon',
comment: i18n.global.t('utils.icon select'),
designType: 'icon',
table: {},
form: {},
...fieldData.icon,
},
{
title: i18n.global.t('utils.color picker'),
name: 'color',
comment: i18n.global.t('utils.color picker'),
designType: 'color',
table: {},
form: {},
...fieldData.color,
},
{
title: i18n.global.t('utils.image') + i18n.global.t('crud.state.Multi'),
name: 'images',
comment: i18n.global.t('utils.image'),
designType: 'images',
table: {},
form: {},
...fieldData.images,
},
{
title: i18n.global.t('utils.file') + i18n.global.t('crud.state.Multi'),
name: 'files',
comment: i18n.global.t('utils.file'),
designType: 'files',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.files,
},
{
title: i18n.global.t('utils.select') + i18n.global.t('crud.state.Multi'),
name: 'selects',
comment: i18n.global.t('crud.state.Select:opt0=Option1,opt1=Option2'),
designType: 'selects',
table: {},
form: {},
...fieldData.selects,
},
{
title: i18n.global.t('crud.state.Remote Select (Multi)'),
name: 'remote_select',
comment: i18n.global.t('utils.remote select'),
designType: 'remoteSelects',
tableBuildExclude: true,
table: {},
form: {},
...fieldData.remoteSelects,
},
],
}
const tableBaseAttr = {
render: {
type: 'select',
value: 'none',
options: {
none: i18n.global.t('None'),
icon: 'Icon',
switch: i18n.global.t('utils.switch'),
image: i18n.global.t('utils.image'),
images: i18n.global.t('utils.multi image'),
tag: 'Tag',
tags: 'Tags',
url: 'URL',
datetime: i18n.global.t('utils.time date'),
color: i18n.global.t('utils.color'),
},
},
operator: {
type: 'select',
value: 'eq',
options: {
false: i18n.global.t('crud.state.Disable Search'),
eq: 'eq =',
ne: 'ne !=',
gt: 'gt >',
egt: 'egt >=',
lt: 'lt <',
elt: 'elt <=',
LIKE: 'LIKE',
'NOT LIKE': 'NOT LIKE',
IN: 'IN',
'NOT IN': 'NOT IN',
RANGE: 'RANGE',
'NOT RANGE': 'NOT RANGE',
NULL: 'NULL',
'NOT NULL': 'NOT NULL',
FIND_IN_SET: 'FIND_IN_SET',
},
},
comSearchRender: {
type: 'select',
value: 'string',
options: {
string: i18n.global.t('utils.string'),
select: i18n.global.t('utils.select'),
remoteSelect: i18n.global.t('utils.remote select'),
time: i18n.global.t('utils.time') + i18n.global.t('utils.choice'),
date: i18n.global.t('utils.date') + i18n.global.t('utils.choice'),
datetime: i18n.global.t('utils.time date') + i18n.global.t('utils.choice'),
},
},
comSearchInputAttr: {
type: 'textarea',
value: '',
placeholder: i18n.global.t('crud.crud.comSearchInputAttrTip'),
attr: {
rows: 3,
},
},
sortable: {
type: 'select',
value: 'false',
options: {
false: i18n.global.t('Disable'),
custom: i18n.global.t('Enable'),
},
},
}
const formBaseAttr = {
validator: {
type: 'selects',
value: [],
options: validatorType,
},
validatorMsg: {
type: 'textarea',
value: '',
placeholder: i18n.global.t('crud.state.If left blank, the verifier title attribute will be filled in automatically'),
attr: {
rows: 3,
},
},
}
export const getTableAttr = (type: keyof typeof tableBaseAttr, val: string) => {
return {
...tableBaseAttr[type],
value: val,
}
}
const getFormAttr = (type: keyof typeof formBaseAttr, val: string[]) => {
return {
...formBaseAttr[type],
value: val,
}
}
export const designTypes: anyObj = {
pk: {
name: i18n.global.t('crud.state.Primary key'),
table: {
width: {
type: 'number',
value: 70,
},
operator: getTableAttr('operator', 'RANGE'),
sortable: getTableAttr('sortable', 'custom'),
},
form: {},
},
spk: {
name: i18n.global.t('crud.state.Primary key (Snowflake ID)'),
table: {
width: {
type: 'number',
value: 180,
},
operator: getTableAttr('operator', 'RANGE'),
sortable: getTableAttr('sortable', 'custom'),
},
form: {},
},
weigh: {
name: i18n.global.t('crud.state.Weight (automatically generate drag sort button)'),
table: {
operator: getTableAttr('operator', 'RANGE'),
sortable: getTableAttr('sortable', 'custom'),
},
form: formBaseAttr,
},
timestamp: {
name: i18n.global.t('crud.state.Time date (timestamp storage)'),
table: {
render: getTableAttr('render', 'datetime'),
operator: getTableAttr('operator', 'RANGE'),
comSearchRender: getTableAttr('comSearchRender', 'datetime'),
comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
sortable: getTableAttr('sortable', 'custom'),
width: {
type: 'number',
value: 160,
},
timeFormat: {
type: 'string',
value: 'yyyy-mm-dd hh:MM:ss',
},
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['date']),
},
},
string: {
name: i18n.global.t('utils.string'),
table: {
render: getTableAttr('render', 'none'),
sortable: getTableAttr('sortable', 'false'),
operator: getTableAttr('operator', 'LIKE'),
},
form: formBaseAttr,
},
password: {
name: i18n.global.t('utils.password'),
table: {
operator: getTableAttr('operator', 'false'),
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['password']),
},
},
number: {
name: i18n.global.t('utils.number'),
table: {
render: getTableAttr('render', 'none'),
sortable: getTableAttr('sortable', 'false'),
operator: getTableAttr('operator', 'RANGE'),
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['number']),
step: {
type: 'number',
value: 1,
},
},
},
float: {
name: i18n.global.t('utils.float'),
table: {
render: getTableAttr('render', 'none'),
sortable: getTableAttr('sortable', 'false'),
operator: getTableAttr('operator', 'RANGE'),
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['float']),
step: {
type: 'number',
value: 1,
},
},
},
radio: {
name: i18n.global.t('utils.radio'),
table: {
operator: getTableAttr('operator', 'eq'),
sortable: getTableAttr('sortable', 'false'),
render: getTableAttr('render', 'tag'),
},
form: formBaseAttr,
},
checkbox: {
name: i18n.global.t('utils.checkbox'),
table: {
sortable: getTableAttr('sortable', 'false'),
render: getTableAttr('render', 'tags'),
operator: getTableAttr('operator', 'FIND_IN_SET'),
},
form: formBaseAttr,
},
switch: {
name: i18n.global.t('utils.switch'),
table: {
operator: getTableAttr('operator', 'eq'),
sortable: getTableAttr('sortable', 'false'),
render: getTableAttr('render', 'switch'),
},
form: formBaseAttr,
},
textarea: {
name: i18n.global.t('utils.textarea'),
table: {
operator: getTableAttr('operator', 'false'),
},
form: {
...formBaseAttr,
rows: {
type: 'number',
value: 3,
},
},
},
array: {
name: i18n.global.t('utils.array'),
table: {
operator: getTableAttr('operator', 'false'),
},
form: formBaseAttr,
},
datetime: {
name: i18n.global.t('utils.time date') + i18n.global.t('utils.choice'),
table: {
operator: getTableAttr('operator', 'RANGE'),
comSearchRender: getTableAttr('comSearchRender', 'datetime'),
comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
sortable: getTableAttr('sortable', 'custom'),
width: {
type: 'number',
value: 160,
},
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['date']),
},
},
year: {
name: i18n.global.t('utils.year') + i18n.global.t('utils.choice'),
table: {
operator: getTableAttr('operator', 'RANGE'),
sortable: getTableAttr('sortable', 'custom'),
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['date']),
},
},
date: {
name: i18n.global.t('utils.date') + i18n.global.t('utils.choice'),
table: {
operator: getTableAttr('operator', 'RANGE'),
comSearchRender: getTableAttr('comSearchRender', 'date'),
comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
sortable: getTableAttr('sortable', 'custom'),
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['date']),
},
},
time: {
name: i18n.global.t('utils.time') + i18n.global.t('utils.choice'),
table: {
operator: getTableAttr('operator', 'RANGE'),
comSearchRender: getTableAttr('comSearchRender', 'time'),
comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
sortable: getTableAttr('sortable', 'custom'),
},
form: formBaseAttr,
},
select: {
name: i18n.global.t('utils.select'),
table: {
operator: getTableAttr('operator', 'eq'),
sortable: getTableAttr('sortable', 'false'),
render: getTableAttr('render', 'tag'),
},
form: {
...formBaseAttr,
'select-multi': {
type: 'switch',
value: false,
},
},
},
selects: {
name: i18n.global.t('utils.select') + i18n.global.t('crud.state.Multi'),
table: {
sortable: getTableAttr('sortable', 'false'),
render: getTableAttr('render', 'tags'),
operator: getTableAttr('operator', 'FIND_IN_SET'),
},
form: {
...formBaseAttr,
'select-multi': {
type: 'switch',
value: true,
},
},
},
remoteSelect: {
name: i18n.global.t('utils.remote select') + i18n.global.t('utils.choice'),
table: {
render: getTableAttr('render', 'tags'),
operator: getTableAttr('operator', 'LIKE'),
comSearchRender: getTableAttr('comSearchRender', 'string'),
comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
},
form: {
...formBaseAttr,
'select-multi': {
type: 'switch',
value: false,
},
'remote-pk': {
type: 'string',
value: 'id',
},
'remote-field': {
type: 'string',
value: 'name',
},
'remote-table': {
type: 'string',
value: '',
},
'remote-controller': {
type: 'string',
value: '',
},
'remote-model': {
type: 'string',
value: '',
},
'relation-fields': {
type: 'string',
value: '',
},
'remote-url': {
type: 'string',
value: '',
placeholder: i18n.global.t('crud.state.If it is not input, it will be automatically analyzed by the controller'),
},
'remote-primary-table-alias': {
type: 'string',
value: '',
},
'remote-source-config-type': {
type: 'hidden',
value: '',
},
},
},
remoteSelects: {
name: i18n.global.t('utils.remote select') + i18n.global.t('utils.choice') + i18n.global.t('crud.state.Multi'),
table: {
render: getTableAttr('render', 'tags'),
operator: getTableAttr('operator', 'FIND_IN_SET'),
comSearchRender: getTableAttr('comSearchRender', 'remoteSelect'),
comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
},
form: {
...formBaseAttr,
'select-multi': {
type: 'switch',
value: true,
},
'remote-pk': {
type: 'string',
value: 'id',
},
'remote-field': {
type: 'string',
value: 'name',
},
'remote-table': {
type: 'string',
value: '',
},
'remote-controller': {
type: 'string',
value: '',
},
'remote-model': {
type: 'string',
value: '',
},
'relation-fields': {
type: 'string',
value: '',
},
'remote-url': {
type: 'string',
value: '',
placeholder: i18n.global.t('crud.state.If it is not input, it will be automatically analyzed by the controller'),
},
'remote-primary-table-alias': {
type: 'string',
value: '',
},
'remote-source-config-type': {
type: 'hidden',
value: '',
},
},
},
editor: {
name: i18n.global.t('utils.rich Text'),
table: {
operator: getTableAttr('operator', 'false'),
},
form: {
...formBaseAttr,
validator: getFormAttr('validator', ['editorRequired']),
},
},
city: {
name: i18n.global.t('utils.city select'),
table: {
operator: getTableAttr('operator', 'false'),
},
form: formBaseAttr,
},
image: {
name: i18n.global.t('utils.image') + i18n.global.t('Upload'),
table: {
render: getTableAttr('render', 'image'),
operator: getTableAttr('operator', 'false'),
},
form: {
...formBaseAttr,
'image-multi': {
type: 'switch',
value: false,
},
},
},
images: {
name: i18n.global.t('utils.image') + i18n.global.t('Upload') + i18n.global.t('crud.state.Multi'),
table: {
render: getTableAttr('render', 'images'),
operator: getTableAttr('operator', 'false'),
},
form: {
...formBaseAttr,
'image-multi': {
type: 'switch',
value: true,
},
},
},
file: {
name: i18n.global.t('utils.file') + i18n.global.t('Upload'),
table: {
render: getTableAttr('render', 'none'),
operator: getTableAttr('operator', 'false'),
},
form: {
...formBaseAttr,
'file-multi': {
type: 'switch',
value: false,
},
},
},
files: {
name: i18n.global.t('utils.file') + i18n.global.t('Upload') + i18n.global.t('crud.state.Multi'),
table: {
render: getTableAttr('render', 'none'),
operator: getTableAttr('operator', 'false'),
},
form: {
...formBaseAttr,
'file-multi': {
type: 'switch',
value: true,
},
},
},
icon: {
name: i18n.global.t('utils.icon select'),
table: {
render: getTableAttr('render', 'icon'),
operator: getTableAttr('operator', 'false'),
},
form: formBaseAttr,
},
color: {
name: i18n.global.t('utils.color picker'),
table: {
render: getTableAttr('render', 'color'),
operator: getTableAttr('operator', 'false'),
},
form: formBaseAttr,
},
}
export const tableFieldsKey = ['quickSearchField', 'formFields', 'columnFields']

View File

@@ -0,0 +1,36 @@
<template>
<div>
<component :is="state.step"></component>
</div>
</template>
<script setup lang="ts">
import { onActivated, onDeactivated, onUnmounted, onMounted } from 'vue'
import Start from '/@/views/backend/crud/start.vue'
import Design from '/@/views/backend/crud/design.vue'
import { state } from '/@/views/backend/crud/index'
import { closeHotUpdate, openHotUpdate } from '/@/utils/vite'
defineOptions({
name: 'crud/crud',
components: { Start, Design },
})
onMounted(() => {
closeHotUpdate('crud')
})
onActivated(() => {
closeHotUpdate('crud')
})
onDeactivated(() => {
openHotUpdate('crud')
})
onUnmounted(() => {
openHotUpdate('crud')
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,578 @@
<template>
<div>
<el-dialog
@close="emits('update:modelValue', false)"
width="70%"
:model-value="modelValue"
class="ba-crud-log-dialog"
:title="t('crud.crud.CRUD record')"
:append-to-body="true"
:destroy-on-close="true"
>
<TableHeader :buttons="['refresh', 'quickSearch', 'columnDisplay']" :quick-search-placeholder="t('crud.log.quick Search Fields')">
<template v-if="baAccount.token">
<el-tooltip :content="t('crud.log.Upload the selected design records to the cloud for cross-device use')" placement="top">
<el-button
v-blur
:disabled="baTable.table.selection!.length > 0 ? false : true"
@click="onUpload"
class="table-header-operate"
type="success"
>
<Icon color="#ffffff" name="fa fa-cloud-upload" />
<span class="table-header-operate-text">{{ t('Upload') }}</span>
</el-button>
</el-tooltip>
<el-tooltip :content="t('crud.log.Design records that have been synchronized to the cloud')" placement="top">
<el-button v-blur class="table-header-operate" @click="onLoadLogs" type="success">
<Icon color="#ffffff" name="fa fa-cloud-download" />
<span class="table-header-operate-text">{{ t('crud.log.Cloud record') }}</span>
</el-button>
</el-tooltip>
<el-button v-blur @click="toggleShowConfig(true)" class="table-header-operate" type="primary">
<Icon name="fa fa-gear" />
<span class="table-header-operate-text">{{ t('crud.log.Settings') }}</span>
</el-button>
<el-button v-blur @click="toggleShowBaAccount(true)" class="table-header-operate" type="primary">
<Icon name="fa fa-user-o" />
<span class="table-header-operate-text">{{ t('layouts.Member information') }}</span>
</el-button>
</template>
<template v-else>
<el-button v-blur @click="toggleShowBaAccount(true)" class="table-header-operate" type="primary">
<Icon name="fa fa-chain" />
<span class="table-header-operate-text">{{ t('crud.log.Login for backup design') }}</span>
</el-button>
</template>
</TableHeader>
<Table ref="tableRef">
<template #tableName>
<el-table-column :show-overflow-tooltip="true" prop="table_name" align="center" :label="t('crud.log.table_name')">
<template #default="scope">
{{ (scope.row.table.databaseConnection ? scope.row.table.databaseConnection + '.' : '') + scope.row.table.name }}
</template>
</el-table-column>
</template>
<template #sync>
<el-table-column prop="sync" align="center" :label="t('crud.log.sync')">
<template #default="scope">
<el-tag :type="scope.row.sync > 0 ? 'primary' : 'info'">
{{ scope.row.sync > 0 ? t('crud.log.sync yes') : t('crud.log.sync no') }}
</el-tag>
</template>
</el-table-column>
</template>
</Table>
</el-dialog>
<el-dialog v-model="state.showConfig" :title="t('crud.log.Settings')">
<div class="ba-operate-form" :style="config.layout.shrink ? '' : 'width: calc(100% - 90px)'">
<el-form @keyup.enter="onConfigSubmit" :model="state.configForm" label-position="top">
<FormItem
:label="t('crud.log.CRUD design record synchronization scheme')"
v-model="state.configForm.syncType"
type="radio"
:input-attr="{
border: true,
content: { manual: t('crud.log.Manual'), automatic: t('crud.log.automatic') },
}"
:block-help="t('crud.log.You can use the synchronized design records across devices')"
/>
<FormItem
:key="state.configForm.syncType"
v-if="state.configForm.syncType == 'automatic'"
:label="t('crud.log.When automatically synchronizing records, share them to the open source community')"
v-model="state.configForm.syncAutoPublic"
type="radio"
:input-attr="{
border: true,
content: { no: t('crud.log.Not to share'), yes: t('crud.log.Share') },
}"
:block-help="t('crud.log.Enabling sharing can automatically earn community points during development')"
/>
<FormItem
:label="t('crud.log.The synchronized CRUD records are automatically resynchronized when they are updated')"
v-model="state.configForm.syncedUpdate"
type="radio"
:input-attr="{
border: true,
content: { no: t('crud.log.Do not resynchronize'), yes: t('crud.log.Automatic resynchronization') },
}"
/>
</el-form>
</div>
<template #footer>
<div :style="'width: calc(100% - 90px)'">
<el-button @click="toggleShowConfig(false)">{{ t('Cancel') }}</el-button>
<el-button v-blur @click="onConfigSubmit" type="primary"> {{ t('Save') }} </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="state.showUpload" :title="t('Upload')" width="60%">
<div class="ba-operate-form" v-loading="state.uploadValidLoading">
<el-table :empty-text="t('crud.log.No effective design')" :data="state.uploadValidData" stripe class="w100">
<el-table-column prop="table_name" :label="t('crud.log.table_name')" align="center" />
<el-table-column prop="comment" :label="t('crud.log.comment')" align="center" show-overflow-tooltip />
<el-table-column prop="fieldCount" :label="t('crud.log.Number of fields')" align="center" />
<el-table-column :label="t('crud.log.Upload type')" align="center">
<template #default="scope">
<el-tag :type="scope.row.id > 0 ? 'primary' : 'success'">
{{ scope.row.id > 0 ? t('crud.log.Update') : t('crud.log.New added') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="score" :label="t('crud.log.Share to earn points')" align="center">
<template #default="scope">
<el-text :type="scope.row.score <= 0 ? 'info' : 'success'">{{ scope.row.score }}</el-text>
</template>
</el-table-column>
<el-table-column :label="t('crud.log.Share to the open source community')" align="center">
<template #default="scope">
<el-switch v-model="scope.row.public" />
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div :style="'width: calc(100% - 90px)'">
<el-button @click="toggleShowUpload(false)">{{ t('Cancel') }}</el-button>
<el-button v-blur @click="onSaveUpload" type="primary"> {{ t('Upload') }} </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="state.showDownload" :title="t('crud.log.Cloud record')">
<div class="download-table-header">
<el-button v-blur @click="onLoadLogs" color="#40485b" class="download-table-header-operate" type="info">
<Icon color="#fff" size="14" name="fa fa-refresh" />
</el-button>
<el-popconfirm
@confirm="onBatchDelLog"
:confirm-button-text="t('Delete')"
:cancel-button-text="t('Cancel')"
confirmButtonType="danger"
:title="t('Are you sure to delete the selected record?')"
:disabled="state.downloadSelection.length > 0 ? false : true"
>
<template #reference>
<el-button
v-blur
:disabled="state.downloadSelection.length > 0 ? false : true"
class="download-table-header-operate"
type="danger"
>
<Icon color="#fff" size="14" name="fa fa-trash" />
<span class="download-table-header-operate-text">{{ t('Delete') }}</span>
</el-button>
</template>
</el-popconfirm>
<div class="download-table-search">
<el-input
v-model="state.downloadQuickSearch"
class="xs-hidden download-quick-search"
@input="onSearchDownloadInput"
:placeholder="t('Search')"
clearable
/>
</div>
</div>
<el-table
v-loading="state.downloadLoading"
@selection-change="onSelectionChange"
:empty-text="t('crud.log.No design record')"
:data="state.downloadData"
stripe
class="w100"
>
<el-table-column type="selection" align="center" />
<el-table-column :show-overflow-tooltip="true" align="center" :label="t('crud.log.table_name')">
<template #default="scope">
{{ (scope.row.table.databaseConnection ? scope.row.table.databaseConnection + '.' : '') + scope.row.table.name }}
</template>
</el-table-column>
<el-table-column prop="comment" :label="t('crud.log.comment')" align="center" show-overflow-tooltip />
<el-table-column :label="t('crud.log.Field')" align="center">
<template #default="scope">
<el-popover :width="460" class="box-item" :title="t('crud.log.Field information')" placement="left">
<template #reference>
<el-text class="cp" type="primary">{{ scope.row.fieldCount }}</el-text>
</template>
<el-table :empty-text="t('crud.log.No field')" :data="scope.row.fields" stripe class="w100">
<el-table-column prop="name" :label="t('crud.log.Field name')" align="center" />
<el-table-column prop="comment" :label="t('crud.log.Note')" align="center" show-overflow-tooltip />
<el-table-column :label="t('crud.log.Type')" align="center" show-overflow-tooltip>
<template #default="typeScope">
<el-text>{{ typeScope.row.dataType ?? typeScope.row.type }}</el-text>
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</el-table-column>
<el-table-column :label="t('Operate')" align="center">
<template #default="scope">
<el-popconfirm :title="t('crud.crud.Start CRUD design with this record?')" @confirm="onLoadLog(scope.row.id)">
<template #reference>
<el-button type="primary" link>
<div>{{ t('crud.log.Load') }}</div>
</el-button>
</template>
</el-popconfirm>
<el-popconfirm :title="t('crud.log.Delete cloud records?')" @confirm="onDelLog([scope.row.id])">
<template #reference>
<el-button type="danger" link>
<div>{{ t('Delete') }}</div>
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="log-pagination">
<el-pagination
:currentPage="state.downloadPage"
:page-size="10"
background
:layout="config.layout.shrink ? 'prev, next, jumper' : 'total, ->, prev, pager, next, jumper'"
:total="state.downloadTotal"
@current-change="onDownloadCurrentChange"
></el-pagination>
</div>
</el-dialog>
<BaAccountDialog v-model="state.showBaAccount" :login-callback="onBaAccountLoginSuccess" />
</div>
</template>
<script setup lang="ts">
import { ElNotification } from 'element-plus'
import { debounce } from 'lodash-es'
import { nextTick, onMounted, provide, reactive, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { delLog, logs, postDel, uploadCompleted, uploadLog } from '/@/api/backend/crud'
import { baTableApi } from '/@/api/common'
import FormItem from '/@/components/formItem/index.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import BaAccountDialog from '/@/layouts/backend/components/baAccount.vue'
import { useBaAccount } from '/@/stores/baAccount'
import { useConfig } from '/@/stores/config'
import baTableClass from '/@/utils/baTable'
import { auth, getArrayKey } from '/@/utils/common'
import { changeStep, state as crudState } from '/@/views/backend/crud/index'
interface Props {
modelValue: boolean
}
const config = useConfig()
const baAccount = useBaAccount()
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
})
const emits = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const state = reactive({
ready: false,
configForm: {
syncType: config.crud.syncType,
syncedUpdate: config.crud.syncedUpdate,
syncAutoPublic: config.crud.syncAutoPublic,
},
showUpload: false,
showConfig: false,
showDownload: false,
showBaAccount: false,
uploadScoreSum: 0,
uploadValidData: [] as anyObj[],
uploadValidLoading: false,
downloadPage: 1,
downloadData: [] as anyObj[],
downloadTotal: 0,
downloadLoading: false,
downloadSelection: [] as anyObj[],
downloadQuickSearch: '',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = [
{
render: 'confirmButton',
name: 'copy',
title: 'crud.crud.copy',
text: '',
type: 'primary',
icon: 'fa fa-copy',
class: 'table-row-copy',
popconfirm: {
confirmButtonText: t('Confirm'),
cancelButtonText: t('Cancel'),
confirmButtonType: 'primary',
title: t('crud.crud.Start CRUD design with this record?'),
width: '220px',
},
disabledTip: false,
click: (row) => {
crudState.startData.logId = row[baTable.table.pk!]
changeStep('log')
emits('update:modelValue', false)
},
},
{
render: 'confirmButton',
name: 'del',
title: 'crud.log.delete',
text: '',
type: 'danger',
icon: 'fa fa-trash',
class: 'table-row-delete',
popconfirm: {
confirmButtonText: t('crud.crud.Delete Code'),
cancelButtonText: t('Cancel'),
confirmButtonType: 'danger',
title: t('crud.crud.Are you sure to delete the generated CRUD code?'),
width: '248px',
},
disabledTip: false,
click: (row) => {
postDel(row[baTable.table.pk!]).then(() => {
baTable.onTableHeaderAction('refresh', {})
})
},
display: (row) => {
return row['status'] != 'delete' && auth('delete')
},
},
]
const baTable = new baTableClass(
new baTableApi('/admin/crud.Log/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('crud.log.id'), prop: 'id', align: 'center', width: 70, operator: '=', sortable: 'custom' },
{
label: t('crud.log.table_name'),
operator: 'LIKE',
render: 'slot',
slotName: 'tableName',
},
{
label: t('crud.log.comment'),
prop: 'comment',
align: 'center',
showOverflowTooltip: true,
operator: 'LIKE',
},
{
label: t('crud.log.sync'),
prop: 'sync',
align: 'center',
render: 'slot',
slotName: 'sync',
},
{
label: t('crud.log.status'),
prop: 'status',
align: 'center',
render: 'tag',
sortable: false,
replaceValue: {
delete: t('crud.log.status delete'),
success: t('crud.log.status success'),
error: t('crud.log.status error'),
start: t('crud.log.status start'),
},
custom: { delete: 'danger', success: 'success', error: 'warning', start: '' },
},
{
label: t('crud.log.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
sortable: 'custom',
width: 160,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: { status: 'start' },
}
)
provide('baTable', baTable)
const getData = () => {
baTable.getData()?.then(() => {
state.ready = true
})
}
const toggleShowConfig = (status: boolean) => {
state.showConfig = status
}
const toggleShowBaAccount = (status: boolean) => {
state.showBaAccount = status
}
const toggleShowUpload = (status: boolean) => {
state.showUpload = status
}
const toggleShowDownload = (status: boolean) => {
state.showDownload = status
}
const onLoadLog = (id: string) => {
crudState.startData.logId = id
crudState.startData.logType = 'Cloud history'
changeStep('log')
emits('update:modelValue', false)
}
const onBatchDelLog = () => {
let ids: number[] = []
for (const key in state.downloadSelection) {
ids.push(state.downloadSelection[key].id)
}
onDelLog(ids)
}
const onDelLog = (ids: number[]) => {
delLog({ ids }).then((res) => {
uploadCompleted({ syncIds: res.data.syncs, cancelSync: 1 }).finally(() => {
onLoadLogs()
baTable.onTableHeaderAction('refresh', {})
})
})
}
const onConfigSubmit = () => {
toggleShowConfig(false)
config.setCrud('syncType', state.configForm.syncType)
config.setCrud('syncedUpdate', state.configForm.syncedUpdate)
config.setCrud('syncAutoPublic', state.configForm.syncAutoPublic)
ElNotification({
type: 'success',
message: t('axios.Operation successful'),
})
}
const onUpload = () => {
toggleShowUpload(true)
state.uploadValidLoading = true
uploadLog({ logs: baTable.table.selection, save: 0 })
.then((res) => {
state.uploadScoreSum = res.data.scoreSum
state.uploadValidData = res.data.validData
})
.finally(() => {
state.uploadValidLoading = false
})
}
const onSaveUpload = () => {
state.uploadValidLoading = true
const selection = baTable.table.selection
for (const key in selection) {
const s = selection[key as keyof typeof selection] as any
const validDataKey = getArrayKey(state.uploadValidData, 'sync', s.id.toString())
if (validDataKey !== false) {
s['public'] = state.uploadValidData[validDataKey].public ? 1 : 0
}
}
uploadLog({ logs: selection, save: 1 }).then((res) => {
uploadCompleted({ syncIds: res.data.syncIds }).finally(() => {
baTable.onTableHeaderAction('refresh', {})
toggleShowUpload(false)
ElNotification({
type: 'success',
message: res.msg,
})
state.uploadValidLoading = false
})
})
}
const onBaAccountLoginSuccess = () => {
toggleShowBaAccount(false)
}
const onDownloadCurrentChange = (val: number) => {
state.downloadPage = val
onLoadLogs()
}
const onSelectionChange = (newSelection: any[]) => {
state.downloadSelection = newSelection
}
const onSearchDownloadInput = debounce(() => onLoadLogs(), 500)
const onLoadLogs = () => {
toggleShowDownload(true)
state.downloadLoading = true
logs({ page: state.downloadPage, quickSearch: state.downloadQuickSearch })
.then((res) => {
state.downloadData = res.data.list
state.downloadTotal = res.data.total
})
.finally(() => {
state.downloadLoading = false
})
}
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
})
watch(
() => props.modelValue,
(newVal) => {
if (newVal && !state.ready) {
nextTick(() => {
getData()
})
}
}
)
</script>
<style lang="scss">
.ba-crud-log-dialog .el-dialog__body {
padding: 10px 20px;
}
.log-pagination {
padding: 13px 15px;
}
.cp {
cursor: pointer;
}
.download-table-header {
display: flex;
padding: 10px;
padding-top: 0;
.download-table-header-operate-text {
margin-left: 6px;
font-size: 14px;
}
.download-table-search {
margin-left: auto;
}
}
</style>

View File

@@ -0,0 +1,319 @@
<template>
<div class="default-main">
<div class="crud-title">{{ t('crud.crud.start') }}</div>
<div class="start-opt">
<el-row :gutter="20">
<el-col :xs="24" :span="8">
<div @click="changeStep('create')" class="start-item suspension">
<div class="start-item-title">{{ t('crud.crud.create') }}</div>
<div class="start-item-remark">{{ t('crud.crud.New background CRUD from zero') }}</div>
</div>
</el-col>
<el-col @click="onShowDialog('db')" :xs="24" :span="8">
<div class="start-item suspension">
<div class="start-item-title">{{ t('crud.crud.Select Data Table') }}</div>
<div class="start-item-remark">{{ t('crud.crud.Select a designed data table from the database') }}</div>
</div>
</el-col>
<el-col @click="state.showLog = true" :xs="24" :span="8">
<div class="start-item suspension">
<div class="start-item-title">{{ t('crud.crud.CRUD record') }}</div>
<div class="start-item-remark">{{ t('crud.crud.Start with previously generated CRUD code') }}</div>
</div>
</el-col>
</el-row>
<el-row justify="center">
<el-col :span="20" class="ba-markdown crud-tips suspension">
<b>{{ t('crud.crud.Fast experience') }}</b>
<ol>
<li>
{{ t('crud.crud.experience 1 1') }}
<a target="_blank" href="https://doc.buildadmin.com/guide/other/developerMustSee.html" rel="noopener noreferrer">
{{ t('crud.crud.experience 1 2') }}
</a>
{{ t('crud.crud.experience 1 3') }}
</li>
<li>
{{ t('crud.crud.experience 2 1') }}
<code>{{ t('crud.crud.create') }}</code>
{{ t('crud.crud.or') }}
<code> {{ t('crud.crud.experience 2 2') }}{{ t('crud.crud.experience 2 3') }} </code>
</li>
<li>
{{ t('crud.crud.experience 3 1') }} <code>{{ t('crud.crud.experience 3 2') }}</code>
{{ t('crud.crud.experience 3 3') }}
<code>{{ t('crud.crud.experience 3 4') }}</code>
</li>
</ol>
<el-alert v-if="!isDev()" class="no-dev" type="warning" :show-icon="true" :closable="false">
<template #title>
<span>{{ t('crud.crud.experience 4 1') }}</span>
<a target="_blank" href="https://doc.buildadmin.com/guide/other/developerMustSee.html" rel="noopener noreferrer">
{{ t('crud.crud.experience 4 2') }}
</a>
<span>
{{ t('crud.crud.experience 4 3') }}<code>{{ t('crud.crud.experience 4 4') }}</code>
</span>
</template>
</el-alert>
</el-col>
</el-row>
<el-dialog
class="ba-operate-dialog select-table-dialog"
v-model="state.dialog.visible"
:title="state.dialog.type == 'sql' ? t('crud.crud.Please enter SQL') : t('crud.crud.Please select a data table')"
:destroy-on-close="true"
>
<el-form
:label-width="140"
@keyup.enter="onSubmit()"
class="select-table-form"
ref="formRef"
:model="crudState.startData"
:rules="rules"
>
<template v-if="state.dialog.type == 'sql'">
<el-input
class="sql-input"
prop="sql"
ref="sqlInputRef"
v-model="crudState.startData.sql"
type="textarea"
:placeholder="t('crud.crud.table create SQL')"
:rows="10"
@keyup.enter.stop=""
@keyup.ctrl.enter="onSubmit()"
/>
</template>
<template v-else-if="state.dialog.type == 'db'">
<FormItem
:label="t('Database connection')"
v-model="crudState.startData.databaseConnection"
type="remoteSelect"
:label-width="140"
:block-help="t('Database connection help')"
:input-attr="{
pk: 'key',
field: 'key',
remoteUrl: getDatabaseConnectionListUrl,
onChange: onDatabaseChange,
}"
:placeholder="t('Please select field', { field: t('Database connection') })"
/>
<FormItem
:label="t('crud.crud.data sheet')"
v-model="crudState.startData.table"
type="remoteSelect"
:key="crudState.startData.databaseConnection"
:placeholder="t('crud.crud.Please select a data table')"
:label-width="140"
:block-help="t('crud.crud.data sheet help')"
:input-attr="{
pk: 'table',
field: 'comment',
params: {
connection: crudState.startData.databaseConnection,
samePrefix: 1,
excludeTable: [
'area',
'token',
'captcha',
'admin_group_access',
'config',
'admin_log',
'user_money_log',
'user_score_log',
],
},
remoteUrl: getTableListUrl,
onRow: onTableStartChange,
}"
prop="table"
/>
<el-alert
v-if="state.successRecord"
class="success-record-alert"
:title="t('crud.crud.The selected table has already generated records You are advised to start with historical records')"
:show-icon="true"
:closable="false"
type="warning"
/>
</template>
</el-form>
<template #footer>
<div :style="{ width: 'calc(100% * 0.9)' }">
<el-button @click="state.dialog.visible = false">{{ $t('Cancel') }}</el-button>
<el-button :loading="state.loading" @click="onSubmit()" v-blur type="primary">{{ t('Confirm') }}</el-button>
<el-button v-if="state.successRecord" @click="onLogStart" v-blur type="success">
{{ t('crud.crud.Start with the historical record') }}
</el-button>
</div>
</template>
</el-dialog>
<CrudLog v-model="state.showLog" />
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, useTemplateRef } from 'vue'
import { checkCrudLog } from '/@/api/backend/crud'
import FormItem from '/@/components/formItem/index.vue'
import { changeStep, state as crudState } from '/@/views/backend/crud/index'
import { ElNotification } from 'element-plus'
import type { FormItemRule } from 'element-plus'
import { buildValidatorData } from '/@/utils/validate'
import CrudLog from '/@/views/backend/crud/log.vue'
import { useI18n } from 'vue-i18n'
import { getDatabaseConnectionListUrl, getTableListUrl } from '/@/api/common'
const { t } = useI18n()
const formRef = useTemplateRef('formRef')
const sqlInputRef = useTemplateRef('sqlInputRef')
const state = reactive({
dialog: {
type: '',
visible: false,
},
showLog: false,
loading: false,
successRecord: 0,
})
const onShowDialog = (type: string) => {
state.dialog.type = type
state.dialog.visible = true
if (type == 'sql') {
setTimeout(() => {
sqlInputRef.value?.focus()
}, 200)
} else if (type == 'db') {
state.successRecord = 0
crudState.startData.table = ''
}
}
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
table: [buildValidatorData({ name: 'required', message: t('crud.crud.Please select a data table') })],
})
const onSubmit = () => {
if (state.dialog.type == 'sql' && !crudState.startData.sql) {
ElNotification({
type: 'error',
message: t('crud.crud.Please enter the table creation SQL'),
})
return
}
formRef.value?.validate((valid) => {
if (valid) {
changeStep(state.dialog.type)
}
})
}
const onDatabaseChange = () => {
state.successRecord = 0
crudState.startData.table = ''
}
const onTableStartChange = () => {
if (crudState.startData.table) {
// 检查是否有CRUD记录
state.loading = true
checkCrudLog(crudState.startData.table, crudState.startData.databaseConnection)
.then((res) => {
state.successRecord = res.data.id
})
.finally(() => {
state.loading = false
})
}
}
const onLogStart = () => {
if (state.successRecord) {
crudState.startData.logId = state.successRecord.toString()
changeStep('log')
}
}
const isDev = () => {
return import.meta.env.DEV
}
</script>
<style scoped lang="scss">
:deep(.select-table-dialog) .el-dialog__body {
height: unset;
.select-table-form {
width: 88%;
padding: 40px 0;
}
.success-record-alert {
width: calc(100% - 140px);
margin-left: 140px;
margin-bottom: 30px;
margin-top: -10px;
}
}
.crud-title {
display: flex;
align-items: center;
justify-content: center;
font-size: var(--el-font-size-extra-large);
font-weight: bold;
padding-top: 120px;
}
.start-opt {
display: block;
width: 60%;
margin: 40px auto;
}
.start-item {
background-color: #e1eaf9;
border-radius: var(--el-border-radius-base);
padding: 25px;
margin-bottom: 20px;
cursor: pointer;
}
.start-item-title {
font-size: var(--el-font-size-large);
color: var(--ba-color-primary-light);
}
.start-item-remark {
display: block;
line-height: 18px;
min-height: 48px;
padding-top: 12px;
color: #92969a;
}
.sql-input {
margin: 20px 0;
}
.crud-tips {
margin-top: 60px;
padding: 20px;
background-color: rgba($color: #ffffff, $alpha: 0.6);
border-radius: var(--el-border-radius-base);
color: var(--el-color-info);
b {
font-size: 15px;
padding-left: 10px;
}
.no-dev {
margin-top: 10px;
}
}
@at-root .dark {
.start-item {
background-color: #1d1e1f;
}
.crud-tips {
background-color: rgba($color: #1d1e1f, $alpha: 0.4);
}
}
</style>