AgentSkillsCN

model-structure

前端模型结构模式。适用于创建新模型(例如用户、产品)、添加列表/详情页面,或在 front/src/model/ 中探讨模型架构时使用。

SKILL.md
--- frontmatter
name: model-structure
description: Frontend model structure patterns. Use when creating a new model (e.g., user, product), adding list/detail pages, or asking about model architecture in front/src/model/.

Model Structure

Reference implementation: front/src/model/article

Design Philosophy

  • OOUI-based: One directory per domain model under model/
  • Feature-split: Separate list/ and detail/ for different use cases
  • Slice pattern: Each input field is an independent Zustand slice
  • Phase-based validation: Validate progressively (onChange → onDraftSubmit → onConfirmedSubmit)
  • Preview separation: Pure View components (props) vs store-connected wrappers

Key Patterns

Input Slice

Each form field has its own slice with value, setter, and validation getters.

code
inputs/[field]/
├── slice.ts      # Generate slice with createInputSlice factory
├── validation.ts # ArkType schema + InputValidation (when validation needed)
├── hook.ts       # useXxxInput() - select from store, compute errors
└── index.tsx     # 'use client' UI component

Slice Factory (recommended):

Use createInputSlice factory to generate slices. Reduces boilerplate.

ts
import { createInputSlice, type InputSliceShape } from '@/model/common/lib/slice-factory'
import { titleValidation } from './validation'

// With validation
export type TitleSlice = InputSliceShape<'title', string>
export const createTitleSlice = createInputSlice('title', titleValidation)

// Without validation (enums, etc.)
export type CategorySlice = InputSliceShape<'category', ArticleCategory>
export const createCategorySlice = createInputSlice<'category', ArticleCategory>('category')

Generated slice shape: { [key], set[Key], get[Key]ErrorMessages, get[Key]IsValid }

Irregular patterns (cross-field deps, custom logic):

  • Write slice manually, or
  • Create base with factory, then spread-merge additional methods

Store Composition

Combine all slices into one store with meta functions.

  • Spread each slice: ...createXxxSlice(initialValue)(...args)
  • Add getFormIsValid() and getFormValues() to MetaSlice
  • Provide via Context + useRef pattern

Validation

  • Use constraint values from @ckda-cms/core/model/article (articleConstraints)
  • Use validateWithStandardSchema() from model/common/lib/validation.ts
  • Return InputValidation with three phases: onChange, onDraftSubmit, onConfirmedSubmit
  • Returns ValidationError[] (no isOk - errors only generated when invalid)

Pattern:

ts
import { articleConstraints, titleSchema } from '@ckda-cms/core/model/article'
import {
  validateWithStandardSchema,
  withValidationErrorMessages,
} from '@/model/common/lib/validation'

const c = articleConstraints.title

// withValidationErrorMessagesでメッセージを追加(otherwiseはデフォルト「不正な値です」)
const titleOnSubmitSchema = withValidationErrorMessages(titleSchema, {
  minLength: 'タイトルは必須です',
  maxLength: `${c.maxLength}文字以内で入力してください`,
})

export const titleValidation = (value: string): InputValidation => ({
  onChange: validateWithStandardSchema(onChangeSchema, value),
  onConfirmedSubmit: validateWithStandardSchema(titleOnSubmitSchema, value),
  onDraftSubmit: [],
})

Query

  • Use use cache directive with cacheTag() for data fetching
  • Located in query.ts under each feature

Actions

  • Server Actions in actions.ts
  • Call API via apiClient from model/common/rpc-client.ts
  • Use revalidateTag() to invalidate cache after mutations

Preview

  • model/[entity]/preview/[field]/index.tsx: Pure View (receives value via props)
  • model/[entity]/detail/preview/[field]/index.tsx: Connects to store and renders View

Directory Structure

code
model/[entity]/
├── actions.ts
├── list/
│ ├── query.ts
│ ├── search/store/ + inputs/
│ └── table/
└── detail/
├── query.ts
├── store/
├── form/inputs/
└── preview/

Read front/src/model/xxx for concrete examples.