first commit

This commit is contained in:
liu
2026-07-13 15:23:29 +08:00
commit 50885a98c8
473 changed files with 33772 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
---
name: xinadmin-development
description: "TRIGGER when building a new CRUD module or feature in XinAdmin. Covers the full-stack development flow: database migrations, backend controller/model/form-request with AnnoRoute attribute routing, XinForm/XinTable frontend pages, and menu rules and permissions in seeder. Also activate when the user references AnnoRoute attribute routing, #[GetRoute]/#[PostRoute]/#[PutRoute]/#[DeleteRoute] attributes, XinAdmin controller patterns, or XinAdmin CRUD page development."
license: MIT
---
# XinAdmin Development
Best practices for building features in XinAdmin, organized by topic. Each rule teaches what to do and why.
## Consistency First
Before applying any rule, check what the application already does. XinAdmin has established patterns — the best choice is the one the codebase already uses, even if another pattern would be theoretically better.
Check sibling controllers, related pages, or existing seed data for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
## Quick Reference
### 1. CRUD Development Workflow → `rules/crud-workflow.md`
End-to-end flow for building a new feature in four phases:
- **Phase 1:** Database migration — `php artisan make:migration`
- **Phase 2:** Backend — Controller with AnnoRoute attributes, Eloquent model, FormRequest
- **Phase 3:** Frontend — Domain types, API wrappers, i18n, XinTable page component
- **Phase 4:** Menu routes, permission rules in `SysUserSeeder`, menu translations
### 2. AnnoRoute Attribute Routing → `rules/annoroute.md`
- `#[RequestAttribute]` on the controller class sets shared prefix and permission prefix
- `#[GetRoute]` / `#[PostRoute]` / `#[PutRoute]` / `#[DeleteRoute]` on methods declare HTTP routes
- `authorize` parameter controls access: `'query'` → ability `prefix.query`, `false` → public route
- `where` parameter for route parameter regex constraints
- Route registration via `AnnoRoute->register(path)` in ServiceProvider `boot()`
- Controller MUST have `#[RequestAttribute]` to be discovered
- Inherit `BaseController` for `success()` / `error()` response helpers
### 3. i18n Locale Conventions → `rules/i18n-conventions.md`
- Locale files mirror page paths: `pages/system/user.tsx``locales/zh_CN/system/user.ts`, prefix `system.user`
- `index.tsx` pages drop `/index`: `pages/ai/chat/index.tsx``locales/zh_CN/ai/chat.ts`, prefix `ai.chat`
- Shared component translations go in `components/` directory: `xin.form.*`, `xin.table.*`, `xin.crud.*`
- Layout translations go in `layout/` directory: `layout.*`
- Standalone files (`menu.ts`, `login.ts`) stay at locale root
- All keys dot-separated, double quotes, 2-space indent
## How to Apply
Always use a sub-agent to read rule files and explore this skill's content.
1. Identify what you're building (new CRUD → workflow + annoroute; routing only → annoroute)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Work through the phases in order — each builds on the last
@@ -0,0 +1,213 @@
# AnnoRoute Attribute Routing
AnnoRoute is a PHP 8 Attribute-based route registration module built into XinAdmin. Routes are declared via controller annotations, with automatic Sanctum authentication and permission verification integration.
## Core Concepts
### Route Composition
The final route URL is: `RequestAttribute.routePrefix` + `methodAttribute.route`
```php
#[RequestAttribute('/system/user', 'system.user')]
class SysUserController extends BaseController
{
#[GetRoute('/role', 'role')]
public function role(): JsonResponse { }
}
// Final route: GET /system/user/role
```
The final permission string is: `RequestAttribute.abilitiesPrefix` + `.` + `methodAttribute.authorize`
```php
// abilitiesPrefix = "system.user", authorize = "role"
// Final ability: "system.user.role"
```
### Route Registration
Routes are registered by scanning directories for `*Controller.php` files. In your ServiceProvider:
```php
use Modules\AnnoRoute\AnnoRoute;
public function boot(AnnoRoute $annoRoute): void
{
$annoRoute->register(base_path('modules/YourModule/Http/Controllers'));
}
```
The scanner reads each controller file, extracts namespace + class name, then uses reflection to find and register attributes. Only classes with `#[RequestAttribute]` are registered.
## Attribute Reference
### Class-Level: `#[RequestAttribute]`
Defines the shared prefix and auth configuration for all routes within the controller.
```php
use Modules\AnnoRoute\Attribute\RequestAttribute;
#[RequestAttribute(
routePrefix: '/admin/user',
abilitiesPrefix: 'admin.user',
middleware: 'log', // string or array — additional middleware for all routes
authGuard: 'admin', // optional — Sanctum guard provider
)]
class UserController { }
```
| Parameter | Type | Default | Description |
|-------------------|------------------|---------|--------------------------------------------------|
| `routePrefix` | `string` | `''` | URL prefix shared by all routes in this controller |
| `abilitiesPrefix` | `string` | `''` | Prefix for permission ability strings |
| `middleware` | `string\|array` | `''` | Additional middleware applied to every route |
| `authGuard` | `?string` | `null` | Sanctum auth guard provider name |
### Method-Level Attributes
#### `#[GetRoute]` / `#[PostRoute]` / `#[PutRoute]` / `#[DeleteRoute]`
All method attributes share an identical constructor signature:
```php
use Modules\AnnoRoute\Attribute\{GetRoute, PostRoute, PutRoute, DeleteRoute};
#[GetRoute(
route: '/{id}',
authorize: 'update',
middleware: 'throttle:10,1',
where: ['id' => '[0-9]+'],
)]
public function show(int $id): JsonResponse { }
```
| Parameter | Type | Default | Description |
|--------------|------------------|---------|----------------------------------------------------------------|
| `route` | `string` | `''` | Route path appended to the class `routePrefix` |
| `authorize` | `string\|bool` | `true` | Permission ability string; `false` disables auth entirely |
| `middleware` | `string\|array` | `''` | Route-specific middleware |
| `where` | `array` | `[]` | Regex constraints for route parameters, e.g. `['id' => '[0-9]+']` |
### Authorization Behavior
When `authorize` is not `false` or empty, these middleware are automatically added:
1. `auth:sanctum` — Sanctum authentication
2. `authGuard:{guard}` — guard check (or `authGuard` without a specific guard)
3. `abilities:{prefix}.{authorize}` — permission check
When `authorize` is `false`, no auth middleware is applied (public route).
## Usage Examples
### Basic CRUD Controller
```php
use Modules\AnnoRoute\Attribute\{RequestAttribute, GetRoute, PostRoute, PutRoute, DeleteRoute};
#[RequestAttribute('/system/dict', 'system.dict')]
class SysDictController extends BaseController
{
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
// GET /system/dict — ability: system.dict.query
return $this->success($data);
}
#[PostRoute(authorize: 'create')]
public function create(FormRequest $request): JsonResponse
{
// POST /system/dict — ability: system.dict.create
return $this->success();
}
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, FormRequest $request): JsonResponse
{
// PUT /system/dict/123 — ability: system.dict.update
return $this->success();
}
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
public function delete(int $id): JsonResponse
{
// DELETE /system/dict/123 — ability: system.dict.delete
return $this->success();
}
}
```
### Public Routes (No Auth)
```php
#[GetRoute('/public-data', authorize: false)]
public function publicData(): JsonResponse
{
return $this->success($data);
}
```
### Custom Route Path
When the method route is empty string (default), the controller routePrefix is the full route:
```php
#[RequestAttribute('/dashboard', 'dashboard')]
class DashboardController extends BaseController
{
#[GetRoute(authorize: 'index')]
public function index(): JsonResponse
{
// GET /dashboard — ability: dashboard.index
}
}
```
### Additional Middleware
```php
#[GetRoute('/export', 'export', middleware: 'throttle:5,1')]
public function export(): JsonResponse { }
#[PostRoute('/batch', 'batch', middleware: ['log', 'transaction'])]
public function batchProcess(): JsonResponse { }
```
### Route with Multiple Parameters
```php
#[GetRoute(
route: '/{deptId}/user/{userId}',
authorize: 'detail',
where: ['deptId' => '[0-9]+', 'userId' => '[0-9]+'],
)]
public function detail(int $deptId, int $userId): JsonResponse { }
```
## Key Rules
- Namespace: `Modules\AnnoRoute\Attribute\`
- Controller classes MUST use `#[RequestAttribute]` to be discovered; methods are only registered when the class has this attribute
- Method attributes: `GetRoute`, `PostRoute`, `PutRoute`, `DeleteRoute`
- Route registration: `AnnoRoute->register(path)` in ServiceProvider `boot()`
- Scanner only looks for `*Controller.php` files
- All auth middleware is auto-assembled — only declare `authorize` strings, not auth middleware directly
- Inherit `BaseController` for the `success()` / `error()` response helpers
- Controllers must return `Illuminate\Http\JsonResponse`
## Common Pitfalls
### Forgetting #[RequestAttribute] on the Class
If the class attribute is missing, no routes from that controller will be registered — regardless of method attributes.
### Duplicate Route Prefix
The final route is `routePrefix + route`. Convention is leading slash on both — they concatenate directly (no double slash).
### authorize vs abilitiesPrefix
The full permission string is `abilitiesPrefix.authorize`. If `abilitiesPrefix` is empty, the raw `authorize` value is used. Omitting `abilitiesPrefix` means you must pass the full ability string in each method's `authorize`.
@@ -0,0 +1,205 @@
# CRUD Development Workflow
The standard end-to-end workflow for building a new feature in XinAdmin follows four phases. Each phase builds on the last — always work in order.
## Phase 1: Database Migration
Create the database table structure.
```
php artisan make:migration create_xxx_table
```
- Define columns, indexes, and foreign keys in the migration file
- Use `$table->id()` for auto-increment or `$table->string('id', 36)->primary()` for UUIDs
- Use `constrained()` for foreign keys referencing other tables
- Run `php artisan migrate` to apply
## Phase 2: Backend — Controller, Model, FormRequest
### Controller
Create the controller in the appropriate module under `modules/{Module}/Http/Controllers/`. Use AnnoRoute attributes for routing and authorization:
```php
#[RequestAttribute('/system/xxx', 'system.xxx')]
class XxxController extends BaseController
{
protected array $searchField = ['name' => 'like'];
protected array $quickSearchField = ['name'];
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$perPage = (int) ($params['pageSize'] ?? 10);
$data = $this->buildSearch($params, Model::query())
->orderBy('id', 'desc')
->paginate($perPage);
return $this->success($data->toArray());
}
#[PostRoute(authorize: 'create')]
public function create(FormRequest $request): JsonResponse { }
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, FormRequest $request): JsonResponse { }
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
public function delete(int $id): JsonResponse { }
}
```
**Key points:**
- `#[RequestAttribute]` sets route prefix and permission prefix — AnnoRoute auto-registers routes (see `rules/annoroute.md`)
- Always use `->paginate($perPage)->toArray()` and pass to `$this->success()` — the PaginationProvider returns `{ data, total, pageSize, current }` which XinTable expects
- Use `$this->buildSearch()` for filter/keyword/sort query building
- Return `$this->success()` or `$this->error()` from BaseController
### Model
Create or reuse the Eloquent model:
```php
class XxxModel extends Model
{
protected $table = 'xxx';
protected $fillable = ['name', 'status', /* ... */];
protected $casts = ['status' => 'integer'];
}
```
### FormRequest
Create for validation on create/update:
```bash
php artisan make:request XxxFormRequest
```
Place in `modules/{Module}/Http/Requests/`. Define `rules()` and `messages()`. Inject via controller method parameter for auto-validation.
## Phase 3: Frontend — Page, Domain, API, i18n
### Domain (`web/domain/`)
TypeScript interfaces matching backend model fields:
```typescript
export interface IXxx {
id?: number;
name?: string;
status?: number;
created_at?: string;
updated_at?: string;
}
```
### API (`web/api/{module}/`)
Typed Axios wrappers for each endpoint. Use `createAxios` from `@/utils/request`:
```typescript
import createAxios from '@/utils/request';
export async function getList(params?: Record<string, any>) {
return createAxios<PaginatorData<IXxx>>({ url: '/system/xxx', method: 'get', params });
}
```
XinTable handles list/create/update/delete automatically — custom API functions are only needed for additional endpoints.
### i18n (`web/locales/{zh_CN,en_US}/`)
Translation keys for the page. Each feature gets its own file:
```typescript
export default {
'xxx.page.title': 'XXX Management',
'xxx.id': 'ID',
'xxx.name': 'Name',
// ...
};
```
Register in `web/locales/{zh_CN,en_US}/index.ts` by importing and spreading into the default export.
### Page (`web/pages/{module}/xxx/index.tsx`)
Use `<XinTable>` for standard CRUD list pages:
```tsx
import XinTable from '@/components/XinTable';
import type { XinTableColumn } from '@/components/XinTable/typings';
import type { IXxx } from '@/domain/xxx';
import { useTranslation } from 'react-i18next';
export default function XxxPage() {
const { t } = useTranslation();
const columns: XinTableColumn<IXxx>[] = [
{ title: t('xxx.id'), dataIndex: 'id', hideInForm: true, width: 80 },
{ title: t('xxx.name'), dataIndex: 'name', valueType: 'text',
rules: [{ required: true, message: t('xxx.name.required') }] },
];
return (
<>
<Title level={3}>{t('xxx.page.title')}</Title>
<XinTable<IXxx>
api="/system/xxx"
columns={columns}
rowKey="id"
accessName="system.xxx"
formProps={{ grid: true, colProps: { span: 12 }, layout: 'vertical' }}
modalProps={{ width: 800 }}
/>
</>
);
}
```
File-system routing auto-maps: `web/pages/system/xxx/index.tsx``/system/xxx`
## Phase 4: Menu Routes and Permissions
Add the menu entry in `database/seeders/SysUserSeeder.php` under the appropriate parent menu:
```php
[
'type' => "route",
'key' => "system.xxx",
'name' => "XXX Management",
"path" => "/system/xxx",
'local' => "menu.system.xxx",
'children' => [
['type' => 'rule', 'name' => '查询列表', 'key' => 'system.xxx.query'],
['type' => 'rule', 'name' => '新增', 'key' => 'system.xxx.create'],
['type' => 'rule', 'name' => '更新', 'key' => 'system.xxx.update'],
['type' => 'rule', 'name' => '删除', 'key' => 'system.xxx.delete'],
]
],
```
Add the menu translation key in `web/locales/{zh_CN,en_US}/menu.ts`:
```text
"menu.system.xxx": "XXX Management",
```
After seeding, the super admin role (role_id=1) automatically gets all permissions via the seeder's auto-assignment logic.
Run `php artisan db:seed --class=SysUserSeeder` to apply.
## Quick Checklist
1. Migration → `database/migrations/`
2. Controller → `modules/{Module}/Http/Controllers/` (with AnnoRoute attributes)
3. Model → `modules/{Module}/Models/` or vendor model
4. FormRequest → `modules/{Module}/Http/Requests/` (for create/update validation)
5. Domain types → `web/domain/xxx.ts`
6. API wrappers → `web/api/{module}/xxx.ts`
7. i18n files → `web/locales/{zh_CN,en_US}/xxx.ts` + register in `index.ts`
8. Page component → `web/pages/{module}/xxx/index.tsx`
9. Menu entry + rules → `database/seeders/SysUserSeeder.php`
10. Menu translation → `web/locales/{zh_CN,en_US}/menu.ts`
@@ -0,0 +1,101 @@
# i18n Conventions
Locale files must follow a consistent directory structure and naming convention. All keys are dot-separated, organized by module prefix matching the page path.
## Directory Structure
Locale files mirror page paths, with special directories for components and layout:
```
web/locales/zh_CN/ (en_US mirrors exactly)
├── index.ts # Aggregates all modules
├── menu.ts # Standalone files (no page path prefix)
├── login.ts
├── dashboard/ # dashboard/*.tsx pages
│ ├── analysis.ts
│ ├── monitor.ts
│ └── workplace.ts
├── system/ # system/*.tsx pages
│ ├── info.ts
│ ├── user.ts
│ ├── rule.ts
│ └── ...
├── ai/ # ai/*.tsx pages
│ ├── chat.ts
│ ├── conversation.ts
│ └── agent.ts
├── user/ # user/*.tsx pages
│ └── profile.ts
├── components/ # Shared component translations
│ ├── xin-form.ts
│ ├── xin-table.ts
│ └── xin-crud.ts
└── layout/ # Layout translations
└── layout.ts
```
## Key Naming Rules
### 1. Page translations follow page path
The key prefix equals the page file path (without extension, `/index` dropped):
| Page file | Locale file | Key prefix |
|-----------|-------------|------------|
| `pages/system/info.tsx` | `locales/zh_CN/system/info.ts` | `system.info` |
| `pages/system/user.tsx` | `locales/zh_CN/system/user.ts` | `system.user` |
| `pages/system/dict/index.tsx` | `locales/zh_CN/system/dict.ts` | `system.dict` |
| `pages/ai/chat/index.tsx` | `locales/zh_CN/ai/chat.ts` | `ai.chat` |
| `pages/dashboard/analysis.tsx` | `locales/zh_CN/dashboard/analysis.ts` | `dashboard.analysis` |
### 2. `index.tsx` pages drop "/index"
`pages/ai/chat/index.tsx` → file goes at `ai/chat.ts`, not `ai/chat/index.ts`.
### 3. Components go in `components/` directory
| Component | Locale file | Key prefix |
|-----------|-------------|------------|
| `XinForm` + sub-components | `components/xin-form.ts` | `xin.form` |
| `XinTable` + sub-components | `components/xin-table.ts` | `xin.table` |
| XinCrud shared keys | `components/xin-crud.ts` | `xin.crud` |
### 4. Layout goes in `layout/` directory
Layout translations are in `layout/layout.ts` with prefix `layout.*`.
### 5. Standalone files stay at root
Files without a page path prefix (like `menu.ts`, `login.ts`) stay at the root of the locale directory.
## File Format
```typescript
export default {
// Section comment (Chinese in zh_CN, English in en_US)
"prefix.page.title": "页面标题",
"prefix.field.name": "字段名",
"prefix.field.name.required": "字段名为必填项",
};
```
- **Quotes**: Always double quotes for keys and values
- **Indentation**: 2 spaces
- **Trailing commas**: Yes (after last entry)
- **Comments**: Section comments in the locale's own language
- **No `index.ts` files** inside subdirectories — only at the language root for aggregation
## Registering New Files
Import and spread new locale files in the language root `index.ts`:
```typescript
import moduleName from "./path/to/file";
export default {
...moduleName,
// ... other modules
};
```
Both `zh_CN/index.ts` and `en_US/index.ts` must be updated identically.