# AGENTS.md

This file provides guidance to agents when working with code in this repository.

## Build Commands

- `pnpm dev` - Start dev server (config: `config/vite.config.dev.ts`)
- `pnpm build` - Production build (config: `config/vite.config.prod.ts`)
- `pnpm lint` - Run ESLint + Prettier
- `pnpm lint:eslint --fix` - Fix ESLint issues
- `pnpm lint:prettier --write` - Fix Prettier formatting
- No test framework configured

## Critical Architecture Notes

### Development Constraints

- Code is not deployed yet; do not spend effort preserving backward compatibility with old code.

### Vite Config Location

Config files are in `config/` directory, NOT root. All vite commands reference `./config/vite.config.*.ts`.

### Two Axios Instances

- [`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
- 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

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

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()`](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

- No semicolons (Prettier enforced)
- Single quotes, trailing commas (es5)
- Print width: 140 characters
- Path alias: `@/` → `src/`
- **Add JSDoc comments** for interfaces, types, and functions

  ```ts
  /** 服务器类型 */
  export interface ServerItem { ... }

  /** 获取服务器列表（分页） */
  export const fetchServerList = (params?: ServerListParams) => { ... }
  ```

## 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.

## 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
