编辑 | blame | 历史 | 原始文档

前端开发文档

本文档定义了项目的开发规范、组件复用指南和代码风格要求,确保团队开发风格统一。


一、开发规范

1.1 组件开发规范

组件命名

// ✅ 正确:组件文件使用 PascalCase
UserList.vue
DictTag.vue
TableAction.vue

// ❌ 错误:使用 kebab-case 或其他命名
user-list.vue
dict_tag.vue

组件定义

<script lang="ts" setup>
// ✅ 正确:统一使用 <script setup lang="ts">
defineOptions({ name: 'UserList' });

// Props 定义
interface Props {
  userId: number;
  userName?: string;
}

const props = withDefaults(defineProps<Props>(), {
  userName: '',
});

// Emits 定义
interface Emits {
  (e: 'update', value: string): void;
  (e: 'delete', id: number): void;
}

const emit = defineEmits<Emits>();
</script>

TypeScript 规范

// ✅ 正确:禁止使用 any,使用具体类型
interface UserInfo {
  id: number;
  name: string;
  email?: string;
}

function getUser(id: number): Promise<UserInfo> {
  return request.get(`/user/${id}`);
}

// ❌ 错误:使用 any
function getUser(id: any): any {
  return request.get(`/user/${id}`);
}

// ✅ 正确:使用 import type 导入类型
import type { UserInfo } from '#/api/system/user';

// ❌ 错误:普通导入类型
import { UserInfo } from '#/api/system/user';

1.2 目录结构规范

src/
├── api/              # API 接口定义,按模块划分
│   ├── system/       # 系统模块
│   │   ├── user/
│   │   │   └── index.ts
│   │   └── dept/
│   │       └── index.ts
│   └── ...
├── components/       # 公共组件(全局注册)
├── views/            # 页面组件,按业务模块划分
│   └── system/
│       └── user/
│           ├── index.vue        # 列表页
│           ├── data.ts          # 数据定义
│           └── modules/         # 子组件/弹窗
│               └── form.vue
├── locales/          # 国际化
├── router/           # 路由配置
├── stores/           # Pinia 状态管理
├── utils/            # 工具函数
└── adapter/          # 适配器配置

1.3 API 接口规范

// src/api/system/user/index.ts

import { request } from '#/utils/request';

export namespace SystemUserApi {
  /** 用户信息 */
  export interface User {
    id: number;
    username: string;
    nickname: string;
    email?: string;
    status: number;
    deptId?: number;
    createTime: string;
  }

  /** 用户分页查询参数 */
  export interface PageParams {
    pageNo: number;
    pageSize: number;
    username?: string;
    status?: number;
    deptId?: number;
  }

  /** 分页结果 */
  export interface PageResult {
    list: User[];
    total: number;
  }
}

/** 获取用户分页列表 */
export function getUserPage(params: SystemUserApi.PageParams) {
  return request.get<SystemUserApi.PageResult>('/system/user/page', { params });
}

/** 获取用户详情 */
export function getUser(id: number) {
  return request.get<SystemUserApi.User>(`/system/user/get?id=${id}`);
}

/** 创建用户 */
export function createUser(data: SystemUserApi.User) {
  return request.post('/system/user/create', data);
}

/** 更新用户 */
export function updateUser(data: SystemUserApi.User) {
  return request.put('/system/user/update', data);
}

/** 删除用户 */
export function deleteUser(id: number) {
  return request.delete(`/system/user/delete?id=${id}`);
}

1.4 样式规范

<template>
  <div class="user-list">
    <!-- 优先使用 Tailwind CSS 类 -->
    <div class="flex items-center justify-between p-4 mb-2 bg-white rounded-lg">
      <span class="text-base font-medium text-gray-900">标题</span>
    </div>
  </div>
</template>

<style lang="scss" scoped>
/* 组件样式使用 scoped */
.user-list {
  /* 颜色使用 CSS 变量 */
  background-color: var(--component-background);

  /* 必要时覆盖组件库样式 */
  :deep(.ant-table) {
    border-radius: 8px;
  }
}
</style>

二、核心组件复用指南

2.1 表格组件 (useVbenVxeGrid)

项目使用 VxeTable 封装的 useVbenVxeGrid 作为核心表格组件。

基础用法

<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';

import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
import { getUserPage } from '#/api/system/user';

// 表格列配置
const columns: VxeTableGridOptions['columns'] = [
  { type: 'checkbox', width: 50 },
  { field: 'username', title: '用户名', width: 120 },
  { field: 'nickname', title: '昵称', width: 120 },
  {
    field: 'status',
    title: '状态',
    width: 100,
    cellRender: {
      name: 'CellDict',
      props: { type: 'common_status' },
    },
  },
  { field: 'createTime', title: '创建时间', width: 180 },
  { field: 'action', title: '操作', width: 200, slots: { default: 'actions' } },
];

// 表格实例
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: [
      {
        fieldName: 'username',
        label: '用户名',
        component: 'Input',
      },
      {
        fieldName: 'status',
        label: '状态',
        component: 'Select',
        componentProps: {
          options: getDictOptions('common_status'),
        },
      },
    ],
  },
  gridOptions: {
    columns,
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getUserPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
  } as VxeTableGridOptions<SystemUserApi.User>,
});

// 刷新表格
function handleRefresh() {
  gridApi.query();
}
</script>

<template>
  <Grid table-title="用户列表">
    <template #actions="{ row }">
      <TableAction
        :actions="[
          {
            label: '编辑',
            type: 'link',
            icon: ACTION_ICON.EDIT,
            auth: ['system:user:update'],
            onClick: handleEdit.bind(null, row),
          },
          {
            label: '删除',
            type: 'link',
            danger: true,
            icon: ACTION_ICON.DELETE,
            auth: ['system:user:delete'],
            popConfirm: {
              title: `确认删除【${row.username}】吗?`,
              confirm: handleDelete.bind(null, row),
            },
          },
        ]"
      />
    </template>
  </Grid>
</template>

表格列渲染器

渲染器 用途 示例
CellImage 单图展示 cellRender: { name: 'CellImage' }
CellImages 多图展示 cellRender: { name: 'CellImages' }
CellLink 链接按钮 cellRender: { name: 'CellLink', props: { text: '查看' } }
CellTag 标签展示 cellRender: { name: 'CellTag', props: { color: 'blue' } }
CellDict 字典标签 cellRender: { name: 'CellDict', props: { type: 'common_status' } }
CellSwitch 开关切换 cellRender: { name: 'CellSwitch', attrs: { beforeChange } }
CellOperation 操作按钮 cellRender: { name: 'CellOperation', options: ['edit', 'delete'] }

2.2 表单弹窗 (useVbenModal)

<script lang="ts" setup>
import { useVbenModal } from '#/packages/effects/common-ui/src';
import Form from './modules/form.vue';

// 弹窗实例
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});

// 打开新增弹窗
function handleCreate() {
  formModalApi.setData(null).open();
}

// 打开编辑弹窗
function handleEdit(row: any) {
  formModalApi.setData(row).open();
}
</script>

<template>
  <FormModal @success="handleRefresh" />
</template>

表单组件示例:

<!-- modules/form.vue -->
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user';

import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { useVbenForm } from '#/adapter/form';
import { createUser, updateUser } from '#/api/system/user';

const emit = defineEmits(['success']);

const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const values = await formApi.getValues();
    if (formData.value?.id) {
      await updateUser({ ...values, id: formData.value.id });
    } else {
      await createUser(values);
    }
    emit('success');
    modalApi.close();
  },
});

const formData = ref<SystemUserApi.User>();

const [Form, formApi] = useVbenForm({
  schema: [
    {
      fieldName: 'username',
      label: '用户名',
      rules: 'required',
      component: 'Input',
    },
    {
      fieldName: 'nickname',
      label: '昵称',
      rules: 'required',
      component: 'Input',
    },
    {
      fieldName: 'email',
      label: '邮箱',
      rules: 'email',
      component: 'Input',
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'TreeSelect',
    },
  ],
});

// 监听弹窗打开
modalApi.onOpen((data) => {
  formData.value = data;
  if (data) {
    formApi.setValues(data);
  }
});
</script>

<template>
  <Modal title="用户信息">
    <Form />
  </Modal>
</template>

2.3 字典组件

DictTag - 字典标签

用于展示数据字典值的标签形式。

<script lang="ts" setup>
import DictTag from '#/components/dict-tag/dict-tag.vue';
</script>

<template>
  <DictTag type="common_status" :value="1" />
</template>

DictSelect - 字典选择器

用于表单中从字典选择值。

<script lang="ts" setup>
import DictSelect from '#/components/form-create/components/dict-select.vue';
</script>

<template>
  <!-- 下拉选择 -->
  <DictSelect v-model="form.status" dict-type="common_status" />

  <!-- 单选按钮 -->
  <DictSelect
    v-model="form.status"
    dict-type="common_status"
    select-type="radio"
  />

  <!-- 复选框 -->
  <DictSelect
    v-model="form.types"
    dict-type="common_status"
    select-type="checkbox"
  />
</template>

字典工具函数

import { getDictOptions, getDictLabel, getDictObj } from '#/packages/effects/hooks/src';

// 获取字典选项列表(用于 Select/Radio 等组件)
const options = getDictOptions('common_status', 'number');

// 获取字典标签文本
const label = getDictLabel('common_status', 1); // "开启"

// 获取字典完整对象
const dictObj = getDictObj('common_status', 1);
// { label: '开启', value: 1, colorType: 'success', cssClass: '' }

2.4 表格操作组件 (TableAction)

用于表格中的操作按钮,支持权限控制和二次确认。

<script lang="ts" setup>
import { TableAction, ACTION_ICON } from '#/adapter/vxe-table';
</script>

<template>
  <TableAction
    :actions="[
      {
        label: '编辑',
        type: 'link',
        icon: ACTION_ICON.EDIT,
        auth: ['system:user:update'],
        onClick: handleEdit,
      },
      {
        label: '删除',
        type: 'link',
        danger: true,
        icon: ACTION_ICON.DELETE,
        auth: ['system:user:delete'],
        popConfirm: {
          title: '确认删除吗?',
          confirm: handleDelete,
        },
      },
    ]"
    :drop-down-actions="[
      {
        label: '分配角色',
        auth: ['system:permission:assign-user-role'],
        onClick: handleAssignRole,
      },
    ]"
  />
</template>

2.5 文件上传组件

ImageUpload - 图片上传

<script lang="ts" setup>
import ImageUpload from '#/components/upload/image-upload.vue';

const imageUrl = ref('');
const imageList = ref<string[]>([]);
</script>

<template>
  <!-- 单图上传 -->
  <ImageUpload v-model="imageUrl" :max-number="1" />

  <!-- 多图上传 -->
  <ImageUpload v-model="imageList" :max-number="5" />

  <!-- 限制大小和格式 -->
  <ImageUpload
    v-model="imageUrl"
    :max-size="5"
    :accept="['.jpg', '.jpeg', '.png']"
  />
</template>

FileUpload - 文件上传

<script lang="ts" setup>
import FileUpload from '#/components/upload/file-upload.vue';

const fileList = ref<string[]>([]);
</script>

<template>
  <FileUpload
    v-model="fileList"
    :max-number="3"
    :max-size="10"
    :accept="['.pdf', '.doc', '.docx']"
  />
</template>

2.6 描述列表组件 (Description)

用于详情页展示数据。

<script lang="ts" setup>
import Description from '#/components/description/description.vue';

const data = {
  username: 'admin',
  nickname: '管理员',
  email: 'admin@example.com',
  status: 0,
  createTime: '2024-01-01 12:00:00',
};

const schema = [
  { field: 'username', label: '用户名' },
  { field: 'nickname', label: '昵称' },
  { field: 'email', label: '邮箱' },
  {
    field: 'status',
    label: '状态',
    render: (value) => h(DictTag, { type: 'common_status', value }),
  },
  { field: 'createTime', label: '创建时间' },
];
</script>

<template>
  <Description title="基本信息" :data="data" :schema="schema" />
</template>

三、状态管理

3.1 使用 Pinia Store

项目使用 Pinia 进行状态管理,Store 定义在 src/packages/stores/src 中。

常用 Store

Store 用途 导入路径
useUserStore 用户信息 #/packages/stores/src
useAccessStore 权限/Token #/packages/stores/src
useDictStore 数据字典 #/packages/stores/src

示例

import { useUserStore, useAccessStore } from '#/packages/stores/src';

const userStore = useUserStore();
const accessStore = useAccessStore();

// 获取用户信息
const userInfo = userStore.userInfo;

// 检查权限
const { hasAccessByCodes } = useAccess();
const canEdit = hasAccessByCodes(['system:user:update']);

四、权限控制

4.1 按钮权限

<script lang="ts" setup>
import { useAccess } from '#/packages/effects/access/src';

const { hasAccessByCodes } = useAccess();
</script>

<template>
  <!-- 方式一:通过 TableAction 的 auth 属性 -->
  <TableAction
    :actions="[
      {
        label: '编辑',
        auth: ['system:user:update'],
        onClick: handleEdit,
      },
    ]"
  />

  <!-- 方式二:通过 v-if 手动判断 -->
  <Button v-if="hasAccessByCodes(['system:user:create'])" type="primary">
    新增
  </Button>
</template>

4.2 路由权限

在路由配置中通过 meta.authorities 设置权限:

{
  path: 'user',
  name: 'SystemUser',
  component: () => import('#/views/system/user/index.vue'),
  meta: {
    title: '用户管理',
    authorities: ['system:user:visit'],
  },
}

五、国际化

5.1 使用方式

<script lang="ts" setup>
import { $t } from '#/locales';
</script>

<template>
  <Button>{{ $t('common.save') }}</Button>
  <span>{{ $t('ui.actionMessage.deleteSuccess', ['用户']) }}</span>
</template>

5.2 添加国际化文本

src/locales 目录下的语言文件中添加:

// src/locales/zh-CN.json
{
  "common": {
    "save": "保存",
    "cancel": "取消"
  },
  "ui": {
    "actionMessage": {
      "deleteSuccess": "删除【{0}】成功"
    }
  }
}

六、常用 Hooks

6.1 usePagination - 分页

import { usePagination } from '#/packages/effects/hooks/src';

const { page, pageSize, total, setPage, setTotal } = usePagination();

6.2 useTabs - 标签页操作

import { useTabs } from '#/packages/effects/hooks/src';

const { closeOtherTabs, closeTab, refreshTab } = useTabs();

// 关闭其他标签页
await closeOtherTabs();

// 刷新当前页
await refreshTab();

6.3 useWatermark - 水印

import { useWatermark } from '#/packages/effects/hooks/src';

const { updateWatermark, destroyWatermark } = useWatermark();

// 设置水印
await updateWatermark({
  content: '用户名',
});

// 销毁水印
destroyWatermark();

七、工具函数

7.1 常用工具

import {
  formatDateTime,      // 日期格式化
  formatDate,          // 日期格式化(不含时间)
  downloadFileFromBlobPart, // 文件下载
  isEmpty,             // 判空
  isFunction,          // 判断函数
  isString,            // 判断字符串
} from '#/packages/utils/src';

// 日期格式化
formatDateTime('2024-01-01T12:00:00'); // "2024-01-01 12:00:00"

// 文件下载
downloadFileFromBlobPart({
  fileName: '用户列表.xlsx',
  source: blobData,
});

// 判空
isEmpty([]); // true
isEmpty({}); // true
isEmpty(null); // true

7.2 数字格式化

import {
  erpNumberFormatter,    // 数字格式化
  fenToYuan,             // 分转元
  formatFileSize,        // 文件大小格式化
} from '#/packages/utils/src';

// 保留两位小数
erpNumberFormatter(1234.567, 2); // "1,234.57"

// 分转元
fenToYuan(12345); // 123.45

// 文件大小
formatFileSize(1024 * 1024); // "1.00 MB"

八、代码示例:标准 CRUD 页面

<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';

import { ref } from 'vue';

import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src';

import { message } from 'ant-design-vue';

import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
import {
  getUserPage,
  deleteUser,
  createUser,
  updateUser,
  exportUser,
} from '#/api/system/user';
import { $t } from '#/locales';

import Form from './modules/form.vue';

// ========== 弹窗定义 ==========
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});

// ========== 表格定义 ==========
const checkedIds = ref<number[]>([]);

const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: [
      {
        fieldName: 'username',
        label: '用户名',
        component: 'Input',
        componentProps: { placeholder: '请输入用户名' },
      },
      {
        fieldName: 'status',
        label: '状态',
        component: 'Select',
        componentProps: {
          options: getDictOptions('common_status', 'number'),
          placeholder: '请选择状态',
        },
      },
    ],
  },
  gridOptions: {
    columns: [
      { type: 'checkbox', width: 50 },
      { field: 'username', title: '用户名', width: 120 },
      { field: 'nickname', title: '昵称', width: 120 },
      {
        field: 'status',
        title: '状态',
        width: 100,
        cellRender: { name: 'CellDict', props: { type: 'common_status' } },
      },
      { field: 'createTime', title: '创建时间', width: 180 },
      { field: 'action', title: '操作', width: 180, slots: { default: 'actions' } },
    ],
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getUserPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SystemUserApi.User>,
  gridEvents: {
    checkboxAll: ({ records }) => {
      checkedIds.value = records.map((item) => item.id!);
    },
    checkboxChange: ({ records }) => {
      checkedIds.value = records.map((item) => item.id!);
    },
  },
});

// ========== 操作方法 ==========
function handleRefresh() {
  gridApi.query();
}

async function handleExport() {
  const data = await exportUser(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '用户.xls', source: data });
}

function handleCreate() {
  formModalApi.setData(null).open();
}

function handleEdit(row: SystemUserApi.User) {
  formModalApi.setData(row).open();
}

async function handleDelete(row: SystemUserApi.User) {
  await deleteUser(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', [row.username]));
  handleRefresh();
}

async function handleDeleteBatch() {
  await confirm($t('ui.actionMessage.deleteBatchConfirm'));
  // ... 批量删除逻辑
}
</script>

<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />

    <Grid table-title="用户列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['用户']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['system:user:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['system:user:export'],
              onClick: handleExport,
            },
            {
              label: $t('ui.actionTitle.deleteBatch'),
              type: 'primary',
              danger: true,
              icon: ACTION_ICON.DELETE,
              disabled: isEmpty(checkedIds),
              auth: ['system:user:delete'],
              onClick: handleDeleteBatch,
            },
          ]"
        />
      </template>

      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['system:user:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['system:user:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.username]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>

九、注意事项

  1. 路径别名#/* 指向 ./src/*,导入时使用 #/components/xxx 而非相对路径
  2. 表单验证:使用 VeeValidate + Zod,在 schema 中通过 rules 属性定义
  3. HTML 安全:用户输入的 HTML 使用 v-dompurify-html 处理
  4. 敏感数据:使用 secure-ls 加密存储
  5. 代码注释:只在 WHY 不明显时添加注释,避免无意义的注释
  6. 组件命名:必须使用 defineOptions({ name: 'ComponentName' }) 定义组件名

十、相关资源