Hướng dẫn: dựng trang admin đầu tiên
Bài này đi từ một dự án trống tới một trang admin có menu, dashboard, bảng dữ liệu, form có kiểm tra, xác nhận xoá và đổi ngôn ngữ/giao diện. Ví dụ dùng Vue 3 + Vite; cuối mỗi bước có ghi chú cho React và Angular.
Yêu cầu: Node 20+, và một trong các framework: Vue ≥ 3.5.2, React ≥ 18, Angular ≥ 20.
1. Tạo dự án và cài thl-ui
npm create vite@latest my-admin -- --template vue-ts
cd my-admin
npm i @thl-ui/vue @thl-ui/styles vue-router@thl-ui/core, @thl-ui/locales, @thl-ui/tokens, @thl-ui/icons và @thl-ui/charts được cài tự động. Không cần Bootstrap, jQuery hay font từ Google: CSS và font Open Sans đều nằm trong @thl-ui/styles.
React / Angular
npm i @thl-ui/react @thl-ui/styles react-router # React
npm i @thl-ui/angular @thl-ui/styles # Angular2. Khai báo plugin và CSS
src/main.ts:
import { createApp } from 'vue';
import ThlUI from '@thl-ui/vue';
import '@thl-ui/styles/thl-ui.css';
import App from './App.vue';
import { router } from './router';
createApp(App)
.use(router)
.use(ThlUI, {
locale: 'vi', // ngôn ngữ mặc định: en · ja · vi · zh
theme: 'default', // 1 trong 12 skin
colorMode: 'auto', // light · dark · auto (theo hệ điều hành)
persist: true, // nhớ lựa chọn của người dùng trong localStorage
})
.mount('#app');Thêm class thl-body vào <body> trong index.html để có font, màu nền và cỡ tiêu đề cơ bản:
<body class="thl-body">React / Angular
React: bọc app trong <ThlProvider config={{ locale: 'vi', persist: true }}>. Angular: bootstrapApplication(App, { providers: [provideZonelessChangeDetection(), provideThlUI({ locale: 'vi' })] }) và thêm node_modules/@thl-ui/styles/dist/thl-ui.css vào styles trong angular.json.
3. Khung trang: menu, topbar, sidebar
Menu là một mảng MenuItem. label có thể là khoá i18n (tự dịch) hoặc chữ thường.
src/menu.ts:
import type { MenuItem } from '@thl-ui/core';
export const menu: MenuItem[] = [
{ key: 'dash', label: 'menu.dashboard', icon: 'layout-dashboard', href: '/' },
{ key: 'users', label: 'Người dùng', icon: 'users', href: '/users' },
{ key: 'settings', label: 'Cài đặt', icon: 'settings', children: [
{ key: 'profile', label: 'Hồ sơ', href: '/settings/profile' },
] },
];src/App.vue:
<script setup lang="ts">
import { useRoute, RouterLink, RouterView } from 'vue-router';
import { ThlAppLayout } from '@thl-ui/vue';
import { menu } from './menu';
const route = useRoute();
const user = { name: 'An Nguyễn', role: 'Quản trị viên', avatar: '/me.jpg' };
const Nodes = (p: { nodes: unknown }) => p.nodes;
</script>
<template>
<ThlAppLayout :menu="menu" :current-path="route.path" :user="user" brand="My Admin">
<!-- để menu dùng RouterLink thay vì <a href> (không tải lại trang) -->
<template #link="{ item, content, class: cls, current }">
<RouterLink :to="item.href" :class="cls" :aria-current="current ? 'page' : undefined"><Nodes :nodes="content" /></RouterLink>
</template>
<RouterView />
</ThlAppLayout>
</template>Bạn có ngay: topbar có tìm kiếm, nút đổi ngôn ngữ và sáng/tối, menu người dùng; sidebar nhiều cấp tự đánh dấu mục đang mở; panel cài đặt (nút ⚙) để người dùng đổi skin, layout, chế độ màu, ngôn ngữ. Trên màn hình < 1170px sidebar tự thu nhỏ, < 768px thành menu trượt.
Các slot có sẵn: brand, topbar-left, topbar-right, link, sidebar-footer, footer.
4. Một trang dashboard
Mỗi trang bọc trong ThlPage (tiêu đề + breadcrumb). Bố cục dùng lưới 12 cột thl-row / thl-col-*: dưới 768px mọi cột tự xếp thành một cột.
src/pages/Dashboard.vue:
<script setup lang="ts">
import { ThlPage, ThlInfoBox, ThlCard, ThlLineChart, ThlLabel } from '@thl-ui/vue';
const series = [{ name: 'Doanh thu', data: [12, 19, 14, 25, 22, 30] }];
const labels = ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'];
</script>
<template>
<ThlPage title="Tổng quan" :breadcrumb="[{ label: 'Trang chủ', href: '/' }, { label: 'Tổng quan' }]">
<div class="thl-row">
<div class="thl-col-12 thl-col-sm-6 thl-col-lg-3">
<ThlInfoBox icon="shopping-cart" value="154" title="Đơn hàng">
<template #footer>Mục tiêu <ThlLabel color="success" rounded>300</ThlLabel></template>
</ThlInfoBox>
</div>
<!-- …3 ô nữa… -->
</div>
<div class="thl-row">
<div class="thl-col-12 thl-col-lg-8">
<ThlCard title="Doanh thu 6 tháng"><ThlLineChart :series="series" :labels="labels" smooth area /></ThlCard>
</div>
</div>
</ThlPage>
</template>Lớp lưới: thl-col-{1..12} (mọi kích thước), thl-col-sm-* (≥ 768px), thl-col-md-* (≥ 1024px), thl-col-lg-* (≥ 1170px).
5. Bảng dữ liệu có tìm kiếm, sắp xếp, phân trang
<script setup lang="ts">
import { ref } from 'vue';
import { ThlPage, ThlCard, ThlDataTable, ThlButton, ThlLabel, confirm, useToast } from '@thl-ui/vue';
const toast = useToast();
const users = ref([
{ id: 1, name: 'An', email: '[email protected]', role: 'admin' },
{ id: 2, name: 'Bình', email: '[email protected]', role: 'user' },
]);
const columns = [
{ key: 'name', title: 'Tên', sortable: true },
{ key: 'email', title: 'Email' },
{ key: 'role', title: 'Quyền', sortable: true },
{ key: 'actions', title: '', searchable: false },
];
async function remove(row: { id: number; name: string }) {
if (!(await confirm({ title: `Xoá ${row.name}?`, icon: 'warning', confirmVariant: 'danger' }))) return;
users.value = users.value.filter((u) => u.id !== row.id);
toast.success('Đã xoá');
}
</script>
<template>
<ThlPage title="Người dùng">
<ThlCard>
<ThlDataTable :data="users" :columns="columns" row-key="id" selectable :page-size="10">
<!-- tuỳ biến ô bằng slot cell-{key} -->
<template #cell-role="{ value }">
<ThlLabel :color="value === 'admin' ? 'danger' : 'info'" rounded>{{ value }}</ThlLabel>
</template>
<template #cell-actions="{ row }">
<ThlButton size="sm" variant="danger" outline icon="trash-2" :aria-label="`Xoá ${row.name}`" @click="remove(row)" />
</template>
</ThlDataTable>
</ThlCard>
</ThlPage>
</template>Dữ liệu lớn từ server: thêm manual và :total="tongSoBanGhi", rồi tải dữ liệu trong sự kiện @query-change="({ page, pageSize, sort, search }) => …".
6. Form có kiểm tra dữ liệu
Thông báo lỗi tự dịch theo ngôn ngữ hiện tại. Lỗi chỉ hiện sau lần bấm Submit đầu tiên, rồi cập nhật ngay khi sửa.
<script setup lang="ts">
import { reactive } from 'vue';
import { ThlForm, ThlField, ThlInput, ThlSelect, ThlButton, ThlFormActions, rules, useToast } from '@thl-ui/vue';
const toast = useToast();
const v = reactive({ name: '', email: '', role: null as string | null });
const roles = [{ label: 'Quản trị', value: 'admin' }, { label: 'Người dùng', value: 'user' }];
const save = (ok: boolean) => ok && toast.success('Đã lưu');
</script>
<template>
<ThlForm :values="v" @submit="save">
<ThlField label="Họ tên" name="name" :value="v.name" :rules="[rules.required(), rules.minLength(2)]" required v-slot="{ id }">
<ThlInput :id="id" v-model="v.name" />
</ThlField>
<ThlField label="Email" name="email" :value="v.email" :rules="[rules.required(), rules.email()]" required v-slot="{ id }">
<ThlInput :id="id" v-model="v.email" type="email" />
</ThlField>
<ThlField label="Quyền" name="role" :value="v.role" :rules="[rules.required()]" required>
<ThlSelect v-model="v.role" :options="roles" />
</ThlField>
<ThlFormActions>
<ThlButton type="submit" variant="primary">Lưu</ThlButton>
</ThlFormActions>
</ThlForm>
</template>Các luật có sẵn: required, email, minLength(n), maxLength(n), min(n), max(n), number, pattern(re), match('tenTruong'). Tự viết luật: hàm trả về null (hợp lệ) hoặc { key: 'khoa.i18n', params }.
7. Thông báo và hộp xác nhận
import { useToast, confirm } from '@thl-ui/vue';
const toast = useToast();
toast.success('Đã lưu');
toast.error('Không kết nối được', { title: 'Lỗi', duration: 0 }); // 0 = không tự đóng
if (await confirm({ title: 'Đăng xuất?', icon: 'question' })) logout();AppLayout đã gắn sẵn chỗ hiển thị toast và hộp xác nhận. Nếu không dùng AppLayout, hãy đặt <ThlToaster /> và <ThlConfirmHost /> một lần ở gốc app.
8. Ngôn ngữ, giao diện, chế độ màu
Người dùng tự đổi trong panel ⚙. Từ code:
import { useThlConfig, useI18n } from '@thl-ui/vue';
const { set } = useThlConfig();
const { t, setLocale } = useI18n();
set({ theme: 'purple-dark', colorMode: 'dark', layout: 'horizontal' });
setLocale('en');
t('common.save'); // "Save"Thêm chuỗi dịch của riêng bạn khi khởi tạo:
app.use(ThlUI, {
locale: 'vi',
messages: {
vi: { app: { welcome: 'Xin chào, {name}!' } },
en: { app: { welcome: 'Hello, {name}!' } },
},
});
// trong template: {{ $t('app.welcome', { name: user.name }) }}Đổi màu thương hiệu: ghi đè biến CSS, ví dụ
:root { --thl-theme: #7c3aed; --thl-on-theme: #fff; --thl-theme-text: #6d28d9; }Xem Theme & dark mode để biết mọi biến và cách đảm bảo độ tương phản.
9. Trang đăng nhập và trang lỗi
Các trang không có sidebar dùng layout riêng:
<ThlAuthLayout title="Đăng nhập">
<ThlForm :values="v" @submit="login">…</ThlForm>
</ThlAuthLayout>
<ThlErrorLayout :code="404" /> <!-- tiêu đề và mô tả tự dịch -->Trong router, đánh dấu các route này để App.vue không bọc chúng trong ThlAppLayout (xem meta.blank trong apps/playground-vue).
10. Đưa lên production
npm run build: Vite tự đóng gói CSS và font (chỉ các subset font cần thiết được tải).- Chỉ import component bạn dùng (
import { ThlButton } from '@thl-ui/vue') và cài plugin không đăng ký toàn cục (app.use(ThlUI)→import { ThlUI } from '@thl-ui/vue') để bundle nhỏ hơn. - SSR (Nuxt, Next, Angular SSR) được hỗ trợ: component không đụng
windowkhi render trên server.
Tiếp theo
- Danh sách component: demo và bảng API của từng component.
- Cấu hình toàn cục: mọi tuỳ chọn và giá trị mặc định cho từng component.
- Hỏi đáp & xử lý sự cố.