Files
2026-08-31 11:33:59 +08:00

626 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: frontend-admin-gen
description: Generate Vue frontend pages for madong module. Supports both app (main project) and plugin targets. Creates index.vue with BasicCrud, schemas, and API service files following madong frontend conventions.
---
# Step 7: Generate Frontend Pages
Create Vue frontend pages and API service for module.
## File Locations
**App (主项目):**
```
frontend/admin/src/apps/{module}/
├── views/{model}/index.vue
├── views/{model}/schemas/index.tsx
├── api/{module}/index.ts
└── locales/lang/
├── zh-cn/{module}.json
└── en/{module}.json
```
**Plugin (插件):**
```
backend/plugin/{plugin}/resource/template/admin/
├── api/{module}/index.ts
├── lang/
│ ├── zh-cn/{module}.json
│ └── en/{module}.json
└── views/{plugin_key}/{model}/
├── index.vue
└── schemas/index.tsx
安装插件时会由 `core\plugin\traits\TemplateTrait` 将上述 `resource/template/admin/*`
整体复制到:
frontend/admin/src/apps/{plugin}/
├── api/{module}/index.ts
├── lang/
│ ├── zh-cn/{module}.json
│ └── en/{module}.json
└── views/{plugin_key}/{model}/
├── index.vue
└── schemas/index.tsx
```
插件菜单必须通过 `backend/plugin/{plugin}/resource/menu/admin.php` 在安装时导入系统菜单,
不要在插件前端模板中生成 `routes/index.ts` 作为侧边栏菜单入口,避免未安装插件也被前端自动扫描并显示导航。
插件菜单中的 `component` 应包含插件 key 目录段,确保与生成器输出路径一致,
例如页面为 `views/{plugin_key}/index/index.vue` 时填写 `/{plugin_key}/index/index`。
插件 API 文件应从 `@/utils/http` 导入默认请求对象,并使用 `request.get()`、`request.post()` 等方法;
不要从 `@/api/request` 默认导入,因为该文件只转发命名导出,不提供 default export。
如果插件页面直接使用 `$t('{plugin}.title')`、`$t('{plugin}.description')` 等根级 key,
语言文件应生成到 `lang/zh-cn/zh.json` 和 `lang/en/en.json`,文件内容包含 `{plugin}` 根对象;
不要只生成 `index.json`,否则 apps 语言加载器会把 key 包装为 `{plugin}.index.*`。
后端插件必须生成 `config/translation.php`,因为插件请求上下文中 `__()` 会读取 `plugin.{plugin}.translation`,
缺少该配置会导致 `Undefined array key "locale"`。
## i18n Key Pattern
| Target | Pattern | Example |
|--------|---------|---------|
| App | `{module}.{model}.*` | `ask.question.*` |
| Plugin | `{plugin}.{module}.{model}.*` | `official.ask.question.*` |
## index.vue Template
```vue
<template>
<div class="{model}-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from "@/components/crud";
import { crudSchema } from "./schemas";
import { $t } from "@/locales";
const [BasicCrud, crudApi] = useCrud({
...crudSchema(),
beforeFetch: (params: any) => {
return params;
},
// Custom table actions
tableActions: [
{
label: $t('{i18n_prefix}.{model}.actions.detail'),
type: "primary",
sort: -1,
link: true,
icon: "ant-design:eye-outlined",
auth: "{permission}:read",
onClick(e: Event, record: any) {},
},
],
// Custom dropdown actions
dropDownActions: [
{
label: $t('{i18n_prefix}.{model}.actions.edit'),
type: "primary",
sort: -1,
link: true,
icon: "ant-design:edit-outlined",
auth: "{permission}:update",
onClick(e: Event, record: any) {},
},
{
label: $t('{i18n_prefix}.{model}.actions.delete'),
type: "danger",
sort: -1,
link: true,
icon: "ant-design:delete-outlined",
auth: "{permission}:delete",
onClick(e: Event, record: any) {},
},
],
});
</script>
```
## schemas/index.tsx Template
```typescript
import { {Model}Service } from '@/apps/{app_name}/api/{module}'
import { DictEnum } from '@/apps/{app_name}/enums/dict-enum'
import { CrudSchema } from '@/components/crud'
import { formatDateTime } from '@/utils'
import { h } from 'vue'
import { ElTag } from 'element-plus'
import { $t } from '@/locales'
// CRUD配置
export const crudSchema = (): CrudSchema => {
return {
// 接口地址
api: {Model}Service.list,
dialogTitle: $t('{i18n_prefix}.{model}.dialog_title'),
crudApi: {
remove: {Model}Service.remove,
view: {Model}Service.get,
add: {Model}Service.create,
edit: {Model}Service.update,
},
useCrud: true,
hasAdd: true,
hasRemove: true,
hasEdit: true,
hasView: true,
addAuth: '{permission}:create',
removeAuth: '{permission}:delete',
editAuth: '{permission}:update',
viewAuth: '{permission}:read',
rowKey: 'id',
columns: [
{
type: 'selection'
},
{
prop: 'id',
label: $t('{i18n_prefix}.{model}.table.columns.id'),
align: 'center',
width: 80,
visible: false
},
{
prop: 'name',
label: $t('{i18n_prefix}.{model}.table.columns.name'),
align: 'left',
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'sort',
label: $t('{i18n_prefix}.{model}.table.columns.sort'),
align: 'center',
width: 100,
},
{
prop: 'status',
label: $t('{i18n_prefix}.{model}.table.columns.status'),
align: 'center',
width: 100,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_NORMAL_DISABLE
}
},
{
prop: 'created_at',
label: $t('{i18n_prefix}.{model}.table.columns.created_at'),
minWidth: 170,
align: 'center',
formatter: (row: any) => {
return formatDateTime(row.created_at)
}
},
],
// 当前项目 CrudSchema 列类型不支持 `type: 'operate'`,操作列由 BasicCrud 根据 tableActions/dropDownActions 自动生成。
dialogProps: {
width: '50%',
},
searchFormSchema: {
labelWidth: '100px',
schema: [
{
label: $t('{i18n_prefix}.{model}.search_form.name'),
prop: 'name',
component: 'Input',
colSpan: 6,
componentProps: {
placeholder: $t('{i18n_prefix}.{model}.search_form.name_placeholder')
}
},
{
label: $t('{i18n_prefix}.{model}.search_form.status'),
prop: 'status',
component: 'ApiDict',
colSpan: 6,
componentProps: {
code: DictEnum.SYS_NORMAL_DISABLE,
placeholder: $t('{i18n_prefix}.{model}.search_form.status_placeholder')
}
}
]
},
formSchema: {
labelWidth: 120,
gutter: 20,
schema: [
{
label: $t('{i18n_prefix}.{model}.form.name'),
prop: 'name',
component: 'Input',
componentProps: {
placeholder: $t('{i18n_prefix}.{model}.form.name_placeholder'),
maxlength: 50
},
colSpan: 24,
rules: [{ required: true, message: $t('{i18n_prefix}.{model}.validate.name_required') }]
},
{
label: $t('{i18n_prefix}.{model}.form.sort'),
prop: 'sort',
component: 'InputNumber',
componentProps: {
placeholder: $t('{i18n_prefix}.{model}.form.sort_placeholder'),
min: 0,
max: 9999
},
colSpan: 24,
},
{
label: $t('{i18n_prefix}.{model}.form.status'),
prop: 'status',
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_NORMAL_DISABLE
},
colSpan: 24,
rules: [{ required: true, message: $t('{i18n_prefix}.{model}.validate.status_required') }]
}
]
}
}
}
```
## API Service Template
Current project convention: prefer `BaseService` from `@/api/core/base` for standard CRUD endpoints and `request` from `@/utils/http` for custom methods. Do not use the old `@/support/http` string-only service style when extending the current admin project.
```typescript
import BaseService from '@/api/core/base'
import request from '@/utils/http'
/**
* {Model} API 服务
*/
const baseUrl = '/{api_prefix}'
export const {Model}Service = {
...BaseService<any>({ baseUrl }),
/**
* 自定义方法 - 设置状态
*/
setStatus: (id: number, status: number) => {
return request.post({ url: `${baseUrl}/${id}/status`, data: { status } })
},
}
export default {Model}Service
```
## Complete Example (App)
### File: frontend/admin/src/apps/ask/api/question/index.ts
```typescript
import http from "@/support/http";
export const QuestionService = {
list: '/ask/question',
get: '/ask/question/{id}',
create: '/ask/question',
update: '/ask/question/{id}',
remove: '/ask/question/{id}',
batchRemove: '/ask/question',
select: '/ask/question',
};
```
### i18n: frontend/admin/src/apps/ask/locales/lang/zh-cn/question.json
```json
{
"ask": {
"question": {
"dialog_title": "问题管理",
"table": {
"columns": {
"id": "ID",
"title": "标题",
"category_id": "分类",
"price": "价格",
"status": "状态",
"created_at": "创建时间"
}
},
"search_form": {
"title": "标题",
"title_placeholder": "请输入标题",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"title": "标题",
"title_placeholder": "请输入标题",
"category_id": "分类",
"price": "价格",
"status": "状态"
},
"actions": {
"detail": "详情",
"edit": "编辑",
"delete": "删除"
},
"validate": {
"title_required": "请输入标题"
}
}
}
}
```
## Complete Example (Plugin)
### File: frontend/admin/src/apps/official/api/question/index.ts
```typescript
import http from "@/support/http";
export const QuestionService = {
list: '/ask/question',
get: '/ask/question/{id}',
create: '/ask/question',
update: '/ask/question/{id}',
remove: '/ask/question/{id}',
batchRemove: '/ask/question',
select: '/ask/question',
};
```
### i18n: frontend/admin/src/apps/official/locales/lang/zh-cn/question.json
```json
{
"official": {
"ask": {
"question": {
"dialog_title": "问题管理",
"table": {
"columns": {
"id": "ID",
"title": "标题",
"category_id": "分类",
"price": "价格",
"status": "状态",
"created_at": "创建时间"
}
},
"search_form": {
"title": "标题",
"title_placeholder": "请输入标题",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"title": "标题",
"title_placeholder": "请输入标题",
"category_id": "分类",
"price": "价格",
"status": "状态"
},
"actions": {
"detail": "详情",
"edit": "编辑",
"delete": "删除"
},
"validate": {
"title_required": "请输入标题"
}
}
}
}
}
```
## Custom Form Component (自定义表单)
When you need custom form logic (e.g., rich text editor with upload, conditional fields), disable built-in CRUD forms and use custom form component:
### 1. Disable Built-in Forms in schemas/index.tsx
```typescript
export const crudSchema = (): CrudSchema => {
return {
// ... other config
useCrud: true,
hasAdd: false, // Disable built-in add
hasEdit: false, // Disable built-in edit
hasView: false, // Disable built-in view
hasRemove: true, // Keep built-in delete
formSchema: {}, // Empty form schema
// ...
}
}
```
### 2. Create Custom Form Component (form.vue)
```vue
<template>
<Dialog>
<BasicForm />
</Dialog>
</template>
<script setup lang="tsx">
import { FormItemSchema, useForm } from "@/components/form";
import { useDialog } from "@/components/dialog";
import { CrudMethods } from "@/components/crud";
import { ref, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { $t } from "@/locales";
const record = ref<any>({});
const crudApi = ref<CrudMethods>();
const formType = ref("add");
// Define form schema with JSON
const schema: FormItemSchema[] = [
{
label: $t('{i18n_prefix}.{model}.form.title'),
prop: "title",
component: "Input",
colSpan: 24,
rules: [{ required: true, message: "请输入标题" }],
},
{
label: $t('{i18n_prefix}.{model}.form.content'),
prop: "content",
component: "Editor", // Rich text editor
colSpan: 24,
componentProps: {
uploadConfig: {
server: `${import.meta.env.VITE_API_URL}/files/common/wangeditor`,
meta: { sub_dir: "content" },
},
},
},
];
const [BasicForm, formApi] = useForm({
labelPosition: "right",
labelWidth: "100px",
schema: schema,
});
const [Dialog, dialogApi] = useDialog({
title: $t('{i18n_prefix}.{model}.dialog_title'),
width: "80%",
dialogType: "drawer",
async onConfirm() {
formApi.validate().then(async () => {
const values = await formApi.getValues();
if (formType.value === "add") {
await {Model}Service.create(values);
crudApi?.value?.refreshCreate();
ElMessage.success("新增成功");
dialogApi.close();
} else if (formType.value === "edit") {
await {Model}Service.update(record.value.id, values);
crudApi?.value?.refreshCreate();
ElMessage.success("更新成功");
dialogApi.close();
}
});
},
});
defineExpose({
async show({ data, getCrudApi, type }: any) {
record.value = data;
formType.value = type || "add";
crudApi.value = getCrudApi();
dialogApi.open();
nextTick(() => {
formApi.setValues(record.value);
});
},
});
</script>
```
### 3. Use Custom Form in index.vue
```vue
<template>
<div class="{model}-page art-full-height">
<BasicCrud />
<Form ref="formRef" />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from "@/components/crud";
import { crudSchema } from "./schemas";
import Form from "./form.vue";
const formRef = ref<any>(null);
const [BasicCrud, crudApi] = useCrud({
...crudSchema(),
toolbarActions: [
{
label: "新增",
type: "primary",
auth: "{permission}:create",
dialogRef: formRef, // Reference to custom form
dialogParams() {
return {
data: {},
getCrudApi() { return crudApi; },
type: "add",
};
},
},
],
tableActions: [
{
label: "编辑",
type: "primary",
link: true,
auth: "{permission}:update",
dialogRef: formRef, // Reference to custom form
dialogParams(record: any) {
return {
data: record,
getCrudApi() { return crudApi; },
type: "edit",
};
},
},
],
});
</script>
```
## Column Component Types
| Component | Description |
|-----------|-------------|
| `ApiDict` | Dictionary select (uses DictEnum codes) |
| `ApiSelect` | API-based select dropdown |
| `Input` | Text input |
| `InputNumber` | Number input |
| `DatePicker` | Date picker |
| `DatePicker` + `componentProps.type = 'datetime'` | Date time picker |
| `Switch` | Toggle switch |
| `RadioGroup` | Radio group |
| `CheckboxGroup` | Checkbox group |
| `Upload` | File upload |
| `ImageUpload` | Image upload |
| `Editor` | Rich text editor |
## DictEnum Common Codes
| Code | Description |
|------|-------------|
| `DictEnum.SYS_NORMAL_DISABLE` | Status: Normal/Disable |
| `DictEnum.YES_NO` | Yes/No |
| `DictEnum.USER_SEX` | User gender |
## Auto-generation Checklist
When generating frontend:
- [ ] Create directory structure based on target (app/plugin)
- [ ] Generate index.vue with BasicCrud
- [ ] Generate schemas/index.tsx with CRUD config
- [ ] Generate API service file
- [ ] Generate i18n JSON files (zh-cn and en)
- [ ] Use $t() for all text
- [ ] Use proper i18n key pattern based on target
- [ ] Use proper DictEnum codes
- [ ] Configure proper permissions
### Custom Form Checklist (when using custom form)
- [ ] Set `hasAdd: false, hasEdit: false, hasView: false` in schemas
- [ ] Set `formSchema: {}` in schemas
- [ ] Create form.vue with `useForm` + `useDialog`
- [ ] Import custom Form in index.vue
- [ ] Pass `dialogRef: formRef` in toolbarActions/tableActions
- [ ] Add required i18n keys: `add_title`, `edit_title`, `detail_title`, `validate.*`, `tips.*`