952 lines
27 KiB
Markdown
952 lines
27 KiB
Markdown
# Vue 3 + TypeScript 前端开发规范
|
||
|
||
## 技术栈规范
|
||
- Vue 3.5.13
|
||
- TypeScript 5.8.3
|
||
- Element Plus UI
|
||
- Art Design Pro UI 设计规范
|
||
|
||
## 表格组件开发规范
|
||
|
||
### 1. 表格行双击查看功能(强制性)
|
||
|
||
#### 实现要求
|
||
- **✅ 必须实现**: 所有数据表格必须支持行双击查看详情功能
|
||
- **🎯 目标**: 提升用户体验,快速查看数据详情
|
||
|
||
#### 标准实现方式
|
||
```vue
|
||
<!-- 表格组件 -->
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="dataList"
|
||
@selection-change="handleSelectionChange"
|
||
@row-dblclick="handleView">
|
||
<!-- 表格列配置 -->
|
||
</el-table>
|
||
|
||
<!-- 查看详情对话框 -->
|
||
<el-dialog
|
||
:title="viewDialog.title"
|
||
v-model="viewDialog.visible"
|
||
width="800px"
|
||
append-to-body>
|
||
<!-- 详情表单内容 -->
|
||
</el-dialog>
|
||
```
|
||
|
||
#### 标准事件处理
|
||
```typescript
|
||
/** 查看按钮操作 */
|
||
const handleView = async (row: any) => {
|
||
// 1. 复制数据到查看表单
|
||
Object.assign(viewForm, row);
|
||
|
||
// 2. 处理特殊字段(如图片列表等)
|
||
if (row.images) {
|
||
viewImageList.value = row.images.split(',').filter((img: string) => img.trim() !== '');
|
||
}
|
||
|
||
// 3. 显示对话框
|
||
viewDialog.visible = true;
|
||
viewDialog.title = '查看详情';
|
||
};
|
||
```
|
||
|
||
#### 最佳实践建议
|
||
1. 查看对话框宽度建议 800px-1000px,根据内容自适应
|
||
2. 详情展示采用只读表单形式,提供良好的数据展示结构
|
||
3. 对于图片、文件等特殊字段,需要合适的预览组件
|
||
4. 长文本内容建议使用滚动容器限制高度
|
||
5. 必要时可以在详情页面添加相关操作按钮
|
||
|
||
### 2. 表格长文本显示规范(强制性)
|
||
|
||
#### 实现要求
|
||
- **✅ 必须实现**: 所有表格中的长文本字段必须限制显示行数
|
||
- **🎯 目标**: 保持表格整洁美观,同时提供完整内容的查看方式
|
||
|
||
#### 标准实现方式
|
||
```vue
|
||
<!-- 表格长文本列配置 -->
|
||
<el-table-column label="描述文本" align="center" prop="description">
|
||
<template #default="scope">
|
||
<div class="description-cell">{{ scope.row.description }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
```
|
||
|
||
#### 标准样式定义
|
||
```css
|
||
/* 长文本单元格样式 */
|
||
.description-cell {
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 3; /* 显示3行 */
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
line-height: 1.5;
|
||
max-height: 4.5em; /* 3行 * 1.5行高 */
|
||
text-align: left;
|
||
padding: 0 10px;
|
||
}
|
||
```
|
||
|
||
#### 最佳实践建议
|
||
1. 统一使用3行显示限制,超出显示省略号
|
||
2. 使用左对齐方式,提高长文本可读性
|
||
3. 设置合适的行高和内边距
|
||
4. 配合表格行双击查看功能,在详情弹窗中显示完整内容
|
||
5. 必要时可以添加鼠标悬停提示(el-tooltip)显示完整内容
|
||
|
||
### 3. 组件开发标准和页面模板
|
||
|
||
```vue
|
||
<template>
|
||
<div class="component-container">
|
||
<!-- 组件内容 -->
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="ComponentName">
|
||
// 1. 引入依赖
|
||
import { ref, reactive, computed, onMounted } from 'vue';
|
||
import type { ComponentProps, ComponentEmits } from './types';
|
||
|
||
// 2. 定义Props和Emits
|
||
const props = defineProps<ComponentProps>();
|
||
const emits = defineEmits<ComponentEmits>();
|
||
|
||
// 3. 响应式数据
|
||
const loading = ref(false);
|
||
const formData = reactive({});
|
||
|
||
// 4. 计算属性
|
||
const computedValue = computed(() => {
|
||
return /* 计算逻辑 */;
|
||
});
|
||
|
||
// 5. 方法定义
|
||
const handleClick = () => {
|
||
// 方法实现
|
||
};
|
||
|
||
// 6. 生命周期
|
||
onMounted(() => {
|
||
// 组件挂载后的逻辑
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.component-container {
|
||
// 样式实现
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### 列表页面标准模板
|
||
|
||
```vue
|
||
<template>
|
||
<div class="p-2 page-container">
|
||
<!-- 搜索区域 -->
|
||
<transition
|
||
:enter-active-class="proxy?.animate.searchAnimate.enter"
|
||
:leave-active-class="proxy?.animate.searchAnimate.leave"
|
||
>
|
||
<div v-show="showSearch" class="mb-[10px]">
|
||
<el-card shadow="hover">
|
||
<el-form
|
||
ref="queryFormRef"
|
||
:model="queryParams"
|
||
:inline="true"
|
||
label-width="100px"
|
||
>
|
||
<!-- 搜索表单项 -->
|
||
<el-form-item>
|
||
<el-button type="primary" icon="Search" @click="handleQuery">
|
||
搜索
|
||
</el-button>
|
||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 表格区域 -->
|
||
<el-card shadow="never" class="button-card table-card">
|
||
<template #header>
|
||
<el-row :gutter="10" class="mb8">
|
||
<el-col :span="1.5">
|
||
<!-- ✅ 第一个新增按钮:type="primary" plain -->
|
||
<el-button type="primary" plain icon="Plus" @click="handleAdd">
|
||
新增
|
||
</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<!-- ✅ 其他按钮:无type,使用默认样式 -->
|
||
<el-button icon="Edit" :disabled="single" @click="handleUpdate()">
|
||
修改
|
||
</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||
删除
|
||
</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-button icon="Download" @click="handleExport">导出</el-button>
|
||
</el-col>
|
||
<right-toolbar
|
||
v-model:showSearch="showSearch"
|
||
@queryTable="getList"
|
||
></right-toolbar>
|
||
</el-row>
|
||
</template>
|
||
|
||
<!-- 数据表格 -->
|
||
<el-table v-loading="loading" border :data="dataList" :height="tableHeight">
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<!-- 数据列 -->
|
||
<el-table-column label="操作" align="center" width="160">
|
||
<template #default="scope">
|
||
<el-tooltip content="修改" placement="top">
|
||
<!-- ✅ 操作列按钮:link type="primary" -->
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
icon="Edit"
|
||
@click="handleUpdate(scope.row)"
|
||
></el-button>
|
||
</el-tooltip>
|
||
<el-tooltip content="删除" placement="top">
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
icon="Delete"
|
||
@click="handleDelete(scope.row)"
|
||
></el-button>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页组件 -->
|
||
<pagination
|
||
v-show="total > 0"
|
||
:total="total"
|
||
v-model:page="queryParams.pageNum"
|
||
v-model:limit="queryParams.pageSize"
|
||
@pagination="getList"
|
||
/>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
#### 表单页面标准模板
|
||
|
||
```vue
|
||
<template>
|
||
<div class="app-container">
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<span>基本信息</span>
|
||
</template>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="名称" prop="name">
|
||
<el-input v-model="form.name" placeholder="请输入名称" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="状态" prop="status">
|
||
<el-select v-model="form.status" placeholder="请选择状态">
|
||
<el-option label="启用" value="0" />
|
||
<el-option label="禁用" value="1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item>
|
||
<!-- ✅ 提交按钮:type="primary" -->
|
||
<el-button type="primary" @click="submitForm">提交</el-button>
|
||
<!-- ✅ 重置按钮:无type,使用默认样式 -->
|
||
<el-button @click="resetForm">重置</el-button>
|
||
</el-form-item>
|
||
</el-card>
|
||
</el-form>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive } from 'vue';
|
||
import type { FormInstance, FormRules } from 'element-plus';
|
||
|
||
// 表单引用
|
||
const formRef = ref<FormInstance>();
|
||
|
||
// 表单数据
|
||
const form = reactive({
|
||
name: '',
|
||
status: '0',
|
||
});
|
||
|
||
// 验证规则
|
||
const rules = reactive<FormRules>({
|
||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||
});
|
||
|
||
// 提交表单
|
||
const submitForm = async () => {
|
||
if (!formRef.value) return;
|
||
await formRef.value.validate((valid) => {
|
||
if (valid) {
|
||
// 提交逻辑
|
||
console.log('提交表单:', form);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 重置表单
|
||
const resetForm = () => {
|
||
if (!formRef.value) return;
|
||
formRef.value.resetFields();
|
||
};
|
||
</script>
|
||
```
|
||
|
||
### 3. API接口定义和状态管理
|
||
|
||
```typescript
|
||
// api/[module]/index.ts
|
||
import request from '@/utils/request';
|
||
import type { AxiosPromise } from 'axios';
|
||
import type { DataVO, DataForm, DataQuery } from './types';
|
||
|
||
/**
|
||
* 查询数据列表
|
||
* @param query 查询参数
|
||
*/
|
||
export const listData = (query?: DataQuery): AxiosPromise<DataVO[]> => {
|
||
return request({
|
||
url: '/api/data/list',
|
||
method: 'get',
|
||
params: query,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 查询数据详情
|
||
* @param id 数据ID
|
||
*/
|
||
export const getData = (id: string | number): AxiosPromise<DataVO> => {
|
||
return request({
|
||
url: `/api/data/${id}`,
|
||
method: 'get',
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 新增数据
|
||
* @param data 数据信息
|
||
*/
|
||
export const addData = (data: DataForm): AxiosPromise<void> => {
|
||
return request({
|
||
url: '/api/data',
|
||
method: 'post',
|
||
data: data,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 修改数据
|
||
* @param data 数据信息
|
||
*/
|
||
export const updateData = (data: DataForm): AxiosPromise<void> => {
|
||
return request({
|
||
url: '/api/data',
|
||
method: 'put',
|
||
data: data,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 删除数据
|
||
* @param ids 数据ID数组
|
||
*/
|
||
export const delData = (ids: Array<string | number>): AxiosPromise<void> => {
|
||
return request({
|
||
url: `/api/data/${ids}`,
|
||
method: 'delete',
|
||
});
|
||
};
|
||
```
|
||
|
||
### TypeScript 类型定义
|
||
|
||
```typescript
|
||
// api/[module]/types.ts
|
||
|
||
// 查询参数
|
||
export interface DataQuery extends PageQuery {
|
||
name?: string;
|
||
status?: string;
|
||
createTime?: [DateModelType, DateModelType];
|
||
}
|
||
|
||
// 数据表单
|
||
export interface DataForm {
|
||
id?: string | number;
|
||
name: string;
|
||
status: string;
|
||
remark?: string;
|
||
}
|
||
|
||
// 数据视图对象
|
||
export interface DataVO {
|
||
id: string | number;
|
||
name: string;
|
||
status: string;
|
||
createTime: string;
|
||
updateTime: string;
|
||
remark?: string;
|
||
}
|
||
```
|
||
|
||
## 5. 状态管理规范
|
||
|
||
### Pinia Store 结构
|
||
|
||
```typescript
|
||
// stores/modules/[module].ts
|
||
import { defineStore } from 'pinia';
|
||
import { ref, computed } from 'vue';
|
||
import type { DataVO, DataQuery } from '@/api/[module]/types';
|
||
import { listData, getData } from '@/api/[module]';
|
||
|
||
export const useDataStore = defineStore('[module]', () => {
|
||
// 状态
|
||
const dataList = ref<DataVO[]>([]);
|
||
const loading = ref(false);
|
||
const total = ref(0);
|
||
|
||
// 计算属性
|
||
const hasData = computed(() => dataList.value.length > 0);
|
||
|
||
// 方法
|
||
const getList = async (query: DataQuery) => {
|
||
loading.value = true;
|
||
try {
|
||
const { data } = await listData(query);
|
||
dataList.value = data.rows;
|
||
total.value = data.total;
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const getInfo = async (id: string | number) => {
|
||
const { data } = await getData(id);
|
||
return data;
|
||
};
|
||
|
||
const reset = () => {
|
||
dataList.value = [];
|
||
total.value = 0;
|
||
};
|
||
|
||
return {
|
||
// 状态
|
||
dataList,
|
||
loading,
|
||
total,
|
||
// 计算属性
|
||
hasData,
|
||
// 方法
|
||
getList,
|
||
getInfo,
|
||
reset,
|
||
};
|
||
});
|
||
```
|
||
|
||
## 6. 路由管理规范
|
||
|
||
### 路由定义标准
|
||
|
||
```typescript
|
||
// router/modules/[module].ts
|
||
import type { RouteRecordRaw } from 'vue-router';
|
||
import Layout from '@/layout/index.vue';
|
||
|
||
const moduleRouter: RouteRecordRaw = {
|
||
path: '/[module]',
|
||
component: Layout,
|
||
redirect: '/[module]/list',
|
||
name: 'Module',
|
||
meta: {
|
||
title: '模块管理',
|
||
icon: 'example',
|
||
alwaysShow: true,
|
||
},
|
||
children: [
|
||
{
|
||
path: 'list',
|
||
component: () => import('@/views/[module]/index.vue'),
|
||
name: 'ModuleList',
|
||
meta: {
|
||
title: '列表管理',
|
||
activeMenu: '/[module]/list',
|
||
},
|
||
},
|
||
{
|
||
path: 'add',
|
||
component: () => import('@/views/[module]/form.vue'),
|
||
name: 'ModuleAdd',
|
||
meta: {
|
||
title: '新增数据',
|
||
activeMenu: '/[module]/list',
|
||
},
|
||
hidden: true,
|
||
},
|
||
{
|
||
path: 'edit/:id(\\d+)',
|
||
component: () => import('@/views/[module]/form.vue'),
|
||
name: 'ModuleEdit',
|
||
meta: {
|
||
title: '编辑数据',
|
||
activeMenu: '/[module]/list',
|
||
},
|
||
hidden: true,
|
||
},
|
||
],
|
||
};
|
||
|
||
export default moduleRouter;
|
||
```
|
||
|
||
## 7. 搜索表单布局规范(强制性)
|
||
|
||
### 7.1 设计目标
|
||
|
||
1. **一致性**:所有列表页面的检索栏保持统一外观和交互。
|
||
2. **响应式**:在 1920/1440/1280 等常用分辨率下均能保持整洁布局。
|
||
3. **易维护**:通过统一的栅格 + 样式类,后续页面可快速复用。
|
||
|
||
### 7.2 栅格布局
|
||
|
||
1. 采用 Element Plus `el-row` / `el-col` 栅格系统。
|
||
2. **每行固定 4 个检索元素**,每项 `:span="6"`(24 ÷ 4)。
|
||
3. 若检索项不足 4 个,也应保持占位对齐,避免按钮漂移。
|
||
|
||
```vue
|
||
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
|
||
<!-- 第一行 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="6">
|
||
<el-form-item label="字段1" prop="field1">
|
||
<el-input v-model="queryParams.field1" placeholder="请输入字段1" clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="字段2" prop="field2">
|
||
<el-input v-model="queryParams.field2" placeholder="请输入字段2" clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
<!-- 其余两列同理 -->
|
||
</el-row>
|
||
|
||
<!-- 更多行按需添加,仍保持 4 列规则 -->
|
||
|
||
<!-- 操作按钮行 -->
|
||
<el-row>
|
||
<el-col :span="24" class="search-btn-container">
|
||
<el-form-item>
|
||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
```
|
||
|
||
### 7.3 按钮固定右侧
|
||
|
||
1. 统一在搜索表单尾部添加按钮行。
|
||
2. 使用 `.search-btn-container` 类,将按钮通过 Flex **右对齐**。
|
||
|
||
```scss
|
||
/* 搜索按钮容器样式(全局复用,可写在组件 scoped 内部)*/
|
||
.search-btn-container {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 10px;
|
||
|
||
.el-form-item {
|
||
margin-bottom: 0 !important; // 清除默认 margin
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.4 应用示例
|
||
|
||
所有新页面或改造页面 **必须** 遵循以上规范。例如:
|
||
|
||
* `hq/pm/repairOrder/index.vue`
|
||
* `hq/pm/repairCategory/index.vue`
|
||
* `hq/hallAudit/index.vue`
|
||
* `hq/hallInfo/index.vue`
|
||
* `hq/hallReservation/index.vue`
|
||
|
||
### 7.5 代码检查要点
|
||
|
||
1. 检查是否存在 `:inline="true"`,如有应移除。
|
||
2. 确认每行 4 列,使用 `span=6`。
|
||
3. 搜索/重置按钮是否放置在 `.search-btn-container` 内。
|
||
4. scoped 样式内是否包含上述 `.search-btn-container` 定义。
|
||
|
||
遵循此规范,可显著提升列表页检索区域的统一性与用户体验。
|
||
|
||
## 8. 项目架构规范
|
||
|
||
### 目录结构
|
||
|
||
```
|
||
web/src/
|
||
├── api/ # API接口定义
|
||
│ ├── [module]/
|
||
│ │ ├── index.ts # 接口方法
|
||
│ │ └── types.ts # 类型定义
|
||
├── assets/ # 静态资源
|
||
│ ├── images/ # 图片资源
|
||
│ └── styles/ # 样式文件
|
||
│ ├── art-variables.scss # Art Design Pro 变量
|
||
│ ├── art-element-ui.scss # Element Plus 样式定制
|
||
│ ├── art-global.scss # 全局样式
|
||
│ ├── art-dark.scss # 暗色主题
|
||
│ └── index.scss # 样式入口
|
||
├── components/ # 全局组件
|
||
├── hooks/ # 组合式函数
|
||
├── layout/ # 布局组件
|
||
├── plugins/ # 插件配置
|
||
├── router/ # 路由配置
|
||
├── stores/ # 状态管理
|
||
├── types/ # 全局类型定义
|
||
├── utils/ # 工具函数
|
||
├── views/ # 页面组件
|
||
└── main.ts # 应用入口
|
||
```
|
||
|
||
### 文件命名规范
|
||
|
||
- **组件文件**: PascalCase,如 `UserForm.vue`
|
||
- **页面文件**: kebab-case,如 `user-list.vue`
|
||
- **工具文件**: camelCase,如 `formatUtils.ts`
|
||
- **类型文件**: PascalCase,如 `UserTypes.ts`
|
||
|
||
## 9. 样式开发规范
|
||
|
||
### SCSS 变量使用
|
||
|
||
```scss
|
||
// 使用 Art Design Pro 变量系统
|
||
.custom-component {
|
||
background-color: var(--art-main-bg-color);
|
||
border: 1px solid var(--art-border-color);
|
||
border-radius: var(--border-radius-lg);
|
||
padding: var(--space-lg);
|
||
|
||
.title {
|
||
color: var(--art-text-gray-900);
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.content {
|
||
color: var(--art-text-gray-700);
|
||
margin-top: var(--space-md);
|
||
}
|
||
}
|
||
```
|
||
|
||
### UnoCSS 工具类
|
||
|
||
```vue
|
||
<template>
|
||
<!-- 使用原子化CSS类 -->
|
||
<div class="p-4 bg-white rounded-lg shadow-md">
|
||
<h3 class="text-lg font-medium text-gray-900 mb-4">标题</h3>
|
||
<p class="text-gray-700">内容</p>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
## 10. 性能优化规范
|
||
|
||
### 组件懒加载
|
||
|
||
```typescript
|
||
// 路由级别的懒加载
|
||
const UserList = () => import('@/views/user/index.vue');
|
||
|
||
// 组件级别的懒加载
|
||
const AsyncComponent = defineAsyncComponent(() => import('@/components/HeavyComponent.vue'));
|
||
```
|
||
|
||
### 图片优化
|
||
|
||
```vue
|
||
<template>
|
||
<!-- 使用适当的图片格式和尺寸 -->
|
||
<img :src="imageSrc" :alt="imageAlt" loading="lazy" width="200" height="150" />
|
||
</template>
|
||
```
|
||
|
||
## 11. 开发最佳实践
|
||
|
||
### 代码规范
|
||
|
||
1. **组件命名**: 使用 PascalCase
|
||
2. **方法命名**: 使用 camelCase,语义化命名
|
||
3. **常量命名**: 使用 UPPER_SNAKE_CASE
|
||
4. **注释规范**: 使用 JSDoc 格式
|
||
|
||
### 错误处理
|
||
|
||
```typescript
|
||
// API错误处理
|
||
const handleSubmit = async () => {
|
||
try {
|
||
loading.value = true;
|
||
await submitData(formData);
|
||
ElMessage.success('操作成功');
|
||
} catch (error) {
|
||
console.error('提交失败:', error);
|
||
ElMessage.error('操作失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
```
|
||
|
||
### 类型安全
|
||
|
||
```typescript
|
||
// 严格的类型定义
|
||
interface UserForm {
|
||
name: string;
|
||
email: string;
|
||
status: 0 | 1;
|
||
}
|
||
|
||
// 类型断言
|
||
const user = data as UserForm;
|
||
```
|
||
|
||
## 12. 测试规范
|
||
|
||
### 单元测试
|
||
|
||
```typescript
|
||
// 使用 Vitest 进行单元测试
|
||
import { describe, it, expect } from 'vitest';
|
||
import { mount } from '@vue/test-utils';
|
||
import UserForm from '@/components/UserForm.vue';
|
||
|
||
describe('UserForm', () => {
|
||
it('should render correctly', () => {
|
||
const wrapper = mount(UserForm);
|
||
expect(wrapper.exists()).toBe(true);
|
||
});
|
||
});
|
||
```
|
||
|
||
## 13. 构建与部署
|
||
|
||
### 环境配置
|
||
|
||
```typescript
|
||
// .env.development
|
||
VITE_APP_BASE_API = '/dev-api';
|
||
VITE_APP_PORT = 80;
|
||
|
||
// .env.production
|
||
VITE_APP_BASE_API = '/prod-api';
|
||
VITE_APP_PORT = 80;
|
||
```
|
||
|
||
### 构建优化
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
export default defineConfig({
|
||
build: {
|
||
rollupOptions: {
|
||
output: {
|
||
chunkFileNames: 'js/[name]-[hash].js',
|
||
entryFileNames: 'js/[name]-[hash].js',
|
||
assetFileNames: '[ext]/[name]-[hash].[ext]',
|
||
},
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
本规范文档涵盖了杭州第一技师学院智慧校园管理系统前端开发的所有核心规范,包括最新的品牌色配置和统一的按钮使用规范。开发者应严格遵循这些规范,确保代码质量和用户体验的一致性。
|
||
|
||
**🔴 重要提醒**:
|
||
|
||
1. **按钮规范**: 工具栏第一个新增按钮必须使用 `type="primary" plain`,其他按钮使用默认样式
|
||
2. **颜色系统**: 使用最新的 Art Design Pro 颜色变量
|
||
3. **文档同步**: 代码变更后必须同步更新相关文档
|
||
|
||
## 4. 附件管理统一规范(强制性)
|
||
|
||
> 适用范围:所有使用 **Element Plus** + **Vue 3 + TS** 的前端页面(弹窗、新建、编辑、详情、预览等)涉及“附件/文件”功能的模块,必须遵循本规范,以保持 UI 及交互的一致性。
|
||
|
||
### 4.1 组件结构
|
||
|
||
| 组件 | 说明 |
|
||
|------|------|
|
||
| `<el-upload list-type="picture-card">` | 核心上传组件,拖拽 + 点击上传,**单文件最大 10 MB**,**最多 15 个文件** |
|
||
| 自定义 `#file` 模板 | 自绘「卡片样式」:缩略图 + 文件名 + 操作按钮组 |
|
||
| `<el-button-group>` | 卡片底部操作按钮:下载(success)、删除(danger) |
|
||
| 自定义 CSS 类 | `.upload-file-card / thumb / name / actions` 等,详见样式规范 |
|
||
|
||
### 4.2 功能实现
|
||
|
||
1. **上传 Upload**
|
||
- `auto-upload="true"` + 立即上传
|
||
- `accept="image/*,video/*,.pdf,.doc,.docx,.txt"`
|
||
- 上传前 (`before-upload`) 校验大小 <10 MB
|
||
- 上传成功:
|
||
- 新建模式:将返回的 `id` 收集到 `tempAttachmentIds`
|
||
- 编辑模式:文件已自动关联到工单
|
||
- 上传失败:Element Plus message 提示
|
||
|
||
2. **下载 Download (强制使用 fetch + blob)**
|
||
- 统一函数 `handleDownloadAttachment(file)` / `downloadAttachment(attachment)`
|
||
- 流程:`fetch(filePath) → blob → URL.createObjectURL → <a download>`
|
||
- 使用 `ElLoading.service()` 显示 *正在下载文件…*
|
||
- 下载完成后 `revokeObjectURL` 释放内存
|
||
- 解决浏览器对图片/PDF 默认预览导致无法下载的问题
|
||
|
||
3. **删除 Delete(延迟删除机制)**
|
||
- 点击删除 → `proxy.$modal.confirm()` 二次确认
|
||
- 已上传文件:只把 `attachment.id` 放入 `pendingDeleteAttachmentIds`,**不立即调用后端**
|
||
- 表单保存时统一调用 `deleteAttachment` 批量删除
|
||
- 新增未上传文件:直接从 `tempFileList` / `attachmentList` 移除
|
||
|
||
4. **预览 Preview**
|
||
- 判断 `fileType === 'image'` → 使用 `Element Plus ImageViewer` 预览
|
||
- 其他类型文件 `window.open(filePath,'_blank')`
|
||
- 详情弹窗附件列表同样遵循该预览逻辑
|
||
|
||
### 4.3 样式规范(SCSS)
|
||
|
||
```scss
|
||
/* 让附件卡片一行展示 4 个 */
|
||
:deep(.el-upload-list--picture-card) {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 9px;
|
||
margin-top: 12px;
|
||
.el-upload-list__item {
|
||
width: 166px !important;
|
||
min-height: 180px !important;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
|
||
/* 卡片整体布局 */
|
||
.upload-file-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 180px;
|
||
}
|
||
.upload-file-thumb { height: 120px; overflow: hidden; }
|
||
.upload-file-name { font-size: 12px; padding: 8px 4px 4px; text-align: center; overflow: hidden; }
|
||
.upload-file-card-actions {
|
||
padding: 6px 8px;
|
||
border-top: 1px solid #f0f0f0;
|
||
background: #fafafa;
|
||
display: flex; justify-content: center;
|
||
}
|
||
```
|
||
|
||
### 4.4 统一变量 & Hook
|
||
|
||
```ts
|
||
const attachmentList = ref<any[]>([]); // el-upload file-list
|
||
const tempFileList = ref<File[]>([]); // 新建时未上传的本地文件
|
||
const tempAttachmentIds = ref<string[]>([]); // 新建时已上传待关联附件 id
|
||
const pendingDeleteAttachmentIds = ref<string[]>([]); // 编辑时标记待删除附件 id
|
||
```
|
||
|
||
### 4.5 集成步骤(页面级)
|
||
|
||
1. 在 `<script setup>` 引入上述变量 + 上传/下载/删除/预览函数
|
||
2. 使用统一的上传组件模板(参考 `web/src/views/hq/pm/repairOrder/index.vue`)
|
||
3. 在 *保存/提交* 逻辑中调用 `handlePendingAttachments(orderId)` 处理上传 & 删除
|
||
4. 样式直接复制 *样式规范* 中的片段或通过全局样式引入
|
||
|
||
### 4.6 强制性要求
|
||
|
||
- **🚨 不得** 在其他页面自定义下载逻辑(必须使用 fetch+blob)
|
||
- **🚨 不得** 直接删除附件数据库记录;必须延迟删除
|
||
- **🚨 不得** 修改附件卡片 UI 结构及尺寸;需保持一致
|
||
- **✅ 必须** 在新页面开发时引用本规范并注明 *附件管理统一规范* 版本
|
||
|
||
> **版本**: 2025-01-27 附件管理统一规范 v1.0
|
||
|
||
---
|
||
|
||
## 5. 构建问题解决方案
|
||
|
||
### 5.1 JavaScript heap out of memory 解决方案(2025-01-27)
|
||
|
||
#### 问题描述
|
||
在执行 `npm run build:prod` 时出现内存溢出错误:
|
||
```
|
||
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
|
||
```
|
||
|
||
#### 根本原因
|
||
- 项目使用大量图标插件(unplugin-icons、svg-icons、element-plus图标等)
|
||
- 构建时需要加载大量图标文件到内存处理
|
||
- Node.js默认堆内存限制(~1.4GB)不足
|
||
|
||
#### 标准解决方案
|
||
修改 `package.json` 中的构建脚本,增加Node.js堆内存限制:
|
||
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"build:prod": "node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode production",
|
||
"build:dev": "node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode development"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 参数说明
|
||
- `--max-old-space-size=8192`:设置堆内存限制为8GB
|
||
- 可根据项目规模和机器内存调整数值(4096=4GB, 8192=8GB)
|
||
|
||
#### 适用场景
|
||
- 大型前端项目构建
|
||
- 使用大量图标组件的项目
|
||
- 需要处理大量静态资源的项目
|
||
|
||
> **版本**: 2025-01-27 构建问题解决方案 v1.0
|