626 lines
16 KiB
Markdown
626 lines
16 KiB
Markdown
---
|
||||
|
|
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.*`
|