In the background management system project, because it is data management, most of the pages are CURD. Such as:

For this kind of page, we can completely design a component, use the way of drag and drop, drag components one by one to the specified area, structural assembly, and then write a rendering component of the assembly data, rendering into a page. As follows:

Problems that need to be dealt with

  • Assembly of data structures
  • Component list selection
  • Component drag and drop handling
  • Component configuration information Configuration
  • Processing of requests
  • Pull-down option data processing
  • Table component design
  • Handle buttons and pop-ups
  • Popover and table data linkage
  • Custom slot

The following content is only a specific design analysis, not a detailed code display, the content is too much to show one by one

Assembly of data structures

Since this is an assembly of components, we first define the data structure of the specific component:

class Component {
    type: string = 'componentName'
    properties: Record<string.any> = {}
    children: Record<string.any= > [] []}Copy the code
  • Type: the name of the component
  • Properties: Properties of the component
  • Children: A child of the current component for nesting

Because of this design, the entire page is one big component and follows the same structure, so our final data structure should look like this:

const pageConfig = {
    type: 'page'.properties: {},
    search: {
        type: 'search'.id: 'xxx'.properties: {},
        children: [{type: 'input'.id: 'xxx'.properties: {}}// ...]},table: {
        type: 'table'
        id: 'xxx'.properties: {},
        children: [{type: 'column'.id: 'xxx'.properties: {}}, {type: 'column'.id: 'xxx'.properties: {},
                children: [{type: 'button'.id: 'xxx'.properties: {}}]}// ...].buttons: [{type: 'button'.id: 'xxx'.properties: {}}// ...]}}Copy the code

For the first layer, due to the limitation of the scene, the search component and the table component are fixed positions, so it is directly determined. If you want to directly drag and drop the location, you can directly add the children field at the top of the data, and then you can drag and drop the sorting position. For the sorting function of internal sibling components, because the Vue framework already provides the transition-group component, you can use it directly. The buttons array under table is a function of the row of buttons above the normal table component for adding and batch operations.

Component list selection

For the data management page, the components that can be used are input, SELECT, date, checkbox, button and other commonly used form components, and we need to repackage search, table and other business components in the configuration page, after sorting out all the components to be used, We need a file to summarize the properties of all components:

// Page structure
class CommonType { title? :stringcode? :stringfilter? :stringreadVariable? :stringwriteVariable? :string
}
export class Common {
  hide = true
  type = 'common'
  properties = new CommonType
  dialogTemplate = []
}

// search
class SearchType extends FormType {
  gutter = 20
  searchBtnText = 'search'
  searchIcon = 'Search'
  resetBtnText = 'reset'
  resetIcon = 'Refresh'round? :boolean
}
export class Search {
  bui = true
  type = 'search'
  properties = new SearchType
  children = []
}
// ...
Copy the code

Component drag and drop handling

For component drag processing, we can directly use H5 draggable. First, each component in the left component list can be dragged. When dragging to the middle display area, we need to obtain the target element of the drop event, and then combine the information of the dragstart event. Determine who the parent of the current drag component is, and then do the data assembly, all of which is done by the DROP event. After the data assembly is complete, update the render area in the middle.

Component configuration information Configuration

The configuration information of each component is actually different. These specific properties, except general information such as prop and ID, need to be determined according to their own situation, but these properties are one-to-one corresponding to the properties of the component. Since each attribute of the component has a different type, some are input boxes, some are drop-down selection, some are switches, etc., we will describe each attribute in detail:

const componentName = [
    {
        label: 'Placeholder prompt text'.value: 'placeholder'.type: 'input'
    },
    {
        label: 'cleanable'.value: 'clearable'.type: 'switch'
    },
    {
        label: 'Tag position'.value: 'labelPosition'.type: 'select'.children: [{label: 'left'.value: 'left'
            },
            {
                label: 'right'.value: 'right'
            },
            {
                label: 'top'.value: 'top'}}]// ...
]
Copy the code

After defining the basic information, we need to deal with two special cases:

  • Processing when a property in a component actually depends on the specific value of another property
  • Components under different parent components apply different properties

In the first case, when a property depends on one or more properties, we can set an array of rules, such as:

[{label: Attributes' 1 '.value: 'type'.type: 'select'.children: [{label: 'url'.value: 'url'
            },
            {
                label: 'other'.value: 'other'}]}, {rules: [{originValue: 'type'.destValue: ['url']}],label: Attributes' 2 '.value: 'prop2'.type: 'input'}]Copy the code

If type is url, we display attribute 2. If type is url, we display attribute 2.

For example, the input component does not require a validation field under the search component, but does require a validation field on the form form, so we can add a field use:

const formItem = [
    {
        use: ['search'.'dialog'].label: 'tags'.value: 'label'.type: 'input'
    }
    // ...
]
Copy the code

The label properties of the formItem component are used in the Search and Dialog components and are not displayed in other parent components.

After the configuration information of all components is configured, we can screen out the operable attributes with the program when focusing on the specific components in the preview area.

// Process the right hand manipulable property
const getShowProperties = computed(() = > {
  const properties = propertyTemplate[activeComponents.value.type]
  if(! properties) {return[]}let props: Record<string.any> = []
  properties.forEach((item: Record<string.any>) = > {
    if (
      (!item.use || item.use.includes(activeParent.value)) && 
      getConditionResult(item.rules)
    ) {
      props.push(item)
    }
  })
  return props
})
// Calculates whether the property is operable
const getConditionResult = (rules: { originValue: string, destValue: string[] }[]) = > {
  if(! rules) {return true
  }
  for (let i = 0; i < rules.length; i++) {
    const item = rules[i]
    if( item.destValue && item.originValue && ! item.destValue.includes(activeComponents.value.properties[item.originValue]) ) {return false}}return true
}
Copy the code

This is done using a loop rendering of the getShowProperties data.

Processing of requests

In fully encapsulated inside pages, most of the action configuration, request to trigger in addition to the initialization, tend to be triggered by clicking the button request, or is the change event of medium components, but the request of page depends on the project request encapsulation, so above the properties of the internal components need to add the requested information. It mainly includes: URL, type and params. When clicking the button to trigger the request, the request information will be obtained from inside the properties. Since the request method depends on the project, request encapsulation will not be done inside the component, and the encapsulated request method will be passed in from outside.

// External common request methods
import HTTP from '@/http'

export const commonRequest = (
  url: string, 
  params: Record<string.any> = {}, 
  type: 'post' | 'get' = 'get'
) = > {
  return HTTP[` $The ${type}`](url, params)
}
Copy the code

When the requested URL and params need to use variables, we can agree on the format of variables, parse and replace them internally, as follows:

/ / property
const properties = {
    api: '/{type}/get-data'.type: 'get'.params: 'id={id}'
}

/** * Parsing method * URL request path to parse * params parameter to parse * parent parent dependent data */
const parseApiInfo = (url: string, params: string, parent: Record<string.any>) = > {
  const depData = {
    / /... GlobalData // globalData
    ..parant
  }
  const newUrl = url.replace(/ \ {(. *?) \}/g.(a: string, b: string) = > {
    return depData[b] || a
  })
  const newParams = params.replace(/ \ {(. *?) \}/g.(a: string, b: string) = > {
    return depData[b] || a
  })
  const obj: Record<string.string> = {}
  newParams.replace(/([a-zA-Z0-9]+?) = (. +?) (&|$)/g.(a: string, b: string, c: string) = > {
    obj[b] = c
    return a
  })

  return {
    url: newUrl,
    params: obj
  }
}
Copy the code

After the URL and params are parsed, commonRequest is used to execute the request, which basically completes the processing of the request.

Pull-down option data processing

The processing of drop-down option data can be roughly divided into two situations:

  • Static data
  • Dynamic data

Static data

Static data is easier to handle because it is immutable, so we can configure it directly in the front end, for example:

const options = {
    optionsId: [{label: 'tags'.value: 'val'
        }
        // ...]}Copy the code

Dynamic data

Dynamic data will be a bit more troublesome, because it needs the cooperation of the back end to provide a fixed interface, so that we can directly get all the drop-down data needed by the whole page at one time, in the format as above.

Table component design

The table component is the main data display component in the page, so the function should be considered more perfect.

Table component related button:

  • The buttons above the table mainly upload, add, batch delete, batch edit, etc. The buttons here rely on the data in the search bar component and the data selected in the multiple boxes of the table
  • The button inside the column component of the table, because it is an inline button, depends on the data selected by the upper button to replace the data of the current row

Column component design:

  • The column component can be classified into three types: Selection, default, and Operate.
    • Selection is a multi-select column used for the first column of a table
    • Default is the default value. No other configuration is required
    • Operate is an operable column. This type of column can contain child components, such as buttons and switches
  • Custom text, divided into two cases:
    • Generic state transition text, such as 0 -> on; 1 – > close
    • The value of the dropdown option, here we need a specific dropdown data ID, is the top dropdown data processing, and then use a script to parse and replace.

Handle buttons and pop-ups

In this kind of page, the button component should be the most used component, such as: popup, table, column, search, etc., all need to be used, and the button in different position, can handle different functions, button functions are mainly divided into the following:

  • Confirmation dialog box
  • Popup window
  • request
  • jump
  • download

Except for pop-ups, other functions can be completed by their own property fields, but pop-ups are a special and very important function. Pop-ups in management systems generally need to be added, edited, or viewed, so the function of the form component needs to be taken into account inside the popover component.

Because the contents of the popover are customized and full of contents, for example, there is a table inside the popover, there are buttons inside the table, and the buttons can open the popover, etc., we need to level the content data of the popover, otherwise the structure will be too deeply nested and difficult to parse.

const pageConfig = {
    type: 'page'.properties: {},
    search: {},
    table: {},
    // Popover data
    dialogTemplates: [{id: 'xxx'.type: 'dialog'.properties: {},
            // Popover internal form component
            children: [{type: 'input'.id: 'xxx'.properties: {}}// ...].// Popover bottom button
            buttons: [{type: 'button'.id: 'xxx'.properties: {}}// ...]}// ...]}Copy the code

By adding the dialogId field to the Button component, we can point to the popover data with the dialogId id in the dialogTemplates array.

The number of popovers on a page cannot be limited, so in the design of popovers, ordinary labels cannot be used to achieve, we need to use the service mode to invoke popovers, for those who do not know the vUE service mode, please see: Use the service mode to invoke VUE components, so we realize the popover function.

Popover and table data linkage

Popup window within the add and edit most of the data will affect the table list, and then there is the inline button pop-up window will default to carry inline data as a popup window in the form of initial data, so we in the popup window operation is completed, to refresh the table data, so we’re going to the page button function within a unified encapsulated, unified management. As follows:

interfaceButtonParams { params? : Record<string.any> callback? :() = > void
}

export const btnClick = (btn: Record<string.any>, data: ButtonParams, pageId: string) = > {
  if(! commonRequest) { commonRequest = globalMap.get('request')}return new Promise((res: (_v? :any) = >void) = > {
    if (btn.type === 'dialog') {  // dialog
      const dialogMap = globalMap.get(pageId).dialogMap
      if (dialogMap) {
        // Call the popoverDpDialogService({ ... dialogMap.get(btn.dialogTemplateId),params: data.params, pageId, callback: data.callback })
      }
      res()
      return
    }
    const row = data.params && data.params._row
    if (data.params) {
      delete data.params._row
    }
    if (btn.type === 'confirm') { // confirm
      ElMessageBox.confirm(btn.message, btn.title, {
        type: 'warning'.draggable: true
      }).then(() = > {
        const { url, params } = parseApiInfo(btn.api, btn.requestParams, row, pageId)
        if(url) { commonRequest(url, { ... data.params, ... params }, btn.requestType).then((ret: any = {}) = > {
            data.callback && data.callback()
            res(ret.result)
          })
        }
      })
    } else if (btn.type === 'link') { // link
      const route = parseApiInfo(btn.url, ' ', row, pageId)
      if (btn.api) {
        const { url, params } = parseApiInfo(btn.api, btn.requestParams, row, pageId)
        if(url) { commonRequest(url, { ... data.params, ... params }, btn.requestType).then((ret: any = {}) = > {
            res(ret.result)
            if (route.url) {
              if (btn.externalLink) {
                // New window jumps
                openNewTab(route.url)
              } else {
                // The current window jumps
                router.push(route.url)
              }
            }
          })
        }
      } else {
        if (route.url) {
          if (btn.externalLink) {
            // New window jumps
            openNewTab(route.url)
          } else {
            // The current window jumps
            router.push(route.url)
          }
        }
        res()
      }
    } else if (btn.type === 'none') { // none
      const { url, params } = parseApiInfo(btn.api, btn.requestParams, row, pageId)
      if(url) { commonRequest(url, { ... data.params, ... params }, btn.requestType).then((ret: any = {}) = > {
          data.callback && data.callback()
          res(ret.result)
        })
      }
    } else if (btn.type === 'download') {
      const { url } = parseApiInfo(btn.api, ' ', row, pageId)
      if (url) {
        window.open(url)
      }
    }
  })
}
Copy the code

For the encapsulation of the above buttons, such as clicking the popover and updating the table, we need to put the method of updating the table into the callback function, and execute the callback function to refresh the table after the popover confirms the success of the interface. Functions that depend on the popover can be implemented through this method.

Custom slot

For some special form functions that cannot be configured, we need to open up two slots for developers to step in and develop manually.

  • The first location is the button location area above the table
  • The second position is the button position area for the column operation column

The last

Background management system can drag components, the general design ideas on this. It is mainly divided into two parts: page configuration and page rendering two components.

Page configuration component: divided into three modules (sub-component list, preview area, property configuration area). It is easy to configure the relationship between components.

Page rendering component: this component is to get the configuration component configuration data for rendering, and the implementation of business logic.

The overall function is not difficult, is more details, need to be in each component, each position to want to be more comprehensive. If you want to do a good job, it is best to get back end support, this component can cover at least 80-90% of the management system scenarios.

Write more rough, have any questions or better ideas, welcome to comment pointed out.