Skip to content

扩展开发指南

本章给出一条完整的二次开发路径。目标不是限制业务设计,而是保证新增模块继续使用项目已有的数据、权限、表格、主题和工程基础设施,避免项目维护半年后出现多套互不兼容的写法。

开始前先做四个决定

新增模块前确认:

  1. 业务边界:模块负责什么,不负责什么。
  2. 数据来源:先使用预置数据评审,还是直接连接后端。
  3. 权限模型:哪些角色可见,包含哪些按钮权限,是否存在数据范围。
  4. 路由结构:一级菜单、列表、详情和隐藏页面如何组织。

如果这些问题尚未明确,不要先复制大量页面。页面结构一旦扩散,后续统一字段、权限和路由成本会更高。

推荐目录

以客户管理为例:

text
src/
├── views/customer/
│   ├── list/
│   │   ├── index.vue
│   │   └── modules/
│   │       ├── customer-search.vue
│   │       └── customer-dialog.vue
│   └── detail/
│       └── index.vue
├── api/customer.ts              # 已接真实接口时使用
├── mock/customer/               # 评审阶段需要预置数据时使用
├── types/api/customer.d.ts
├── router/modules/customer.ts
└── locales/                     # 增加中英文菜单与页面文案

不必同时创建 apimock 两套完整实现。根据当前阶段选择需要的目录,迁移时保持函数契约即可。

第一步:定义类型

先定义页面真正需要的类型:

ts
declare namespace Api {
  namespace Customer {
    interface SearchParams extends Api.Common.PaginationParams {
      keyword?: string
      status?: 'active' | 'disabled'
    }

    interface ListItem {
      id: number
      name: string
      status: 'active' | 'disabled'
      createdAt: string
    }

    interface SaveParams {
      name: string
      status: 'active' | 'disabled'
    }
  }
}

类型应描述前端使用的稳定契约,不要把数据库表结构原样复制到浏览器。敏感字段和页面不使用的内部字段不应进入响应。

第二步:准备数据函数

直接接后端

ts
// src/api/customer.ts
import request from '@/utils/http'

export function fetchCustomerList(params: Api.Customer.SearchParams) {
  return request.get<Api.Common.PaginatedResponse<Api.Customer.ListItem>>({
    url: '/api/v1/customers',
    params
  })
}

export function createCustomer(data: Api.Customer.SaveParams) {
  return request.post<Api.Customer.ListItem>({
    url: '/api/v1/customers',
    data,
    showSuccessMessage: true
  })
}

先使用预置数据

src/mock/customer 中只实现页面需要的查询和筛选,返回深拷贝分页结果。写操作继续使用 showDemoActionNotice,不要模拟生产保存。

第三步:创建列表页

优先参考 src/views/system/user

  • 页面根节点使用 art-full-height
  • 搜索组件放在页面 modules
  • 表格内容使用 ElCard.art-table-card
  • 使用 ArtTableHeaderArtTableuseTable
  • 新增和编辑表单放在独立弹窗组件。
ts
const table = useTable({
  core: {
    apiFn: fetchCustomerList,
    apiParams: {
      current: 1,
      size: 20
    },
    columnsFactory: () => [
      { type: 'index', width: 60, label: '序号' },
      { prop: 'name', label: '客户名称' },
      { prop: 'status', label: '状态' },
      { prop: 'operation', label: '操作', width: 120, fixed: 'right' }
    ]
  }
})

第四步:注册路由

ts
// src/router/modules/customer.ts
const customerRoutes: AppRouteRecord = {
  path: '/customer',
  name: 'Customer',
  component: '/index/index',
  meta: {
    title: 'menus.customer.title',
    icon: 'ri:customer-service-2-line',
    roles: ['R_ADMIN', 'R_SALES']
  },
  children: [
    {
      path: 'list',
      name: 'CustomerList',
      component: '/customer/list',
      meta: {
        title: 'menus.customer.list',
        permissionPrefix: 'customer:list',
        authList: [
          { title: '新增客户', authMark: 'add' },
          { title: '编辑客户', authMark: 'edit' },
          { title: '删除客户', authMark: 'delete' }
        ]
      }
    },
    {
      path: 'detail/:id',
      name: 'CustomerDetail',
      component: '/customer/detail',
      meta: {
        title: 'menus.customer.detail',
        isHide: true
      }
    }
  ]
}

远程菜单模式下,需要把同样的结构同步到菜单接口。隐藏详情页也必须返回,否则动态路由无法注册。

第五步:接入按钮权限

模板中:

vue
<ElButton v-auth="'add'" type="primary">新增客户</ElButton>

脚本中:

ts
const { hasAuth } = useAuth()

const canEdit = computed(() => hasAuth('edit'))

后端用户接口返回完整权限码:

json
{
  "buttons": [
    "customer:list:add",
    "customer:list:edit"
  ]
}

业务 API 仍要在服务端重新判断权限和资源归属。

第六步:国际化与品牌文案

新增:

  • 中文菜单 key。
  • 英文菜单 key。
  • 页面固定文案。
  • 空状态、校验和错误提示。

客户可在后台修改的名称、Logo 和欢迎语应进入站点配置,不要写死在语言包或页面组件中。

第七步:处理完整页面状态

专业页面不能只验证“有数据且接口成功”。至少覆盖:

  • 首次加载。
  • 空列表。
  • 搜索无结果。
  • 慢请求。
  • 网络失败和服务端错误。
  • 无权限。
  • 表单校验失败。
  • 重复提交。
  • 删除确认和取消。
  • 超长文本。
  • 移动端与窄屏。
  • 亮色与暗色。

接入已有后端的适配原则

在 API 层适配字段

后端字段与页面模型不一致时,优先在对应 API 模块中转换:

ts
export async function fetchCustomerList(params: Api.Customer.SearchParams) {
  const result = await request.get<BackendCustomerPage>({
    url: '/legacy/customer/query',
    params: {
      pageNum: params.current,
      pageSize: params.size,
      keyword: params.keyword
    }
  })

  return {
    records: result.rows.map(mapBackendCustomer),
    total: result.total,
    current: params.current,
    size: params.size
  }
}

不要让页面到处判断后端字段或业务码。

只在必要时修改全局请求层

只有所有接口都共享同一种响应结构、Token 规则或错误格式时,才修改 src/utils/http。单个业务域的特殊行为应保留在该域 API 中,不要为此把接口移动到 Service。

代码质量与提交

开发中建议:

bash
pnpm fix
pnpm build

提交前至少执行与改动范围匹配的检查。准备交付时执行:

bash
pnpm release:check

新增动态 HTML 时使用 ArtSafeHtml;新增本地存储时使用项目存储工具和命名空间;不要调用 localStorage.clear()

完成检查清单

  • 目录、命名和类型与现有业务域一致。
  • 查询、分页、搜索和重置行为正确。
  • 写操作不会重复提交,失败时保留输入。
  • 权限账号和无权限账号都已验证。
  • URL 直达详情页与刷新正常。
  • local 与 remote 菜单模式符合项目需求。
  • 中英文、亮暗主题和移动端正常。
  • 不含密钥、真实客户数据和伪持久化逻辑。
  • pnpm release:check 通过。

开发过程中应同时遵循Hooks 与组合式能力通用组件代码规范。完成开发后,按构建部署准备生产交付。

根据 MIT 许可证发布