AMS-AI Frontend Development Conventions
Overview
UI Framework: Ant Design v6 + React 18 + TypeScript
Reference implementation: app-web/src/pages/admin/RoleManagementPage.tsx
Standard CRUD page features:
- Separate search input state from query state
- Server-side pagination (
page, size, x-total-count)
- Dialog-based create/edit with relationship selection
Development Workflow
- Define Types →
lib/types.ts or features/*/types.ts
- Add i18n →
i18n/locales/zh-CN.json and en-US.json
- Add API →
features/*/queries.ts (GraphQL) or mutations.ts (REST)
- Build Page →
pages/*/XManagementPage.tsx
- Configure Routes →
Router.tsx
- Browser Verification → Use
frontend-ui-verification skill
Internationalization (i18n)
All user-visible text must use i18n
Language Files
app-web/src/i18n/locales/zh-CN.json
app-web/src/i18n/locales/en-US.json
Naming Conventions
| Pattern | Example | Description |
|---|
pages.{pageName}.title | pages.roleManagement.title | Page title |
pages.{pageName}.columns.{field} | pages.roleManagement.columns.name | Table columns |
pages.{pageName}.form.{field} | pages.roleManagement.form.name | Form labels |
pages.{pageName}.dialog.{action} | pages.roleManagement.dialog.createTitle | Dialog titles |
pages.{pageName}.messages.{type} | pages.roleManagement.messages.createSuccess | Toast messages |
Usage
tsx
1import { useTranslation } from 'react-i18next';
2
3const { t } = useTranslation();
4
5// Page title
6<CardTitle>{t('pages.xManagement.title')}</CardTitle>
7
8// Input placeholder
9<Input placeholder={t('pages.xManagement.searchPlaceholder')} />
10
11// Toast message
12message.success(t('pages.xManagement.messages.createSuccess'));
Predefined Common Keys
common.loading, common.submit, common.cancel, common.confirm
common.save, common.delete, common.edit, common.add, common.search
common.loadFailed, common.retry
API Architecture
Directory Structure
lib/
├── apiClient.ts # Axios REST client
├── graphqlClient.ts # Graffle GraphQL client
├── queryClient.ts # React Query config
├── queryKeys.ts # Query key definitions
├── types.ts # Shared types (UserItem, RoleItem, etc.)
└── utils.ts
features/admin/*/
├── mutations.ts # REST commands + useMutation
├── queries.ts # GraphQL queries
├── types.ts # Feature-specific types
└── components/ # Dialog components
Query (GraphQL)
typescript
1// features/admin/roles/queries.ts
2import { graphqlClient } from '@/lib/graphqlClient';
3import { useQuery } from '@tanstack/react-query';
4import { queryKeys } from '@/lib/queryKeys';
5
6export const ROLES_QUERY = `
7 query Roles($page: Int, $size: Int, $keyword: String) {
8 roles(page: $page, size: $size, keyword: $keyword) {
9 content { id name code description }
10 totalElements
11 }
12 }
13`;
14
15export function useRoles(page: number, size: number, keyword?: string) {
16 return useQuery({
17 queryKey: queryKeys.roles.list(page, size, keyword),
18 queryFn: () => graphqlClient.request(ROLES_QUERY, { page, size, keyword }),
19 });
20}
Mutation (REST)
typescript
1// features/admin/roles/mutations.ts
2import { useMutation, useQueryClient } from '@tanstack/react-query';
3import { apiClient } from '@/lib/apiClient';
4import { queryKeys } from '@/lib/queryKeys';
5
6export function useCreateRole() {
7 const queryClient = useQueryClient();
8 return useMutation({
9 mutationFn: (data: RolePayload) => apiClient.post('/api/system/roles', data),
10 onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.roles.all() }),
11 });
12}
Type Definitions
typescript
1// lib/types.ts
2export type Id = number | string;
3
4export interface PageResponse<T> {
5 content?: T[];
6 items?: T[];
7 totalElements?: number;
8 totalCount?: number;
9}
10
11// features/admin/roles/types.ts
12export interface RoleItem {
13 id: Id;
14 name: string;
15 code: string;
16 description?: string;
17}
18
19export interface RolePayload {
20 name: string;
21 code: string;
22 description?: string;
23}
All forms must use horizontal layout (labels left, inputs right):
tsx
1<Form form={form} layout="horizontal" onFinish={handleSubmit}>
2 <Form.Item label={t('pages.xxx.form.name')} name="name" rules={[{ required: true }]}>
3 <Input />
4 </Form.Item>
5</Form>
tsx
1import { Form, Modal, Input } from 'antd';
2import { useTranslation } from 'react-i18next';
3
4interface Props {
5 open: boolean;
6 mode: 'create' | 'edit';
7 initialValues?: RoleItem;
8 onClose: () => void;
9 onSubmit: (values: RolePayload) => Promise<void>;
10}
11
12export function RoleFormDialog({ open, mode, initialValues, onClose, onSubmit }: Props) {
13 const { t } = useTranslation();
14 const [form] = Form.useForm();
15 const [loading, setLoading] = useState(false);
16
17 useEffect(() => {
18 if (open) {
19 form.resetFields();
20 if (initialValues) form.setFieldsValue(initialValues);
21 }
22 }, [open, initialValues, form]);
23
24 const handleOk = async () => {
25 const values = await form.validateFields();
26 setLoading(true);
27 try {
28 await onSubmit(values);
29 onClose();
30 } finally {
31 setLoading(false);
32 }
33 };
34
35 return (
36 <Modal
37 open={open}
38 title={mode === 'create' ? t('pages.roleManagement.dialog.createTitle') : t('pages.roleManagement.dialog.editTitle')}
39 onOk={handleOk}
40 onCancel={onClose}
41 confirmLoading={loading}
42 >
43 <Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
44 <Form.Item label={t('pages.roleManagement.form.name')} name="name" rules={[{ required: true }]}>
45 <Input />
46 </Form.Item>
47 <Form.Item label={t('pages.roleManagement.form.code')} name="code" rules={[{ required: true }]}>
48 <Input />
49 </Form.Item>
50 </Form>
51 </Modal>
52 );
53}
Required Field Markers
Ant Design automatically shows red asterisk * when rules={[{ required: true }]} is set.
tsx
1// ✅ Correct - Use rules for required
2<Form.Item name="name" rules={[{ required: true, message: t('common.required') }]}>
3 <Input />
4</Form.Item>
5
6// ❌ Wrong - Don't add asterisk manually
7<Form.Item label="Name*">
8 <Input />
9</Form.Item>
Page Component Construction
State Patterns
typescript
1const [items, setItems] = useState<XItem[]>([]);
2const [loading, setLoading] = useState(false);
3const [error, setError] = useState<Error | null>(null);
4
5// Search: Separate input state from query state
6const [searchKeyword, setSearchKeyword] = useState('');
7const [queryKeyword, setQueryKeyword] = useState('');
8
9// Pagination: UI uses 1-based
10const [currentPage, setCurrentPage] = useState(1);
11const [pageSize, setPageSize] = useState(20);
12const [total, setTotal] = useState(0);
13
14// Dialog
15const [dialogOpen, setDialogOpen] = useState(false);
16const [dialogMode, setDialogMode] = useState<'create' | 'edit'>('create');
17const [editingItem, setEditingItem] = useState<XItem | null>(null);
Loading Pattern
typescript
1const loadItems = useCallback(async () => {
2 setLoading(true);
3 setError(null);
4 try {
5 const params = {
6 page: Math.max(currentPage - 1, 0), // Convert to zero-based
7 size: pageSize,
8 keyword: queryKeyword || undefined,
9 };
10 const res = await xApi.getList(params);
11 const list = res.data.content ?? res.data.items ?? [];
12 const totalHeader = res.headers?.['x-total-count'];
13 const totalCount = Number(totalHeader) || res.data.totalElements || list.length;
14
15 setItems(list);
16 setTotal(totalCount);
17 } catch (err) {
18 setError(err instanceof Error ? err : new Error('Load failed'));
19 } finally {
20 setLoading(false);
21 }
22}, [currentPage, pageSize, queryKeyword]);
Table with Actions
tsx
1const columns: ColumnsType<XItem> = [
2 { title: t('pages.xxx.columns.code'), dataIndex: 'code', key: 'code' },
3 { title: t('pages.xxx.columns.name'), dataIndex: 'name', key: 'name' },
4 {
5 title: t('pages.xxx.columns.actions'),
6 key: 'actions',
7 render: (_, record) => (
8 <Space>
9 <Button type="link" onClick={() => handleEdit(record)}>{t('common.edit')}</Button>
10 <Popconfirm title={t('common.deleteConfirm')} onConfirm={() => handleDelete(record.id)}>
11 <Button type="link" danger>{t('common.delete')}</Button>
12 </Popconfirm>
13 </Space>
14 ),
15 },
16];
17
18<Table
19 columns={columns}
20 dataSource={items}
21 rowKey="id"
22 loading={loading}
23 pagination={{
24 current: currentPage,
25 pageSize,
26 total,
27 showSizeChanger: true,
28 showTotal: (total) => t('common.totalItems', { total }),
29 onChange: (page, size) => {
30 setCurrentPage(page);
31 setPageSize(size);
32 },
33 }}
34/>
Common Mistakes
| Mistake | Correction |
|---|
| Hardcoded text in components | Use t('pages.xxx.key') for all user text |
| Adding i18n key to only one language file | Must add to both zh-CN.json and en-US.json |
Using searchKeyword directly when loading | Keep queryKeyword separate from input state |
Sending 1-based page to backend | Use Math.max(currentPage - 1, 0) to convert |
Ignoring x-total-count header | Prefer response header, then fallback to body |
| JS long integer precision issues | Use string or number | string for ID fields |
| Not handling empty page after delete | Go back one page when current page becomes empty |
Verification
bash
1cd app-web && pnpm lint
2cd app-web && pnpm build
After completion, use the frontend-ui-verification skill for browser verification.
Ant Design Reference
Documentation URLs
Key Design Docs
Key Components
| Component | Use Case | Key Props |
|---|
Form | Data entry forms | layout="horizontal", Form.Item, name, rules |
Table | Data display with pagination | columns, dataSource, pagination, rowKey |
Modal | Dialogs | open, onCancel, onOk, title, confirmLoading |
Select | Dropdown selection | options, value, onChange, mode="multiple" |
Input | Text input | placeholder, value, onChange |
Button | Actions | type, loading, disabled, icon |
Card | Content container | title, extra |
Space | Layout spacing | direction, size |
message | Toast notifications | success(), error(), warning() |
Popconfirm | Delete confirmation | title, onConfirm, okText |
Type Utilities (v5+)
tsx
1import type { GetProps, GetRef, GetProp } from 'antd';
2
3type SelectProps = GetProps<typeof Select>;
4type SelectRef = GetRef<typeof Select>;
5type OptionType = GetProp<typeof Select, 'options'>[number];