tao-ui-master-page
Tạo Master Page. Cấu trúc: Header→Filters→Table→Pagination, cột Thao tác (Xem/Sửa/Nhân bản/Xóa), table header bg-[#f8f9fa], data-qa bắt buộc. Luôn load kèm tao-ui-giao-dien + filter-phan-trang.
Files20
View on GitHubSKILL.md.claude/skills/_archived-sasuco/tao-ui-master-page/SKILL.md
GitHubMaster Page
Foundation:
tao-ui-giao-dien. Logic:filter-phan-trang. Kiểm tra:check-master-page.cjs.
Cấu Trúc: Header → Filters → Table → Pagination → Dialogs (ngoài CardContent)
Header
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3'>
<div><h1 className='text-xl font-semibold text-gray-900'>Tiêu đề</h1><p className='text-sm text-gray-500'>Mô tả</p></div>
<div className='flex gap-2'>
<Button data-qa='btn_lam_moi' className='btn-secondary' disabled={refreshing} onClick={refetch}>
<RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} /> Làm mới</Button>
<Button data-qa='btn_them_moi' className='btn-primary' onClick={openCreate}><Plus className='h-4 w-4' /> Thêm mới</Button>
</div>
</div>
Filters
<div className='flex flex-wrap gap-3 items-end'>
<Input data-qa='i_tim_kiem' className='flex-1 min-w-[200px]' placeholder='Tìm kiếm...' value={search} onChange={e => setSearch(e.target.value)} />
{/* DatePicker: w-[150px], Select: w-[160-200px] */}
</div>
Table
<div className='overflow-x-auto'>
<Table>
<TableHeader className='bg-[#f8f9fa]'>
<TableRow>
<TableHead className='text-center w-[60px]'>STT</TableHead>
<TableHead>Tên</TableHead>
<TableHead className='text-right'>Số tiền</TableHead>
<TableHead className='text-center w-[140px]'>Thao tác</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? <PageLoader colSpan={4} /> :
items.length === 0 ? <div className='py-12 text-center text-gray-500'>Không có dữ liệu</div> :
items.map((item, i) => (
<TableRow key={item.id} className='hover:bg-gray-50'>
<TableCell className='text-center'>{startIndex + i + 1}</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell className='text-right'>{formatCurrency(item.amount)}</TableCell>
<TableCell>{/* ActionCell */}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
Cột Thao Tác: Xem → Sửa → Nhân bản → Xóa (thứ tự cứng)
QUAN TRỌNG: Không có cột "Thao tác" visible trong table. Action buttons hiện dạng overlay khi hover row, neo vào cột sticky phải
width=0để luôn ở mép phải viewport.
Pattern KetoanApp (DmTable) — header:
{/* Cột neo sticky phải — width=0, không hiển thị */}
<DmTableHead className='sticky right-0 z-30 bg-[#f0f2f6]' style={{ width: 0, minWidth: 0, padding: 0, border: 'none' }} />
Pattern KetoanApp (DmTable) — body row:
{/* Cột neo sticky phải — width=0, action hiện khi hover row */}
<DmTableCell className='sticky right-0 z-20 bg-transparent' style={{ width: 0, minWidth: 0, padding: 0, border: 'none', overflow: 'visible' }}>
<div className='absolute right-0 top-0 bottom-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-150 bg-gradient-to-l from-white via-white/90 to-transparent pl-12 pr-2'>
<DmRowActions actions={getRowActions(item)} />
</div>
</DmTableCell>
Pattern shadcn/ui Table:
<TableHead className='w-0 sticky right-0 z-20 bg-[#f8f9fa]' style={{ padding: 0, border: 'none' }} />
<TableCell className='sticky right-0 z-10 bg-transparent' style={{ width: 0, minWidth: 0, padding: 0, border: 'none', overflow: 'visible' }}>
<div className='absolute right-0 top-0 bottom-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-150 bg-gradient-to-l from-white via-white/90 to-transparent pl-12 pr-2'>
<Button variant='ghost' size='sm' className='icon-primary' ...><Eye className='h-4 w-4'/></Button>
...
</div>
</TableCell>
| Icon | Class | data-qa |
|---|---|---|
Eye |
icon-primary |
btn_xem_{id} |
Pencil |
icon-warning |
btn_sua_{id} |
Copy |
icon-success |
btn_nhan_ban_{id} |
Trash2 |
icon-danger |
btn_xoa_{id} |
<Button variant='ghost' size='sm' className='icon-primary' data-qa={`btn_xem_${id}`} title='Xem' onClick={view}><Eye className='h-4 w-4'/></Button>
<Button variant='ghost' size='sm' className='icon-warning' data-qa={`btn_sua_${id}`} title='Sửa' onClick={edit}><Pencil className='h-4 w-4'/></Button>
<Button variant='ghost' size='sm' className='icon-danger' data-qa={`btn_xoa_${id}`} title='Xóa' onClick={del}><Trash2 className='h-4 w-4'/></Button>
Xóa phải qua
ConfirmDialog, không gọi API trực tiếp.variant='ghost' size='sm'+title.
Pagination & Dialogs
<PagingUtils currentPage={} totalItems={} itemsPerPage={} onPageChange={} onItemsPerPageChange={} pageSizeOptions={[10,20,50,100]} />
{/* Dialogs NGOÀI CardContent */}
<ConfirmDialog open={delOpen} onConfirm={handleDelete} variant='destructive' />
PAGE_ID + PAGE_FEATURES (BẮT BUỘC)
Mọi trang Master Page PHẢI khai báo
PAGE_IDvàPAGE_FEATURESđể sơ đồ dự án và export menu permission hoạt động.
// Metadata cho so do du an
export const PAGE_ID = 'ten-page-id' // phai khop navItem.id trong NavMenu
export const PAGE_FEATURES = [
{ label: 'Làm mới', code: 'btn-refresh' },
{ label: 'Tạo mới...', code: 'btn-create' },
{ label: 'Xem chi tiết', code: 'row-view' },
{ label: 'Sửa', code: 'row-edit' },
{ label: 'Xóa', code: 'row-delete' },
]
// ---
PAGE_IDphải khớp vớinavItem.idtrongNavMenuportal tương ứngPAGE_FEATURESliệt kê TẤT CẢ các nút/thao tác thực tế có trong trangcodeprefix:btn-cho nút toolbar/header,row-cho action trên dòng,batch-cho thao tác hàng loạt- Đặt ở đầu file, ngay trước
export function XxxPage() - Xem chi tiết tại
export-menu-page-permission/SKILL.md
Script
node .claude/skills/tao-ui-master-page/check-master-page.cjs src/modules/.../pages/XxxPage.tsx
node .claude/skills/tao-ui-master-page/check-master-page.cjs "src/modules/CrmApp/**/pages/*Page.tsx"
16 checks → JSON summary.allPassed. Exit 0=pass.