Procházet zdrojové kódy

feat: 代码生成

DESKTOP-USV654P\pc před 5 měsíci
rodič
revize
b1bd1369e8

+ 50 - 0
apps/web-baicai/src/api/system/generate/generate.ts

@@ -0,0 +1,50 @@
+import type { BasicFetchResult, BasicPageParams } from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace GenerateApi {
+  export interface PageParams extends BasicPageParams {
+    businessName?: string;
+    dbName?: string;
+  }
+
+  export interface BasicRecordItem {
+    dbName: string;
+    businessName: string;
+    templateIds: number[];
+  }
+
+  export interface PreviewItem {
+    id: number;
+    template: string;
+    templateName: string;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type PageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<PageResult>('/generate/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/generate/entity', {
+      params: { id },
+    });
+
+  export const preview = (id: number) =>
+    requestClient.get<PreviewItem[]>('/generate/preview', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/generate', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/generate', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/generate', { data: { id } });
+}

+ 2 - 0
apps/web-baicai/src/api/system/generate/index.ts

@@ -0,0 +1,2 @@
+export * from './generate';
+export * from './template';

+ 45 - 0
apps/web-baicai/src/api/system/generate/template.ts

@@ -0,0 +1,45 @@
+import type {
+  BasicFetchResult,
+  BasicOptionResult,
+  BasicPageParams,
+} from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace CodeTemplateApi {
+  export interface PageParams extends BasicPageParams {
+    name?: string;
+  }
+
+  export interface BasicRecordItem {
+    name: string;
+    codeTemplateType: number;
+    template: string;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type PageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<PageResult>('/code/template/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/code/template/entity', {
+      params: { id },
+    });
+
+  export const getOptions = () =>
+    requestClient.get<BasicOptionResult[]>('/code/template/options');
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/code/template', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/code/template', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/code/template', { data: { id } });
+}

+ 1 - 0
apps/web-baicai/src/api/system/index.ts

@@ -5,6 +5,7 @@ export * from './department';
 export * from './dictionary';
 export * from './enum';
 export * from './file';
+export * from './generate';
 export * from './job';
 export * from './log';
 export * from './menu';

+ 3 - 0
apps/web-baicai/src/api/system/tenant.ts

@@ -37,6 +37,9 @@ export namespace TenantApi {
   export const getOptions = () =>
     requestClient.get<BasicOptionResult[]>('/tenant/options');
 
+  export const getList = () =>
+    requestClient.get<BasicOptionResult[]>('/tenant/list');
+
   export const getDetail = (id: number) =>
     requestClient.get<RecordItem>('/tenant/entity', {
       params: { id },

+ 1 - 0
apps/web-baicai/src/components/bc-monaco/index.ts

@@ -0,0 +1 @@
+export { default as BcMonaco } from './src/bc-monaco.vue';

+ 154 - 0
apps/web-baicai/src/components/bc-monaco/src/bc-monaco.vue

@@ -0,0 +1,154 @@
+<script lang="ts" setup>
+import type { PropType } from 'vue';
+
+import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
+
+import { alert } from '@vben/common-ui';
+
+import { useVModel } from '@vueuse/core';
+import * as monaco from 'monaco-editor';
+import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
+import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
+import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
+import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
+import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
+
+defineOptions({
+  name: 'BcMonaco',
+});
+const props = defineProps({
+  value: {
+    type: String as PropType<string>,
+    default: () => {
+      return '';
+    },
+  },
+  language: {
+    type: String as PropType<string>,
+    default: 'typescript',
+  },
+});
+
+const emit = defineEmits(['update:value']);
+
+const modelValue = useVModel(props, 'value', emit, {
+  defaultValue: props.value,
+  passive: true,
+});
+
+// eslint-disable-next-line no-restricted-globals
+self.MonacoEnvironment = {
+  getWorker: (_: string, label: string) => {
+    if (label === 'json') {
+      return new JsonWorker();
+    }
+    if (['css', 'less', 'scss'].includes(label)) {
+      return new CssWorker();
+    }
+    if (['handlebars', 'html', 'razor'].includes(label)) {
+      return new HtmlWorker();
+    }
+    if (['javascript', 'typescript'].includes(label)) {
+      return new TsWorker();
+    }
+    return new EditorWorker();
+  },
+};
+const monacoEditorRef = ref();
+// 初始化monacoEditor对象
+let monacoEditor: any = null;
+
+const initMonacoEditor = () => {
+  monacoEditor = monaco.editor.create(monacoEditorRef.value, {
+    theme: 'vs-dark', // 主题 vs vs-dark hc-black
+    value: '', // 默认显示的值
+    language: 'javascript',
+    formatOnPaste: true,
+    wordWrap: 'on', // 自动换行,注意大小写
+    wrappingIndent: 'indent',
+    folding: true, // 是否折叠
+    foldingHighlight: true, // 折叠等高线
+    foldingStrategy: 'indentation', // 折叠方式  auto | indentation
+    showFoldingControls: 'always', // 是否一直显示折叠 always | mouSEOver
+    disableLayerHinting: true, // 等宽优化
+    emptySelectionClipboard: false, // 空选择剪切板
+    selectionClipboard: false, // 选择剪切板
+    automaticLayout: true, // 自动布局
+    codeLens: false, // 代码镜头
+    scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
+    colorDecorators: true, // 颜色装饰器
+    accessibilitySupport: 'auto', // 辅助功能支持  "auto" | "off" | "on"
+    lineNumbers: 'on', // 行号 取值: "on" | "off" | "relative" | "interval" | function
+    lineNumbersMinChars: 5, // 行号最小字符   number
+    // enableSplitViewResizing: false,
+    readOnly: false, // 是否只读  取值 true | false
+  });
+
+  monacoEditor.onDidChangeModelContent(() => {
+    const scriptCode = monacoEditor?.getValue();
+    if (['javascript', 'json'].includes(props.language)) {
+      try {
+        modelValue.value = JSON.parse(scriptCode);
+      } catch {
+        alert({ content: '数据格式错误', icon: 'error' });
+      }
+    } else {
+      modelValue.value = scriptCode;
+    }
+  });
+};
+
+onMounted(() => {
+  initMonacoEditor();
+  nextTick(() => {
+    if (props.language) {
+      setLanguage(props.language);
+    }
+    if (props.value) {
+      setValue(props.value);
+    }
+  });
+});
+
+const setValue = (value: string) => {
+  let newValue = value;
+  if (['javascript', 'json'].includes(props.language)) {
+    newValue = JSON.stringify(value);
+  }
+  const scriptCode = monacoEditor?.getValue();
+  if (scriptCode !== newValue) {
+    monacoEditor?.setValue(newValue);
+    monacoEditor?.getAction('editor.action.formatDocument').run();
+  }
+};
+
+const setLanguage = (language: string) => {
+  monaco.editor.setModelLanguage(monacoEditor.getModel()!, language);
+};
+
+onUnmounted(() => {
+  monacoEditor.dispose();
+  monacoEditor = null;
+});
+
+watch(
+  () => props.language,
+  (value) => {
+    setLanguage(value);
+  },
+  { deep: true },
+);
+
+watch(
+  () => props.value,
+  (value) => {
+    if (monacoEditor) {
+      setValue(value);
+    }
+  },
+  { immediate: true },
+);
+</script>
+<template>
+  <div ref="monacoEditorRef" class="h-full w-full"></div>
+</template>

+ 63 - 4
apps/web-baicai/src/views/system/design/database/components/edit.vue

@@ -1,11 +1,11 @@
 <script lang="ts" setup>
 import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
 
-import { computed, ref, unref } from 'vue';
+import { computed, reactive, ref, unref } from 'vue';
 
-import { alert, useVbenModal } from '@vben/common-ui';
+import { alert, prompt, useVbenModal } from '@vben/common-ui';
 
-import { Button } from 'ant-design-vue';
+import { Button, Select } from 'ant-design-vue';
 
 import {
   useFormOptions,
@@ -15,7 +15,16 @@ import {
 } from '#/adapter';
 import { DatabaseApi } from '#/api';
 
-import { useColumnColumns, useSchema } from '../data.config';
+import {
+  entityBase,
+  entityBaseId,
+  entityDepartment,
+  entityTenant,
+  entityTenantDepartment,
+  entityTenantId,
+  useColumnColumns,
+  useSchema,
+} from '../data.config';
 import FormEdit from './editColumn.vue';
 
 defineOptions({
@@ -26,6 +35,25 @@ const emit = defineEmits(['success']);
 const modelRef = ref<Record<string, any>>({});
 const isUpdate = ref(true);
 
+const state = reactive({
+  defaultColumns: [
+    { value: 'entityBase', label: 'entityBase', data: entityBase },
+    { value: 'entityTenantId', label: 'entityTenantId', data: entityTenantId },
+    { value: 'entityBaseId', label: 'entityBaseId', data: entityBaseId },
+    {
+      value: 'entityDepartment',
+      label: 'entityDepartment',
+      data: entityDepartment,
+    },
+    { value: 'entityTenant', label: 'entityTenant', data: entityTenant },
+    {
+      value: 'entityTenantDepartment',
+      label: 'entityTenantDepartment',
+      data: entityTenantDepartment,
+    },
+  ],
+});
+
 const [Form, { validate, setValues, getValues, updateSchema }] = useVbenForm(
   useFormOptions({
     schema: useSchema(),
@@ -179,6 +207,29 @@ const [Grid, gridApi] = useVbenVxeGrid(
     } as VxeTableGridOptions,
   }),
 );
+
+const handleDefault = () => {
+  prompt({
+    component: Select,
+    componentProps: {
+      options: state.defaultColumns,
+      placeholder: '请选择',
+      popupClassName: 'pointer-events-auto',
+    },
+    content: '选择基类实体',
+    icon: 'info',
+    modelPropName: 'value',
+  }).then((val) => {
+    if (val) {
+      const defaultColumn = state.defaultColumns.find(
+        (item) => item.value === val,
+      );
+      if (defaultColumn) {
+        gridApi.grid.insert(defaultColumn.data);
+      }
+    }
+  });
+};
 </script>
 <template>
   <Modal class="h-[800px] w-[1000px]" :title="getTitle">
@@ -190,6 +241,14 @@ const [Grid, gridApi] = useVbenVxeGrid(
       <div class="h-[calc(100% - 178px)] h-full">
         <Grid>
           <template #toolbar-tools>
+            <Button
+              class="mr-2"
+              type="primary"
+              v-access:code="'table:add'"
+              @click="handleDefault"
+            >
+              新增默认列
+            </Button>
             <Button
               class="mr-2"
               type="primary"

+ 153 - 0
apps/web-baicai/src/views/system/design/database/data.config.ts

@@ -287,3 +287,156 @@ export const formColumnOptions: VbenFormProps = {
   schema: [],
   wrapperClass: 'grid-cols-1',
 };
+
+export const entityBaseId: DatabaseApi.BasicColumnRecord[] = [
+  {
+    columnName: 'id',
+    dataType: 'bigint',
+    decimalDigits: 0,
+    description: '主键',
+    isIdentity: false,
+    isNullable: false,
+    isPrimarykey: true,
+    length: 0,
+  },
+];
+
+export const entityBase: DatabaseApi.BasicColumnRecord[] = [
+  ...entityBaseId,
+  {
+    columnName: 'createTime',
+    dataType: 'datetime',
+    decimalDigits: 0,
+    description: '创建时间',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 0,
+  },
+  {
+    columnName: 'updateTime',
+    dataType: 'datetime',
+    decimalDigits: 0,
+    description: '更新时间',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 0,
+  },
+  {
+    columnName: 'createUserId',
+    dataType: 'bigint',
+    decimalDigits: 0,
+    description: '创建者Id',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 0,
+  },
+  {
+    columnName: 'createUserName',
+    dataType: 'nvarchar',
+    decimalDigits: 0,
+    description: '创建者姓名',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 64,
+  },
+  {
+    columnName: 'updateUserId',
+    dataType: 'bigint',
+    decimalDigits: 0,
+    description: '修改者Id',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 0,
+  },
+  {
+    columnName: 'updateUserName',
+    dataType: 'nvarchar',
+    decimalDigits: 0,
+    description: '修改者姓名',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 64,
+  },
+  {
+    columnName: 'isDelete',
+    dataType: 'boolean',
+    decimalDigits: 0,
+    description: '软删除',
+    isIdentity: false,
+    isNullable: false,
+    isPrimarykey: false,
+    length: 0,
+  },
+];
+
+export const entityDepartment: DatabaseApi.BasicColumnRecord[] = [
+  ...entityBase,
+  {
+    columnName: 'departmentId',
+    dataType: 'bigint',
+    decimalDigits: 0,
+    description: '创建者部门Id',
+    isIdentity: false,
+    isNullable: false,
+    isPrimarykey: false,
+    length: 0,
+  },
+  {
+    columnName: 'departmentName',
+    dataType: 'nvarchar',
+    decimalDigits: 0,
+    description: '创建者部门名称',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 64,
+  },
+];
+
+export const entityTenantId: DatabaseApi.BasicColumnRecord[] = [
+  ...entityBaseId,
+  {
+    columnName: 'tenantId',
+    dataType: 'bigint',
+    decimalDigits: 0,
+    description: '租户Id',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 0,
+  },
+];
+
+export const entityTenant: DatabaseApi.BasicColumnRecord[] = [
+  ...entityBase,
+  {
+    columnName: 'tenantId',
+    dataType: 'bigint',
+    decimalDigits: 0,
+    description: '租户Id',
+    isIdentity: false,
+    isNullable: false,
+    isPrimarykey: false,
+    length: 0,
+  },
+];
+
+export const entityTenantDepartment: DatabaseApi.BasicColumnRecord[] = [
+  ...entityDepartment,
+  {
+    columnName: 'tenantId',
+    dataType: 'bigint',
+    decimalDigits: 0,
+    description: '租户Id',
+    isIdentity: false,
+    isNullable: true,
+    isPrimarykey: false,
+    length: 0,
+  },
+];

+ 100 - 0
apps/web-baicai/src/views/system/design/generate/info/components/edit.vue

@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+import { computed, onMounted, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { Transfer } from 'ant-design-vue';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CodeTemplateApi, GenerateApi } from '#/api';
+
+import { useSchema } from '../data.config';
+
+defineOptions({
+  name: 'RoleEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+const targetKeys = ref<string[]>([]);
+const templateData = ref<Record<string, any>[]>([]);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm(
+  useFormOptions({
+    wrapperClass: 'grid-cols-2',
+    schema: useSchema(),
+  }),
+);
+
+const [Modal, { close, setState, getData, lock, unlock }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  closeOnClickModal: false,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const { valid } = await validate();
+      if (!valid) return;
+      const values = await getValues();
+      lock();
+      const postParams = unref(modelRef);
+      values.templateIds = targetKeys.value;
+      Object.assign(postParams, values);
+      await (unref(isUpdate)
+        ? GenerateApi.editDetail(postParams as GenerateApi.RecordItem)
+        : GenerateApi.addDetail(postParams as GenerateApi.BasicRecordItem));
+      alert('操作成功');
+
+      emit('success');
+      close();
+    } finally {
+      unlock();
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      targetKeys.value = [];
+      if (unref(isUpdate)) {
+        const entity = await GenerateApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        targetKeys.value = entity.templateIds.map((item: number) => `${item}`);
+        setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+});
+
+const getTitle = computed(() =>
+  unref(isUpdate) ? '编辑代码生成' : '新增代码生成',
+);
+
+onMounted(async () => {
+  const data = await CodeTemplateApi.getOptions();
+  templateData.value = data.map((item) => ({
+    key: `${item.value}`,
+    title: item.label,
+  }));
+});
+</script>
+<template>
+  <Modal class="w-[1000px]" :title="getTitle">
+    <Form>
+      <template #templateIds>
+        <Transfer
+          :data-source="templateData"
+          :render="(item) => item.title"
+          v-model:target-keys="targetKeys"
+          :list-style="{ flex: '1' }"
+          class="flex w-full"
+        />
+      </template>
+    </Form>
+  </Modal>
+</template>

+ 96 - 0
apps/web-baicai/src/views/system/design/generate/info/components/preview.vue

@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import { computed, reactive, ref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { useClipboard } from '@vueuse/core';
+import { Button, message, Segmented } from 'ant-design-vue';
+
+import { GenerateApi } from '#/api';
+import { BcMonaco } from '#/components/bc-monaco';
+
+defineOptions({
+  name: 'RoleEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>[]>([]);
+const state = reactive<{
+  activeKey: string;
+  loading: boolean;
+  tableData: string[];
+}>({
+  activeKey: '',
+  tableData: [],
+  loading: false,
+});
+const { copy } = useClipboard({ legacy: true });
+
+const [Modal, { close, setState, getData, lock, unlock }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  closeOnClickModal: false,
+  fullscreen: true,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      lock();
+
+      alert('操作成功');
+
+      emit('success');
+      close();
+    } finally {
+      unlock();
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      try {
+        state.loading = false;
+        modelRef.value = await GenerateApi.preview(data.baseData.id);
+        state.tableData = modelRef.value.map((item) => item.templateName);
+        state.activeKey = state.tableData[0] ?? '';
+        state.loading = true;
+      } finally {
+        setState({ loading: false });
+      }
+    }
+  },
+});
+
+const getTitle = computed(() => '预览模板');
+
+const getTemplate = computed(() => {
+  const template = modelRef.value.find(
+    (item) => item.templateName === state.activeKey,
+  )?.template;
+  return template;
+});
+
+const handleCopy = () => {
+  copy(getTemplate.value);
+  message.success('复制成功!');
+};
+</script>
+<template>
+  <Modal class="w-[1000px]" :title="getTitle">
+    <div class="flex h-full flex-col">
+      <Segmented
+        v-model:value="state.activeKey"
+        :options="state.tableData"
+        size="large"
+      />
+      <div v-if="state.loading" class="h-full">
+        <BcMonaco v-model:value="getTemplate" language="csharp" />
+      </div>
+    </div>
+
+    <template #prepend-footer>
+      <Button danger @click="handleCopy">复制</Button>
+    </template>
+  </Modal>
+</template>

+ 220 - 0
apps/web-baicai/src/views/system/design/generate/info/data.config.ts

@@ -0,0 +1,220 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { DatabaseApi, GenerateApi, TenantApi } from '#/api';
+import { dbTypeOptions } from '#/api/model';
+
+export const useSearchSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'businessName',
+      label: '业务名',
+    },
+    {
+      component: 'Input',
+      fieldName: 'tableName',
+      label: '表名称',
+    },
+  ];
+};
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<GenerateApi.RecordItem>,
+): VxeTableGridOptions<GenerateApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'configId',
+      title: '数据库',
+      width: 150,
+    },
+    { align: 'left', field: 'tableName', title: '表名称', width: 120 },
+    { align: 'left', field: 'businessName', title: '业务名称' },
+    { align: 'left', field: 'nameSpace', title: '命名空间', width: 200 },
+    { align: 'left', field: 'authorName', title: '作者', width: 120 },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'configId',
+          nameTitle: '数据库',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'detail',
+            label: '配置',
+            auth: ['generate:detail'],
+          },
+          {
+            code: 'edit',
+            auth: ['generate:edit'],
+          },
+          {
+            code: 'view',
+            label: '预览',
+            auth: ['generate:view'],
+          },
+          {
+            code: 'code',
+            label: '生成',
+            auth: ['generate:code'],
+          },
+          {
+            code: 'delete',
+            auth: ['generate:delete'],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'ApiSelect',
+      componentProps: (value) => {
+        return {
+          placeholder: '请选择',
+          api: TenantApi.getList,
+          optionFilterProp: 'remark',
+          showSearch: true,
+          numberToString: true,
+          valueField: 'configId',
+          labelField: 'remark',
+          onChange: (_: any, option: any) => {
+            value.dbType = option.dbType;
+            value.connectionString = option.connection;
+          },
+        };
+      },
+      fieldName: 'configId',
+      label: '数据库',
+      rules: 'required',
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请输入',
+        options: dbTypeOptions,
+      },
+      fieldName: 'dbType',
+      label: '库类型',
+      rules: 'required',
+      disabled: true,
+    },
+    {
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'connectionString',
+      label: '库连接',
+      rules: 'required',
+      disabled: true,
+      formItemClass: 'col-span-2 ',
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请选择',
+        optionFilterProp: 'label',
+        showSearch: true,
+      },
+      dependencies: {
+        componentProps: async (values) => {
+          if (values.configId) {
+            const data = await DatabaseApi.getList(values.configId);
+            return {
+              options: data.map((item) => {
+                return {
+                  label: item.description,
+                  value: item.name,
+                };
+              }),
+            };
+          }
+          return {};
+        },
+        triggerFields: ['configId'],
+      },
+      fieldName: 'tableName',
+      label: '数据库表',
+      rules: 'selectRequired',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'businessName',
+      label: '业务名',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'className',
+      label: '类名',
+      rules: 'required',
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请输入',
+        options: [
+          { label: 'EntityBase', value: 'EntityBase' },
+          { label: 'EntityTenant', value: 'EntityTenant' },
+          { label: 'EntityDepartment', value: 'EntityDepartment' },
+          { label: 'EntityTenantDepartment', value: 'EntityTenantDepartment' },
+          { label: 'EntityTenantId', value: 'EntityTenantId' },
+          { label: 'EntityBaseId', value: 'EntityBaseId' },
+        ],
+      },
+      fieldName: 'baseClassName',
+      label: '基类',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'nameSpace',
+      label: '命名空间',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'authorName',
+      label: '作者',
+      defaultValue: 'Baicai',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'templateIds',
+      formItemClass: 'col-span-2',
+      label: '模板',
+    },
+  ];
+};

+ 114 - 0
apps/web-baicai/src/views/system/design/generate/info/index.vue

@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { Button, message } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { GenerateApi } from '#/api';
+
+import FormEdit from './components/edit.vue';
+import FormPreview from './components/preview.vue';
+import { useColumns, useSearchSchema } from './data.config';
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const [FormPreviewModal, formPreviewApi] = useVbenModal({
+  connectedComponent: FormPreview,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await GenerateApi.deleteDetail(id);
+  message.success('数据删除成功');
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id },
+    })
+    .open();
+};
+
+const handlePreview = (record: any) => {
+  formPreviewApi
+    .setData({
+      baseData: { id: record.id },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<GenerateApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+    case 'view': {
+      handlePreview(row);
+      break;
+    }
+  }
+};
+
+const [Grid, { reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    formOptions: {
+      schema: useSearchSchema(),
+    },
+    gridOptions: {
+      columns: useColumns(handleActionClick),
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await GenerateApi.getPage({
+              pageIndex: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal @success="handelSuccess" />
+    <FormPreviewModal />
+    <Grid>
+      <template #table-title>
+        <span class="border-l-primary border-l-8 border-solid pl-2">
+          代码生成列表
+        </span>
+      </template>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'generate:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增代码生成
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 75 - 0
apps/web-baicai/src/views/system/design/generate/template/components/edit.vue

@@ -0,0 +1,75 @@
+<script lang="ts" setup>
+import { computed, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CodeTemplateApi } from '#/api';
+
+import { useSchema } from '../data.config';
+
+defineOptions({
+  name: 'RoleEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm(
+  useFormOptions({
+    wrapperClass: 'grid-cols-2',
+    schema: useSchema(),
+  }),
+);
+
+const [Modal, { close, setState, getData, lock, unlock }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  closeOnClickModal: false,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const { valid } = await validate();
+      if (!valid) return;
+      const values = await getValues();
+      lock();
+      const postParams = unref(modelRef);
+      Object.assign(postParams, values);
+      await (unref(isUpdate)
+        ? CodeTemplateApi.editDetail(postParams as CodeTemplateApi.RecordItem)
+        : CodeTemplateApi.addDetail(
+            postParams as CodeTemplateApi.BasicRecordItem,
+          ));
+      alert('操作成功');
+
+      emit('success');
+      close();
+    } finally {
+      unlock();
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      if (unref(isUpdate)) {
+        const entity = await CodeTemplateApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+});
+
+const getTitle = computed(() => (unref(isUpdate) ? '编辑模板' : '新增模板'));
+</script>
+<template>
+  <Modal class="w-[1000px]" :title="getTitle">
+    <Form />
+  </Modal>
+</template>

+ 114 - 0
apps/web-baicai/src/views/system/design/generate/template/data.config.ts

@@ -0,0 +1,114 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { CodeTemplateApi, EnumApi } from '#/api';
+
+export const codeTemplateTypeOptions = [
+  { color: '#3399CC', label: '实体', value: 0 },
+  { color: '#FFCC99', label: '服务接口', value: 1 },
+  { color: '#99CCCC', label: '服务实现', value: 2 },
+  { color: '#3399CC', label: '输入参数', value: 3 },
+  { color: '#FFCC99', label: '输出参数', value: 4 },
+  { color: '#99CCCC', label: '服务', value: 5 },
+];
+
+export const useSearchSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '模板名称',
+    },
+  ];
+};
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<CodeTemplateApi.RecordItem>,
+): VxeTableGridOptions<CodeTemplateApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'name',
+      title: '模板名称',
+      width: 150,
+    },
+    {
+      align: 'center',
+      field: 'codeTemplateType',
+      title: '模板类型',
+      width: 120,
+      cellRender: {
+        name: 'CellTag',
+        options: codeTemplateTypeOptions,
+      },
+    },
+    { align: 'left', field: 'template', title: '模板内容' },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'name',
+          nameTitle: '模板名称',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'edit',
+            auth: ['c-template:edit'],
+          },
+          {
+            code: 'delete',
+            auth: ['c-template:delete'],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'name',
+      label: '模板名称',
+      rules: 'required',
+    },
+    {
+      component: 'ApiSelect',
+      componentProps: {
+        placeholder: '请选择',
+        api: EnumApi.getList,
+        params: { name: 'CodeTemplateType' },
+        showSearch: true,
+      },
+      fieldName: 'codeTemplateType',
+      label: '模板类型',
+      rules: 'required',
+    },
+    {
+      component: 'InputCode',
+      componentProps: {
+        placeholder: '请输入',
+        language: 'plaintext',
+      },
+      fieldName: 'template',
+      label: '模板内容',
+      rules: 'required',
+    },
+  ];
+};

+ 96 - 0
apps/web-baicai/src/views/system/design/generate/template/index.vue

@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { Button, message } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { CodeTemplateApi } from '#/api';
+
+import FormEdit from './components/edit.vue';
+import { useColumns, useSearchSchema } from './data.config';
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await CodeTemplateApi.deleteDetail(id);
+  message.success('数据删除成功');
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<CodeTemplateApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+  }
+};
+
+const [Grid, { reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    formOptions: {
+      schema: useSearchSchema(),
+    },
+    gridOptions: {
+      columns: useColumns(handleActionClick),
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await CodeTemplateApi.getPage({
+              pageIndex: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal @success="handelSuccess" />
+    <Grid>
+      <template #table-title>
+        <span class="border-l-primary border-l-8 border-solid pl-2">
+          模板列表
+        </span>
+      </template>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'c-template:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增模板
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>