Walnut Admin Component Development
This skill guides you through creating components following Walnut Admin's established patterns.
Component Structure
Every component follows this folder structure:
ComponentName/
├── index.ts # Public exports
└── index.vue # Component implementation
Basic Component Template
index.ts
import type { App } from 'vue'
import Component from './index.vue'
export * from './types' // if exists
export { Component as YourComponent }
export default {
install(app: App) {
app.component('WYourComponent', Component)
},
}
index.vue
<script lang="ts" setup>
import type { YourProps } from './types'
defineOptions({
name: 'WCompYourComponent', // Naming convention: WComp{Category}{Name}
})
const props = withDefaults(defineProps<YourProps>(), {
// defaults
})
const emit = defineEmits<{
change: [value: string]
}>()
// Component logic
</script>
<template>
<div class="w-your-component">
<!-- Template -->
</div>
</template>
<style scoped>
.w-your-component {
/* Styles */
}
</style>
Component Categories
Components are organized by category in src/components/:
| Category | Path | Description |
|---|---|---|
| Advanced | Advanced/ |
High-level composite components (CRUD, ApiSelect) |
| App | App/ |
App-level components (Lock, Search, Settings) |
| Business | Business/ |
Business logic components (Dict, AreaCascader) |
| Extra | Extra/ |
Extra utilities (Copy, QRCode, Password) |
| Global | Global/ |
Global components (Cap, DevSettings) |
| HOC | HOC/ |
Higher-order components |
| UI | UI/ |
UI wrapper components (Form, Table, Modal) |
| Vendor | Vendor/ |
Third-party wrappers (CodeMirror, ECharts, Tinymce) |
Component Naming Convention
- Folder: PascalCase matching component name
- Component name:
WComp{Category}{Name}WCompUIFormWCompAppLockWCompExtraPassword
The "Register + Methods" Pattern
Many components use this pattern for parent-child communication:
Component Side (Child)
<script lang="ts" setup>
const props = defineProps<...>()
const emit = defineEmits<{
hook: [inst: YourInst]
}>()
// Methods to expose
const methods = {
validate: async () => { /* ... */ },
reset: () => { /* ... */ },
setProps: (newProps) => { /* ... */ },
}
// Expose for template ref access
defineExpose(methods)
// Emit hook for useXxx pattern
emit('hook', {
...methods,
// add setProps if using useProps
})
</script>
Parent Side (Consumer)
<script lang="ts" setup>
// Pattern: [register, methods] = useXxx(props)
const [register, { validate, reset }] = useForm({
// initial props
})
</script>
<template>
<WForm @hook="register" />
</template>
Context Injection Pattern
For passing data through deep component trees:
Create Context
// hooks/useContext.ts
import type { InjectionKey } from 'vue'
export interface Context {
formRef: Ref<FormInst | null>
formSchemas: Ref<Schema[]>
// ...
}
const key: InjectionKey<Context> = Symbol('WFormContext')
export function setFormContext(ctx: Context) {
provide(key, ctx)
}
export function useFormContext() {
return inject(key)!
}
Use Context
<script lang="ts" setup>
// Parent component
import { setFormContext } from './hooks/useContext'
setFormContext({
formRef,
formSchemas,
// ...
})
</script>
<script lang="ts" setup>
// Child component
import { useFormContext } from './hooks/useContext'
const { formRef, formSchemas } = useFormContext()
</script>
useProps Pattern
For dynamic prop updates:
import { useProps } from '@/hooks/core/useProps'
const props = defineProps<...>()
const propsCtx = useProps(props)
const { setProps, getProps } = propsCtx
// In template, use getProps
const getComputedProp = computed(() => getProps.value.someProp)
// Parent can update props dynamically
setProps({ someProp: 'new value' })
Async Component Pattern
For code-splitting heavy components:
import { createAsyncComponent } from '@/utils/factory/asyncComponent'
const HeavyComponent = createAsyncComponent(() => import('./HeavyComponent'))
Component Types Pattern
types.ts
export interface ICompUIYourComponentProps {
modelValue?: string
disabled?: boolean
}
export interface ICompUIYourComponentInst {
focus: () => void
blur: () => void
}
Slots Pattern
<template>
<div>
<!-- Default slot -->
<slot />
<!-- Named slot -->
<slot name="header" :header-data="headerData" />
<!-- Scoped slot with fallback -->
<slot name="item" :item="item" :index="index">
<span>{{ item.label }}</span>
</slot>
</div>
</template>
Styling Guidelines
Use UnoCSS for utility classes:
<template> <div class="flex items-center gap-2 p-4">Scoped styles for component-specific CSS:
<style scoped> .custom-element { @apply text-primary; } </style>CSS variables for theming:
<style scoped> .component { color: var(--primary-color); } </style>
Auto-Import Considerations
This project uses unplugin-auto-import. Common imports are automatic:
- Vue:
ref,reactive,computed,watch, etc. - Vue Router:
useRoute,useRouter - Vue I18n:
useI18n - Pinia:
defineStore,storeToRefs - VueUse: Most functions
Explicit imports preferred for:
- Component types
- Utils from
@/utils/* - Hooks from
@/hooks/*
Best Practices
- Always use
defineOptionsfor component name - Use TypeScript generics when component handles data types:
<script lang="ts" setup generic="T extends IModels.Base"> - Expose methods via
defineExposefor template ref access - Emit 'hook' event for register pattern compatibility
- Use
shallowReffor component template refs
Converted and distributed by TomeVault — claim your Tome and manage your conversions.