first commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# XinAdmin
|
||||
|
||||
XinAdmin is a full-stack development framework: PHP8.2 + Laravel12 + React19 + TypeScript + Ant Design6 + Zustand + Tailwind CSS4. Licensed under MIT, free for commercial use without authorization.
|
||||
|
||||
## Built-in Features
|
||||
|
||||
- Dashboard: Echarts-based dashboards with demo pages
|
||||
- Administrators: Backend user management with groups, permissions, and settings
|
||||
- Role & Department Management: Role-based menu permission control, enterprise org structure
|
||||
- System Settings: Visual form-based server variable configuration
|
||||
- File Management: Backend file manager with folders, multi-select, grouping
|
||||
- Dictionary Management: Maintenance of frequently used static data
|
||||
- Mail & Storage Configuration: Visual config and testing for Laravel mail/filesystem
|
||||
- AI Configuration: Visual config and testing for Laravel AI SDK
|
||||
- Frontend Members: Permission, grouping, lists, balance records
|
||||
|
||||
# AnnoRoute
|
||||
|
||||
AnnoRoute is a PHP 8 Attribute-based route registration module. Routes are declared via controller annotations — no manual route files needed. Sanctum auth and permission verification are auto-integrated.
|
||||
|
||||
## Usage
|
||||
|
||||
- `#[GetRoute]` / `#[PostRoute]` / `#[PutRoute]` / `#[DeleteRoute]` on methods declare HTTP routes
|
||||
- The `authorize` parameter controls access: `'query'` becomes ability `prefix.query`, `false` makes a route public
|
||||
- `#[RequestAttribute]` params: `routePrefix`, `abilitiesPrefix`, `middleware` (string|array), `authGuard` (?string)
|
||||
- Method attributes share: `route` (path appended to prefix), `authorize` (string|bool), `middleware`, `where` (regex array)
|
||||
- Final route = `routePrefix + route`; final ability = `abilitiesPrefix + '.' + authorize`
|
||||
- When `authorize` is not falsy, `auth:sanctum` + `authGuard` + `abilities:` middleware are auto-assembled
|
||||
- Extend `Modules\Common\Http\Controllers\BaseController` for `success()` / `error()` response helpers
|
||||
|
||||
# Frontend
|
||||
|
||||
Source lives in `web/`. Bundled with Vite, outputting to `public/`. Package manager: pnpm.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `web/api/` | Typed Axios wrappers per backend module |
|
||||
| `web/components/` | Reusable UI: `AuthButton`, `DictTag`, `IconFont`, `XinForm`, `XinTable` |
|
||||
| `web/domain/` | TypeScript interfaces for API models |
|
||||
| `web/hooks/` | `useAuth`, `useLanguage`, `useMobile`, `useRequest` |
|
||||
| `web/layout/` | Layout engine (4 modes), menu, header, breadcrumbs, theme |
|
||||
| `web/locales/` | i18n (i18next), zh_CN + en_US |
|
||||
| `web/pages/` | Auto-discovered page components (file-system routing) |
|
||||
| `web/router/` | React Router v7 `createBrowserRouter` |
|
||||
| `web/stores/` | Zustand: `global` (app/theme), `user` (auth/perms), `dict` (cache) |
|
||||
| `web/utils/` | Axios instance with dedup, auth headers, error handling |
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Path alias `@/` → `web/`
|
||||
- Zustand stores: `State` + `Actions` → `persist` + `devtools` → localStorage
|
||||
- Access stores via selector: `useXxxStore(state => state.field)`
|
||||
- All user-facing text via `useTranslation()` (react-i18next)
|
||||
- Locale files mirror page paths: `pages/system/user.tsx` → `locales/zh_CN/system/user.ts`, key prefix `system.user`
|
||||
- All keys dot-separated, double quotes, 2-space indent. See `@skill:xinadmin-development` for details
|
||||
- Permission checks: `<AuthButton auth="system.user.create">` or `useAuth().auth('permission')`
|
||||
- HTTP client auto-attaches `Authorization: Bearer`, `User-Language`, handles 401 auto-logout
|
||||
- Pages in `web/pages/` are auto-routed; `index.tsx` maps to parent directory; root `/` → `/dashboard/analysis`
|
||||
- Pages outside layout: add to `excludePaths` array in router config
|
||||
|
||||
# Antd
|
||||
|
||||
Ant Design 6 is the UI component library. Components are imported from `antd` and themed via `<ConfigProvider>` with tokens from `web/layout/theme.ts`.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Use `antd` MCP tools (`antd_info`, `antd_doc`, `antd_demo`) to verify component APIs before writing code
|
||||
- Never use deprecated props or components — check with `antd_changelog` when upgrading or referencing older examples
|
||||
- Theme tokens flow: `web/layout/theme.ts` → `<ConfigProvider theme={...}>` → Ant Design components
|
||||
- Common components: `Table`, `Form`, `Modal`, `Drawer`, `Button`, `Input`, `Select`, `DatePicker`, `Switch`, `Tag`, `Card`, `App` ...
|
||||
|
||||
# Layout
|
||||
|
||||
Wraps authenticated pages. Supports 4 modes set via global store: `side` (default), `top`, `mix`, `columns`.
|
||||
|
||||
Menus are fetched from `/system/menu` and stored in `LayoutContext` (React Context). Menu type: `'menu'` (folder), `'route'` (page), `'rule'` (perm-only). Server filters by user role — no client-side filtering needed. Labels support i18n via `node.local`.
|
||||
|
||||
Theme tokens (20+ properties) are managed in `web/layout/theme.ts`, applied via Ant Design `<ConfigProvider>`, persisted to localStorage under `global-storage`.
|
||||
|
||||
# XinForm And XinTable
|
||||
|
||||
Two declarative JSON-driven CRUD components. Define columns once with metadata — the same definition drives table display, search form, and create/edit forms.
|
||||
|
||||
## XinForm
|
||||
|
||||
Use for settings/config pages or standalone forms. Supports 3 layout modes: `'Form'` (inline), `'ModalForm'`, `'DrawerForm'`.
|
||||
|
||||
```tsx
|
||||
<XinForm
|
||||
columns={[
|
||||
{ dataIndex: 'username', title: 'Username', valueType: 'text', rules: [{ required: true }] },
|
||||
{ dataIndex: 'role_id', title: 'Role', valueType: 'select', fieldProps: { options: roleOptions } },
|
||||
]}
|
||||
layoutType="ModalForm"
|
||||
grid
|
||||
trigger={<Button type="primary">New</Button>}
|
||||
modalProps={{ title: 'Create User', width: 600 }}
|
||||
onFinish={async (values) => { await save(values); return true; }}
|
||||
/>
|
||||
```
|
||||
|
||||
Key `FormColumn` fields: `dataIndex` (supports nested paths `['a','b']`), `valueType` (26 types: `text`, `password`, `select`, `date`, `switch`, etc.), `fieldProps`, `fieldRender` (custom render), `dependency` (field linkage: `{ dependencies, visible?, disabled?, fieldProps? }`), `hideIn*` visibility flags.
|
||||
|
||||
`formRef` exposes `open()`, `close()`, `isOpen()`, `setLoading()` plus all Ant Design `FormInstance` methods.
|
||||
|
||||
## XinTable
|
||||
|
||||
Use for standard CRUD pages (list + create + update + delete). Auto-handles API calls, permissions, search, and toolbar.
|
||||
|
||||
```tsx
|
||||
<XinTable<ISysUser>
|
||||
api="/system/user"
|
||||
columns={[
|
||||
{ title: 'ID', dataIndex: 'id', hideInForm: true, width: 80 },
|
||||
{ title: 'Username', dataIndex: 'username', valueType: 'text', rules: [{ required: true }] },
|
||||
{ title: 'Status', dataIndex: 'status', valueType: 'radio', render: (v) => <Tag>{v === 1 ? 'Active' : 'Inactive'}</Tag> },
|
||||
]}
|
||||
rowKey="id"
|
||||
accessName="system.user"
|
||||
formProps={{ grid: true, colProps: { span: 12 }, layout: 'vertical' }}
|
||||
modalProps={{ width: 800 }}
|
||||
/>
|
||||
```
|
||||
|
||||
Required props: `api` (REST endpoint), `accessName` (permission prefix), `rowKey` (PK field), `columns`.
|
||||
|
||||
Default REST behavior — `GET {api}` for list, `POST {api}` for create, `PUT {api}/{id}` for update, `DELETE {api}/{id}` for delete. Add/edit/delete buttons auto-wrapped in `<AuthButton auth={accessName + '.create'}>`.
|
||||
|
||||
Customize with: `handleRequest` (full custom fetch), `requestParams` (transform before send), `handleFinish` (custom submit), `actionBarRender` / `toolBarRender` / `operateRender` (slot overrides).
|
||||
|
||||
## Choosing Between Them
|
||||
|
||||
- **XinTable**: Full CRUD pages (users, roles, dicts, files)
|
||||
- **XinForm** (inline/ModalForm): Settings pages with a single form (mail, storage, AI config)
|
||||
- **XinForm** (ModalForm + trigger): Add/edit without a table (dept management)
|
||||
- Use `hideInForm` / `hideInTable` / `hideInSearch` to control per-context visibility
|
||||
|
||||
# Development Workflow
|
||||
|
||||
When building a new CRUD feature, follow this four-phase workflow. See `@skill:xinadmin-development` for complete details.
|
||||
|
||||
1. **Database Migration** — create table structure, indexes, foreign keys
|
||||
2. **Backend** — Controller (AnnoRoute attributes), Model, FormRequest
|
||||
3. **Frontend** — Page (file-system routing), Domain types, API wrappers, i18n
|
||||
4. **Menu & Permissions** — seeder menu entry + rules, menu translation keys
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user