チュートリアル: 最初の管理画面を作る
このチュートリアルでは、空のプロジェクトから、メニュー、ダッシュボード、データテーブル、バリデーション付きフォーム、削除確認、言語 / テーマの切り替えを備えた管理画面までを作成します。例では 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: 'ja', // デフォルトの言語: en · ja · vi · zh
theme: 'default', // 12 種類のスキンのいずれか
colorMode: 'auto', // light · dark · auto(OS に従う)
persist: true, // ユーザーの選択を localStorage に保存
})
.mount('#app');index.html の <body> に thl-body クラスを付けると、ベースのフォント、背景色、見出しサイズが適用されます。
<body class="thl-body">React / Angular
React: アプリを <ThlProvider config={{ locale: 'ja', persist: true }}> でラップします。 Angular: bootstrapApplication(App, { providers: [provideZonelessChangeDetection(), provideThlUI({ locale: 'ja' })] }) とし、angular.json の styles に node_modules/@thl-ui/styles/dist/thl-ui.css を追加します。
3. ページの骨組み: メニュー、トップバー、サイドバー
メニューは MenuItem の配列です。label には i18n キー(自動的に翻訳されます)または通常のテキストを指定できます。
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">
<!-- メニューで <a href> の代わりに RouterLink を使う(ページ全体を再読み込みしない) -->
<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 未満ではすべてのカラムが 1 列に積み重なります。
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. トーストと確認ダイアログ
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 はトーストと確認ダイアログのホストをすでにマウントしています。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('ja');
t('common.save'); // "保存"セットアップ時に独自の翻訳文字列を追加します。
app.use(ThlUI, {
locale: 'ja',
messages: {
ja: { app: { welcome: 'こんにちは、{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 し(
import { ThlButton } from '@thl-ui/vue')、グローバル登録なしでプラグインをインストールします(app.use(ThlUI)→import { ThlUI } from '@thl-ui/vue')。 - SSR(Nuxt、Next、Angular SSR)に対応しています。サーバーでのレンダリング時、コンポーネントは
windowに触れません。
次のステップ
- コンポーネント: すべてのコンポーネントのデモと API 表。
- グローバル設定: すべてのオプションとコンポーネントごとのデフォルト値。
- FAQ とトラブルシューティング。