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:
// 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:
// 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:
<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:
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:
| Property | Type | Description |
|---|---|---|
label | string | Button text |
type | 'primary' | 'error' | ... | Button style type |
auth | string | Permission code (same as v-auth) |
confirm | string | Confirmation message (auto-shows dialog) |
onClick | (row) => void | Promise | Click handler |
show | (row) => boolean | Dynamic visibility |
disabled | (row) => boolean | Dynamic disable state |
5. Build the Form Dialog
Create components/save.vue for create/edit forms:
<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:
<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:
<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