<template>
<plugin-setting
v-if="isShow"
:fixed-name="PLUGIN_NAME.ModelManager"
:align="align"
title="模型设置"
class="modelmanager-plugin-setting"
>
<template #header>
<button-group>
<tiny-button v-if="showExport" @click="$emit('exportModel', selectedModel?.id)">导出SQL</tiny-button>
<tiny-button type="primary" @click="saveModel">保存</tiny-button>
<svg-button name="delete" v-if="selectedModel?.id" @click="deleteModel"></svg-button>
<svg-button name="close" @click="closeModelSettingPanel"></svg-button>
</button-group>
</template>
<template #content>
<tiny-collapse v-model="activeName" class="page-setting-collapse">
<tiny-collapse-item title="基本设置" name="general">
<model-basic-form ref="modelBasicFormRef" :model="selectedModel"></model-basic-form>
</tiny-collapse-item>
<tiny-collapse-item class="base-setting" title="字段管理" name="fields">
<field-manager
ref="fieldManagerRef"
:model="selectedModel"
:expand-config="expandConfig"
:available-models="models"
@add-field="handleAddField"
@insert-enum-after="insertEnumValueAfter"
@remove-enum="removeEnumValue"
>
</field-manager>
</tiny-collapse-item>
</tiny-collapse>
</template>
</plugin-setting>
</template>
<script>
import { ref, watch, nextTick } from 'vue'
import { Button, Collapse, CollapseItem, Notify } from '@opentiny/vue'
import { PluginSetting, ButtonGroup, SvgButton } from '@opentiny/tiny-engine-common'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
import ModelBasicForm from './ModelBasicForm.vue'
import FieldManager from './FieldManager.vue'
import { createModel, updateModel } from '../composable/useModelManager'
const isShow = ref(false)
export const openModelSettingPanel = () => {
isShow.value = true
}
export const closeModelSettingPanel = () => {
isShow.value = false
}
export default {
components: {
PluginSetting,
SvgButton,
ButtonGroup,
ModelBasicForm,
FieldManager,
TinyButton: Button,
TinyCollapse: Collapse,
TinyCollapseItem: CollapseItem
},
props: {
model: {
type: Object,
default: () => ({})
},
models: {
type: Array,
default: () => []
},
showExport: {
type: Boolean,
default: false
}
},
emits: ['editCallback', 'exportModel', 'deleteCallback'],
setup(props, { emit }) {
const { PLUGIN_NAME } = useLayout()
const activeName = ref(['general', 'fields'])
const selectedModel = ref()
const modelBasicFormRef = ref()
// 展开行配置
const expandConfig = ref({
expandAll: false,
trigger: 'row',
expandRowKeys: [],
accordion: false,
activeMethod: (row) => row.type === 'Enum' || row.type === 'ModelRef', // 枚举类型和模型引用类型都显示展开箭头
showIcon: (row) => row.type === 'Enum' || row.type === 'ModelRef' // 枚举类型和模型引用类型都显示展开箭头
})
// 添加字段,自动进入编辑状态
const handleAddField = () => {
if (!selectedModel.value) return
const newField = {
prop: '',
type: 'String',
required: false,
description: '',
isEditing: true,
isNew: true // 新增字段标记
}
selectedModel.value.parameters.push(newField)
nextTick(() => {
const nameInputs = document.querySelectorAll('.editing-cell .tiny-input')
if (nameInputs.length > 0) nameInputs[nameInputs.length - 1].focus()
})
}
// 在当前行后插入一条枚举值
const insertEnumValueAfter = (field, index) => {
if (!field.options) {
field.options = []
}
field.options.splice(index + 1, 0, { value: '', label: '' })
}
// 删除枚举值
const removeEnumValue = (field, index) => {
if (!Array.isArray(field.options)) return
if (field.options.length <= 1) {
// 只剩一条时,不删除,清空内容
field.options[0] = { value: '', label: '' }
return
}
field.options.splice(index, 1)
}
const isSaving = ref(false)
// 保存模型时一并保存version字段
const saveModel = async () => {
// 从子组件获取最新的数据
const latestModelData = modelBasicFormRef.value?.getLocalValue()
modelBasicFormRef.value
.validate()
.then(async (valid) => {
if (valid) {
isSaving.value = true // 设置保存标志
// 深拷贝一份数据用于保存
const newModel = {
description: latestModelData.description,
modelUrl: latestModelData.modelUrl,
nameCn: latestModelData.nameCn,
nameEn: latestModelData.nameEn,
version: latestModelData.version,
id: latestModelData.id,
parameters: JSON.parse(JSON.stringify(latestModelData.parameters.filter((item) => Boolean(item.prop))))
}
let isModelRefRelative = true
let propertyName = ''
if (newModel.parameters?.length > 0) {
newModel.parameters.forEach((item) => {
if (item.type === 'Enum') {
// 保存时序列化为字符串
item.options = JSON.stringify(item.options || [])
}
if (item.type === 'ModelRef') {
item.isModel = true
delete item.options
item.defaultValue = item.defaultValue || null
isModelRefRelative = !!item.defaultValue
propertyName = item.prop
}
})
}
if (!isModelRefRelative) {
Notify({
type: 'error',
message: `字段${propertyName}未关联模型引用`
})
isSaving.value = false
return
}
if (latestModelData.id === null) {
delete newModel.id
await createModel(newModel)
} else {
await updateModel(newModel.id, newModel)
}
emit('editCallback')
Notify({
type: 'success',
message: '保存成功'
})
// 关闭面板
closeModelSettingPanel()
selectedModel.value = null
isSaving.value = false
}
})
.catch(() => {
isSaving.value = false
})
}
const deleteModel = () => {
emit('deleteCallback', selectedModel.value)
closeModelSettingPanel()
}
// 监听 props 变化,同步到本地(当选择不同模型时)
watch(
() => props.model,
(newModel) => {
if (!isSaving.value && newModel) {
// 只在非保存过程中处理
// 深拷贝避免修改props
const modelCopy = JSON.parse(JSON.stringify(newModel))
// 加载时反序列化枚举字段
if (modelCopy.parameters) {
modelCopy.parameters.forEach((param) => {
if (param.type === 'Enum' && typeof param.options === 'string') {
try {
param.options = JSON.parse(param.options)
} catch (e) {
param.options = [{ value: '', label: '' }]
}
}
})
}
selectedModel.value = modelCopy
} else {
selectedModel.value = newModel
}
},
{ deep: true, immediate: true }
)
return {
isShow,
PLUGIN_NAME,
activeName,
modelBasicFormRef,
expandConfig,
selectedModel,
closeModelSettingPanel,
handleAddField,
insertEnumValueAfter,
removeEnumValue,
saveModel,
deleteModel
}
}
}
</script>
<style lang="less" scoped>
.modelmanager-plugin-setting {
:deep(.tiny-collapse .tiny-collapse-item) {
.tiny-collapse-item__header {
&,
&.is-active {
&::before {
border: none;
}
}
.svg-icon {
margin-right: 6px;
}
}
.tiny-collapse-item__wrap .tiny-collapse-item__content {
padding: 0 12px 12px;
.section {
padding: 0;
}
}
}
:deep(.plugin-setting-header) {
border: 0;
}
:deep(.plugin-setting-content) {
padding: 0 0 16px 0;
}
:deep(.tiny-collapse) {
border-bottom: 0;
}
}
</style>