# Web前端开发最佳实践
## 1. 页面布局规范
### 1.1 标题和查询条件区域
参考引航计划的布局方式,页面应包含以下区域:
```vue
```
### 1.2 样式规范
```css
.page-container {
padding: 15px;
height: 100vh;
overflow-y: auto;
}
.page-container h2 {
margin: 0 0 15px 0;
font-size: 18px;
color: #303133;
}
.search-section {
background: #fff;
padding: 15px;
margin-bottom: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.search-section h3 {
margin: 0 0 12px 0;
font-size: 15px;
color: #606266;
}
.search-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.search-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.search-row .el-form-item {
margin-bottom: 0;
min-width: 200px;
}
.search-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.action-section {
margin-bottom: 15px;
display: flex;
gap: 8px;
}
.table-container {
background: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
flex: 1;
display: flex;
flex-direction: column;
}
```
**关键间距说明:**
- 页面容器padding:15px
- 标题h2:font-size: 18px,margin-bottom: 15px
- 查询条件区域padding:15px,margin-bottom: 15px
- 标题h3:font-size: 15px,margin-bottom: 12px
- search-row gap:12px
- search-actions gap:8px
- 操作按钮区域margin-bottom:15px
- 表格容器padding:15px
- 分页margin-top:15px
- 对话框按钮gap:8px
**注意:** 所有间距都应保持紧凑,避免过大的空白区域,确保界面紧凑、专业。
## 2. 表格设计规范
### 2.1 表格属性
```vue
```
### 2.2 列设计规范
- **序号列**:宽度50px,固定居中
- **选择列**:宽度55px,固定居中
- **操作列**:宽度120px,固定右侧
- **状态列**:宽度80px,居中显示,使用el-tag
- **日期时间列**:宽度160px,居中显示
- **用户名列**:宽度100px
- **账号列**:宽度100px
- **用户角色列**:宽度150px
- **企业微信账号列**:宽度120px
- **其他文本列**:使用min-width,支持文本溢出显示
```vue
{{ scope.row.recordStatusName }}
{{ formatDateTime(scope.row.createTime) }}
{{ formatDateTime(scope.row.modifyTime) }}
编辑
删除
```
**重要说明:**
- 使用固定宽度(width)而不是最小宽度(min-width)可以避免标题栏分行
- 序号列使用50px而不是60px,更加紧凑
- 操作列使用120px,足够容纳两个按钮
- 状态列使用80px,足够显示状态标签
- 用户名和账号列使用100px,适合大多数情况
- 用户角色列使用150px,可以容纳多个角色名称(用逗号分隔)
- 企业微信账号列使用120px,适合大多数微信号长度
- 日期时间列使用160px,可以完整显示日期时间格式
### 2.3 日期时间格式化
```javascript
const formatDateTime = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
```
## 3. 删除操作规范
### 3.1 删除确认对话框
所有删除操作都必须显示确认对话框,防止误操作。
#### 单个删除
```javascript
const deleteUser = async (id) => {
try {
await ElMessageBox.confirm(
'确定要删除该用户吗?删除后用户及其角色关联将被永久删除,不可恢复。',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await request.delete(`/user/delete/${id}`)
ElMessage.success('删除成功')
getUsers()
} catch (error) {
if (error !== 'cancel') {
console.error('删除用户失败:', error)
ElMessage.error('删除用户失败')
}
}
}
```
#### 批量删除
```javascript
const deleteSelectedUsers = async () => {
if (selectedUserIds.value.length === 0) {
return
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedUserIds.value.length} 个用户吗?删除后用户及其角色关联将被永久删除,不可恢复。`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await request.delete('/user/delete', {
data: selectedUserIds.value
})
ElMessage.success('删除成功')
selectedUserIds.value = []
getUsers()
} catch (error) {
if (error !== 'cancel') {
console.error('删除用户失败:', error)
ElMessage.error('删除用户失败')
}
}
}
```
### 3.2 后端删除逻辑
删除主表数据时,必须先删除关联表数据。
```java
@Transactional
public void deleteUsers(List userIds) {
for (String userId : userIds) {
// 先删除关联表数据
sysUserRoleRepository.deleteByUserId(userId);
// 再删除主表数据
userRepository.deleteById(userId);
}
}
```
## 4. 排序功能
### 4.1 前端排序
```javascript
const handleSortChange = ({ prop, order }) => {
if (order) {
queryForm.value.sortBy = prop
queryForm.value.sortOrder = order === 'ascending' ? 'asc' : 'desc'
} else {
queryForm.value.sortBy = null
queryForm.value.sortOrder = null
}
getUsers()
}
```
### 4.2 后端排序
```java
public Page queryUsers(UserQuery query, Pageable pageable) {
Sort sort = pageable.getSort();
if (query.getSortBy() != null && query.getSortOrder() != null) {
sort = Sort.by(
"asc".equalsIgnoreCase(query.getSortOrder()) ? Sort.Direction.ASC : Sort.Direction.DESC,
query.getSortBy()
);
}
return userRepository.findAll(pageable.withSort(sort));
}
```
## 5. API请求规范
### 5.1 使用request.js
所有API请求都应该使用统一的request工具,自动处理认证和错误。
```javascript
import request from '@/utils/request'
export const getUsers = (params) => {
return request.get('/user/list', { params })
}
export const createUser = (data) => {
return request.post('/user/create', data)
}
export const deleteUser = (data) => {
return request.delete('/user/delete', { data })
}
```
### 5.2 错误处理
```javascript
try {
await request.post('/api/endpoint', data)
ElMessage.success('操作成功')
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
```
## 6. 表单验证
### 6.1 前端验证
```vue
```
### 6.2 后端验证
```java
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在2-50之间")
private String name;
@NotBlank(message = "账号不能为空")
private String loginId;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6位")
private String password;
}
```
## 7. 代码规范
### 7.1 命名规范
- 组件名:使用PascalCase,如`UserManagement.vue`
- 方法名:使用camelCase,如`getUsers`、`handleSortChange`
- 变量名:使用camelCase,如`userList`、`selectedIds`
- 常量名:使用UPPER_SNAKE_CASE,如`MAX_PAGE_SIZE`
### 7.2 注释规范
```javascript
/**
* 获取用户列表
*/
const getUsers = async () => {
// 实现代码
}
/**
* 处理排序变化
* @param {Object} param - 排序参数
* @param {String} param.prop - 排序字段
* @param {String} param.order - 排序方向
*/
const handleSortChange = ({ prop, order }) => {
// 实现代码
}
```
## 8. 性能优化
### 8.1 防抖和节流
```javascript
import { debounce } from 'lodash-es'
const search = debounce(() => {
getUsers()
}, 300)
```
### 8.2 虚拟滚动
对于大量数据,使用虚拟滚动提高性能。
```vue
```
## 9. 安全规范
### 9.1 XSS防护
使用Vue的插值表达式,避免直接使用v-html。
```vue
{{ user.name }}
```
### 9.2 CSRF防护
所有POST、PUT、DELETE请求都应该携带CSRF token。
```javascript
import request from '@/utils/request'
request.interceptors.request.use(config => {
const token = localStorage.getItem('csrf_token')
if (token) {
config.headers['X-CSRF-TOKEN'] = token
}
return config
})
```
## 10. 可访问性
### 10.1 语义化HTML
```vue
页面标题
```
### 10.2 键盘导航
确保所有交互元素都可以通过键盘访问。
```vue
点击
```