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