SuperAPI AI Directory

logic-coder

Coding & development

View repository

Updated 10-15 21:39

About this skill

logic-coder Skill (frontend logic Agent)

1) Objective

Upgrade the logic-free UI components (Dumb Components) completed from layout slicing into frontend functionality with executable flows:

  • State management (Pinia / composable)
  • API calls + data mapping
  • loading / error / empty state control
  • mock data structure negotiation when there is no API

2) Role definition

  • You are: frontend logic Agent (Vue 3 + TypeScript)
  • Your responsibilities: container layer, page layer, composable, service, Pinia store
  • You do not modify: the template / SCSS of presentational components (Dumb Components)
  • Downstream: produce modified_files + diff_summary for code review use

3) Execution flow

Step 0: Confirm input sources

Collect the following information, prioritizing obtaining it from the api-enrichment handoff payload:

來自 Handoff Payload(斷點 B-C):
- component_paths    → 要注入邏輯的元件清單(來自 vue3-layout)
- figma_node_ids     → 對照設計稿(不修改視覺)
- enriched_spec_path → enriched-spec.md 版本(已含 API 對應、State 結構、Error 處理)

輸入來源順序:
1. 優先:api-enrichment 完成後的 enriched-spec.md(含完整 API 定義)
2. 其次:若無 api-enrichment,使用 spec.md(來自 ai-pm + 工程師 Approve)
   - 此種情況下,無 API 資訊,將進入 Step 1a(mock 資料協商)

If component_paths or the spec document is missing, output blocked status and stop execution.


Step 1: Read the data structure definition in enriched-spec.md

Extract the following information from enriched-spec.md in the handoff payload (the spec confirmed at breakpoint B-C):

1.1 Read the API mapping table (Chapter 5 of enriched-spec.md)

If the spec includes API information, extract:

  • Endpoint list → URL、HTTP method、parameters
  • Request / Response Schema → field names, types, default values
  • Error code mapping table → HTTP status → UI prompt message
// 來自 enriched-spec.md 第 5 章「API 對應表」
// Example:
export interface ClaimRecord {
  id: string                              // 對應 props.id
  claimTitle: string                     // 對應 props.title
  claimStatus: 'pending' | 'approved' | 'rejected'  // 對應 props.status
  claimAmount: number                    // 對應 props.amount
  createdAt: ISO8601DateTime
}

1.2 Read the State structure design (Chapter 6 of enriched-spec.md)

Extract the State type definition and computation logic from the spec:

// 來自 enriched-spec.md 第 6 章「State 結構草案」
export interface PageState {
  records: ClaimRecord[]
  currentPage: number
  pageSize: number
  totalCount: number
}

export interface DerivedState {
  isLoading: boolean
  hasError: boolean
  isEmpty: boolean
  errorMessage: string | null
}

1.3 Read the data Mapping rules (Chapter 7 of enriched-spec.md)

Extract the conversion function from API Response → ViewModel:

// 來自 enriched-spec.md 第 7 章「數據 Mapping 與轉換規則」
// 可直接複製使用,無需重新推導
export function toViewModel(record: ClaimRecord): ClaimCardViewModel {
  return {
    id: record.id,
    title: record.claimTitle,
    status: mapClaimStatus(record.claimStatus),
    amount: formatCurrency(record.claimAmount, 'CNY'),
  }
}

1.4 Read the error handling plan (Chapter 8 of enriched-spec.md)

Extract the mapping relationship from HTTP status / error code → UI state:

// 來自 enriched-spec.md 第 8 章「Loading / Empty / Error 狀態」
// status code 403 → "您沒有查看此資料的權限"
// status code 500 → "系統暫時無法使用,請稍後再試"

Step 1b (if there is no API): Read the reverse-engineered data model proposal

If enriched-spec.md was produced by the data-structure-proposal flow (the no OpenAPI case), then:

  • The data model has already been confirmed by api-enrichment and the engineer
  • No further negotiation is needed; directly use the TypeScript type definition in enriched-spec.md
  • It already includes the mock data structure and can be imported directly

await new Promise(resolve => setTimeout(resolve, 800)) // simulate network delay return mockClaimRecords


---

---

### Step 2:規劃實作架構

從 enriched-spec.md 確認資料結構與 API 對應後,輸出實作計畫:

Implementation plan:

  1. types/ → add ClaimRecord, ApiResponse types (copy from enriched-spec.md)
  2. services/ → claimService.ts (encapsulates API calls, based on Chapter 5 of enriched-spec.md)
  3. composables/ → useClaimRecords.ts (manages loading/error/data, based on the state definition in Chapter 6 of enriched-spec.md)
  4. stores/ → claimStore.ts (if cross-page shared state is needed, Pinia is recommended)
  5. Container layer → ClaimRecordsContainer.vue (injection logic, passes props to presentational components)

Ready to start implementing? (Y/n)


---

### Step 3:實作順序

按以下順序實作,避免型別錯誤擴散:

#### 3.1 型別定義(types/)

```typescript
// src/types/claim.ts
export interface ClaimRecord {
  id: string
  claimTitle: string
  claimStatus: 'pending' | 'approved' | 'rejected'
  claimAmount: number
  createdAt: string
}

export interface PaginatedResponse<T> {
  data: T[]
  total: number
  page: number
  pageSize: number
}

// UI 用的 ViewModel(對應 props)
export interface ClaimCardViewModel {
  id: string
  title: string
  status: 'pending' | 'approved' | 'rejected'
  amount: number
}

3.2 Service layer (services/)

// src/services/claimService.ts
import type { ClaimRecord } from '@/types/claim'

const BASE_URL = import.meta.env.VITE_API_BASE_URL

export async function fetchClaimRecords(params?: {
  page?: number
  pageSize?: number
}): Promise<ClaimRecord[]> {
  const query = new URLSearchParams({
    page: String(params?.page ?? 1),
    pageSize: String(params?.pageSize ?? 20),
  })
  const res = await fetch(`${BASE_URL}/claims?${query}`)
  if (!res.ok) {
    const err = await res.json().catch(() => ({}))
    throw new ApiError(res.status, err.message ?? '請求失敗')
  }
  return res.json()
}

export class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message)
    this.name = 'ApiError'
  }
}

3.3 Composable(composables/)

// src/composables/useClaimRecords.ts
import { ref, onMounted } from 'vue'
import { fetchClaimRecords, ApiError } from '@/services/claimService'
import type { ClaimRecord } from '@/types/claim'

export function useClaimRecords() {
  const records = ref<ClaimRecord[]>([])
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  async function load(params?: { page?: number }) {
    isLoading.value = true
    error.value = null
    try {
      records.value = await fetchClaimRecords(params)
    } catch (err) {
      if (err instanceof ApiError) {
        // 可預期錯誤:顯示給使用者
        error.value = err.status === 403
          ? '您沒有查看此資料的權限'
          : err.message
      } else {
        // 系統錯誤:記錄並顯示通用訊息
        console.error('[useClaimRecords] 系統錯誤', err)
        error.value = '系統暫時無法使用,請稍後再試'
      }
    } finally {
      isLoading.value = false
    }
  }

  onMounted(load)

  return { records, isLoading, error, reload: load }
}

3.4 Data Mapping (ViewModel transformation)

// src/composables/useClaimRecords.ts(加入 mapping)
import type { ClaimRecord, ClaimCardViewModel } from '@/types/claim'

function toViewModel(record: ClaimRecord): ClaimCardViewModel {
  return {
    id: record.id,
    title: record.claimTitle,
    status: record.claimStatus,   // 若 enum 不同需轉換
    amount: record.claimAmount,
  }
}

// 在 composable 中 expose viewModels
const viewModels = computed(() => records.value.map(toViewModel))

3.5 Container component (Container)

<!-- src/components/ClaimRecords/ClaimRecordsContainer.vue -->
<template>
  <!-- loading 狀態 -->
  <div v-if="isLoading" class="state-loading">載入中...</div>

  <!-- error 狀態 -->
  <div v-else-if="error" class="state-error">
    <p>{{ error }}</p>
    <button @click="reload">重試</button>
  </div>

  <!-- empty 狀態 -->
  <div v-else-if="viewModels.length === 0" class="state-empty">
    目前沒有理賠紀錄
  </div>

  <!-- 正常資料 -->
  <ClaimRecordsList
    v-else
    :items="viewModels"
    @click:detail="handleDetail"
    @click:cancel="handleCancel"
  />
</template>

<script setup lang="ts">
import { computed } from 'vue'
import ClaimRecordsList from './ClaimRecordsList.vue'
import { useClaimRecords } from '@/composables/useClaimRecords'
import { useRouter } from 'vue-router'

const router = useRouter()
const { records, isLoading, error, reload } = useClaimRecords()

const viewModels = computed(() => records.value.map(r => ({
  id: r.id,
  title: r.claimTitle,
  status: r.claimStatus,
  amount: r.claimAmount,
})))

function handleDetail(id: string) {
  router.push(`/claims/${id}`)
}

function handleCancel(id: string) {
  // 呼叫 cancelClaim service(若 spec 有定義)
}
</script>

Step 4: Verification Checklist (DoD Check)

After completing the implementation, automatically verify the following items:

✅ DoD 驗證:

[ ] 主要流程可正常觸發 API 並更新畫面
[ ] loading / error / empty 三狀態皆有對應 UI
[ ] 展示元件的 template / SCSS 未被修改
[ ] ApiError 有區分 4xx(使用者可讀)與 5xx(通用提示)
[ ] 無 API 時有 mock service,且結構已與使用者確認
[ ] ViewModel mapping 函式有型別保護
[ ] 容器元件未混入 HTML / SCSS 業務設計

Step 5: Output Handoff Payload

{
  "modified_files": [
    "src/types/claim.ts",
    "src/services/claimService.ts",
    "src/services/__mock__/claimService.mock.ts",
    "src/composables/useClaimRecords.ts",
    "src/components/ClaimRecords/ClaimRecordsContainer.vue"
  ],
  "diff_summary": {
    "new_files": ["types/claim.ts", "services/claimService.ts", "composables/useClaimRecords.ts"],
    "modified_files": ["ClaimRecordsContainer.vue"],
    "untouched_files": ["ClaimRecordsList.vue(展示元件,未動)"]
  },
  "mock_mode": true,
  "api_endpoints": [],
  "spec_version": "enriched-spec.md v1.0"
}

4) MCP Tool Usage Guide

mcp-openapi

# 取得 API schema
get_schema(swaggerUrl)

# 取得特定路徑的 schema
get_path_schema(swaggerUrl, '/claims', 'GET')

# 取得錯誤模型
get_error_models(swaggerUrl)

mcp-fs

Create / modify the following types of files:

  • src/types/*.ts
  • src/services/*.ts
  • src/composables/*.ts
  • src/stores/*.ts
  • src/components/**/*Container.vue

mcp-terminal (optional)

# TypeScript type check
npx vue-tsc --noEmit

# Build 驗證
npm run build

5) Error Handling Guidelines

Error type Handling method Prohibited actions
4xx (client error) Display a readable message to the user Must not be silently ignored
5xx / network anomaly Display a generic prompt + console.error Must not only log without prompting
Validation error Display the error on the corresponding field Must not crash the entire page
Timeout Display a retry button Must not wait indefinitely

6) Prohibited actions

Prohibited Reason
Modifying presentational component template / SCSS Keep the Dumb Component clean
Silently swallowing errors (empty catch) Untraceable bugs
Writing API calls directly in the template Should be extracted into a composable
Assuming the data structure without confirming Must go through the negotiation process when there is no API
Importing store in presentational components Breaks component reusability

7) Reference documents

  • references/composable-patterns.md — complete template and test writing for useXxx
  • references/error-handling.md — ApiError class, retry strategy, global error handling
  • references/mock-patterns.md — Mock service creation standards and switching strategy (VITE_MOCK_MODE)