Skip to content

教程:构建你的第一个后台页面 ​

本教程将带你从一个空项目开始,构建一个包含菜单、仪表盘、数据表格、带校验的表单、删除确认以及语言/主题切换的后台页面。示例使用 Vue 3 + Vite;每一步末尾都附有 React 和 Angular 的说明。

要求:Node 20+,以及以下之一:Vue ≥ 3.5.2、React ≥ 18、Angular ≥ 20。

1. 创建项目并安装 thl-ui ​

sh
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
sh
npm i @thl-ui/react @thl-ui/styles react-router        # React
npm i @thl-ui/angular @thl-ui/styles                    # Angular

2. 注册插件和 CSS ​

src/main.ts:

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 类,以获得基础字体、背景色和标题字号:

html
<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:

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:

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:

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. 带搜索、排序和分页的数据表格 ​

vue
<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. 带校验的表单 ​

错误信息会被翻译成当前语言。错误只在首次提交后出现,之后会在用户编辑时立即更新。

vue
<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 与确认对话框 ​

ts
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. 语言、主题、颜色模式 ​

用户可以在 ⚙ 面板中修改这些设置。通过代码修改:

ts
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'); // "保存"

在配置时添加你自己的翻译文案:

ts
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 变量修改品牌色,例如:

css
:root { --thl-theme: #7c3aed; --thl-on-theme: #fff; --thl-theme-text: #6d28d9; }

所有变量以及如何保持足够的对比度,请参见 主题与暗色模式。

9. 登录页与错误页 ​

没有侧边栏的页面使用各自的布局:

vue
<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。

下一步 ​