--- 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 ``` ## 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({ 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 ``` ### 3. Use Custom Form in index.vue ```vue ``` ## 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.*`