代码敲累了吧,来试试一行代码不写,搭一个后台系统吧!

697 阅读7分钟

简介

vue-dynamic-admin 是一个基于【vue vben admin】免费开源的动态后台模版。使用了最新的vue3,vite2,TypeScript等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。

吊炸天效果

点我预览--> guoshao-service.test.upcdn.net/file-path/v…

特性

  • 最新技术栈:使用 Vue3/vite2 等前端前沿技术开发
  • TypeScript: 应用程序级 JavaScript 的语言
  • 主题:可配置的主题
  • 国际化:内置完善的国际化方案
  • Mock 数据 内置 Mock 数据方案
  • 权限 内置完善的动态路由权限生成方案
  • 组件 二次封装了多个常用的组件

项目地址

文档

文档地址

准备

安装使用

  • 获取项目代码
git clone https://github.com/guoshaonb/vue-dynamic-admin.git
  • 安装依赖
cd vue-dynamic-admin

pnpm install

  • 运行
pnpm serve
  • 打包
pnpm build

系统动态配置模块的大概逻辑

流程:通过【前端配置字段->存储服务器->服务器解析->根据配置渲染页面】这个流程来实现的。

图示:

微信截图_20230212164342.png

微信截图_20230212141903.png

动态配置的主要部分代码如下:

<template>
  <div style="width: 100%">
    <div class="config-header">
      <div>
        <span class="config-header-titp">操作按钮</span>
        <a-button type="success" @click="initConfig">创建配置</a-button>
        <a-button type="danger" @click="delConfig">删除配置</a-button>
        <a-button type="primary" @click="updConfig">更新配置</a-button>
      </div>
      <div v-if="configData">
        <span class="config-header-titp">配置分类</span>
        <dynamicTag
          :id="id"
          :pageId="menu_id"
          :configClassifys="configClassifys"
          @saveClassifys="saveClassifys"
        />
      </div>
    </div>
    <a-tabs
      v-if="configData"
      tab-position="left"
      :style="{ height: '800px', overflowY: 'scroll' }"
      v-model:activeKey="activeKey"
    >
      <a-tab-pane
        v-for="i in 50"
        :key="i"
        :tab="`配置-${i}${configData['c_' + i]?.includes('字段') ? '(未用)' : ''}`"
      >
        <h4 class="config-content-header">
          表单配置{{ i }}
          <span style="color: red" v-if="configData['c_' + i]?.includes('字段')">(待配置)</span>
        </h4>
        <commonForm
          :ref="sonRefFormObj[i]"
          :configOption="configData['c_' + i]"
          :configClassifys="configClassifys"
        />
        <dynamicOptions :ref="sonRefOptionObj[i]" :configOption="configData['c_' + i]" />
      </a-tab-pane>
    </a-tabs>
  </div>
</template>
<script lang="ts">
//  系统引入项
import { defineComponent, reactive, toRefs, ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Alert, Card, message } from 'ant-design-vue';
//  组件引入项
import commonForm from './components/commonForm.vue';
import dynamicOptions from './components/dynamicOptions.vue';
import dynamicTag from './components/dynamicTag.vue';
//  api引入项
import {
  getGeneralConfigList,
  addGeneralConfig,
  editGeneralConfig,
  delGeneralConfig,
  getConfigclassifyList,
  addConfigclassify,
  editConfigclassify,
  delConfigclassify,
} from '/@/api/demo/system';
import classObj from './classObj';
import { USER_INFO_KEY } from '/@/enums/cacheEnum';
import { Persistent } from '/@/utils/cache/persistent';
import { isObjectValueEqual } from '/@/utils/common';

let sonRefFormObj = {};
let sonRefOptionObj = {};
let configObj = {}
for (let i = 1; i <= 50; i++) {
  configObj['c_' + i] = `字段${i},field${i},f,0;`
}
export default defineComponent({
  components: {
    commonForm,
    dynamicOptions,
    dynamicTag,
  },
  setup() {
    const route = useRoute();
    const router = useRouter();
    const userId = ref(null);
    const state = reactive({
      id: 0,
      class_id: 0,
      menu_id: 0,
      activeKey: 1,
      configData: null,
      configClassifys: null,
    });

    // 获取动态字段配置
    const getDynamicConfig = async () => {
      // 获取配置数据
      const dynamicResult = await getGeneralConfigList({
        menu_id: route.query.menu_id,
      });
      const dynamicData = dynamicResult?.[0];
      if (dynamicData) {
        state.id = dynamicData.id;
        state.menu_id = dynamicData.menu_id;
        state.configData = dynamicData;
      } else {
        state.configData = null;
        state.configClassifys = null;
      }

      // 获取配置分类
      const classifyResult = await getConfigclassifyList({
        menu_id: route.query.menu_id,
      });
      const classData = classifyResult?.[0];
      if (classData) {
        let configClassifys = [];
        state.class_id = classData.id;
        Object.keys(classData).forEach((item) => {
          if (item.includes('class') && classData[item] !== '0') {
            configClassifys.push(classData[item]);
          }
        });
        state.configClassifys = configClassifys;
      }

      for (let i = 1; i <= 50; i++) {
        sonRefFormObj[i] = ref(null);
        sonRefOptionObj[i] = ref(null);
      }
      userId.value = Persistent.getLocal(USER_INFO_KEY).id;
    };
    getDynamicConfig();

    // 重新加载数据
    const reloadContent = (content) => {
      message.success(content);
      getDynamicConfig();
    };

    /**
     * ------------------------配置按钮事件------------------------
     */
    // 创建配置
    const initConfig = async () => {
      if (!state.configData) {
        const dynamicData = {
          menu_id: route.query.menu_id,
          user_id: userId.value,
          ...configObj,
        };
        const classData = {
          menu_id: route.query.menu_id,
          user_id: userId.value,
          ...classObj,
          class1: '基础配置',
        };
        await addGeneralConfig(dynamicData);
        const result = await addConfigclassify(classData);
        if (result) {
          reloadContent('创建配置成功!');
        }
      } else {
        message.warning('已包含配置了,请先删除改配置再初始化哦!');
      }
    };

    // 更新配置
    const updConfig = () => {
      const configData = {};
      const configNames: any = [];
      for (let i = 1; i <= 50; i++) {
        const formData = sonRefFormObj[i].value?.[0]?.getOptionVal();
        const optionData = sonRefOptionObj[i].value?.[0]?.getOptionVal();
        if (formData && !isObjectValueEqual(state.configData['c_' + i], formData)) {
          if (formData.configName.includes('字段')) {
            return message.error('字段说明不能包含字段2字哦');
          }
          if (configNames.includes(formData.configName)) {
            return message.error('字段说明不能重复哦');
          }
          configNames.push(formData.configName);
          const options: any = [];
          Object.values(optionData).forEach((value) => {
            options.push(value);
          });
          configData['c_' + i] =
            formData.configName +
            ',' +
            formData.configField +
            ',t,' +
            formData.configType +
            ';' +
            options.join(',') +
            '`' +
            formData.selectType;
        }
      }
      const data = {
        ...state.configData,
        ...configData,
      };
      editGeneralConfig({
        id: state.id,
        user_id: userId.value,
        menu_id: state.menu_id,
        ...data,
      }).then(() => {
        reloadContent('更新配置成功!');
      });
    };

    // 删除配置
    const delConfig = () => {
      delGeneralConfig({ id: state.id }).then(() => {
        reloadContent('删除配置成功!');
      });
    };

    // 保存分类
    const saveClassifys = (row) => {
      if (!row) return;
      const classObj = {};
      JSON.parse(row || '')?.forEach((item, index) => {
        classObj['class' + (index + 1)] = item;
      });
      editConfigclassify({
        id: state.class_id,
        user_id: userId.value,
        menu_id: state.menu_id,
        ...classObj,
      }).then(() => {
        reloadContent('配置分类成功!');
      });
    };

    return {
      ...toRefs(state),
      sonRefFormObj,
      sonRefOptionObj,
      initConfig,
      updConfig,
      delConfig,
      saveClassifys,
    };
  },
});
</script>

然后是界面部分

<template>
  <PageWrapper dense contentFullHeight fixedHeight contentClass="flex">
    <BasicTable
      ref="tableRef"
      :title="route.query.name + '列表'"
      :columns="columns"
      :api="getGeneralDataList"
      :canResize="canResize"
      :loading="loading"
      :striped="striped"
      :bordered="border"
      :pagination="{ pageSize: 10 }"
      :formConfig="{
        labelWidth: 120,
        schemas: searchFormSchema,
      }"
      :beforeFetch="
        (row) => {
          row.menu_id = route.query.menu_id;
        }
      "
      :useSearchForm="true"
      :showTableSetting="true"
      :showIndexColumn="false"
      :actionColumn="{
        width: 120,
        title: '操作',
        dataIndex: 'action',
        slots: { customRender: 'action' },
        fixed: undefined,
      }"
      rowKey="id"
      :rowSelection="{ type: 'checkbox' }"
    >
      <template #toolbar>
        <a-button type="danger" @click="handleDeletes"> 批量删除 </a-button>
        <a-button type="primary" @click="handleCreate"> 新增 </a-button>
        <a-button type="primary" @click="deriveExcelData"> 导出 </a-button>
      </template>
      <template #action="{ record }">
        <TableAction
          :actions="[
            {
              icon: 'clarity:note-edit-line',
              onClick: handleEdit.bind(null, record),
            },
            {
              icon: 'ant-design:delete-outlined',
              color: 'error',
              popConfirm: {
                title: '是否确认删除',
                confirm: handleDelete.bind(null, record),
              },
            },
          ]"
        />
      </template>
    </BasicTable>
    <pageModal
      :configObj="configObj"
      :classifys="classifys"
      @register="registerModal"
      @success="handleSuccess"
    />
  </PageWrapper>
</template>
<script lang="ts">
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { createVNode, defineComponent, ref, reactive, toRefs } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { PageWrapper } from '/@/components/Page';
import {
  getGeneralConfigList,
  getGeneralDataList,
  addGeneralData,
  editGeneralData,
  delGeneralData,
  delsGeneralData,
  getConfigclassifyList,
} from '/@/api/demo/system';
import { operationApi } from '/@/utils/event/operation';
import { useModal } from '/@/components/Modal';
import pageModal from './pageModal.vue';
import { columns, searchFormSchema } from './data';
import { exportExcel } from '/@/utils/excel/exportExcel';
import { message, Modal } from 'ant-design-vue';

export default defineComponent({
  components: { TableAction, BasicTable, PageWrapper, pageModal },
  setup() {
    const tableRef = ref<Nullable<TableActionType>>(null);
    const [registerModal, { openModal }] = useModal();
    const route = useRoute();
    const state: any = reactive({
      searchFormSchema: [],
      columns: [],
      configObj: {},
      classifys: '',
    });

    // 获取动态字段配置
    const getDynamicConfig = async () => {
      // 获取配置数据
      const dynamicResult = await getGeneralConfigList({
        menu_id: route.query.menu_id,
      });
      if (dynamicResult) {
        const configObj = {};
        const configNapeData = dynamicResult?.[0] || [];
        Object.keys(configNapeData).forEach((key, index) => {
          if (key.includes('c_')) {
            const configNapeDatakey = configNapeData[key].toString();
            const configNapeDatake = configNapeData[key].toString();
            const typeCommonArray = 
            [ 
              'Input', 
              'InputNumber',
              'InputTextArea',
              'Select',
              'RadioGroup',
              'CheckboxGroup',
              'Switch',
              'DatePicker',
              'RangePicker',
            ];
            const leftOpions = configNapeDatakey?.split(';')?.[0];
            const optionsArray =
              configNapeDatakey?.split('`')?.[0]?.split(';')?.[1]?.split(',') || [];
            configObj['c_' + key?.split('c_')[1]] = {
              name: configNapeDatakey?.split(',')?.[0],
              isExist: leftOpions?.split(',')?.[2],
              field: leftOpions?.split(',')?.[1],
              type: typeCommonArray[leftOpions?.split(',')?.[3] || 0],
              options: [],
              class: configNapeDatakey.split('`')?.[1] || '基础配置',
            };
            optionsArray?.forEach((element) => {
              configObj['c_' + key?.split('c_')[1]]['options'].push({
                label: element,
                value: element,
              });
            });
          }
        });
        // 设置表格、表单
        state.configObj = configObj;
        state.columns = columns(configObj);
        state.searchFormSchema = searchFormSchema(configObj);
      }

      // 获取配置分类
      const classifyResult = await getConfigclassifyList({
        menu_id: route.query.menu_id,
      })
      if(classifyResult) {
        let configClassifys = [];
        const classifyData = classifyResult?.[0] || [];
        Object.keys(classifyData).forEach((item) => {
          if (item.includes('class') && classifyData[item] !== '0') {
            configClassifys.push(classifyData[item]);
          }
        });
        state.classifys = configClassifys;
      }
    };
    getDynamicConfig()

    function handleCreate() {
      openModal(true, {
        isUpdate: false,
      });
    }

    function handleEdit(record: Recordable) {
      openModal(true, {
        record,
        isUpdate: true,
        id: record.id,
      });
    }

    function handleDelete(record: Recordable) {
      operationApi('del', record, {
        delAction: delGeneralData,
      });
      handleSuccess();
    }

    const showDeleteConfirm = (callBack) => {
      Modal.confirm({
        title: '确定批量删除吗?',
        icon: createVNode(ExclamationCircleOutlined),
        content: '',
        okText: '是',
        okType: 'danger',
        cancelText: '否',
        onOk() {
          callBack();
        },
        onCancel() {
          message.warning('您取消了删除');
        },
      });
    };

    function handleDeletes(record: Recordable) {
      const ids = tableRef.value.getRowSelection().selectedRowKeys;
      if (ids.length > 0) {
        showDeleteConfirm(() => {
          const params = {
            ids,
            is_del: 1,
          };
          operationApi('del', params, {
            delAction: delsGeneralData,
          });
          handleSuccess();
        });
      } else {
        message.warning('请选择数据后再删除哦');
      }
    }

    function handleSuccess() {
      setTimeout(() => {
        tableRef.value.reload();
      }, 200);
    }

    // 导出excel
    const deriveExcelData = () => {
      const tableData = tableRef.value.getDataSource();
      exportExcel(state.columns, tableData, route.query.name + '数据');
    };

    return {
      ...toRefs(state),
      tableRef,
      route,
      registerModal,
      handleCreate,
      handleEdit,
      handleDelete,
      handleDeletes,
      handleSuccess,
      deriveExcelData,
      getGeneralDataList,
    };
  },
});
</script>

pageModal.vue代码

<template>
  <BasicModal
    centered
    v-bind="$attrs"
    @register="registerModal"
    :title="getTitle"
    @ok="handleSubmit"
  >
    <BasicForm
      ref="BasicForm"
      :labelWidth="100"
      :schemas="schemas"
      :showActionButtonGroup="false"
      @submit="handleSubmitForm"
    />
  </BasicModal>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, toRefs, computed, unref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import {
  getGeneralConfigList,
  getGeneralDataList,
  addGeneralData,
  editGeneralData,
  delGeneralData,
} from '/@/api/demo/system';
import { columns, formSchema } from './data';
import { getDeptList } from '/@/api/demo/system';
import { operationApi } from '/@/utils/event/operation';
import { message } from 'ant-design-vue';
import { json } from 'stream/consumers';
import { USER_INFO_KEY } from '/@/enums/cacheEnum';
import { Persistent } from '/@/utils/cache/persistent';

export default defineComponent({
  name: 'DeptModal',
  components: { BasicModal, BasicForm },
  emits: ['success', 'register'],
  props: {
    configObj: {
      type: Object,
      default: {},
    },
    classifys: {
      type: Object,
      default: {},
    },
  },
  setup(props, { emit }) {
    const BasicForm = ref(null);
    const isUpdate = ref(true);
    const route = useRoute();
    const state = reactive({
      schemas: [],
    });
    const userId = ref('');
    const pageId = ref('');

    (() => {
      userId.value = Persistent.getLocal(USER_INFO_KEY)?.id;
      setTimeout(() => {
        state.schemas = formSchema(props.configObj, props.classifys);
      }, 1000);
    })();

    const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
      BasicForm.value.resetFields();
      setModalProps({ confirmLoading: false });
      pageId.value = data?.id;
      isUpdate.value = !!data?.isUpdate;
      if (unref(isUpdate)) {
        state.schemas = state.schemas.map((item) => {
          switch (item.component) {
            case 'Switch':
              item.defaultValue = data.record[item.field] === '1';
              break;
            case 'RangePicker':
              item.defaultValue =
                data.record[item.field] != '' ? data.record[item.field].split(',') : '';
              break;
            default:
              item.defaultValue = data.record[item.field];
              break;
          }
          return item;
        });
      } else {
        state.schemas = state.schemas.map((item) => {
          switch (item.component) {
            case 'Switch':
              item.defaultValue = item.componentProps?.options?.[0]?.value === '1';
              break;
            case 'RangePicker':
              item.defaultValue = '';
              break;
            default:
              item.defaultValue = item.componentProps?.options?.[0]?.value;
              break;
          }
          return item;
        });
      }
    });

    const getTitle = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));

    async function handleSubmit() {
      BasicForm.value.submit();
    }

    async function handleSubmitForm(values) {
      try {
        Object.keys(values).forEach((item) => {
          if (values[item] instanceof Array) {
            values[item] = values[item].join(',');
          }
        });

        const params = {
          user_id: userId.value,
          menu_id: route.query.menu_id,
          ...values,
        };

        operationApi('addOredit', params, {
          id: pageId.value,
          addAction: addGeneralData,
          editAction: editGeneralData,
        }).then(() => {
          closeModal();
          emit('success');
        });
      } finally {
      }
    }

    return {
      ...toRefs(state),
      BasicForm,
      registerModal,
      getTitle,
      handleSubmit,
      handleSubmitForm,
    };
  },
});
</script>

data.ts文件代码

import { h } from 'vue';
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { Switch } from 'ant-design-vue';
import { setRoleStatus } from '/@/api/demo/system';
import { useMessage } from '/@/hooks/web/useMessage';

export const columns = function (data?) {
  const startArray = [
    {
      title: 'ID',
      dataIndex: 'id',
      width: 150,
    }
  ]
  const middleArray: any = []
  const afterArray = [{
    title: '创建时间',
    dataIndex: 'createdAt',
    width: 150,
  },
  {
    title: '更新时间',
    dataIndex: 'updatedAt',
    width: 150,
  }]
  for (let i = 1; i <= 50; i++) {
    middleArray.push({
      title: data?.['c_' + i]?.name,
      dataIndex: 'data' + i,
      width: 150,
      isExist: data?.['c_' + i]?.isExist,
    })
  }
  return JSON.stringify(data) === '{}' ? [] :
    [...startArray, ...middleArray, ...afterArray]?.
      filter(item => item?.isExist !== 'f')
}

export const searchFormSchema = function (data?) {
  let options: any = []
  if (!(JSON.stringify(data) === '{}')) {
    for (let i = 1; i <= 50; i++) {
      if (!data?.['c_' + i]?.name?.includes("字段")) {
        options.push({
          label: data?.['c_' + i]?.name,
          value: 'data' + i,
          key: 'data' + i,
        })
      }
    }
  }
  return [
    {
      field: 'selectField',
      label: '选择查询条件',
      component: 'Select',
      colProps: { span: 8 },
      componentProps: {
        options,
      },
    },
    {
      field: 'inputValue',
      label: '输入查询内容',
      component: 'Input',
      colProps: { span: 8 },
    }
  ]
}

export const formSchema = function (data?, classifys?) {
  const configArray: any = []
  for (const item of classifys) {
    if (classifys?.length > 1) {
      configArray.push({
        field: 'divider-basic',
        component: 'Divider',
        label: item,
        colProps: {
          span: 24,
        },
        isExist: 't'
      })
    }
    for (let i = 1; i <= 50; i++) {
      if (data?.['c_' + i]?.class === item) {
        configArray.push({
          field: 'data' + i,
          label: data?.['c_' + i]?.name,
          component: data?.['c_' + i]?.type,
          componentProps: {
            options: data?.['c_' + i]?.options,
          },
          isExist: data?.['c_' + i]?.isExist
        })
      }
    }
  }
  return configArray?.filter(item => item?.isExist !== 'f')
}

提示:调接口获取动态配置后,解析出每一项的type,然后存入配置项columns【保存type为component属性的值】,最后再用component的值渲染对应的控件。渲染界面的时候,重点在这一段【核心】:

for (let i = 1; i <= 50; i++) { // 遍历50个字段
  configArray.push({
      field: 'data' + i, // 字段标识
      label: data?.['c_' + i]?.name, // 表单项label
      component: data?.['c_' + i]?.type, // 要渲染的组件
      componentProps: { 
        options: data?.['c_' + i]?.options, // 组件的options配置项【复选框、下拉列表的数据】
      },
      isExist: data?.['c_' + i]?.isExist // 字段是否已使用
  })
}