教程:构建你的第一个后台页面
本教程将带你从一个空项目开始,构建一个包含菜单、仪表盘、数据表格、带校验的表单、删除确认以及语言/主题切换的后台页面。示例使用 Vue 3 + Vite;每一步末尾都附有 React 和 Angular 的说明。
要求:Node 20+,以及以下之一:Vue ≥ 3.5.2、React ≥ 18、Angular ≥ 20。
1. 创建项目并安装 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 和 @thl-ui/charts 会自动安装。你不需要 Bootstrap、jQuery 或 Google Fonts:CSS 和 Open Sans 字体都已包含在 @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. 注册插件和 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: 'zh', // 默认语言:en · ja · vi · zh
theme: 'default', // 12 套皮肤之一
colorMode: 'auto', // light · dark · auto(跟随操作系统)
persist: true, // 将用户的选择保存到 localStorage
})
.mount('#app');在 index.html 中为 <body> 添加 thl-body 类,以获得基础字体、背景色和标题字号:
<body class="thl-body">React / Angular
React: 用 <ThlProvider config={{ locale: 'zh', persist: true }}> 包裹应用。 Angular: bootstrapApplication(App, { providers: [provideZonelessChangeDetection(), provideThlUI({ locale: 'zh' })] }),并将 node_modules/@thl-ui/styles/dist/thl-ui.css 添加到 angular.json 的 styles 中。
3. 页面外壳:菜单、顶栏、侧边栏
菜单是一个 MenuItem 数组。label 可以是 i18n key(自动翻译)或普通文本。
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: '用户', icon: 'users', href: '/users' },
{ key: 'settings', label: '设置', icon: 'settings', children: [
{ key: 'profile', label: '个人资料', 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: '管理员', avatar: '/me.jpg' };
const Nodes = (p: { nodes: unknown }) => p.nodes;
</script>
<template>
<ThlAppLayout :menu="menu" :current-path="route.path" :user="user" brand="My Admin">
<!-- 让菜单使用 RouterLink 代替 <a href>(不会整页刷新) -->
<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>开箱即得:带搜索、语言和浅色/暗色切换以及用户菜单的顶栏;高亮当前项的多级侧边栏;以及一个设置面板(⚙ 按钮),用户可在其中修改皮肤、布局、颜色模式和语言。宽度低于 1170px 时侧边栏会自动收起;低于 768px 时变为滑出式菜单。
可用插槽:brand、topbar-left、topbar-right、link、sidebar-footer、footer。
4. 仪表盘页面
用 ThlPage(标题 + 面包屑)包裹每个页面。布局使用 12 列的 thl-row / thl-col-* 栅格:低于 768px 时所有列都会堆叠成单列。
src/pages/Dashboard.vue:
<script setup lang="ts">
import { ThlPage, ThlInfoBox, ThlCard, ThlLineChart, ThlLabel } from '@thl-ui/vue';
const series = [{ name: '营收', data: [12, 19, 14, 25, 22, 30] }];
const labels = ['1月', '2月', '3月', '4月', '5月', '6月'];
</script>
<template>
<ThlPage title="概览" :breadcrumb="[{ label: '首页', href: '/' }, { label: '概览' }]">
<div class="thl-row">
<div class="thl-col-12 thl-col-sm-6 thl-col-lg-3">
<ThlInfoBox icon="shopping-cart" value="154" title="订单">
<template #footer>目标 <ThlLabel color="success" rounded>300</ThlLabel></template>
</ThlInfoBox>
</div>
<!-- …另外 3 个统计框… -->
</div>
<div class="thl-row">
<div class="thl-col-12 thl-col-lg-8">
<ThlCard title="近 6 个月营收"><ThlLineChart :series="series" :labels="labels" smooth area /></ThlCard>
</div>
</div>
</ThlPage>
</template>栅格类名:thl-col-{1..12}(所有尺寸)、thl-col-sm-*(≥ 768px)、thl-col-md-*(≥ 1024px)、thl-col-lg-*(≥ 1170px)。
5. 带搜索、排序和分页的数据表格
<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: '姓名', sortable: true },
{ key: 'email', title: '邮箱' },
{ key: 'role', title: '角色', sortable: true },
{ key: 'actions', title: '', searchable: false },
];
async function remove(row: { id: number; name: string }) {
if (!(await confirm({ title: `删除 ${row.name}?`, icon: 'warning', confirmVariant: 'danger' }))) return;
users.value = users.value.filter((u) => u.id !== row.id);
toast.success('已删除');
}
</script>
<template>
<ThlPage title="用户">
<ThlCard>
<ThlDataTable :data="users" :columns="columns" row-key="id" selectable :page-size="10">
<!-- 使用 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="`删除 ${row.name}`" @click="remove(row)" />
</template>
</ThlDataTable>
</ThlCard>
</ThlPage>
</template>大型服务端数据集:添加 manual 和 :total="totalCount",然后在 @query-change="({ page, pageSize, sort, search }) => …" 事件中加载数据。
6. 带校验的表单
错误信息会被翻译成当前语言。错误只在首次提交后出现,之后会在用户编辑时立即更新。
<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: '管理员', value: 'admin' }, { label: '用户', value: 'user' }];
const save = (ok: boolean) => ok && toast.success('已保存');
</script>
<template>
<ThlForm :values="v" @submit="save">
<ThlField label="姓名" 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="邮箱" 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="角色" name="role" :value="v.role" :rules="[rules.required()]" required>
<ThlSelect v-model="v.role" :options="roles" />
</ThlField>
<ThlFormActions>
<ThlButton type="submit" variant="primary">保存</ThlButton>
</ThlFormActions>
</ThlForm>
</template>内置规则:required、email、minLength(n)、maxLength(n)、min(n)、max(n)、number、pattern(re)、match('fieldName')。自定义规则:一个返回 null(有效)或 { key: 'i18n.key', params } 的函数。
7. Toast 与确认对话框
import { useToast, confirm } from '@thl-ui/vue';
const toast = useToast();
toast.success('已保存');
toast.error('无法连接', { title: '错误', duration: 0 }); // 0 = 不自动关闭
if (await confirm({ title: '确定退出登录?', icon: 'question' })) logout();AppLayout 已经挂载了 toast 和确认对话框的宿主组件。如果你不使用 AppLayout,请在应用根部放置一次 <ThlToaster /> 和 <ThlConfirmHost />。
8. 语言、主题、颜色模式
用户可以在 ⚙ 面板中修改这些设置。通过代码修改:
import { useThlConfig, useI18n } from '@thl-ui/vue';
const { set } = useThlConfig();
const { t, setLocale } = useI18n();
set({ theme: 'purple-dark', colorMode: 'dark', layout: 'horizontal' });
setLocale('zh');
t('common.save'); // "保存"在配置时添加你自己的翻译文案:
app.use(ThlUI, {
locale: 'zh',
messages: {
vi: { app: { welcome: 'Xin chào, {name}!' } },
en: { app: { welcome: 'Hello, {name}!' } },
},
});
// 在模板中:{{ $t('app.welcome', { name: user.name }) }}通过覆盖 CSS 变量修改品牌色,例如:
:root { --thl-theme: #7c3aed; --thl-on-theme: #fff; --thl-theme-text: #6d28d9; }所有变量以及如何保持足够的对比度,请参见 主题与暗色模式。
9. 登录页与错误页
没有侧边栏的页面使用各自的布局:
<ThlAuthLayout title="登录">
<ThlForm :values="v" @submit="login">…</ThlForm>
</ThlAuthLayout>
<ThlErrorLayout :code="404" /> <!-- 标题和描述会自动翻译 -->在路由中标记这些路由,使 App.vue 不会用 ThlAppLayout 包裹它们(参见 apps/playground-vue 中的 meta.blank)。
10. 上线生产环境
npm run build:Vite 会自动打包 CSS 和字体(只加载需要的字体子集)。- 只导入你用到的组件(
import { ThlButton } from '@thl-ui/vue'),并安装不带全局注册的插件(app.use(ThlUI)→import { ThlUI } from '@thl-ui/vue'),以减小打包体积。 - 支持 SSR(Nuxt、Next、Angular SSR):组件在服务端渲染时不会访问
window。