web-dev-best-practices.md 12 KB

Web前端开发最佳实践

1. 页面布局规范

1.1 标题和查询条件区域

参考引航计划的布局方式,页面应包含以下区域:

<template>
  <div class="page-container">
    <!-- 页面标题 -->
    <h2>页面名称</h2>
    
    <!-- 查询条件区域 -->
    <div class="search-section">
      <h3>查询条件</h3>
      <el-form :model="queryForm" class="search-form">
        <div class="search-row">
          <el-form-item label="字段名">
            <el-input v-model="queryForm.field" placeholder="请输入..." clearable></el-input>
          </el-form-item>
          <!-- 更多查询条件 -->
          <div class="search-actions">
            <el-button type="primary" @click="query">查询</el-button>
            <el-button @click="reset">重置</el-button>
          </div>
        </div>
      </el-form>
    </div>
    
    <!-- 操作按钮区域 -->
    <div class="action-section">
      <el-button type="primary" @click="add">新增</el-button>
      <el-button type="danger" @click="deleteSelected" :disabled="selectedIds.length === 0">删除</el-button>
    </div>
    
    <!-- 表格区域 -->
    <div class="table-container">
      <el-table>...</el-table>
    </div>
  </div>
</template>

1.2 样式规范

.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 表格属性

<el-table
  :data="tableData"
  stripe
  border
  size="default"
  :header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 'bold' }"
  :height="tableHeight"
  @selection-change="handleSelectionChange"
  @sort-change="handleSortChange">

2.2 列设计规范

  • 序号列:宽度50px,固定居中
  • 选择列:宽度55px,固定居中
  • 操作列:宽度120px,固定右侧
  • 状态列:宽度80px,居中显示,使用el-tag
  • 日期时间列:宽度160px,居中显示
  • 用户名列:宽度100px
  • 账号列:宽度100px
  • 用户角色列:宽度150px
  • 企业微信账号列:宽度120px
  • 其他文本列:使用min-width,支持文本溢出显示
<el-table-column type="selection" width="55" fixed align="center"></el-table-column>
<el-table-column type="index" label="序号" width="50" fixed align="center"></el-table-column>
<el-table-column prop="name" label="用户名" width="100" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="loginId" label="账号" width="100" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="roleName" label="用户角色" width="150" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="weChatUserId" label="企业微信账号" width="120" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="recordStatusName" label="状态" width="80" sortable align="center">
  <template #default="scope">
    <el-tag :type="scope.row.recordStatus === 1 ? 'success' : 'info'" size="small">
      {{ scope.row.recordStatusName }}
    </el-tag>
  </template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" sortable align="center" show-overflow-tooltip>
  <template #default="scope">
    {{ formatDateTime(scope.row.createTime) }}
  </template>
</el-table-column>
<el-table-column prop="modifyTime" label="修改时间" width="160" sortable align="center" show-overflow-tooltip>
  <template #default="scope">
    {{ formatDateTime(scope.row.modifyTime) }}
  </template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
  <template #default="{ row }">
    <el-button link type="primary" size="small" @click="edit(row)">编辑</el-button>
    <el-button link type="danger" size="small" @click="delete(row.id)">删除</el-button>
  </template>
</el-table-column>

重要说明:

  • 使用固定宽度(width)而不是最小宽度(min-width)可以避免标题栏分行
  • 序号列使用50px而不是60px,更加紧凑
  • 操作列使用120px,足够容纳两个按钮
  • 状态列使用80px,足够显示状态标签
  • 用户名和账号列使用100px,适合大多数情况
  • 用户角色列使用150px,可以容纳多个角色名称(用逗号分隔)
  • 企业微信账号列使用120px,适合大多数微信号长度
  • 日期时间列使用160px,可以完整显示日期时间格式

2.3 日期时间格式化

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 删除确认对话框

所有删除操作都必须显示确认对话框,防止误操作。

单个删除

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('删除用户失败')
    }
  }
}

批量删除

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 后端删除逻辑

删除主表数据时,必须先删除关联表数据。

@Transactional
public void deleteUsers(List<String> userIds) {
    for (String userId : userIds) {
        // 先删除关联表数据
        sysUserRoleRepository.deleteByUserId(userId);
        // 再删除主表数据
        userRepository.deleteById(userId);
    }
}

4. 排序功能

4.1 前端排序

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 后端排序

public Page<User> 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工具,自动处理认证和错误。

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 错误处理

try {
  await request.post('/api/endpoint', data)
  ElMessage.success('操作成功')
} catch (error) {
  console.error('操作失败:', error)
  ElMessage.error('操作失败,请稍后重试')
}

6. 表单验证

6.1 前端验证

<el-form :model="form" :rules="rules" ref="formRef">
  <el-form-item label="用户名" prop="name">
    <el-input v-model="form.name" placeholder="请输入用户名"></el-input>
  </el-form-item>
</el-form>

<script>
const rules = {
  name: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
  ]
}
</script>

6.2 后端验证

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,如getUsershandleSortChange
  • 变量名:使用camelCase,如userListselectedIds
  • 常量名:使用UPPER_SNAKE_CASE,如MAX_PAGE_SIZE

7.2 注释规范

/**
 * 获取用户列表
 */
const getUsers = async () => {
  // 实现代码
}

/**
 * 处理排序变化
 * @param {Object} param - 排序参数
 * @param {String} param.prop - 排序字段
 * @param {String} param.order - 排序方向
 */
const handleSortChange = ({ prop, order }) => {
  // 实现代码
}

8. 性能优化

8.1 防抖和节流

import { debounce } from 'lodash-es'

const search = debounce(() => {
  getUsers()
}, 300)

8.2 虚拟滚动

对于大量数据,使用虚拟滚动提高性能。

<el-table
  :data="tableData"
  height="600"
  v-loading="loading">
</el-table>

9. 安全规范

9.1 XSS防护

使用Vue的插值表达式,避免直接使用v-html。

<!-- 正确 -->
<div>{{ user.name }}</div>

<!-- 错误 -->
<div v-html="user.name"></div>

9.2 CSRF防护

所有POST、PUT、DELETE请求都应该携带CSRF token。

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

<nav>...</nav>
<main>
  <h1>页面标题</h1>
  <section class="search-section">...</section>
  <section class="table-section">...</section>
</main>
<footer>...</footer>

10.2 键盘导航

确保所有交互元素都可以通过键盘访问。

<el-button @click="handleClick" @keyup.enter="handleClick">
  点击
</el-button>