Receber por email
← Voltar para o catálogo

vue-skill

Agente para implementação de Vue/TypeScript.

556estrelas
Atualizado há 2 meses

Ver no GitHub ↗ Licença: MIT

Como adicionar

/plugin marketplace add majiayu000/claude-skill-registry

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/vue-skill/svg)](https://www.skillteca.com.br/skills/vue-skill)

Skills relacionadas

Ver todas de Pesquisa e Web →

Alerta por categoria

Receba novas skills de Pesquisa e Web toda segunda

1 email curto, só com as skills novas de Pesquisa e Web. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

あなたはVue 3 + TypeScriptプロジェクトのAIアシスタントです。

単独での実行、他のSubagentからの呼び出し、どちらのケースでも適切に動作し、明確な結果を返します。

Vue/TypeScript実装ガイドライン

このガイドラインは、AI実装で発生しがちな問題パターンとその解決方法をまとめたものです。

プロジェクト概要

このプロジェクト(pedaru-vue)は、React/Next.jsベースのPDFビューワーアプリ「pedaru」をVue3/Nuxtに移植するものです。

目的:

  • Vue3の仕組みを効果的に使った実装
  • 単なる機能移植ではなく、Vue3のベストプラクティスに沿った設計

技術スタック:

  • Vue 3 + Composition API(<script setup>)
  • Nuxt 3
  • TypeScript
  • Pinia(状態管理)

React から Vue3 への変換ガイド

React Hooks → Vue3 Composition API マッピング

| React | Vue 3 | 備考 | |-------|-------|------| | useState | ref / reactive | プリミティブはref、オブジェクトはreactive | | useEffect | watch / watchEffect / onMounted | 依存配列の有無で使い分け | | useCallback | 通常の関数 | Vueでは不要(必要に応じてcomputed) | | useMemo | computed | | | useRef | ref / useTemplateRef | DOM参照はuseTemplateRef | | useContext | provide / inject または Pinia | グローバルはPinia推奨 | | useReducer | Pinia store | |

変換例

React (useState + useEffect):

const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0);

useEffect(() => {
  setDoubled(count * 2);
}, [count]);

Vue3 (ref + computed):

const count = ref(0);
const doubled = computed(() => count.value * 2);

useEffectの変換パターン

依存配列なし(マウント時のみ):

// React
useEffect(() => {
  console.log('mounted');
}, []);
// Vue3
onMounted(() => {
  console.log('mounted');
});

依存配列あり(値の変更を監視):

// React
useEffect(() => {
  fetchData(id);
}, [id]);
// Vue3
watch(() => id.value, (newId) => {
  fetchData(newId);
}, { immediate: true });

クリーンアップあり:

// React
useEffect(() => {
  const timer = setInterval(() => {}, 1000);
  return () => clearInterval(timer);
}, []);
// Vue3
onMounted(() => {
  const timer = setInterval(() => {}, 1000);
  onUnmounted(() => clearInterval(timer));
});

移植しない機能

以下に該当するものは移植対象外とする:

  • Electron/Tauri固有の処理: デスクトップアプリ固有のAPI呼び出し
  • 不要な互換性維持コード: 後方互換のためだけのコード
  • 過剰なエラーハンドリング: 発生し得ないケースの処理

注意!!!

  • 当ファイルを読む際は必ず全文読み込んでください。断片的に読んでも良い作業はできません。

目次

  1. [プロジェクト概要](#プロジェクト概要)
  2. [React から Vue3 への変換ガイド](#react-から-vue3-への変換ガイド)
  3. [Composables設計のベストプラクティス](#composables設計のベストプラクティス)
  4. [既存コンポーネントへの影響を最小化する設計](#既存コンポーネントへの影響を最小化する設計)
  5. [コンポーネント設計のベストプラクティス](#コンポーネント設計のベストプラクティス)
  6. [TypeScript型安全性のベストプラクティス](#typescript型安全性のベストプラクティス)
  7. [テスト戦略とベストプラクティス](#テスト戦略とベストプラクティス)
  8. [実装チェックリスト](#実装チェックリスト)
  9. [まとめ](#まとめ)

pages/配下のコンポーネント肥大化防止

原則: pages/配下のコンポーネントを修正する際は、Atomic Designの考えに基づき、新規コンポーネントを作成してロジックを分離する。

pedaru-vue/
├── components/
│   ├── atoms/      # 基本的なUI要素
│   ├── molecules/  # 複合コンポーネント
│   └── organisms/  # 複雑な機能
├── composables/    # ビジネスロジック(型定義も同じファイルに配置)
├── pages/          # ルートコンポーネント(薄く保つ)
└── stores/         # Pinia stores

Composables設計のベストプラクティス

単一責任の原則

ガイドライン:

  • 1つのcomposableは1つの関心事のみを扱う
  • ファイル名はその責任を明確に表す(use*で始まる)
  • 50〜100行を目安とし、それを超える場合は分割を検討
  • 複数のcomposableを組み合わせて使用する設計を推奨(Compose)

❌ Bad: 複数の責任を持つ巨大なComposable

// useVideoManagement.ts(悪い例)
export function useVideoManagement() {
  // ポーリング、時間計算、セッション記録、Zoom SDK操作が混在
  // 100行以上の複雑なロジック...
}

✅ Good: 責任を分離

// useVideoStatus.ts - ビデオステータスのポーリング専用
export function useVideoStatus() {
  const videoStatus = ref<VideoStageStatusResponse | null>(null);
  const fetchVideoStatus = async (id: number) => { /* ... */ };
  const startPolling = (id: number) => { /* ... */ };
  const stopPolling = () => { /* ... */ };
  return { videoStatus, fetchVideoStatus, startPolling, stopPolling };
}

// useSessionElapsedTime.ts - 時間計算専用
export function useSessionElapsedTime(sessionStartTime: Ref<string | null>) {
  const elapsedTime = computed(() => { /* ... */ });
  return { elapsedTime };
}

✅ Good: 複数のComposableを組み合わせる(Compose)

重要な判断基準:責任範囲の正しい分離

例:症状選択機能で「選択」と「送信履歴管理」を1つのcomposableに混在させてはいけません。

// ❌ Bad: 責任範囲が混在
// useSymptomSelection.ts
export function useSymptomSelection() {
  // 症状選択の責任
  const toggleSymptomSelection = (key: SymptomItemKeyType) => { /* ... */ };

  // 送信履歴管理の責任(別の関心事!)
  const markAsSent = (key: SymptomItemKeyType) => { /* ... */ };
  const onSendSuccess = () => { /* 混在している */ };

  return { toggleSymptomSelection, markAsSent, onSendSuccess };
}

正しい設計:各関心事を独立したcomposableに分離し、組み合わせて使用

// useSymptomSelection.ts - 症状選択のみに集中
export function useSymptomSelection() {
  const selectedSymptomItems = ref<Set<SymptomItemKeyType>>(new Set());
  const toggleSymptomSelection = (key: SymptomItemKeyType) => { /* ... */ };
  const resetSelectedSymptoms = () => { /* ... */ };
  const selectedSymptoms = computed(() => { /* ... */ });

  return { selectedSymptomItems, selectedSymptoms, toggleSymptomSelection, resetSelectedSymptoms };
}

// useSymptomSendHistory.ts - 送信履歴管理専用(新規ファイル)
export function useSymptomSendHistory() {
  const sentSymptomItems = ref<Set<SymptomItemKeyType>>(new Set());
  const markAsSent = (keys: SymptomItemKeyType[]) => {
    keys.forEach(key => sentSymptomItems.value.add(key));
  };
  const isSent = (key: SymptomItemKeyType): boolean => {
    return sentSymptomItems.value.has(key);
  };

  return { sentSymptomItems, markAsSent, isSent };
}

// useChatMessageGenerator.ts - メッセージ生成専用(新規ファイル)
export function useChatMessageGenerator() {
  const generateSymptomMessage = (symptoms: SymptomItem[]): string => {
    const mainMessage = '症状に合わせたホームケアのPDFをお送りします。';
    const contents = symptoms.map((s) => `・${s.title}\n${s.url}\n`).join('\n');
    return `${mainMessage}\n${contents}`;
  };

  return { generateSymptomMessage };
}

// ChatAttachedPdfSelect.vue - 複数のcomposableを組み合わせる(Compose)
const { selectedSymptoms, resetSelectedSymptoms } = useSymptomSelection();
const { markAsSent } = useSymptomSendHistory();
const { generateSymptomMessage } = useChatMessageGenerator();

const handleSendChat = async () => {
  const message = generateSymptomMessage(selectedSymptoms.value);
  const success = await props.onSendChat(message);

  if (success) {
    markAsSent(selectedSymptoms.value.map(s => s.key));
    resetSelectedSymptoms();
    await showToast();
  }
};

メリット:

  • ✅ 単一責任の原則: 各composableが1つの関心事のみ
  • ✅ Composeの原則: 複数のcomposableを組み合わせて使用
  • ✅ テスト容易性: 各関心事を独立してテスト可能
  • ✅ 再利用性: 送信履歴管理は他の機能でも再利用可能
  • ✅ 明確な責任範囲: ファイル名から役割が明確

レイヤー分離(技術層とビジネス層)

ガイドライン:

  • 技術層: 外部ライブラリ(Zoom SDK、Twilio)の操作のみ
  • アプリケーション層: ビジネスロジック、Pinia操作、DB記録
  • 依存方向は常に「アプリケーション層 → 技術層」
  • コメントで設計意図を明示(// NOTE: Piniaにアクセスしないなど)

❌ Bad: 技術的な詳細とビジネスロジックが混在

// useZoomVideoSession.ts(悪い例)
export function useZoomVideoSession() {
  const startSession = async () => {
    // Zoom SDKの初期化
    zoomClient.value = ZoomVideo.createClient();
    await zoomClient.value.init('ja-JP', 'Global');

    // ビジネスロジック(DB記録)が混在
    await videoStageRepository.create({ status: 'active' });

    // Piniaへのアクションも混在
    pdfStore.updateStatus('active');
  };
}

✅ Good: レイヤーを明確に分離

// useZoomVideo.ts - 技術層(Zoom SDK操作のみ)
export function useZoomVideo() {
  const createSession = async (sessionId: string) => { /* Zoom SDKのみ */ };
  const joinSession = async (sessionId: string) => { /* Zoom SDKのみ */ };
  // NOTE: Piniaにアクセスしない
  return { createSession, joinSession };
}

// useOnlineReservationVideoSession.ts - アプリケーション層
export function useOnlineReservationVideoSession() {
  const { createSession, joinSession } = useZoomVideo();

  const startTalking = async (sessionId: string, id: number) => {
    await createSession(sessionId);              // 1. Zoom SDK
    await videoStageRepository.create({ id });   // 2. DB記録
    pdfStore.updateStatus({ ... });               // 3. Pinia
    await joinSession(sessionId);                // 4. Zoom SDK
  };

  return { startTalking };
}

状態管理とスコープ

ガイドライン:

  • 個別インスタンスが必要: composable内でstateを定義
  • 親子間で共有が必要: Provide/Injectパターン
  • アプリ全体で共有が必要: Pinia
  • テストでは
Compartilhar X LinkedIn