本文档定义了项目的开发规范、组件复用指南和代码风格要求,确保团队开发风格统一。
// ✅ 正确:组件文件使用 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>
// ✅ 正确:禁止使用 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';
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/ # 适配器配置
// 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}`);
}
<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>
项目使用 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'] } |
<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>
用于展示数据字典值的标签形式。
<script lang="ts" setup>
import DictTag from '#/components/dict-tag/dict-tag.vue';
</script>
<template>
<DictTag type="common_status" :value="1" />
</template>
用于表单中从字典选择值。
<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: '' }
用于表格中的操作按钮,支持权限控制和二次确认。
<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>
<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>
<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>
用于详情页展示数据。
<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>
项目使用 Pinia 进行状态管理,Store 定义在 src/packages/stores/src 中。
| 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']);
<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>
在路由配置中通过 meta.authorities 设置权限:
{
path: 'user',
name: 'SystemUser',
component: () => import('#/views/system/user/index.vue'),
meta: {
title: '用户管理',
authorities: ['system:user:visit'],
},
}
<script lang="ts" setup>
import { $t } from '#/locales';
</script>
<template>
<Button>{{ $t('common.save') }}</Button>
<span>{{ $t('ui.actionMessage.deleteSuccess', ['用户']) }}</span>
</template>
在 src/locales 目录下的语言文件中添加:
// src/locales/zh-CN.json
{
"common": {
"save": "保存",
"cancel": "取消"
},
"ui": {
"actionMessage": {
"deleteSuccess": "删除【{0}】成功"
}
}
}
import { usePagination } from '#/packages/effects/hooks/src';
const { page, pageSize, total, setPage, setTotal } = usePagination();
import { useTabs } from '#/packages/effects/hooks/src';
const { closeOtherTabs, closeTab, refreshTab } = useTabs();
// 关闭其他标签页
await closeOtherTabs();
// 刷新当前页
await refreshTab();
import { useWatermark } from '#/packages/effects/hooks/src';
const { updateWatermark, destroyWatermark } = useWatermark();
// 设置水印
await updateWatermark({
content: '用户名',
});
// 销毁水印
destroyWatermark();
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
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"
<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>
#/* 指向 ./src/*,导入时使用 #/components/xxx 而非相对路径rules 属性定义v-dompurify-html 处理secure-ls 加密存储defineOptions({ name: 'ComponentName' }) 定义组件名