Skip to content

Page Development ​

Most pages in this repo are list pages. The recommended pattern is the list-page trio: TableToolbar (filters + actions) + a filter form + Table. Reference implementations: pages/system/dict, pages/monitor/alerts.

The List-Page Trio ​

tsx
export default function ItemList() {
  const [list, setList] = useState<Item[]>([])
  const [total, setTotal] = useState(0)
  const [loading, setLoading] = useState(false)
  const [params, setParams] = useState<PageParams>({ page: 1, page_size: 10 })
  const [modalOpen, setModalOpen] = useState(false)
  const [editRecord, setEditRecord] = useState<Item | null>(null)
  const { hasPerm } = usePermission()

  const fetchList = useCallback(async (p: PageParams) => {
    setLoading(true)
    try {
      const data = await getList(p)
      setList(data.list)
      setTotal(data.total)
    } finally {
      setLoading(false)
    }
  }, [])

  useEffect(() => { void fetchList(params) }, [params, fetchList])

  const columns: ColumnsType<Item> = [
    { title: 'Name', dataIndex: 'name' },
    // ...
    { title: 'Status', dataIndex: 'status', render: (v) => <StatusPill value={v} /> },
    { title: 'Actions', render: (_, r) => (
      <Space>
        {hasPerm('xxx:update') && <Button onClick={() => { setEditRecord(r); setModalOpen(true) }}>Edit</Button>}
        {hasPerm('xxx:delete') && <Button danger onClick={() => removeItem(r.id)}>Delete</Button>}
      </Space>
    ) },
  ]

  return (
    <div className="list-page">
      <TableToolbar
        preset="filter"
        formItems={[{ name: 'name', label: 'Name', el: <Input allowClear placeholder="Name" /> }]}
        onSearch={(values) => setParams({ ...params, page: 1, ...values })}
        actions={hasPerm('xxx:create') ? [<Button type="primary" onClick={() => { setEditRecord(null); setModalOpen(true) }}>New</Button>] : []}
      />
      <Table rowKey="id" columns={columns} dataSource={list} loading={loading}
        pagination={{ current: params.page, pageSize: params.page_size, total, showSizeChanger: true }}
        onChange={(pg) => setParams({ ...params, page: pg.current, page_size: pg.pageSize })} />
      <EditModal open={modalOpen} record={editRecord} onClose={() => setModalOpen(false)} onSaved={() => fetchList(params)} />
    </div>
  )
}

State conventions (consistent naming so teammates can pick up any page):

StateMeaning
list / totalTable data & total count
loadingFetching
paramsCurrent filters + pagination ({ page, page_size })
modalOpen / editRecordModal switch & edit target (null = create mode)
submittingForm submitting (prevents double submit)

Data flow: setParams triggers useEffect to refetch; filters reset to page 1 (page: 1).

Pagination Details ​

  • Step back after delete: if the current page becomes empty and it's not page 1, step back one page:
ts
if (list.length === 1 && params.page > 1) {
  setParams({ ...params, page: params.page - 1 })
} else {
  fetchList(params)
}
  • The table's onChange updates only page / page_size, keeping the filters intact.

TableToolbar ​

src/components/common/TableToolbar.tsx standardises the filter + action area:

  • preset: filter-form layout preset (see ToolbarPreset in the component).
  • formItems: filter items (name / label / el).
  • onSearch(values): fires on "Search"; should setParams({ ...params, page: 1, ...values }).
  • actions: right-side action buttons (New/Export…), wrap visibility in hasPerm.
  • Also supports onReset, leading children, etc.

Create / Edit Modal Form ​

tsx
<Modal open={open} title={record ? 'Edit' : 'New'} onOk={submit} confirmLoading={submitting} onCancel={onClose}>
  <Form form={form} layout="vertical" initialValues={record ?? undefined}>
    <Form.Item name="name" label="Name" rules={[{ required: true, message: 'Please enter a name' }]}>
      <Input />
    </Form.Item>
  </Form>
</Modal>
  • On open: create passes record=null, edit passes the record; use useEffect([open, record]) to form.setFieldsValue(record) on open.
  • On submit: call create/update, then message.success + close + refresh; failures are toasted by the request layer, nothing extra to handle.
  • Use either initialValues or setFieldsValue for the edit state — not both.

Shared Components (src/components/) ​

ComponentPurpose
TableToolbarFilter + action area for list pages
StatusPillStatus badge with tones (success/warning/danger/info)
GlassEmptyEmpty-state placeholder
CountUpValueCount-up number animation (dashboard metrics)
ExcelImportModalExcel import modal
GeoMapProvince/city geo distribution map (login logs)
MonitorGaugeCardMonitoring gauge card (CPU/memory/disk)
NotificationBellHeader notification bell
CommandPaletteGlobal command palette (⌘K)
ErrorBoundaryPage error boundary
Bpm* (BpmDynamicForm / BpmTaskActions / BpmInstanceTimeline …)Workflow-specific components

Theme & Style ​

  • Wrap the page root with className="list-page" to inherit the unified layout in list-pages.css.
  • Page styles must use CSS variables from index.css / list-pages.css (auto-adapt to both themes) — don't hardcode colors. See Theme & Styling.

Feedback ​

  • Success toast: import { message } from '@/utils/feedback' → message.success('Saved') (not antd static methods).
  • Confirmations: modal.confirm(...) (also from feedback).

MIT Licensed · Go microservices & business platform