Pārlūkot izejas kodu

feat: 修改框架

DESKTOP-USV654P\pc 6 mēneši atpakaļ
vecāks
revīzija
a820d855a8
42 mainītis faili ar 2875 papildinājumiem un 24 dzēšanām
  1. 1 0
      apps/web-baicai/package.json
  2. 9 7
      apps/web-baicai/src/adapter/form.ts
  3. 37 2
      apps/web-baicai/src/adapter/vxe-table.ts
  4. 45 0
      apps/web-baicai/src/api/cms/article/article.ts
  5. 2 0
      apps/web-baicai/src/api/cms/article/index.ts
  6. 51 0
      apps/web-baicai/src/api/cms/article/type.ts
  7. 2 0
      apps/web-baicai/src/api/cms/index.ts
  8. 47 0
      apps/web-baicai/src/api/cms/site/channel.ts
  9. 38 0
      apps/web-baicai/src/api/cms/site/channelField.ts
  10. 38 0
      apps/web-baicai/src/api/cms/site/domain.ts
  11. 4 0
      apps/web-baicai/src/api/cms/site/index.ts
  12. 50 0
      apps/web-baicai/src/api/cms/site/site.ts
  13. 1 0
      apps/web-baicai/src/api/examples/index.ts
  14. 25 0
      apps/web-baicai/src/api/examples/upload.ts
  15. 1 0
      apps/web-baicai/src/api/index.ts
  16. 2 2
      apps/web-baicai/src/api/plugins/config.ts
  17. 192 0
      apps/web-baicai/src/components/form/components/bc-editor/bc-editor.vue
  18. 5 0
      apps/web-baicai/src/components/form/components/bc-tree-select.vue
  19. 1 1
      apps/web-baicai/src/components/form/components/bc-upload.vue
  20. 15 2
      apps/web-baicai/src/components/form/components/input-code.vue
  21. 16 5
      apps/web-baicai/src/components/form/components/input-code/input-code-modal-ignore.vue
  22. 114 0
      apps/web-baicai/src/views/cms/article/list/components/edit.vue
  23. 215 0
      apps/web-baicai/src/views/cms/article/list/data.config.ts
  24. 160 0
      apps/web-baicai/src/views/cms/article/list/index.vue
  25. 116 0
      apps/web-baicai/src/views/cms/article/type/components/edit.vue
  26. 155 0
      apps/web-baicai/src/views/cms/article/type/data.config.ts
  27. 165 0
      apps/web-baicai/src/views/cms/article/type/index.vue
  28. 78 0
      apps/web-baicai/src/views/cms/site/channel/components/edit.vue
  29. 179 0
      apps/web-baicai/src/views/cms/site/channel/data.config.ts
  30. 121 0
      apps/web-baicai/src/views/cms/site/channel/index.vue
  31. 87 0
      apps/web-baicai/src/views/cms/site/channelField/components/edit.vue
  32. 92 0
      apps/web-baicai/src/views/cms/site/channelField/data.config.ts
  33. 122 0
      apps/web-baicai/src/views/cms/site/channelField/index.vue
  34. 77 0
      apps/web-baicai/src/views/cms/site/domain/components/edit.vue
  35. 85 0
      apps/web-baicai/src/views/cms/site/domain/data.config.ts
  36. 123 0
      apps/web-baicai/src/views/cms/site/domain/index.vue
  37. 74 0
      apps/web-baicai/src/views/cms/site/info/components/edit.vue
  38. 200 0
      apps/web-baicai/src/views/cms/site/info/data.config.ts
  39. 121 0
      apps/web-baicai/src/views/cms/site/info/index.vue
  40. 1 1
      apps/web-baicai/src/views/plugins/config/data.config.ts
  41. 7 3
      apps/web-baicai/src/views/plugins/config/index.vue
  42. 1 1
      apps/web-baicai/src/views/plugins/template/index.vue

+ 1 - 0
apps/web-baicai/package.json

@@ -46,6 +46,7 @@
     "@vben/types": "workspace:*",
     "@vben/utils": "workspace:*",
     "@vueuse/core": "catalog:",
+    "aieditor": "^1.3.8",
     "ant-design-vue": "catalog:",
     "dayjs": "catalog:",
     "monaco-editor": "^0.52.2",

+ 9 - 7
apps/web-baicai/src/adapter/form.ts

@@ -8,17 +8,19 @@ import type { ComponentType } from './component';
 import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
+const modelPropNameMap: any = {
+  Checkbox: 'checked',
+  Radio: 'checked',
+  Switch: 'checked',
+  Upload: 'fileList',
+};
+
 setupVbenForm<ComponentType>({
   config: {
     // ant design vue组件库默认都是 v-model:value
     baseModelPropName: 'value',
     // 一些组件是 v-model:checked 或者 v-model:fileList
-    modelPropNameMap: {
-      Checkbox: 'checked',
-      Radio: 'checked',
-      Switch: 'checked',
-      Upload: 'fileList',
-    },
+    modelPropNameMap,
   },
   defineRules: {
     // 输入项目必填国际化适配
@@ -40,6 +42,6 @@ setupVbenForm<ComponentType>({
 
 const useVbenForm = useForm<ComponentType>;
 
-export { useVbenForm, z };
+export { modelPropNameMap, useVbenForm, z };
 export type VbenFormSchema = FormSchema<ComponentType>;
 export type { VbenFormProps };

+ 37 - 2
apps/web-baicai/src/adapter/vxe-table.ts

@@ -5,7 +5,7 @@ import type { ActionItem } from '#/components/table-action';
 
 import { h } from 'vue';
 
-import { confirm } from '@vben/common-ui';
+import { confirm, globalShareState } from '@vben/common-ui';
 import { IconifyIcon } from '@vben/icons';
 import { $te } from '@vben/locales';
 import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
@@ -18,7 +18,7 @@ import { TableAction } from '#/components/table-action';
 import { $t } from '#/locales';
 import { deepMerge } from '#/utils';
 
-import { useVbenForm } from './form';
+import { modelPropNameMap, useVbenForm } from './form';
 
 setupVbenVxeTable({
   configVxeTable: (vxeUI) => {
@@ -376,6 +376,41 @@ setupVbenVxeTable({
         });
       },
     });
+
+    // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+    // vxeUI.formats.add
+    // 增加编辑组件
+    const components = globalShareState.getComponents();
+    Object.keys(components).forEach((key: any) => {
+      const component = components[key];
+      const modelPropName = modelPropNameMap[key] || 'value';
+      vxeUI.renderer.add(key, {
+        renderTableEdit(renderOpts, params) {
+          const { row, column, $table } = params;
+          return h(component, {
+            ...renderOpts.props,
+            [modelPropName]: row[column.field],
+            [`onUpdate:${modelPropName}`]: (value: any) => {
+              params.row[params.column.field] = value;
+              $table.updateStatus(params);
+            },
+          });
+        },
+        // // 可编辑显示模板
+        // renderTableCell(renderOpts, params) {
+        //   const { props } = renderOpts;
+        //   const comp = (componentMap as any).get(renderOpts.name as any);
+        //   const { column, row } = params;
+        //   const value = get(row, column.field);
+        //   return comp
+        //     ? h(comp, {
+        //         ...props,
+        //         value,
+        //       })
+        //     : value;
+        // },
+      });
+    });
   },
   useVbenForm,
 });

+ 45 - 0
apps/web-baicai/src/api/cms/article/article.ts

@@ -0,0 +1,45 @@
+import type { StatusParams } from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace CmsArticleApi {
+  export interface PageParams {
+    startTime?: Date;
+    endTime?: Date;
+    title?: string;
+    siteId: number;
+    siteChannelId: number;
+  }
+
+  export interface BasicRecordItem {
+    name: string;
+    code: string;
+    siteId: number;
+    siteChannelId: number;
+    status: number;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<RecordItem[]>('/cms/article/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/cms/article/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/cms/article', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/cms/article', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/cms/article', { data: { id } });
+
+  export const updateStatus = (data: StatusParams) =>
+    requestClient.put('/cms/article/status', data);
+}

+ 2 - 0
apps/web-baicai/src/api/cms/article/index.ts

@@ -0,0 +1,2 @@
+export * from './article';
+export * from './type';

+ 51 - 0
apps/web-baicai/src/api/cms/article/type.ts

@@ -0,0 +1,51 @@
+import type { BasicTreeOptionResult, StatusParams } from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace CmsArticleTypeApi {
+  export interface PageParams {
+    code?: string;
+    title?: string;
+    siteId: number;
+    siteChannelId: number;
+  }
+
+  export interface BasicRecordItem {
+    parentId: number;
+    name: string;
+    code: string;
+    siteId: number;
+    siteChannelId: number;
+    status: number;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export const getTree = (params: PageParams) =>
+    requestClient.get<RecordItem[]>('/cms/article/type/tree', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/cms/article/type/entity', {
+      params: { id },
+    });
+
+  export const getTreeOptions = (params: PageParams) =>
+    requestClient.get<BasicTreeOptionResult[]>(
+      '/cms/article/type/tree-options',
+      { params },
+    );
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/cms/article/type', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/cms/article/type', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/cms/article/type', { data: { id } });
+
+  export const updateStatus = (data: StatusParams) =>
+    requestClient.put('/cms/article/type/status', data);
+}

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

@@ -0,0 +1,2 @@
+export * from './article';
+export * from './site';

+ 47 - 0
apps/web-baicai/src/api/cms/site/channel.ts

@@ -0,0 +1,47 @@
+import type {
+  BasicFetchResult,
+  BasicPageParams,
+  StatusParams,
+} from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace CmsSiteChannelApi {
+  export interface PageParams extends BasicPageParams {
+    siteId: number;
+    code?: string;
+    title?: string;
+  }
+
+  export interface BasicRecordItem {
+    code: string;
+    title: string;
+    status: number;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type PageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<PageResult>('/cms/site/channel/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/cms/site/channel/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/cms/site/channel', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/cms/site/channel', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/cms/site/channel', { data: { id } });
+
+  export const updateStatus = (data: StatusParams) =>
+    requestClient.put('/cms/site/channel/status', data);
+}

+ 38 - 0
apps/web-baicai/src/api/cms/site/channelField.ts

@@ -0,0 +1,38 @@
+import type { BasicFetchResult, BasicPageParams } from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace CmsSiteChannelFieldApi {
+  export interface PageParams extends BasicPageParams {
+    siteChannelId: number;
+  }
+
+  export interface BasicRecordItem {
+    fieldName: string;
+    label: string;
+    schema: any;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type PageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<PageResult>('/cms/site/channel/field/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/cms/site/channel/field/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/cms/site/channel/field', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/cms/site/channel/field', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/cms/site/channel/field', { data: { id } });
+}

+ 38 - 0
apps/web-baicai/src/api/cms/site/domain.ts

@@ -0,0 +1,38 @@
+import type { BasicFetchResult, BasicPageParams } from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace CmsSiteDomainApi {
+  export interface PageParams extends BasicPageParams {
+    siteId: number;
+  }
+
+  export interface BasicRecordItem {
+    lang: string;
+    domain: string;
+    siteId: number;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type PageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<PageResult>('/cms/site/domain/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/cms/site/domain/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/cms/site/domain', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/cms/site/domain', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/cms/site/domain', { data: { id } });
+}

+ 4 - 0
apps/web-baicai/src/api/cms/site/index.ts

@@ -0,0 +1,4 @@
+export * from './channel';
+export * from './channelField';
+export * from './domain';
+export * from './site';

+ 50 - 0
apps/web-baicai/src/api/cms/site/site.ts

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

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

@@ -1,2 +1,3 @@
 export * from './status';
 export * from './table';
+export * from './upload';

+ 25 - 0
apps/web-baicai/src/api/examples/upload.ts

@@ -0,0 +1,25 @@
+import { requestClient } from '#/api/request';
+
+interface UploadFileParams {
+  file: File;
+  onError?: (error: Error) => void;
+  onProgress?: (progress: { percent: number }) => void;
+  onSuccess?: (data: any, file: File) => void;
+}
+export async function uploadFile({
+  file,
+  onError,
+  onProgress,
+  onSuccess,
+}: UploadFileParams) {
+  try {
+    onProgress?.({ percent: 0 });
+
+    const data = await requestClient.upload('/file/upload', { file });
+
+    onProgress?.({ percent: 100 });
+    onSuccess?.(data, file);
+  } catch (error) {
+    onError?.(error instanceof Error ? error : new Error(String(error)));
+  }
+}

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

@@ -1,3 +1,4 @@
+export * from './cms';
 export * from './core';
 export * from './examples';
 export * from './plugins';

+ 2 - 2
apps/web-baicai/src/api/plugins/config.ts

@@ -27,8 +27,8 @@ export namespace PluginsConfigApi {
     requestClient.post('/plugins/config/sms', data);
 
   export const getEmailDetail = () =>
-    requestClient.get<SmsConfig>('/plugins/config/email/entity');
+    requestClient.get<EmailConfig>('/plugins/config/email/entity');
 
-  export const editEmailDetail = (data: SmsConfig) =>
+  export const editEmailDetail = (data: EmailConfig) =>
     requestClient.post('/plugins/config/email', data);
 }

+ 192 - 0
apps/web-baicai/src/components/form/components/bc-editor/bc-editor.vue

@@ -0,0 +1,192 @@
+<script lang="ts" setup>
+import type { PropType } from 'vue';
+
+import { computed, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue';
+
+import { usePreferences } from '@vben/preferences';
+import { isNumber } from '@vben/utils';
+
+import { useVModel } from '@vueuse/core';
+import { AiEditor } from 'aieditor';
+
+import { uploadFile } from '#/api';
+
+import 'aieditor/dist/style.css';
+
+defineOptions({
+  name: 'BcEditor',
+});
+
+const props = defineProps({
+  value: {
+    type: String,
+    default: () => {
+      return '';
+    },
+  },
+  disabled: {
+    type: Boolean,
+    default: false,
+  },
+  height: {
+    type: [Number, String] as PropType<number | string>,
+    required: false,
+    default: 600,
+  },
+  width: {
+    type: [Number, String] as PropType<number | string>,
+    required: false,
+    default: '100%',
+  },
+  placeholder: {
+    type: String,
+    default: () => {
+      return '';
+    },
+  },
+});
+
+const emit = defineEmits(['update:value']);
+const editorRef = ref<Element | null>(null);
+let aiEditor: AiEditor | null = null;
+
+const { isDark, locale } = usePreferences();
+
+const langName = computed(() => {
+  const lang = locale.value;
+  return ['en', 'zh_CN'].includes(lang) ? lang : 'zh_CN';
+});
+
+const modelValue = useVModel(props, 'value', emit, {
+  defaultValue: props.value,
+  passive: true,
+});
+const uploader = (file: File): Promise<Record<string, any>> => {
+  return new Promise((resolve, reject) => {
+    uploadFile({ file, onSuccess: resolve, onError: reject });
+  });
+};
+
+const ininEditor = () => {
+  aiEditor = new AiEditor({
+    element: editorRef.value as Element,
+    placeholder: props.placeholder,
+    content: modelValue.value,
+    theme: isDark.value ? 'dark' : 'light',
+    lang: langName.value,
+    editable: !props.disabled,
+    onChange: (_aiEditor) => {
+      modelValue.value = _aiEditor.getHtml();
+    },
+    image: {
+      uploader,
+      uploaderEvent: {
+        onSuccess: (file: File, response: any) => {
+          return {
+            errorCode: 0,
+            data: {
+              src: response.url,
+              alt: file.name,
+            },
+          };
+        },
+      },
+    },
+    video: {
+      uploader,
+      uploaderEvent: {
+        onSuccess: (_, response: any) => {
+          return {
+            errorCode: 0,
+            data: {
+              src: response.url,
+              poster: '',
+            },
+          };
+        },
+      },
+    },
+    attachment: {
+      uploader,
+      uploaderEvent: {
+        onSuccess: (file: File, response: any) => {
+          return {
+            errorCode: 0,
+            data: {
+              href: response.url,
+              fileName: file.name,
+            },
+          };
+        },
+      },
+    },
+  });
+};
+
+watchEffect(() => {
+  const theme = isDark.value ? 'dark' : 'light';
+  aiEditor?.changeTheme(theme);
+});
+
+watchEffect(() => {
+  aiEditor?.changeLang(locale.value);
+});
+
+onUnmounted(() => {
+  aiEditor && aiEditor.destroy();
+});
+
+onMounted(() => {
+  ininEditor();
+});
+
+watch(
+  () => props.disabled,
+  (newValue) => {
+    aiEditor?.setEditable(!newValue);
+  },
+  { immediate: true },
+);
+watch(
+  () => modelValue.value,
+  (newValue) => {
+    if (newValue !== aiEditor?.getHtml()) {
+      aiEditor?.setContent(newValue);
+    }
+  },
+);
+
+const containerWidth = computed(() => {
+  const width = props.width;
+  if (isNumber(width)) {
+    return `${width}px`;
+  }
+  return width;
+});
+const containerHeight = computed(() => {
+  const height = props.height;
+  if (isNumber(height)) {
+    return `${height}px`;
+  }
+  return height;
+});
+</script>
+
+<template>
+  <div
+    ref="editorRef"
+    :style="{ width: containerWidth, height: containerHeight }"
+  ></div>
+</template>
+
+<style lang="less">
+.tinymce-container {
+  position: relative;
+  height: 100%;
+  overflow: hidden;
+}
+
+.tox-tinymce-aux {
+  z-index: 9999 !important;
+}
+</style>

+ 5 - 0
apps/web-baicai/src/components/form/components/bc-tree-select.vue

@@ -48,6 +48,10 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  multiple: {
+    type: Boolean,
+    default: false,
+  },
 });
 const emit = defineEmits(['update:value', 'optionsChange']);
 const modelValue = useVModel(props, 'value', emit, {
@@ -111,6 +115,7 @@ watch(
     :tree-node-filter-prop="fieldNames.label"
     class="w-full"
     @dropdown-visible-change="handleFetch"
+    :multiple="multiple"
   >
     <template v-for="item in Object.keys($slots)" #[item]="data">
       <slot :name="item" v-bind="data || {}"></slot>

+ 1 - 1
apps/web-baicai/src/components/form/components/bc-upload.vue

@@ -87,7 +87,7 @@ const getFileList = async (val: number | string | undefined) => {
             uid: item.id.toString(),
             size: item.sizeKb,
             name: item.fileName,
-            url: item.filePath,
+            url: item.url,
           } as UploadFile;
         })
       : [];

+ 15 - 2
apps/web-baicai/src/components/form/components/input-code.vue

@@ -19,6 +19,14 @@ const props = defineProps({
     >,
     default: undefined,
   },
+  language: {
+    type: String as PropType<string>,
+    default: 'javascript',
+  },
+  placeholder: {
+    type: String as PropType<string>,
+    default: '请输入',
+  },
 });
 const emit = defineEmits(['update:value']);
 const modelValue = useVModel(props, 'value', emit, {
@@ -51,12 +59,13 @@ const handleSuccess = (scriptCode: any) => {
 };
 
 const handleInput = () => {
+  const { language } = props;
   inputCodeApi
     .setData({
       baseData: {
         scriptCode: modelValue.value,
         name: '验证规则',
-        // language: 'javascript',
+        language,
       },
     })
     .open();
@@ -66,7 +75,11 @@ const handleInput = () => {
 <template>
   <div class="w-full">
     <InputCodeModal @success="handleSuccess" />
-    <Input v-model:value="state.value" class="w-full">
+    <Input
+      v-model:value="state.value"
+      class="w-full"
+      :placeholder="placeholder"
+    >
       <template #addonAfter>
         <Icon icon="proicons:nodejs" @click="handleInput" />
       </template>

+ 16 - 5
apps/web-baicai/src/components/form/components/input-code/input-code-modal-ignore.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import { nextTick, reactive, ref, toRaw, unref } from 'vue';
 
-import { useVbenModal } from '@vben/common-ui';
+import { alert, useVbenModal } from '@vben/common-ui';
 import { isString } from '@vben/utils';
 
 import * as monaco from 'monaco-editor';
@@ -74,7 +74,7 @@ const initMonacoEditor = () => {
   });
 };
 
-const [Modal, { close, setState, getData }] = useVbenModal({
+const [Modal, { lock, unlock, close, setState, getData }] = useVbenModal({
   draggable: true,
   fullscreen: false,
   closeOnClickModal: false,
@@ -84,16 +84,27 @@ const [Modal, { close, setState, getData }] = useVbenModal({
   },
   onConfirm: async () => {
     try {
-      setState({ confirmLoading: true });
+      lock();
       const scriptCode = toRaw(monacoEditor).getValue();
-      close();
       if (['javascript', 'json'].includes(state.language)) {
-        emit('success', JSON.parse(scriptCode));
+        try {
+          emit('success', JSON.parse(scriptCode));
+          close();
+        } catch (error) {
+          alert({ content: '数据格式错误', icon: 'error' });
+          // eslint-disable-next-line no-console
+          console.log(
+            '数据格式错误',
+            `语言 ${state.language}  ${error}  ${typeof scriptCode}`,
+          );
+        }
       } else {
         emit('success', scriptCode);
+        close();
       }
     } finally {
       setState({ confirmLoading: false });
+      unlock();
     }
   },
   onOpenChange: async (isOpen: boolean) => {

+ 114 - 0
apps/web-baicai/src/views/cms/article/list/components/edit.vue

@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import { computed, reactive, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { TabPane, Tabs } from 'ant-design-vue';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CmsArticleApi } from '#/api';
+
+import { useSchemaBase, useSchemaExtend } from '../data.config';
+
+defineOptions({
+  name: 'CmsArticleEdit',
+});
+
+const emit = defineEmits(['success']);
+
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+const conditionInfo = reactive<Record<string, any>>({});
+const state = reactive<{
+  activeKey: string;
+}>({
+  activeKey: '1',
+});
+
+const [FormBase, formBaseApi] = useVbenForm(
+  useFormOptions({
+    schema: [],
+  }),
+);
+
+const [FormExtend, formExtendApi] = useVbenForm(
+  useFormOptions({
+    schema: useSchemaExtend(),
+  }),
+);
+
+const [Modal, { close, setState, getData, lock, unlock }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  closeOnClickModal: false,
+  fullscreen: true,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const validBase = await formBaseApi.validate();
+      if (!validBase.valid) {
+        state.activeKey = '1';
+        return;
+      }
+
+      const validExtend = await formExtendApi.validate();
+      if (!validExtend.valid) {
+        state.activeKey = '2';
+        return;
+      }
+
+      const values = await formBaseApi.merge(formExtendApi).submitAllForm(true);
+      lock();
+      const postParams = unref(modelRef);
+      Object.assign(postParams, conditionInfo, values);
+      await (unref(isUpdate)
+        ? CmsArticleApi.editDetail(postParams as CmsArticleApi.RecordItem)
+        : CmsArticleApi.addDetail(postParams as CmsArticleApi.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 };
+      state.activeKey = '1';
+      conditionInfo.siteId = data.conditionInfo.siteId;
+      conditionInfo.siteChannelId = data.conditionInfo.siteChannelId;
+
+      const schema = useSchemaBase(conditionInfo);
+      formBaseApi.setState({ schema });
+
+      if (unref(isUpdate)) {
+        const entity = await CmsArticleApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        formBaseApi.setValues(entity);
+        formExtendApi.setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+});
+
+const getTitle = computed(() => (unref(isUpdate) ? '编辑内容' : '新增内容'));
+</script>
+<template>
+  <Modal class="w-[1000px]" :title="getTitle">
+    <Tabs v-model:active-key="state.activeKey">
+      <TabPane tab="基本信息" key="1">
+        <FormBase />
+      </TabPane>
+      <TabPane tab="扩展选项" key="2" force-render>
+        <FormExtend />
+      </TabPane>
+    </Tabs>
+  </Modal>
+</template>

+ 215 - 0
apps/web-baicai/src/views/cms/article/list/data.config.ts

@@ -0,0 +1,215 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { CmsArticleApi, CmsArticleTypeApi } from '#/api';
+
+export const useSearchSchema = (
+  searchInfo: Record<string, any>,
+): VbenFormSchema[] => {
+  return [
+    {
+      component: 'BcTreeSelect',
+      componentProps: {
+        placeholder: '选择上级',
+        api: {
+          url: CmsArticleTypeApi.getTreeOptions,
+          params: { ...searchInfo },
+        },
+        showSearch: true,
+      },
+      fieldName: 'typeId',
+      label: '分类',
+    },
+    {
+      component: 'Input',
+      fieldName: 'title',
+      label: '标题',
+    },
+    {
+      component: 'RangePicker',
+      fieldName: '__date',
+      label: '创建日期',
+      componentProps: {
+        format: 'YYYY-MM-DD',
+        placeholder: ['开始时间', '结束时间'],
+      },
+    },
+  ];
+};
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<CmsArticleApi.RecordItem>,
+  authCode?: string,
+): VxeTableGridOptions<CmsArticleApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    { align: 'left', field: 'title', title: '标题' },
+    {
+      align: 'left',
+      field: 'typeNames',
+      title: '类型',
+      width: 200,
+    },
+    {
+      align: 'right',
+      field: 'click',
+      title: '浏览数',
+      width: 80,
+    },
+    {
+      field: 'status',
+      title: '状态',
+      width: 82,
+      cellRender: { name: 'CellTag' },
+    },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'title',
+          nameTitle: '标题',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'edit',
+            auth: [`${authCode}:edit`],
+          },
+          {
+            code: 'delete',
+            auth: [`${authCode}:delete`],
+          },
+          {
+            code: 'setStatus',
+            label: (row: CmsArticleApi.RecordItem) => {
+              return row.status === 1 ? '禁用' : '启用';
+            },
+            auth: [`${authCode}:setStatus`],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchemaBase = (
+  searchInfo: Record<string, any>,
+): VbenFormSchema[] => {
+  return [
+    {
+      component: 'BcTreeSelect',
+      componentProps: {
+        placeholder: '分类',
+        api: {
+          url: CmsArticleTypeApi.getTreeOptions,
+          params: { ...searchInfo },
+        },
+        showSearch: true,
+        multiple: true,
+      },
+      fieldName: 'typeIds',
+      label: '分类',
+    },
+    {
+      component: 'BcUpload',
+      componentProps: {
+        placeholder: '请输入',
+        listType: 'picture-card',
+        maxCount: 1,
+      },
+      fieldName: 'imageId',
+      label: '封面',
+    },
+    {
+      component: 'BcUpload',
+      componentProps: {
+        placeholder: '请输入',
+        listType: 'text',
+        maxCount: 1,
+      },
+      fieldName: 'uideoId',
+      label: '视频',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入标题',
+      },
+      fieldName: 'title',
+      label: '标题',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入排序',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'source',
+      label: '文章来源',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'author',
+      label: '作者',
+    },
+  ];
+};
+
+export const useSchemaExtend = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'code',
+      label: '标识',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'linkUrl',
+      label: '外部链接',
+    },
+    {
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'summary',
+      label: '内容摘要',
+    },
+    {
+      component: 'BcEditor',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'content',
+      label: '类别介绍',
+    },
+  ];
+};

+ 160 - 0
apps/web-baicai/src/views/cms/article/list/index.vue

@@ -0,0 +1,160 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { onMounted, reactive, ref, unref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { AccessControl } from '@vben/access';
+import { alert, Fallback, Page, useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { CmsArticleApi, CmsSiteChannelApi } from '#/api';
+
+import FormEdit from './components/edit.vue';
+import { useColumns, useSearchSchema } from './data.config';
+
+const route = useRoute();
+
+const state = reactive<{
+  authCode: string;
+  error: boolean;
+  siteChannelId: string;
+}>({
+  error: false,
+  siteChannelId: (route.query?.id || '') as string,
+  authCode: '',
+});
+
+const modelRef = ref<Record<string, any>>({});
+
+const conditionInfo = reactive<Record<string, any>>({});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await CmsArticleApi.deleteDetail(id);
+  alert('数据删除成功');
+  handelSuccess();
+};
+
+const handleUpdateStatus = async (record: any) => {
+  await CmsArticleApi.updateStatus({
+    id: record.id,
+    status: record.status === 1 ? 2 : 1,
+  });
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id },
+      conditionInfo: { ...conditionInfo },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<CmsArticleApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+    case 'setStatus': {
+      await handleUpdateStatus(row);
+      break;
+    }
+  }
+};
+
+const [Grid, { setState, reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    formOptions: {
+      fieldMappingTime: [['__date', ['startTime', 'endTime']]],
+      schema: [],
+    },
+    gridOptions: {
+      columns: [],
+      proxyConfig: {
+        autoLoad: false,
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await CmsArticleApi.getPage({
+              pageIndex: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+              ...conditionInfo,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+
+const loadPage = async () => {
+  state.authCode = `${unref(modelRef).code}-content`;
+
+  const schema = useSearchSchema(conditionInfo);
+  const columns: VxeTableGridOptions<CmsArticleApi.RecordItem>['columns'] =
+    useColumns(handleActionClick, state.authCode);
+
+  setState({ formOptions: { schema }, gridOptions: { columns } });
+
+  setTimeout(() => {
+    reload();
+  }, 500);
+};
+
+onMounted(async () => {
+  try {
+    modelRef.value = await CmsSiteChannelApi.getDetail(
+      Number(state.siteChannelId),
+    );
+    conditionInfo.siteId = modelRef.value.siteId;
+    conditionInfo.siteChannelId = modelRef.value.id;
+    loadPage();
+  } catch {
+    state.error = true;
+  }
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Fallback status="500" v-if="state.error">
+      <template #action></template>
+    </Fallback>
+    <template v-else>
+      <FormEditModal @success="handelSuccess" />
+      <Grid>
+        <template #toolbar-tools>
+          <AccessControl :codes="[`${state.authCode}:add`]" type="code">
+            <Button
+              class="mr-2"
+              type="primary"
+              @click="() => handleEdit({}, false)"
+            >
+              新增内容
+            </Button>
+          </AccessControl>
+        </template>
+      </Grid>
+    </template>
+  </Page>
+</template>

+ 116 - 0
apps/web-baicai/src/views/cms/article/type/components/edit.vue

@@ -0,0 +1,116 @@
+<script lang="ts" setup>
+import { computed, reactive, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { TabPane, Tabs } from 'ant-design-vue';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CmsArticleTypeApi } from '#/api';
+
+import { useSchemaBase, useSchemaExtend } from '../data.config';
+
+defineOptions({
+  name: 'CmsArticleEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+const conditionInfo = reactive<Record<string, any>>({});
+const state = reactive<{
+  activeKey: string;
+}>({
+  activeKey: '1',
+});
+
+const [FormBase, formBaseApi] = useVbenForm(
+  useFormOptions({
+    schema: [],
+  }),
+);
+
+const [FormExtend, formExtendApi] = useVbenForm(
+  useFormOptions({
+    schema: useSchemaExtend(),
+  }),
+);
+
+const [Modal, { close, setState, getData, lock, unlock }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  closeOnClickModal: false,
+  fullscreen: true,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const validBase = await formBaseApi.validate();
+      if (!validBase.valid) {
+        state.activeKey = '1';
+        return;
+      }
+
+      const validExtend = await formExtendApi.validate();
+      if (!validExtend.valid) {
+        state.activeKey = '2';
+        return;
+      }
+
+      const values = await formBaseApi.merge(formExtendApi).submitAllForm(true);
+      lock();
+      const postParams = unref(modelRef);
+      Object.assign(postParams, conditionInfo, values);
+      await (unref(isUpdate)
+        ? CmsArticleTypeApi.editDetail(
+            postParams as CmsArticleTypeApi.RecordItem,
+          )
+        : CmsArticleTypeApi.addDetail(
+            postParams as CmsArticleTypeApi.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 };
+      state.activeKey = '1';
+      conditionInfo.siteId = data.conditionInfo.siteId;
+      conditionInfo.siteChannelId = data.conditionInfo.siteChannelId;
+
+      const schema = useSchemaBase(conditionInfo);
+      formBaseApi.setState({ schema });
+
+      if (unref(isUpdate)) {
+        const entity = await CmsArticleTypeApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        formBaseApi.setValues(entity);
+        formExtendApi.setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+});
+
+const getTitle = computed(() => (unref(isUpdate) ? '编辑类型' : '新增类型'));
+</script>
+<template>
+  <Modal class="w-[1000px]" :title="getTitle">
+    <Tabs v-model:active-key="state.activeKey">
+      <TabPane tab="基本信息" key="1">
+        <FormBase />
+      </TabPane>
+      <TabPane tab="扩展选项" key="2" force-render>
+        <FormExtend />
+      </TabPane>
+    </Tabs>
+  </Modal>
+</template>

+ 155 - 0
apps/web-baicai/src/views/cms/article/type/data.config.ts

@@ -0,0 +1,155 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { CmsArticleTypeApi } from '#/api';
+
+export const useSearchSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: '标识',
+    },
+    {
+      component: 'Input',
+      fieldName: 'title',
+      label: '标题',
+    },
+  ];
+};
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<CmsArticleTypeApi.RecordItem>,
+  authCode?: string,
+): VxeTableGridOptions<CmsArticleTypeApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    { align: 'left', field: 'title', title: '标题', treeNode: true },
+    {
+      align: 'left',
+      field: 'code',
+      title: '标识',
+      width: 150,
+    },
+    {
+      field: 'status',
+      title: '状态',
+      width: 82,
+      cellRender: { name: 'CellTag' },
+    },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'title',
+          nameTitle: '名称',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'edit',
+            auth: [`${authCode}:edit`],
+          },
+          {
+            code: 'delete',
+            auth: [`${authCode}:delete`],
+          },
+          {
+            code: 'setStatus',
+            label: (row: CmsArticleTypeApi.RecordItem) => {
+              return row.status === 1 ? '禁用' : '启用';
+            },
+            auth: [`${authCode}:setStatus`],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchemaBase = (
+  searchInfo: Record<string, any>,
+): VbenFormSchema[] => {
+  return [
+    {
+      component: 'BcTreeSelect',
+      componentProps: {
+        placeholder: '选择上级',
+        api: {
+          url: CmsArticleTypeApi.getTreeOptions,
+          params: { ...searchInfo },
+        },
+        showSearch: true,
+      },
+      fieldName: 'parentId',
+      label: '上级',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入类别标识',
+      },
+      fieldName: 'code',
+      label: '类别标识',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入标题',
+      },
+      fieldName: 'title',
+      label: '标题',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入排序',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+  ];
+};
+
+export const useSchemaExtend = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'BcUpload',
+      componentProps: {
+        placeholder: '请输入',
+        maxCount: 1,
+        listType: 'picture',
+      },
+      fieldName: 'imageId',
+      label: '显示图片',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'linkUrl',
+      label: '外部链接',
+    },
+    {
+      component: 'BcEditor',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'content',
+      label: '类别介绍',
+    },
+  ];
+};

+ 165 - 0
apps/web-baicai/src/views/cms/article/type/index.vue

@@ -0,0 +1,165 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { onMounted, reactive, ref, unref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { AccessControl } from '@vben/access';
+import { alert, Fallback, Page, useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { CmsArticleTypeApi, CmsSiteChannelApi } from '#/api';
+
+import FormEdit from './components/edit.vue';
+import { useColumns, useSearchSchema } from './data.config';
+
+const route = useRoute();
+
+const state = reactive<{
+  authCode: string;
+  error: boolean;
+  siteChannelId: string;
+}>({
+  error: false,
+  siteChannelId: (route.query?.id || '') as string,
+  authCode: '',
+});
+
+const modelRef = ref<Record<string, any>>({});
+
+const conditionInfo = reactive<Record<string, any>>({});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await CmsArticleTypeApi.deleteDetail(id);
+  alert('数据删除成功');
+  handelSuccess();
+};
+
+const handleUpdateStatus = async (record: any) => {
+  await CmsArticleTypeApi.updateStatus({
+    id: record.id,
+    status: record.status === 1 ? 2 : 1,
+  });
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id },
+      conditionInfo: { ...conditionInfo },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<CmsArticleTypeApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+    case 'setStatus': {
+      await handleUpdateStatus(row);
+      break;
+    }
+  }
+};
+
+const [Grid, { setState, reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    formOptions: {
+      schema: [],
+    },
+    gridOptions: {
+      columns: [],
+      pagerConfig: {
+        enabled: false,
+      },
+      treeConfig: {
+        rowField: 'id',
+        childrenField: 'children',
+        transform: false,
+      },
+      proxyConfig: {
+        autoLoad: false,
+        ajax: {
+          query: async (_, formValues) => {
+            return await CmsArticleTypeApi.getTree({
+              ...formValues,
+              ...conditionInfo,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+
+const loadPage = async () => {
+  state.authCode = `${unref(modelRef).code}-type`;
+
+  const schema = useSearchSchema();
+  const columns: VxeTableGridOptions<CmsArticleTypeApi.RecordItem>['columns'] =
+    useColumns(handleActionClick, state.authCode);
+
+  setState({ formOptions: { schema }, gridOptions: { columns } });
+
+  setTimeout(() => {
+    reload();
+  }, 500);
+};
+
+onMounted(async () => {
+  try {
+    modelRef.value = await CmsSiteChannelApi.getDetail(
+      Number(state.siteChannelId),
+    );
+    conditionInfo.siteId = modelRef.value.siteId;
+    conditionInfo.siteChannelId = modelRef.value.id;
+    loadPage();
+  } catch {
+    state.error = true;
+  }
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Fallback status="500" v-if="state.error">
+      <template #action></template>
+    </Fallback>
+    <template v-else>
+      <FormEditModal @success="handelSuccess" />
+      <Grid>
+        <template #toolbar-tools>
+          <AccessControl :codes="[`${state.authCode}:add`]" type="code">
+            <Button
+              class="mr-2"
+              type="primary"
+              @click="() => handleEdit({}, false)"
+            >
+              新增类型
+            </Button>
+          </AccessControl>
+        </template>
+      </Grid>
+    </template>
+  </Page>
+</template>

+ 78 - 0
apps/web-baicai/src/views/cms/site/channel/components/edit.vue

@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import { computed, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CmsSiteChannelApi } from '#/api';
+
+import { useSchema } from '../data.config';
+
+defineOptions({
+  name: 'SiteEdit',
+});
+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)
+        ? CmsSiteChannelApi.editDetail(
+            postParams as CmsSiteChannelApi.RecordItem,
+          )
+        : CmsSiteChannelApi.addDetail(
+            postParams as CmsSiteChannelApi.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 CmsSiteChannelApi.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>

+ 179 - 0
apps/web-baicai/src/views/cms/site/channel/data.config.ts

@@ -0,0 +1,179 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { CmsSiteApi, CmsSiteChannelApi } from '#/api';
+
+export const useSearchSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: '标识',
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '标题',
+    },
+  ];
+};
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<CmsSiteChannelApi.RecordItem>,
+): VxeTableGridOptions<CmsSiteChannelApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'code',
+      title: '标识',
+      width: 150,
+    },
+    { align: 'left', field: 'title', title: '标题' },
+    { align: 'left', field: 'isComment', title: '开启评论', width: 80 },
+    { align: 'left', field: 'isAlbum', title: '开启相册', width: 80 },
+    { align: 'left', field: 'isAttach', title: '开启附件', width: 80 },
+    { align: 'left', field: 'isContribute', title: '允许投稿', width: 80 },
+    { align: 'left', field: 'sort', title: '排序', width: 80 },
+    {
+      field: 'status',
+      title: '状态',
+      width: 82,
+      cellRender: { name: 'CellTag' },
+    },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'title',
+          nameTitle: '标题',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'field',
+            label: '扩展',
+            auth: ['site-channel:field'],
+          },
+          {
+            code: 'edit',
+            auth: ['site-channel:edit'],
+          },
+          {
+            code: 'delete',
+            auth: ['site-channel:delete'],
+          },
+          {
+            code: 'setStatus',
+            label: (row: CmsSiteChannelApi.RecordItem) => {
+              return row.status === 1 ? '禁用' : '启用';
+            },
+            auth: ['site-channel:setStatus'],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'BcSelect',
+      componentProps: {
+        placeholder: '英文字母',
+        api: {
+          url: CmsSiteApi.getOptions,
+        },
+        showSearch: true,
+      },
+      fieldName: 'siteId',
+      label: '站点',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '英文字母',
+      },
+      fieldName: 'code',
+      label: '频道标识',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'title',
+      label: '名称',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入排序',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Divider',
+      fieldName: 'divider1',
+      formItemClass: 'col-span-2 md:col-span-2 pb-0',
+      hideLabel: true,
+      renderComponentContent() {
+        return {
+          default: () => '其它设置',
+        };
+      },
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入',
+        class: 'w-auto',
+      },
+      fieldName: 'isComment',
+      label: '开启评论',
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入',
+        class: 'w-auto',
+      },
+      fieldName: 'isAlbum',
+      label: '开启相册',
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入',
+        class: 'w-auto',
+      },
+      fieldName: 'isAttach',
+      label: '开启附件',
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入',
+        class: 'w-auto',
+      },
+      fieldName: 'isContribute',
+      label: '允许投稿',
+    },
+  ];
+};

+ 121 - 0
apps/web-baicai/src/views/cms/site/channel/index.vue

@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { alert, Page, useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { CmsSiteChannelApi } from '#/api';
+
+import FormFieldEdit from '../channelField/index.vue';
+import FormEdit from './components/edit.vue';
+import { useColumns, useSearchSchema } from './data.config';
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const [FormFieldEditModal, formFieldEditApi] = useVbenModal({
+  connectedComponent: FormFieldEdit,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await CmsSiteChannelApi.deleteDetail(id);
+  alert('数据删除成功');
+  handelSuccess();
+};
+
+const handleUpdateStatus = async (record: any) => {
+  await CmsSiteChannelApi.updateStatus({
+    id: record.id,
+    status: record.status === 1 ? 2 : 1,
+  });
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id },
+    })
+    .open();
+};
+
+const handleField = (record: any) => {
+  formFieldEditApi
+    .setData({
+      baseData: { ...record },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<CmsSiteChannelApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+    case 'field': {
+      await handleField(row);
+      break;
+    }
+    case 'setStatus': {
+      await handleUpdateStatus(row);
+      break;
+    }
+  }
+};
+
+const [Grid, { reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    formOptions: {
+      schema: useSearchSchema(),
+    },
+    gridOptions: {
+      columns: useColumns(handleActionClick),
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await CmsSiteChannelApi.getPage({
+              pageIndex: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal @success="handelSuccess" />
+    <FormFieldEditModal />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'site-channel:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增频道
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 87 - 0
apps/web-baicai/src/views/cms/site/channelField/components/edit.vue

@@ -0,0 +1,87 @@
+<script lang="ts" setup>
+import { computed, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+import { cloneDeep } from '@vben/utils';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CmsSiteChannelFieldApi } from '#/api';
+
+import { useSchema } from '../data.config';
+
+defineOptions({
+  name: 'SiteChannelFieldEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm(
+  useFormOptions({
+    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);
+      const _schema: any = cloneDeep(postParams.schema);
+      Object.assign(_schema, {
+        fieldName: values.fieldName,
+        label: values.label,
+      });
+      postParams.schema = _schema;
+
+      await (unref(isUpdate)
+        ? CmsSiteChannelFieldApi.editDetail(
+            postParams as CmsSiteChannelFieldApi.RecordItem,
+          )
+        : CmsSiteChannelFieldApi.addDetail(
+            postParams as CmsSiteChannelFieldApi.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 CmsSiteChannelFieldApi.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>

+ 92 - 0
apps/web-baicai/src/views/cms/site/channelField/data.config.ts

@@ -0,0 +1,92 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { CmsSiteChannelFieldApi } from '#/api';
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<CmsSiteChannelFieldApi.RecordItem>,
+): VxeTableGridOptions<CmsSiteChannelFieldApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'fieldName',
+      title: '字段名',
+      width: 200,
+    },
+    { align: 'left', field: 'label', title: '名称' },
+    { align: 'left', field: 'sort', title: '排序', width: 80 },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'fieldName',
+          nameTitle: '字段名',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'edit',
+            auth: ['site-channel:field'],
+          },
+          {
+            code: 'delete',
+            auth: ['site-channel:field'],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'fieldName',
+      label: '字段名',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'label',
+      label: '名称',
+      rules: 'required',
+    },
+    {
+      component: 'InputCode',
+      componentProps: {
+        placeholder: '请输入',
+        language: 'json',
+      },
+      fieldName: 'schema',
+      label: '配置',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+  ];
+};

+ 122 - 0
apps/web-baicai/src/views/cms/site/channelField/index.vue

@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { computed, nextTick, reactive, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { CmsSiteChannelFieldApi } from '#/api';
+
+import FormEdit from './components/edit.vue';
+import { useColumns } from './data.config';
+
+defineOptions({
+  name: 'SiteChannelFieldIndex',
+});
+
+const modelRef = ref<Record<string, any>>({});
+const searchInfo = reactive<Record<string, any>>({});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await CmsSiteChannelFieldApi.deleteDetail(id);
+  alert('数据删除成功');
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id, ...searchInfo },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<CmsSiteChannelFieldApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+  }
+};
+
+const [Grid, { reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    gridOptions: {
+      columns: useColumns(handleActionClick),
+      proxyConfig: {
+        autoLoad: false,
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await CmsSiteChannelFieldApi.getPage({
+              pageIndex: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+              ...searchInfo,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+
+const [Modal, { setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  closeOnClickModal: false,
+  fullscreen: true,
+  footer: false,
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+
+      modelRef.value = { ...data.baseData };
+      searchInfo.siteChannelId = data.baseData.id;
+      nextTick(() => {
+        reload();
+      });
+
+      setState({ loading: false });
+    }
+  },
+});
+const getTitle = computed(() => `扩展字段[${unref(modelRef).title}]`);
+</script>
+
+<template>
+  <Modal class="w-[1000px]" :title="getTitle">
+    <FormEditModal @success="handelSuccess" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'site-channel:field'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增字段
+        </Button>
+      </template>
+    </Grid>
+  </Modal>
+</template>

+ 77 - 0
apps/web-baicai/src/views/cms/site/domain/components/edit.vue

@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import { computed, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CmsSiteDomainApi } from '#/api';
+
+import { useSchema } from '../data.config';
+
+defineOptions({
+  name: 'SiteDomainEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm(
+  useFormOptions({
+    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)
+        ? CmsSiteDomainApi.editDetail(postParams as CmsSiteDomainApi.RecordItem)
+        : CmsSiteDomainApi.addDetail(
+            postParams as CmsSiteDomainApi.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 CmsSiteDomainApi.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>

+ 85 - 0
apps/web-baicai/src/views/cms/site/domain/data.config.ts

@@ -0,0 +1,85 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { CmsSiteDomainApi } from '#/api';
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<CmsSiteDomainApi.RecordItem>,
+): VxeTableGridOptions<CmsSiteDomainApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'lang',
+      title: '语言',
+      width: 100,
+    },
+    { align: 'left', field: 'domain', title: '域名', width: 250 },
+    { align: 'left', field: 'remark', title: '备注' },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'domain',
+          nameTitle: '域名',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'edit',
+            auth: ['site:domain'],
+          },
+          {
+            code: 'delete',
+            auth: ['site:domain'],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'BcSelect',
+      componentProps: {
+        placeholder: '请输入',
+        api: {
+          type: 'dict',
+          params: 'lang',
+        },
+      },
+      fieldName: 'lang',
+      label: '语言',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'domain',
+      label: '域名',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'remark',
+      label: '备注',
+    },
+  ];
+};

+ 123 - 0
apps/web-baicai/src/views/cms/site/domain/index.vue

@@ -0,0 +1,123 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { computed, nextTick, reactive, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { CmsSiteDomainApi } from '#/api';
+
+import FormEdit from './components/edit.vue';
+import { useColumns } from './data.config';
+
+defineOptions({
+  name: 'SiteDomainIndex',
+});
+
+const modelRef = ref<Record<string, any>>({});
+const searchInfo = reactive<Record<string, any>>({});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await CmsSiteDomainApi.deleteDetail(id);
+  alert('数据删除成功');
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id, ...searchInfo },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<CmsSiteDomainApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+  }
+};
+
+const [Grid, { reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    gridOptions: {
+      columns: useColumns(handleActionClick),
+      proxyConfig: {
+        autoLoad: false,
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await CmsSiteDomainApi.getPage({
+              pageIndex: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+              ...searchInfo,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+
+const [Modal, { setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  closeOnClickModal: false,
+  fullscreen: true,
+  footer: false,
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+
+      modelRef.value = { ...data.baseData };
+      searchInfo.siteId = data.baseData.id;
+      nextTick(() => {
+        reload();
+      });
+
+      setState({ loading: false });
+    }
+  },
+});
+
+const getTitle = computed(() => `域名管理[${unref(modelRef).title}]`);
+</script>
+
+<template>
+  <Modal class="w-[1000px]" :title="getTitle">
+    <FormEditModal @success="handelSuccess" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'site:domain'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增域名
+        </Button>
+      </template>
+    </Grid>
+  </Modal>
+</template>

+ 74 - 0
apps/web-baicai/src/views/cms/site/info/components/edit.vue

@@ -0,0 +1,74 @@
+<script lang="ts" setup>
+import { computed, ref, unref } from 'vue';
+
+import { alert, useVbenModal } from '@vben/common-ui';
+
+import { useFormOptions, useVbenForm } from '#/adapter';
+import { CmsSiteApi } from '#/api';
+
+import { useSchema } from '../data.config';
+
+defineOptions({
+  name: 'SiteEdit',
+});
+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)
+        ? CmsSiteApi.editDetail(postParams as CmsSiteApi.RecordItem)
+        : CmsSiteApi.addDetail(postParams as CmsSiteApi.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 CmsSiteApi.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>

+ 200 - 0
apps/web-baicai/src/views/cms/site/info/data.config.ts

@@ -0,0 +1,200 @@
+import type {
+  OnActionClickFn,
+  VbenFormSchema,
+  VxeTableGridOptions,
+} from '#/adapter';
+
+import { CmsSiteApi } from '#/api';
+
+export const useSearchSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: '标识',
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '标题',
+    },
+  ];
+};
+
+export function useColumns(
+  onActionClick?: OnActionClickFn<CmsSiteApi.RecordItem>,
+): VxeTableGridOptions<CmsSiteApi.RecordItem>['columns'] {
+  return [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'code',
+      title: '标识',
+      width: 150,
+    },
+    { align: 'left', field: 'title', title: '标题' },
+    {
+      field: 'status',
+      title: '状态',
+      width: 82,
+      cellRender: { name: 'CellTag' },
+    },
+    {
+      align: 'right',
+      cellRender: {
+        attrs: {
+          nameField: 'title',
+          nameTitle: '名称',
+          onClick: onActionClick,
+        },
+        name: 'CellAction',
+        options: [
+          {
+            code: 'domain',
+            label: '域名',
+            auth: ['site:domain'],
+          },
+          {
+            code: 'edit',
+            auth: ['site:edit'],
+          },
+          {
+            code: 'delete',
+            auth: ['site:delete'],
+          },
+          {
+            code: 'setStatus',
+            label: (row: CmsSiteApi.RecordItem) => {
+              return row.status === 1 ? '禁用' : '启用';
+            },
+            auth: ['site:setStatus'],
+          },
+        ],
+      },
+      field: 'operation',
+      fixed: 'right',
+      headerAlign: 'center',
+      showOverflow: false,
+      title: '操作',
+      width: 100,
+    },
+  ];
+}
+
+export const useSchema = (): VbenFormSchema[] => {
+  return [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '英文字母',
+      },
+      fieldName: 'code',
+      label: '站点标识',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入模板目录名',
+      },
+      fieldName: 'path',
+      label: '模板目录名',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入排序',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Divider',
+      fieldName: 'divider1',
+      formItemClass: 'col-span-2 md:col-span-2 pb-0',
+      hideLabel: true,
+      renderComponentContent() {
+        return {
+          default: () => '网站信息',
+        };
+      },
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'title',
+      label: '网站标题',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'company',
+      label: '单位名称',
+    },
+    {
+      component: 'BcUpload',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'logoId',
+      label: 'Logo',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'tel',
+      label: '联系电话',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'fax',
+      label: '传真号码',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'email',
+      label: '电子邮箱',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'address',
+      label: '单位地址',
+      formItemClass: 'col-span-2 md:col-span-2',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'crod',
+      label: '备案号',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'copyright',
+      label: '版权信息',
+      formItemClass: 'col-span-2 md:col-span-2',
+    },
+  ];
+};

+ 121 - 0
apps/web-baicai/src/views/cms/site/info/index.vue

@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
+
+import { alert, Page, useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
+import { CmsSiteApi } from '#/api';
+
+import FormDomain from '../domain/index.vue';
+import FormEdit from './components/edit.vue';
+import { useColumns, useSearchSchema } from './data.config';
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const [FormDomainModal, formDomainApi] = useVbenModal({
+  connectedComponent: FormDomain,
+});
+
+const handelSuccess = () => {
+  reload();
+};
+const handleDelete = async (id: number) => {
+  await CmsSiteApi.deleteDetail(id);
+  alert('数据删除成功');
+  handelSuccess();
+};
+
+const handleUpdateStatus = async (record: any) => {
+  await CmsSiteApi.updateStatus({
+    id: record.id,
+    status: record.status === 1 ? 2 : 1,
+  });
+  handelSuccess();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id },
+    })
+    .open();
+};
+
+const handleDomain = (record: any) => {
+  formDomainApi
+    .setData({
+      baseData: { ...record },
+    })
+    .open();
+};
+
+const handleActionClick = async ({
+  code,
+  row,
+}: OnActionClickParams<CmsSiteApi.RecordItem>) => {
+  switch (code) {
+    case 'delete': {
+      await handleDelete(row.id);
+      break;
+    }
+    case 'domain': {
+      await handleDomain(row);
+      break;
+    }
+    case 'edit': {
+      handleEdit(row, true);
+      break;
+    }
+    case 'setStatus': {
+      await handleUpdateStatus(row);
+      break;
+    }
+  }
+};
+
+const [Grid, { reload }] = useVbenVxeGrid(
+  useTableGridOptions({
+    formOptions: {
+      schema: useSearchSchema(),
+    },
+    gridOptions: {
+      columns: useColumns(handleActionClick),
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await CmsSiteApi.getPage({
+              pageIndex: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+    } as VxeTableGridOptions,
+  }),
+);
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal @success="handelSuccess" />
+    <FormDomainModal />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'site:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增站点
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 1 - 1
apps/web-baicai/src/views/plugins/config/data.config.ts

@@ -113,7 +113,7 @@ export const useEmailSchema = (): VbenFormSchema[] => {
       componentProps: {
         placeholder: 'SMTP服务器端口,一般是25',
       },
-      fieldName: 'host',
+      fieldName: 'port',
       label: 'SMTP端口',
       rules: 'required',
     },

+ 7 - 3
apps/web-baicai/src/views/plugins/config/index.vue

@@ -15,7 +15,9 @@ const [FormSms, formSmsApi] = useVbenForm(
     showDefaultActions: true,
     schema: useSmsSchema(),
     handleSubmit: async (values: Record<string, any>) => {
-      await PluginsConfigApi.editSmsDetail(values);
+      await PluginsConfigApi.editSmsDetail(
+        values as PluginsConfigApi.SmsConfig,
+      );
       alert('保存成功');
     },
   }),
@@ -26,7 +28,9 @@ const [FormEmail, formEmailApi] = useVbenForm(
     showDefaultActions: true,
     schema: useEmailSchema(),
     handleSubmit: async (values: Record<string, any>) => {
-      await PluginsConfigApi.editEmailDetail(values);
+      await PluginsConfigApi.editEmailDetail(
+        values as PluginsConfigApi.EmailConfig,
+      );
       alert('保存成功');
     },
   }),
@@ -46,7 +50,7 @@ onMounted(async () => {
 
 <template>
   <Page auto-content-height>
-    <Tabs v-model:active-key="state.activeKey" type="card">
+    <Tabs v-model:active-key="state.activeKey">
       <Tabs.TabPane key="1" tab="短信平台">
         <FormSms />
       </Tabs.TabPane>

+ 1 - 1
apps/web-baicai/src/views/plugins/template/index.vue

@@ -36,7 +36,7 @@ const handleEdit = (record: Record<string, any>, isUpdate: boolean) => {
 const handleActionClick = async ({
   code,
   row,
-}: OnActionClickParams<SmsApi.RecordTemplateItem>) => {
+}: OnActionClickParams<PluginsTemplateApi.RecordItem>) => {
   switch (code) {
     case 'delete': {
       await handleDelete(row.id);