This commit is contained in:
2026-04-05 16:14:23 +08:00
parent 9936b2b92c
commit 33d7460ea0
19 changed files with 1515 additions and 2 deletions

133
.kilo/README.md Normal file
View File

@@ -0,0 +1,133 @@
# Kilo Configuration for Vue Admin Project
This directory contains the Kilo AI assistant configuration for this Vue 3 + Arco Design admin project.
## Directory Structure
```
.kilo/
├── agent/ # Agent mode rules
│ ├── rules-code.md # Code writing rules
│ ├── rules-architect.md # Architecture rules
│ ├── rules-debug.md # Debugging rules
│ └── rules-ask.md # Q&A rules
├── command/ # Slash commands
│ ├── new-page.md # Create new page module
│ ├── new-api.md # Create new API module
│ ├── new-component.md # Create new component
│ ├── lint-fix.md # Fix lint issues
│ └── type-check.md # Check TypeScript types
├── skill/ # Skill definitions
│ └── vue-admin-dev.md # Vue admin development skill
├── templates/ # Code templates
│ ├── api-module.ts # API module template
│ ├── columns.ts # Table columns template
│ ├── search-form.ts # Search form template
│ ├── index.vue # Main page template
│ ├── FormDialog.vue # Form dialog template
│ └── Detail.vue # Detail view template
├── logs/ # Log files
└── node_modules/ # Dependencies
```
## Configuration Files
### kilo.json
Main configuration file with:
- Agent settings (default mode, allowed modes)
- Command directory path
- Rules directory path
- Skills directory path
- Hooks (beforeFileWrite, afterTaskComplete)
- Context settings (maxTokens, include/exclude patterns)
- Logging configuration
### AGENTS.md
Project-level guidance file in root directory with:
- Build commands
- Architecture notes
- Module structure patterns
- Code style requirements
- Import order
- Debug tips
## Usage
### Commands
```
/new-page <module-name> <category> # Create new page module
/new-api <module-name> [base-path] # Create new API module
/new-component <name> [type] # Create new component
/lint-fix [path] # Fix lint issues
/type-check # Check TypeScript types
```
### Skills
Load the vue-admin-dev skill for comprehensive development guidance:
```
/skill vue-admin-dev
```
## Key Rules
### Storage
Always use `SafeStorage` with `AppStorageKey` enum - never use localStorage directly.
### API Choice
- Use `request.ts` for workspace-aware APIs (most ops APIs)
- Use `interceptor.ts` for Bearer token APIs expecting `code: 20000`
### API Response Format
Standard response from `request.ts`: `{code: 0, details: {data: [...], total: number}}`
- Success code is `0`, NOT `200` or `20000`
- Access data via `response.details.data`
- Access total via `response.details.total`
### Comments
Add JSDoc comments for interfaces and functions:
```ts
/** 服务器类型 */
export interface ServerItem { ... }
/** 获取服务器列表(分页) */
export const fetchServerList = (params?: ServerListParams) => { ... }
```
### useRequest Hook
Does NOT work in async functions. Use `.bind(null, params)` pattern in setup scope.
### Dynamic Routes
Routes loaded from server via permission guard. Don't modify `isMenuLoading`/`isMenuLoaded` flags.
### Code Style
- No semicolons
- Single quotes
- Print width: 140 characters
- JSDoc comments for types and functions
## Templates
Templates use placeholder syntax:
- `{{Module}}` - PascalCase module name (e.g., `UrlDevice`)
- `{{module}}` - lowercase module name (e.g., `url-device`)
- `{{ModuleTitle}}` - Chinese title (e.g., `URL监控设备`)
- `{{ModuleName}}` - Vue component name (e.g., `UrlDeviceManagement`)
Replace these placeholders when generating new modules.

View File

@@ -0,0 +1,68 @@
# Architect Mode Rules (Non-Obvious Only)
## Architecture Overview
- Vue 3 SPA with dynamic route loading from server
- Pinia stores: `app`, `user`, `tab-bar` (see [`src/store/`](src/store/))
- Two-layer route guard: user login check + permission check
## Dynamic Route System
- Server menu fetched in [`app store`](src/store/modules/app/index.ts) via `fetchServerMenuConfig()`
- Routes registered dynamically in [`permission.ts`](src/router/guard/permission.ts)
- Uses `isMenuLoading`/`isMenuLoaded` flags to prevent duplicate loads
## API Architecture
- Two axios instances for different auth patterns:
- [`request.ts`](src/api/request.ts): Workspace header + custom token format
- [`interceptor.ts`](src/api/interceptor.ts): Bearer token + code 20000 validation
- Choose based on backend API requirements
## API Response Format
- Standard response structure: `{code: 0, details: {data: [...], total: number}}`
- Success code is `0`, NOT `200` or `20000`
- `request.ts` returns `response.data` directly
- Access list data via `response.details.data`
- Access total count via `response.details.total`
## State Management
- [`SafeStorage`](src/utils/safeStorage.ts) required for all localStorage access
- Supports TTL expiry and type-safe keys via enum
## Component Patterns
- Global components registered in [`components/index.ts`](src/components/index.ts)
- ECharts modules manually imported for bundle size optimization
- Use `SearchTable` composite component for list pages
## Directory Structure
```
src/
api/ # API layer (ops/ for business, module/ for auth)
components/ # Global reusable components
hooks/ # Composition functions
router/ # Route config and guards
store/ # Pinia stores
utils/ # Utility functions
views/ops/ # Main business modules
pages/
dc/ # Data center management
monitor/ # Monitoring modules
netarch/ # Network architecture
report/ # Reports
system-settings/ # System config
```
## Module Creation Checklist
1. Create API file in `src/api/ops/`
2. Create page directory in `src/views/ops/pages/`
3. Create `config/columns.ts` for table config
4. Create `config/search-form.ts` for search config
5. Create main `index.vue` with SearchTable
6. Create `components/FormDialog.vue` for CRUD
7. Create `components/Detail.vue` for detail view

47
.kilo/agent/rules-ask.md Normal file
View File

@@ -0,0 +1,47 @@
# Ask Mode Rules (Non-Obvious Only)
## Project Structure
- Vue 3 + Arco Design admin template with Pinia state management
- Vite config files located in `config/` directory (not root)
## Key Directories
- `src/api/` - API layer with two axios instances
- `src/views/ops/` - Main business modules (kb, netarch, asset, etc.)
- `src/router/guard/` - Route guards including dynamic menu loading
- `src/store/modules/app/` - App store with server menu fetching
## Documentation References
- Arco Design Vue: https://arco.design/vue
- Vue Flow (for topology): https://vueflow.dev/
## API Patterns
- Use [`request.ts`](src/api/request.ts) for workspace-aware requests
- Use [`interceptor.ts`](src/api/interceptor.ts) for standard Bearer token auth
## API Response Format
- Standard response: `{code: 0, details: {...}}`
- Success code is `0`, NOT `200` or `20000`
- Access data via `response.details.data`
## Component Libraries
- Arco Design Vue components: `<a-button>`, `<a-table>`, `<a-form>`, etc.
- Global components: `SearchTable`, `SearchForm`, `DataTable`, `Chart`
## Code Style
- No semicolons
- Single quotes
- Print width: 140 characters
- Path alias: `@/``src/`
## Build Commands
- `pnpm dev` - Start dev server
- `pnpm build` - Production build
- `pnpm lint` - Run ESLint + Prettier

85
.kilo/agent/rules-code.md Normal file
View File

@@ -0,0 +1,85 @@
# Code Mode Rules (Non-Obvious Only)
## API Layer
- Two axios instances exist: [`request.ts`](src/api/request.ts) (custom with workspace header) and [`interceptor.ts`](src/api/interceptor.ts) (global with Bearer token). Choose based on whether you need workspace support.
- [`request.ts`](src/api/request.ts) returns `response.data` directly (no nesting)
## Storage
- Always use [`SafeStorage`](src/utils/safeStorage.ts) with `AppStorageKey` enum - never use localStorage directly. Supports TTL via third parameter.
## useRequest Hook
- [`useRequest()`](src/hooks/request.ts) invokes API immediately - does NOT work in async functions. Pass params via `.bind(null, params)` pattern.
## Dynamic Routes
- Routes loaded from server in [`permission.ts`](src/router/guard/permission.ts) using `isMenuLoading`/`isMenuLoaded` flags. Don't modify these flags manually.
## Vue Component Structure
- Use `<script lang="ts" setup>` for main logic
- Add `<script lang="ts">` block with `export default { name: 'ComponentName' }` for component naming
- Style: `<style scoped lang="less">`
## Page Module Pattern
```
src/views/ops/pages/{module}/
index.vue # Main page component
components/
FormDialog.vue # CRUD form dialog
Detail.vue # Detail drawer/view
QuickConfigDialog.vue # Quick config dialog (optional)
config/
columns.ts # Table columns config
search-form.ts # Search form config
```
## API File Pattern
```
src/api/ops/{module}.ts
- Export interfaces for types
- Export fetch functions using `request.get/post/put/patch/delete`
- Name functions: `fetch{Resource}List`, `fetch{Resource}Detail`, `create{Resource}`, `update{Resource}`, `delete{Resource}`
```
## Import Order
1. Vue core (ref, reactive, computed, etc.)
2. Third-party (arco-design, axios, etc.)
3. Local components
4. Local configs
5. Local APIs
6. Types
## Comments (JSDoc Style)
- Add JSDoc comments for interfaces, types, and functions
- Interface comment format: `/** {{TypeName}} */`
- Function comment format: `/** {{Function description}} */`
- Example:
```ts
/** 服务器类型 */
export interface ServerItem { ... }
/** 获取服务器列表(分页) */
export const fetchServerList = (params?: ServerListParams) => { ... }
```
## API Response Format
- Standard response: `{code: 0, details: {data: [...], total: number}}`
- Success code is `0`, NOT `200` or `20000`
- Access data via `response.details.data` and total via `response.details.total`
## Code Style (Prettier)
- No semicolons
- Single quotes
- Trailing commas (es5)
- Print width: 140 characters
- Path alias: `@/` → `src/`

View File

@@ -0,0 +1,60 @@
# Debug Mode Rules (Non-Obvious Only)
## API Response Codes
- [`request.ts`](src/api/request.ts) returns `response.data` directly, expects `{code: 0, details: ...}`
- [`interceptor.ts`](src/api/interceptor.ts) expects `code: 20000` for success
- Token expiry codes: 50008, 50012, 50014 trigger logout modal (interceptor.ts)
- Token expiry status: 401 or error "Token has expired" (request.ts)
## Token Storage
- Tokens stored via [`SafeStorage`](src/utils/safeStorage.ts) with key `AppStorageKey.TOKEN`.
- Token expiry redirects to `/auth/login` (not `/login`).
## Route Loading Issues
- If routes not loading, check `isMenuLoading`/`isMenuLoaded` flags in [`permission.ts`](src/router/guard/permission.ts).
- Server menu fetched via [`fetchServerMenuConfig()`](src/store/modules/app/index.ts).
## Environment
- Dev config: `.env.development`, Prod config: `.env.production`
- API base URL: `VITE_API_BASE_URL`, Workspace: `VITE_APP_WORKSPACE`
## Common Issues
### useRequest Not Working in Async
- useRequest invokes API immediately - use in setup scope only
- For async contexts, call API directly without useRequest
### Menu Not Loading
- Check console for `[Permission Guard]` logs
- Verify `menuFromServer` setting in app store
- Check `fetchServerMenuConfig()` response
### Token Expiry
- Response `status: 401` or `error: 'Token has expired'` triggers logout
- Clear storage via `SafeStorage.clearAppStorage()`
## Debug Commands
```bash
# Check lint errors
pnpm lint
# Type check
pnpm build
# Dev server with logs
pnpm dev
```
## Browser DevTools
- Vue DevTools for component state
- Network tab for API calls
- Console for `[Permission Guard]` logs

27
.kilo/command/lint-fix.md Normal file
View File

@@ -0,0 +1,27 @@
# /lint-fix Command
Run linting and auto-fix code style issues.
## Usage
```
/lint-fix [path]
```
## Parameters
- `path`: Optional file or directory path (defaults to current working directory)
## What This Command Does
1. Runs `pnpm lint:eslint --fix` to fix ESLint issues
2. Runs `pnpm lint:prettier --write` to fix Prettier formatting
3. Reports any remaining issues that need manual fixing
## Example
```
/lint-fix src/views/ops/pages/dc/url-harvest/
```
Fixes lint issues in the url-harvest module.

40
.kilo/command/new-api.md Normal file
View File

@@ -0,0 +1,40 @@
# /new-api Command
Create a new API module with standard interfaces and functions.
## Usage
```
/new-api <module-name> [base-path]
```
## Parameters
- `module-name`: The API module name (e.g., `url-device`, `server`, `alert`)
- `base-path`: Optional API base path (defaults to `/DC-Control/v1/{module-name}`)
## What This Command Does
1. Creates API file: `src/api/ops/{module-name}.ts`
2. Generates standard interfaces:
- `{Module}Item` - Single item type
- `{Module}ListResponse` - List response type
- `{Module}ListParams` - Query parameters
- `{Module}CreateData` - Create payload
- `{Module}UpdateData` - Update payload
3. Generates standard functions:
- `fetch{Module}List` - GET list with pagination
- `fetch{Module}Detail` - GET single item
- `create{Module}` - POST create
- `update{Module}` - PUT update
- `delete{Module}` - DELETE remove
## Example
```
/new-api alert-template /Alert/v1/templates
```
Creates `src/api/ops/alert-template.ts` with full CRUD API functions.

View File

@@ -0,0 +1,33 @@
# /new-component Command
Create a new global reusable component.
## Usage
```
/new-component <component-name> [type]
```
## Parameters
- `component-name`: The component name (e.g., `status-badge`, `time-picker`)
- `type`: Optional component type (dialog, drawer, form, display, chart)
## What This Command Does
1. Creates component file: `src/components/{component-name}/index.vue`
2. Generates template following Arco Design patterns:
- Props definition with TypeScript types
- Emits definition with typed events
- Scoped styles with Less
3. Optionally registers in `src/components/index.ts` for global use
## Example
```
/new-component status-badge display
```
Creates a display component for showing status with color-coded badges.

51
.kilo/command/new-page.md Normal file
View File

@@ -0,0 +1,51 @@
# /new-page Command
Create a new page module with standard structure.
## Usage
```
/new-page <module-name> <category>
```
## Parameters
- `module-name`: The module name (e.g., `url-harvest`, `server`, `database`)
- `category`: The category directory (e.g., `dc`, `monitor`, `netarch`, `report`, `system-settings`)
## What This Command Does
1. Creates directory structure:
```
src/views/ops/pages/{category}/{module-name}/
index.vue
components/
FormDialog.vue
Detail.vue
config/
columns.ts
search-form.ts
```
2. Creates API file:
```
src/api/ops/{module-name}.ts
```
3. Generates template code following project patterns:
- Main page with SearchTable component
- CRUD dialog with form validation
- Detail drawer for viewing records
- Table columns configuration
- Search form configuration
- API interfaces and functions
## Example
```
/new-page asset dc
```
Creates `src/views/ops/pages/dc/asset/` with full CRUD functionality.

View File

@@ -0,0 +1,23 @@
# /type-check Command
Run TypeScript type checking on the project.
## Usage
```
/type-check
```
## What This Command Does
1. Runs `vue-tsc --noEmit` to check TypeScript types
2. Reports type errors with file locations and line numbers
3. Helps catch type issues before build
## Example
```
/type-check
```
Checks all TypeScript files for type errors.

View File

@@ -0,0 +1,219 @@
# Vue Admin Development Skill
## Overview
This skill provides comprehensive guidance for developing features in this Vue 3 + Arco Design admin project.
## Core Patterns
### 1. Page Module Structure
Every page module follows this pattern:
```
src/views/ops/pages/{category}/{module}/
├── index.vue # Main page (SearchTable + CRUD)
├── components/
│ ├── FormDialog.vue # Create/Edit dialog
│ ├── Detail.vue # Detail view
│ └── QuickConfigDialog.vue # Optional quick config
└── config/
├── columns.ts # Table columns config
└── search-form.ts # Search form config
```
### 2. API Module Structure
```
src/api/ops/{module}.ts
├── /** {{Module}}类型 */
├── Interfaces (Item, ListResponse, ListParams, CreateData, UpdateData)
├── /** 获取{{Module}}列表(分页) */
├── fetch{Module}List(params)
├── /** 获取{{Module}}详情 */
├── fetch{Module}Detail(id)
├── /** 创建{{Module}} */
├── create{Module}(data)
├── /** 更新{{Module}} */
├── update{Module}(id, data)
└── /** 删除{{Module}} */
└── delete{Module}(id)
```
### 3. API Response Handling
```ts
// Standard response: {code: 0, details: {...}}
const response = await fetchList(params)
if (response && response.code === 0 && response.details) {
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
```
src/api/ops/{module}.ts
├── Interfaces (Item, ListResponse, ListParams, CreateData, UpdateData)
├── fetch{Module}List(params)
├── fetch{Module}Detail(id)
├── create{Module}(data)
├── update{Module}(id, data)
└── delete{Module}(id)
````
### 3. Component Template
```vue
<template>
<div class="container">
<search-table ...>
<template #toolbar-left>
<!-- Action buttons -->
</template>
<template #column-slot="{ record }">
<!-- Custom column rendering -->
</template>
</search-table>
<FormDialog v-model:visible="..." />
<a-drawer v-model:visible="...">
<Detail :record="..." />
</a-drawer>
</div>
</template>
<script lang="ts" setup>
// Imports in order: Vue → Third-party → Components → Configs → APIs → Types
</script>
<script lang="ts">
export default { name: 'ModuleName' }
</script>
<style scoped lang="less"></style>
````
## Critical Rules
### Storage
```ts
// CORRECT
SafeStorage.set(AppStorageKey.TOKEN, token, 3600000)
SafeStorage.get(AppStorageKey.USER_INFO)
// WRONG
localStorage.setItem('token', token)
```
### API Choice
```ts
// Workspace-aware APIs (most ops APIs)
import { request } from '@/api/request'
request.post('/DC-Control/v1/servers', data)
// Response: {code: 0, details: {data: [...], total: number}}
// Standard Bearer token APIs
import axios from 'axios' // Uses interceptor.ts defaults
// Response: {code: 20000, data: {...}}
```
### Comments (JSDoc Style)
```ts
/** 服务器类型 */
export interface ServerItem {
id: number
name: string
}
/** 获取服务器列表(分页) */
export const fetchServerList = (params?: ServerListParams) => {
return request.get<ServerListResponse>('/DC-Control/v1/servers', { params })
}
```
### API Choice
```ts
// Workspace-aware APIs (most ops APIs)
import { request } from '@/api/request'
request.post('/DC-Control/v1/servers', data)
// Standard Bearer token APIs
import axios from 'axios' // Uses interceptor.ts defaults
```
### useRequest Hook
```ts
// CORRECT - Use in setup scope
const { loading, response } = useRequest(fetchList.bind(null, params))
// WRONG - Use in async function
async function loadData() {
const { loading, response } = useRequest(fetchList()) // Doesn't work!
}
```
### Dynamic Routes
```ts
// Routes auto-loaded by permission guard
// Don't manually add routes except in menu config
// Don't modify isMenuLoading/isMenuLoaded flags
```
## Quick Reference
### Arco Design Components
- Table: `<a-table>` with columns, pagination, slots
- Form: `<a-form>` with `<a-form-item>` and validation
- Dialog: `<a-modal>` with v-model:visible
- Drawer: `<a-drawer>` for side panels
- Message: `Message.success/error/info/warning()`
- Modal: `Modal.confirm/info/error()`
### Common Slots
- `#toolbar-left` - Left toolbar buttons
- `#toolbar-right` - Right toolbar buttons
- `#actions` - Row actions column
- `#enabled` - Status toggle column
- `#{fieldName}` - Custom field rendering
### Import Order
1. Vue: `ref, reactive, computed, watch, onMounted`
2. Arco: `Message, Modal` + Icons
3. Components: `SearchTable, FormDialog`
4. Configs: `searchFormConfig, columns`
5. APIs: `fetchList, createItem, updateItem`
6. Types: `Item, ListParams, CreateData`
## Workflow
### Creating New Module
1. Run `/new-api {module}` to create API
2. Run `/new-page {module} {category}` to create page
3. Adjust columns and search-form configs
4. Customize FormDialog fields
5. Add Detail view content
6. Test with `pnpm dev`
### Adding Feature to Existing Module
1. Check existing patterns in similar modules
2. Add API function if needed
3. Update columns/search-form config
4. Add component or slot if needed
5. Run `pnpm lint` after changes
## Debugging
- Console logs prefixed with `[Permission Guard]` for route issues
- Network tab for API debugging
- Vue DevTools for state inspection
- `pnpm lint` for code issues

View File

@@ -0,0 +1,77 @@
<template>
<div class="detail-container">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="ID">{{ record.id }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ record.name }}</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">{{ record.description || '-' }}</a-descriptions-item>
<a-descriptions-item label="启用状态">
<a-tag :color="record.enabled ? 'green' : 'gray'">
{{ record.enabled ? '已启用' : '已禁用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatTime(record.created_at) }}</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ formatTime(record.updated_at) }}</a-descriptions-item>
</a-descriptions>
<div class="actions">
<a-space>
<a-button type="primary" @click="handleEdit">
<template #icon>
<icon-edit />
</template>
编辑
</a-button>
<a-button status="danger" @click="handleDelete">
<template #icon>
<icon-delete />
</template>
删除
</a-button>
</a-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { IconEdit, IconDelete } from '@arco-design/web-vue/es/icon'
import type { {{Module}}Item } from '@/api/ops/{{module}}'
const props = defineProps<{
record: {{Module}}Item
}>()
const emit = defineEmits<{
(e: 'edit'): void
(e: 'delete'): void
}>()
const formatTime = (time?: string) => {
if (!time) return '-'
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const handleEdit = () => {
emit('edit')
}
const handleDelete = () => {
emit('delete')
}
</script>
<style scoped lang="less">
.detail-container {
.actions {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--color-border);
}
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<a-modal
v-model:visible="visible"
:title="isEdit ? '编辑{{ModuleTitle}}' : '新增{{ModuleTitle}}'"
:width="600"
:mask-closable="false"
unmount-on-close
@ok="handleOk"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<a-form-item field="name" label="名称" required>
<a-input v-model="formData.name" placeholder="请输入名称" :max-length="100" />
</a-form-item>
<a-form-item field="description" label="描述">
<a-textarea v-model="formData.description" placeholder="请输入描述" :max-length="500" :auto-size="{ minRows: 3, maxRows: 5 }" />
</a-form-item>
<a-form-item field="enabled" label="启用状态">
<a-switch v-model="formData.enabled" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { {{Module}}Item, {{Module}}CreateData, {{Module}}UpdateData } from '@/api/ops/{{module}}'
import { create{{Module}}, update{{Module}} } from '@/api/ops/{{module}}'
const props = defineProps<{
visible: boolean
record: {{Module}}Item | null
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}>()
const formRef = ref()
const isEdit = computed(() => !!props.record)
const formData = ref<{{Module}}CreateData>({
name: '',
description: '',
enabled: true,
})
const rules = {
name: [
{ required: true, message: '请输入名称' },
{ max_length: 100, message: '名称不能超过100个字符' },
],
}
watch(
() => props.visible,
(val) => {
if (val && props.record) {
formData.value = {
name: props.record.name,
description: props.record.description || '',
enabled: props.record.enabled,
}
} else if (val) {
formData.value = {
name: '',
description: '',
enabled: true,
}
}
}
)
const handleOk = async () => {
try {
const valid = await formRef.value?.validate()
if (valid) return
if (isEdit.value && props.record) {
const res = await update{{Module}}(props.record.id, formData.value as {{Module}}UpdateData)
if (res && res.code === 0) {
Message.success('更新成功')
emit('success')
emit('update:visible', false)
}
} else {
const res = await create{{Module}}(formData.value)
if (res && res.code === 0) {
Message.success('创建成功')
emit('success')
emit('update:visible', false)
}
}
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败')
}
}
const handleCancel = () => {
emit('update:visible', false)
}
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,66 @@
import { request } from '@/api/request'
/** {{Module}}类型 */
export interface {{Module}}Item {
id: number
created_at: string
updated_at: string
name: string
description?: string
enabled: boolean
}
/** {{Module}}列表响应 */
export interface {{Module}}ListResponse {
total: number
page: number
page_size: number
data: {{Module}}Item[]
}
/** {{Module}}列表请求参数 */
export interface {{Module}}ListParams {
page?: number
size?: number
keyword?: string
enabled?: boolean
}
/** 创建{{Module}}请求参数 */
export interface {{Module}}CreateData {
name: string
description?: string
enabled?: boolean
}
/** 更新{{Module}}请求参数 */
export interface {{Module}}UpdateData {
name?: string
description?: string
enabled?: boolean
}
/** 获取{{Module}}列表(分页) */
export const fetch{{Module}}List = (params?: {{Module}}ListParams) => {
return request.get<{{Module}}ListResponse>('/DC-Control/v1/{{module}}s', { params })
}
/** 获取{{Module}}详情 */
export const fetch{{Module}}Detail = (id: number) => {
return request.get<{{Module}}Item>(`/DC-Control/v1/{{module}}s/${id}`)
}
/** 创建{{Module}} */
export const create{{Module}} = (data: {{Module}}CreateData) => {
return request.post<{ message: string; id: number }>('/DC-Control/v1/{{module}}s', data)
}
/** 更新{{Module}} */
export const update{{Module}} = (id: number, data: {{Module}}UpdateData) => {
return request.put<{ message: string }>(`/DC-Control/v1/{{module}}s/${id}`, data)
}
/** 删除{{Module}} */
export const delete{{Module}} = (id: number) => {
return request.delete<{ message: string }>(`/DC-Control/v1/{{module}}s/${id}`)
}

View File

@@ -0,0 +1,39 @@
export const columns = [
{
dataIndex: 'id',
title: 'ID',
width: 80,
slotName: 'id',
},
{
dataIndex: 'name',
title: '名称',
width: 150,
},
{
dataIndex: 'description',
title: '描述',
width: 200,
ellipsis: true,
tooltip: true,
},
{
dataIndex: 'enabled',
title: '启用状态',
width: 100,
slotName: 'enabled',
},
{
dataIndex: 'created_at',
title: '创建时间',
width: 180,
slotName: 'created_at',
},
{
dataIndex: 'actions',
title: '操作',
width: 180,
fixed: 'right' as const,
slotName: 'actions',
},
]

235
.kilo/templates/index.vue Normal file
View File

@@ -0,0 +1,235 @@
<template>
<div class="container">
<search-table
:form-model="formModel"
:form-items="formItems"
:data="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
title="{{ModuleTitle}}管理"
search-button-text="查询"
reset-button-text="重置"
@update:form-model="handleFormModelUpdate"
@search="handleSearch"
@reset="handleReset"
@page-change="handlePageChange"
@refresh="handleRefresh"
>
<template #toolbar-left>
<a-button type="primary" @click="handleAdd">
<template #icon>
<icon-plus />
</template>
新增
</a-button>
</template>
<template #id="{ record }">
{{ record.id }}
</template>
<template #enabled="{ record }">
<a-tag :color="record.enabled ? 'green' : 'gray'">
{{ record.enabled ? '已启用' : '已禁用' }}
</a-tag>
</template>
<template #created_at="{ record }">
{{ formatTime(record.created_at) }}
</template>
<template #actions="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleDetail(record)">
<template #icon>
<icon-eye />
</template>
详情
</a-button>
<a-button type="text" size="small" @click="handleEdit(record)">
<template #icon>
<icon-edit />
</template>
编辑
</a-button>
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
<template #icon>
<icon-delete />
</template>
删除
</a-button>
</a-space>
</template>
</search-table>
<FormDialog v-model:visible="formDialogVisible" :record="currentRecord" @success="handleFormSuccess" />
<a-drawer v-model:visible="detailVisible" :width="600" title="{{ModuleTitle}}详情" :footer="false" unmount-on-close>
<Detail v-if="currentRecord" :record="currentRecord" @edit="handleDetailEdit" @delete="handleDetailDelete" />
</a-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconPlus, IconEdit, IconDelete, IconEye } from '@arco-design/web-vue/es/icon'
import type { FormItem } from '@/components/search-form/types'
import SearchTable from '@/components/search-table/index.vue'
import FormDialog from './components/FormDialog.vue'
import Detail from './components/Detail.vue'
import { searchFormConfig } from './config/search-form'
import { columns as columnsConfig } from './config/columns'
import { fetch{{Module}}List, delete{{Module}}, type {{Module}}Item, type {{Module}}ListParams } from '@/api/ops/{{module}}'
const loading = ref(false)
const tableData = ref<{{Module}}Item[]>([])
const formDialogVisible = ref(false)
const detailVisible = ref(false)
const currentRecord = ref<{{Module}}Item | null>(null)
const formModel = ref({
keyword: '',
enabled: undefined as boolean | undefined,
})
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
})
const formItems = computed<FormItem[]>(() => searchFormConfig)
const columns = computed(() => columnsConfig)
const formatTime = (time?: string) => {
if (!time) return '-'
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const fetchData = async () => {
loading.value = true
try {
const params: {{Module}}ListParams = {
page: pagination.current,
size: pagination.pageSize,
keyword: formModel.value.keyword,
enabled: formModel.value.enabled,
}
const response: any = await fetch{{Module}}List(params)
if (response && response.code === 0 && response.details) {
tableData.value = response.details?.data || []
pagination.total = response.details?.total || 0
} else {
tableData.value = []
pagination.total = 0
}
} catch (error) {
console.error('获取列表失败:', error)
Message.error('获取列表失败')
tableData.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
fetchData()
}
const handleFormModelUpdate = (value: any) => {
formModel.value = value
}
const handleReset = () => {
formModel.value = {
keyword: '',
enabled: undefined,
}
pagination.current = 1
fetchData()
}
const handlePageChange = (current: number) => {
pagination.current = current
fetchData()
}
const handleRefresh = () => {
fetchData()
Message.success('数据已刷新')
}
const handleAdd = () => {
currentRecord.value = null
formDialogVisible.value = true
}
const handleEdit = (record: {{Module}}Item) => {
currentRecord.value = record
formDialogVisible.value = true
}
const handleDetail = (record: {{Module}}Item) => {
currentRecord.value = record
detailVisible.value = true
}
const handleDetailEdit = () => {
detailVisible.value = false
formDialogVisible.value = true
}
const handleDetailDelete = () => {
detailVisible.value = false
if (currentRecord.value) {
handleDelete(currentRecord.value)
}
}
const handleFormSuccess = () => {
fetchData()
}
const handleDelete = (record: {{Module}}Item) => {
Modal.confirm({
title: '确认删除',
content: `确认删除 "${record.name}" 吗?`,
onOk: async () => {
try {
const res = await delete{{Module}}(record.id)
if (res && res.code === 0) {
Message.success('删除成功')
fetchData()
}
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
},
})
}
fetchData()
</script>
<script lang="ts">
export default {
name: '{{ModuleName}}',
}
</script>
<style scoped lang="less">
.container {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,22 @@
import type { FormItem } from '@/components/search-form/types'
export const searchFormConfig: FormItem[] = [
{
field: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入名称',
span: 6,
},
{
field: 'enabled',
label: '启用状态',
type: 'select',
placeholder: '请选择启用状态',
options: [
{ label: '已启用', value: true },
{ label: '已禁用', value: false },
],
span: 6,
},
]