5. Vue3機能開発

1. 機能開発の基本フロー

フロントエンドでの機能開発は、以下の手順で進めます:

1

APIモジュール作成

src/api/配下にAPIインターフェースモジュールを作成します。

2

定数・列挙定義

必要に応じて定数や列挙をsrc/constants/に定義します。

3

ページコンポーネント作成

src/views/配下にVueコンポーネントを作成します。

4

メニュー設定

バックエンドでメニューを登録し、コンポーネントパスを設定します。

2. APIモジュールの作成

APIモジュールはsrc/api/に領域別に配置します。

// src/api/system/employee-api.js
import { getRequest, postRequest } from '/@/lib/axios';

export const employeeApi = {
  /**
   * 従業員一覧取得
   */
  query: (params) => postRequest('/api/employee/query', params),

  /**
   * 従業員追加
   */
  add: (data) => postRequest('/api/employee/add', data),

  /**
   * 従業員更新
   */
  update: (data) => postRequest('/api/employee/update', data),

  /**
   * 従業員削除
   */
  delete: (employeeId) => postRequest('/api/employee/delete', { employeeId }),

  /**
   * 従業員詳細取得
   */
  getDetail: (employeeId) => getRequest(`/api/employee/detail/${employeeId}`),
};

3. ページコンポーネントの基本構造

Vue 3 Composition API(<script setup>)を使用したページコンポーネントの基本構造です。

<template>
  <div class="employee-page">
    <!-- 検索フォーム -->
    <a-card :bordered="false">
      <a-form layout="inline">
        <a-form-item label="従業員名">
          <a-input v-model:value="queryForm.employeeName" placeholder="従業員名を入力" />
        </a-form-item>
        <a-form-item label="部門">
          <department-select v-model:value="queryForm.departmentId" />
        </a-form-item>
        <a-form-item>
          <a-space>
            <a-button type="primary" @click="queryData">検索</a-button>
            <a-button @click="resetQuery">リセット</a-button>
          </a-space>
        </a-form-item>
      </a-form>
    </a-card>

    <!-- データテーブル -->
    <a-card :bordered="false" style="margin-top: 16px">
      <template #title>
        <a-space>
          <a-button
            v-privilege="'employee:add'"
            type="primary"
            @click="openAddModal"
          >
            追加
          </a-button>
          <a-button @click="queryData">更新</a-button>
        </a-space>
      </template>

      <a-table
        :columns="columns"
        :data-source="dataSource"
        :loading="loading"
        :pagination="pagination"
        @change="handleTableChange"
        row-key="employeeId"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'action'">
            <table-operator
              :row-id="record.employeeId"
              :delete-permission="'employee:delete'"
              @edit="openEditModal"
              @delete="deleteEmployee"
            />
          </template>
        </template>
      </a-table>
    </a-card>

    <!-- 追加・編集モーダル -->
    <employee-modal
      v-model:visible="modalVisible"
      :employee-id="selectedEmployeeId"
      @success="queryData"
    />
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { employeeApi } from '/@/api/system/employee-api';
import DepartmentSelect from '/@/components/system/department-select/index.vue';
import TableOperator from '/@/components/support/table-operator/index.vue';
import EmployeeModal from './components/employee-modal.vue';

// 検索フォーム
const queryForm = reactive({
  employeeName: '',
  departmentId: undefined,
});

// テーブルデータ
const dataSource = ref([]);
const loading = ref(false);

// ページネーション
const pagination = reactive({
  current: 1,
  pageSize: 10,
  total: 0,
  showSizeChanger: true,
  showTotal: (total) => `合計 ${total} 件`,
});

// テーブル列定義
const columns = [
  { title: '従業員ID', dataIndex: 'employeeId', width: 100 },
  { title: '従業員名', dataIndex: 'employeeName' },
  { title: '部門', dataIndex: 'departmentName' },
  { title: '作成日時', dataIndex: 'createTime', width: 180 },
  { title: '操作', dataIndex: 'action', width: 150, fixed: 'right' },
];

// モーダル
const modalVisible = ref(false);
const selectedEmployeeId = ref(null);

/**
 * データ検索
 */
const queryData = async () => {
  loading.value = true;
  try {
    const res = await employeeApi.query({
      ...queryForm,
      pageNum: pagination.current,
      pageSize: pagination.pageSize,
    });
    dataSource.value = res.data.dataList;
    pagination.total = res.data.totalCount;
  } catch (e) {
    message.error('データ取得に失敗しました');
  } finally {
    loading.value = false;
  }
};

/**
 * 検索リセット
 */
const resetQuery = () => {
  queryForm.employeeName = '';
  queryForm.departmentId = undefined;
  pagination.current = 1;
  queryData();
};

/**
 * テーブル変更イベント
 */
const handleTableChange = (pag) => {
  pagination.current = pag.current;
  pagination.pageSize = pag.pageSize;
  queryData();
};

/**
 * 追加モーダルを開く
 */
const openAddModal = () => {
  selectedEmployeeId.value = null;
  modalVisible.value = true;
};

/**
 * 編集モーダルを開く
 */
const openEditModal = (employeeId) => {
  selectedEmployeeId.value = employeeId;
  modalVisible.value = true;
};

/**
 * 従業員削除
 */
const deleteEmployee = (employeeId) => {
  Modal.confirm({
    title: '確認',
    content: 'この従業員を削除してもよろしいですか?',
    onOk: async () => {
      try {
        await employeeApi.delete(employeeId);
        message.success('削除しました');
        queryData();
      } catch (e) {
        message.error('削除に失敗しました');
      }
    },
  });
};

// 初期化
onMounted(() => {
  queryData();
});
</script>

<style lang="less" scoped>
.employee-page {
  padding: 16px;
}
</style>

4. スマートコンポーネントの使用

BaiZe Frameworkには多くの再利用可能なスマートコンポーネントが用意されています。

4.1 部門セレクター

<department-select v-model:value="form.departmentId" />

4.2 従業員セレクター

<employee-select v-model:value="form.employeeId" />

4.3 辞書セレクター

<dict-select v-model:value="form.status" dict-type="employee_status" />

4.4 テーブル操作

<table-operator
  :row-id="record.id"
  :edit-permission="'employee:update'"
  :delete-permission="'employee:delete'"
  @edit="openEditModal"
  @delete="deleteItem"
/>

4.5 ファイルアップロード

<file-upload
  v-model:value="form.fileId"
  :max-count="1"
  list-type="picture-card"
/>

5. 権限の使用

5.1 ディレクティブによる権限制御

<a-button v-privilege="'employee:add'">追加</a-button>

5.2 プログラム的権限チェック

import { useUserStore } from '/@/store/modules/user';

const userStore = useUserStore();

if (userStore.hasPermission('employee:add')) {
  // 権限がある場合の処理
}

6. 定数・列挙の使用

import { FLAG_NUMBER_ENUM, GENDER_ENUM } from '/@/constants/common-const';

// 使用
if (form.status === FLAG_NUMBER_ENUM.YES) {
  // 有効ステータス
}

const genderOptions = Object.keys(GENDER_ENUM).map(key => ({
  label: GENDER_ENUM[key],
  value: key,
}));

7. 開発時の注意事項

7.1 パスエイリアス

  • /@/ は src/ にマッピングされます
  • 一貫性を保つため/@/を使用してインポートします

7.2 コンポーネント命名

  • indexコンポーネントを除き、複数語のコンポーネント名が必要です
  • ルートcomponentNameはメニューmenuIdと一致させる必要があります(keep-alive動作のため)
  • キャッシュ互換のため、ルートガードでコンポーネントが動的にリネームされます

7.3 スタイル

  • Lessプリプロセッサ、カスタム変数は src/theme/custom-variables.js
  • Ant DesignテーマカスタマイズはmodifyVars経由
  • 主色调:#1677ff(設定可能)

7.4 ESLint & Prettier

  • ESLint拡張:plugin:vue/vue3-essential、eslint:recommended
  • Prettier:150文字行幅、シングルクォート、セミコロン、2スペースインデント
  • Vueコンポーネント複数語名称強制(indexは例外)