Skip to content

Adding Business Modules

This guide walks you through creating a new business module from scratch, using the "User Management" page as a reference template (views/templates/system/user/).

Step-by-Step Workflow

1. Define API Interfaces

Create API functions in api/system/<module>/index.ts. All functions wrap the unified HTTP utility:

typescript
// src/api/system/department/index.ts
import api from '@/utils/http'

export const queryListApi = (params?: any) =>
  api.get({ url: '/api/system/department/list', params })

export const createApi = (data: any) =>
  api.post({ url: '/api/system/department/create', data, showSuccessMessage: true })

export const updateApi = (data: any) =>
  api.put({ url: '/api/system/department/update', data, showSuccessMessage: true })

export const deleteApi = (id: number | string) =>
  api.del({ url: `/api/system/department/delete/${id}`, showSuccessMessage: true })

TIP

Write operations (create, update, delete) should always set showSuccessMessage: true so the server's success message is displayed automatically.

2. Prepare Mock Data

Place static JSON files in public/api/system/<module>/ for local development:

json
// public/api/system/department/list.json
{
  "code": 0,
  "msg": "success",
  "data": {
    "list": [
      { "id": 1, "name": "Engineering", "status": 1, "sort": 1 },
      { "id": 2, "name": "Marketing", "status": 1, "sort": 2 }
    ],
    "total": 2
  }
}

3. Build the List Page

Create views/<module>/index.vue using the shared Search and Table components:

vue
<script setup lang="ts">
import { ref } from 'vue'
import Search from '@/components/search/index.vue'
import Table from '@/components/table/index.vue'
import type { SearchItem } from '@/components/search/type'
import { queryListApi } from '@/api/system/department'

const tableRef = ref()
const loading = ref(false)
const loadError = ref(false)
const dataSource = ref([])
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })

// Define search fields declaratively
const searchData: SearchItem[] = [
  { label: 'Name', field: 'name', type: 'input' },
  { label: 'Status', field: 'status', type: 'select', dictType: 'common_status' },
]

// Define table columns
const columns = [
  { title: 'Name', key: 'name' },
  { title: 'Status', key: 'status' },
  { title: 'Sort', key: 'sort' },
  { title: 'Actions', key: 'operate', render: (row: any) => rowActions.render(row) },
]

// Fetch data with loading and error states
async function queryList() {
  loading.value = true
  loadError.value = false
  try {
    const res = await queryListApi({
      page: pagination.page,
      pageSize: pagination.pageSize,
    })
    dataSource.value = res.list
    pagination.itemCount = res.total
  } catch {
    loadError.value = true
  } finally {
    loading.value = false
  }
}

function handleSearch() {
  pagination.page = 1
  queryList()
}

queryList()
</script>

4. Configure Row Actions

Use useRowActions for a clean, declarative operation column:

typescript
import { useRowActions } from '@/composables/useRowActions'

const saveRef = ref()

const rowActions = useRowActions({
  inline: [
    {
      label: 'Edit',
      type: 'primary',
      auth: 'update',
      onClick: (row) => saveRef.value?.open(row)
    },
    {
      label: 'Delete',
      type: 'error',
      auth: 'delete',
      confirm: 'Are you sure you want to delete this item? This action cannot be undone.',
      onClick: async (row) => {
        await deleteApi(row.id)
        await queryList()
      }
    },
  ],
  more: [
    {
      label: 'Reset Password',
      auth: 'update',
      onClick: (row) => resetPassword(row)
    },
  ],
})

Action configuration options:

PropertyTypeDescription
labelstringButton text
type'primary' | 'error' | ...Button style type
authstringPermission code (same as v-auth)
confirmstringConfirmation message (auto-shows dialog)
onClick(row) => void | PromiseClick handler
show(row) => booleanDynamic visibility
disabled(row) => booleanDynamic disable state

5. Build the Form Dialog

Create components/save.vue for create/edit forms:

vue
<script setup lang="ts">
const visible = ref(false)
const formData = ref({})
const formRef = ref()
const dictStore = useDictStore()

// Open for create or edit
function open(row?: any) {
  formData.value = row ? { ...row } : {}
  visible.value = true
}

async function handleSubmit() {
  await formRef.value?.validate()
  if (formData.value.id) {
    await updateApi(formData.value)
  } else {
    await createApi(formData.value)
  }
  emit('success')
  visible.value = false
}

defineExpose({ open })
</script>

TIP

Use dictStore.options('common_status') for status dropdowns instead of hardcoding values. This ensures dictionary data is always in sync with the backend.

6. Handle Batch Operations

For multi-select and bulk actions:

vue
<Table
  ref="tableRef"
  :config="{ showSelection: true }"
  @update:checkedRowKeys="handleSelectionChange"
/>

Use useBatchOptions for batch delete and other bulk operations. After the operation completes, call tableRef.value.clearSelection() to reset the selection state.

7. Handle Load Errors

The Table component supports an error state with built-in retry:

vue
<Table
  :error="loadError"
  @search="queryList"
/>

When loadError is true, the table shows a "Load Failed" empty state with a retry button that triggers the @search event.

Complete File Checklist

For each new business module, create these files:

src/
├── api/system/<module>/index.ts       # API interface definitions
├── views/<module>/
│   ├── index.vue                       # List page
│   └── components/
│       └── save.vue                    # Create/Edit form dialog

public/
└── api/system/<module>/               # Mock JSON data
    ├── list.json
    ├── create.json
    ├── update.json
    └── delete.json

Released under the MIT License.