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,
},
]

155
AGENTS.md
View File

@@ -3,34 +3,187 @@
This file provides guidance to agents when working with code in this repository. This file provides guidance to agents when working with code in this repository.
## Build Commands ## Build Commands
- `pnpm dev` - Start dev server (config: `config/vite.config.dev.ts`) - `pnpm dev` - Start dev server (config: `config/vite.config.dev.ts`)
- `pnpm build` - Production build (config: `config/vite.config.prod.ts`) - `pnpm build` - Production build (config: `config/vite.config.prod.ts`)
- `pnpm lint` - Run ESLint + Prettier - `pnpm lint` - Run ESLint + Prettier
- `pnpm lint:eslint --fix` - Fix ESLint issues
- `pnpm lint:prettier --write` - Fix Prettier formatting
- No test framework configured - No test framework configured
## Critical Architecture Notes ## Critical Architecture Notes
### Vite Config Location ### Vite Config Location
Config files are in `config/` directory, NOT root. All vite commands reference `./config/vite.config.*.ts`. Config files are in `config/` directory, NOT root. All vite commands reference `./config/vite.config.*.ts`.
### Two Axios Instances ### Two Axios Instances
- [`src/api/request.ts`](src/api/request.ts) - Custom instance with workspace header support and `needWorkspace` param
- [`src/api/request.ts`](src/api/request.ts) - Custom instance with workspace header support and `needWorkspace` param. Returns `response.data` directly.
- [`src/api/interceptor.ts`](src/api/interceptor.ts) - Global instance with Bearer token and code 20000 validation - [`src/api/interceptor.ts`](src/api/interceptor.ts) - Global instance with Bearer token and code 20000 validation
- Use `request.ts` for most ops APIs that need workspace header
- Use `interceptor.ts` pattern for APIs expecting `code: 20000` response format
### API Response Format
Standard response from `request.ts`: `{code: 0, details: {data: [...], total: number}}`
- Success code is `0`, NOT `200` or `20000`
- Access list data via `response.details.data`
- Access total count via `response.details.total`
```ts
const response = await fetchList(params)
if (response && response.code === 0 && response.details) {
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
```
### Storage Pattern ### Storage Pattern
Use [`SafeStorage`](src/utils/safeStorage.ts) instead of localStorage directly. Supports TTL expiry and type-safe keys via `AppStorageKey` enum. Use [`SafeStorage`](src/utils/safeStorage.ts) instead of localStorage directly. Supports TTL expiry and type-safe keys via `AppStorageKey` enum.
```ts
// Set with optional TTL (milliseconds)
SafeStorage.set(AppStorageKey.TOKEN, token, 3600000)
// Get
SafeStorage.get(AppStorageKey.USER_INFO)
// Clear all app storage
SafeStorage.clearAppStorage()
```
### Dynamic Route Loading ### Dynamic Route Loading
Routes are loaded from server via [`fetchServerMenuConfig()`](src/store/modules/app/index.ts) with permission guard in [`permission.ts`](src/router/guard/permission.ts). Uses flags `isMenuLoading`/`isMenuLoaded` to prevent duplicate loads. Routes are loaded from server via [`fetchServerMenuConfig()`](src/store/modules/app/index.ts) with permission guard in [`permission.ts`](src/router/guard/permission.ts). Uses flags `isMenuLoading`/`isMenuLoaded` to prevent duplicate loads.
- Do NOT manually modify `isMenuLoading`/`isMenuLoaded` flags
- Routes are registered dynamically via `router.addRoute()`
- Menu data comes from `userPmn` API and transformed via `buildTree` + `transformMenuToRoutes`
### useRequest Hook Limitation ### useRequest Hook Limitation
[`useRequest()`](src/hooks/request.ts) does NOT work in async functions - it immediately invokes the API. Use `.bind(null, params)` to pass arguments. [`useRequest()`](src/hooks/request.ts) does NOT work in async functions - it immediately invokes the API. Use `.bind(null, params)` to pass arguments.
```ts
// CORRECT - Use in setup scope with bind
const { loading, response } = useRequest(fetchList.bind(null, { page: 1 }))
// WRONG - Use in async function
async function loadData() {
const { loading, response } = useRequest(fetchList()) // Won't work!
}
```
## Standard Module Structure
### Page Module Pattern
```
src/views/ops/pages/{category}/{module}/
├── index.vue # Main page component (SearchTable)
├── components/
│ ├── FormDialog.vue # Create/Edit form dialog
│ ├── Detail.vue # Detail drawer/view
│ └── QuickConfigDialog.vue # Optional quick config dialog
└── config/
├── columns.ts # Table columns configuration
└── search-form.ts # Search form configuration
```
### API Module Pattern
```
src/api/ops/{module}.ts
├── {Module}Item # Single item interface
├── {Module}ListResponse # List response interface
├── {Module}ListParams # Query parameters interface
├── {Module}CreateData # Create payload interface
├── {Module}UpdateData # Update payload interface
├── fetch{Module}List() # GET list with pagination
├── fetch{Module}Detail() # GET single item
├── create{Module}() # POST create
├── update{Module}() # PUT update
├── patch{Module}() # PATCH partial update
└── delete{Module}() # DELETE remove
```
### Component Template Pattern
```vue
<template>
<div class="container">
<!-- SearchTable with slots -->
</div>
</template>
<script lang="ts" setup>
// Import order: Vue → Third-party → Components → Configs → APIs → Types
import { ref, reactive, computed } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import SearchTable from '@/components/search-table/index.vue'
import FormDialog from './components/FormDialog.vue'
import { columns } from './config/columns'
import { fetchList } from '@/api/ops/module'
</script>
<script lang="ts">
export default { name: 'ModuleName' }
</script>
<style scoped lang="less"></style>
```
## Code Style ## Code Style
- No semicolons (Prettier enforced) - No semicolons (Prettier enforced)
- Single quotes, trailing commas (es5) - Single quotes, trailing commas (es5)
- Print width: 140 characters - Print width: 140 characters
- Path alias: `@/``src/` - Path alias: `@/``src/`
- **Add JSDoc comments** for interfaces, types, and functions
```ts
/** 服务器类型 */
export interface ServerItem { ... }
/** 获取服务器列表(分页) */
export const fetchServerList = (params?: ServerListParams) => { ... }
```
## Vue/i18n Aliases Required ## Vue/i18n Aliases Required
Vite config includes aliases for `vue-i18n/dist/vue-i18n.cjs.js` and `vue/dist/vue.esm-bundler.js` - don't remove these. Vite config includes aliases for `vue-i18n/dist/vue-i18n.cjs.js` and `vue/dist/vue.esm-bundler.js` - don't remove these.
## Key Components
- [`SearchTable`](src/components/search-table/index.vue) - Composite search + table component
- [`SearchForm`](src/components/search-form/index.vue) - Search form with dynamic fields
- [`DataTable`](src/components/data-table/index.vue) - Enhanced table with toolbar
- [`Chart`](src/components/chart/index.vue) - ECharts wrapper component
## Import Order
1. Vue core (ref, reactive, computed, watch, onMounted)
2. Third-party (Message, Modal, icons from Arco Design)
3. Global components (SearchTable, DataTable)
4. Local components (FormDialog, Detail)
5. Config files (columns, searchForm)
6. API functions (fetchList, createItem)
7. Type interfaces (Item, ListParams)
## API Response Handling
- `request.ts` returns `response.data` directly (no `.data.data` nesting)
- Success code: `0` (NOT `200` or `20000`)
- Access data: `response.details.data`
- Access total: `response.details.total`
- `interceptor.ts` expects `code: 20000` for success
- Token expiry codes: 50008, 50012, 50014 trigger logout (interceptor.ts)
- Status 401 or error "Token has expired" redirects to `/auth/login` (request.ts)
## Debug Tips
- Console logs prefixed with `[Permission Guard]` for route loading issues
- Check `isMenuLoading`/`isMenuLoaded` flags if routes not appearing
- Network tab for API debugging
- Vue DevTools for component state inspection
- Run `pnpm lint` after changes to catch issues

28
kilo.json Normal file
View File

@@ -0,0 +1,28 @@
{
"agents": {
"default": "code",
"allowed": ["code", "architect", "ask", "debug"]
},
"commands": {
"dir": ".kilo/command"
},
"rules": {
"dir": ".kilo/agent"
},
"skills": {
"dir": ".kilo/skill"
},
"hooks": {
"beforeFileWrite": "pnpm lint --fix",
"afterTaskComplete": "pnpm lint"
},
"context": {
"maxTokens": 8000,
"includePatterns": ["src/**/*.ts", "src/**/*.vue", "src/**/*.tsx"],
"excludePatterns": ["node_modules", "dist", ".git"]
},
"logging": {
"level": "info",
"file": ".kilo/logs/kilo.log"
}
}