> 技术文档 > 在线教育系统开发实战(四):前端界面设计与用户体验优化

在线教育系统开发实战(四):前端界面设计与用户体验优化


前言

在前面三篇文章中,我们详细介绍了系统架构、数据库设计和后端核心功能实现。本文将重点讲解前端界面的设计与实现,包括Vue组件开发、Element UI的使用、用户体验优化等内容。

前端技术架构

技术栈选择

  • Vue 2.6.12:渐进式JavaScript框架,易学易用

  • Element UI 2.15.6:基于Vue的企业级UI组件库

  • Vue Router 3.4.9:官方路由管理器

  • Vuex 3.6.0状态管理模式

  • Axios 0.21.0:HTTP客户端库

  • ECharts 4.9.0:数据可视化图表库

项目结构设计

fronteducation/├── public/│   ├── index.html             # 入口HTML文件│   └── favicon.ico           # 网站图标├── src/│   ├── api/                   # API接口定义│   │   ├── system/           # 系统管理相关API│   │   │   ├── exam.js       # 考试管理API│   │   │   ├── student.js   # 学生管理API│   │   │   └── user.js       # 用户管理API│   │   └── login.js         # 登录相关API│   ├── assets/               # 静态资源│   │   ├── images/         # 图片资源│   │   ├── styles/         # 样式文件│   │   └── icons/           # 图标资源│   ├── components/         # 通用组件│   │   ├── Pagination/     # 分页组件│   │   ├── RightToolbar/   # 右侧工具栏│   │   └── IconSelect/     # 图标选择器│   ├── layout/             # 布局组件│   │   ├── components/     # 布局相关组件│   │   └── index.vue       # 主布局│   ├── router/             # 路由配置│   │   └── index.js         # 路由定义│   ├── store/               # Vuex状态管理│   │   ├── modules/         # 模块化store│   │   ├── getters.js       # 全局getters│   │   └── index.js         # store入口│   ├── utils/               # 工具函数│   │   ├── request.js       # HTTP请求封装│   │   ├── auth.js         # 认证相关工具│   │   └── validate.js     # 表单验证工具│   ├── views/               # 页面组件│   │   ├── exam/           # 考试管理页面│   │   ├── sys/             # 系统管理页面│   │   └── login/           # 登录页面│   ├── App.vue             # 根组件│   └── main.js             # 应用入口├── package.json             # 项目配置└── vue.config.js           # Vue CLI配置

核心页面设计与实现

1. 考试管理页面

页面布局设计

考试管理页面采用经典的列表+表单的布局模式,包含搜索区域、操作区域、数据表格和弹窗表单。

  
                                                                                                                                 搜索        重置          ​                      新增                    删除          ​                                                                                      {{ getStatusText(scope.row.status) }}                                                编辑          = 2\"          >删除          = 2\"          >参与对象          = 2\"          >            {{ scope.row.status === 0 ? \'开始\' : scope.row.status === 1 ? \'启动\' : \'已开始\' }}                            ​        0\"      :total=\"total\"      :page.sync=\"queryParams.current\"      :limit.sync=\"queryParams.size\"      @pagination=\"getList\"    />​                                        
核心JavaScript逻辑
import { listExams, addExam, updateExam, deleteExam, getExamById, listPapers } from \'@/api/system/exam\'​export default {  name: \'ExamList\',  data() {    return {      // 遮罩层      loading: true,      // 选中数组      ids: [],      // 非单个禁用      single: true,      // 非多个禁用      multiple: true,      // 显示搜索条件      showSearch: true,      // 总条数      total: 0,      // 考试表格数据      list: [],      // 试卷选项      paperList: [],      // 弹出层标题      title: \"\",      // 是否显示弹出层      open: false,      // 查询参数      queryParams: {        current: 1,        size: 10,        name: undefined,        status: undefined,        creatorId: undefined,        examType: undefined     },      // 表单参数      form: {},      // 表单校验      rules: {        subject: [         { required: true, message: \"考试科目不能为空\", trigger: \"blur\" }       ],        paperId: [         { required: true, message: \"关联试卷不能为空\", trigger: \"change\" }       ],        startTime: [         { required: true, message: \"开始时间不能为空\", trigger: \"change\" }       ],        endTime: [         { required: true, message: \"结束时间不能为空\", trigger: \"change\" }       ],        durationMin: [         { required: true, message: \"考试时长不能为空\", trigger: \"blur\" }       ]     }   } },  created() {    this.getList()    this.getPaperList() },  methods: {    /** 查询考试列表 */    getList() {      this.loading = true      listExams(this.queryParams).then(res => {        if (res && res.code === 200) {          this.list = res.data || []          this.total = res.total || 0       } else {          this.$message.error(res.message || \'获取考试列表失败\')       }        this.loading = false     }).catch(err => {        console.error(\'考试列表错误:\', err)        this.$message.error(\'网络错误,请检查后端服务\')        this.loading = false     })   },        /** 状态显示文字 */    getStatusText(status) {      const statusMap = {        0: \'计划\',        1: \'就绪\',        2: \'进行中\',        3: \'已结束\',        4: \'已评分\',        5: \'已发布\'     }      return statusMap[status] || \'未知\'   },        /** 状态标签类型 */    getStatusType(status) {      const typeMap = {        0: \'info\',        1: \'warning\',        2: \'primary\',        3: \'success\',        4: \'success\',        5: \'success\'     }      return typeMap[status] || \'info\'   },        /** 状态修改 */    handleStatusChange(row) {      let text = row.status === 0 ? \"开始\" : \"启动\"      let newStatus = row.status === 0 ? 1 : 2            this.$confirm(\'确认要\"\' + text + \'\"\"\' + row.subject + \'\"考试吗?\').then(() => {        const updateData = {          examId: row.examId,          paperId: row.paperId,          subject: row.subject,          startTime: row.startTime,          endTime: row.endTime,          durationMin: row.durationMin,          rulesJson: row.rulesJson,          status: newStatus,          creatorId: row.creatorId,          createTime: row.createTime       }                return updateExam(updateData)     }).then((response) => {        if (response && response.code === 200) {          this.$message.success(text + \"成功\")          this.getList()       } else {          this.$message.error(response.message || text + \'失败\')       }     }).catch((error) => {        console.error(\'状态更新失败:\', error)     })   },        /** 参与对象管理 */    handleParticipants(row) {      this.$router.push({        path: \'/exam/participants\',        query: {          examId: row.examId,          examSubject: row.subject       }     })   } }}

2. API接口封装

HTTP请求统一封装
// utils/request.jsimport axios from \'axios\'import { MessageBox, Message } from \'element-ui\'import store from \'@/store\'import { getToken } from \'@/utils/auth\'​// 创建axios实例const service = axios.create({  baseURL: process.env.VUE_APP_BASE_API || \'http://localhost:8089\', // api的base_url  timeout: 5000 // 请求超时时间})​// request拦截器service.interceptors.request.use(  config => {    // 是否需要设置 token    const isToken = (config.headers || {}).isToken === false    if (getToken() && !isToken) {      config.headers[\'Authorization\'] = \'Bearer \' + getToken() // 让每个请求携带自定义token   }    return config },  error => {    console.log(error)    Promise.reject(error) })​// response 拦截器service.interceptors.response.use(  response => {    const res = response.data        // 如果自定义代码不是200,则判断为错误    if (res.code !== 200) {      Message({        message: res.message || \'Error\',        type: \'error\',        duration: 5 * 1000     })            // 401: 未授权      if (res.code === 401) {        MessageBox.confirm(\'登录状态已过期,您可以继续留在该页面,或者重新登录\', \'系统提示\', {          confirmButtonText: \'重新登录\',          cancelButtonText: \'取消\',          type: \'warning\'       }).then(() => {          store.dispatch(\'LogOut\').then(() => {            location.href = \'/login\'         })       })     }      return Promise.reject(new Error(res.message || \'Error\'))   } else {      return res   } },  error => {    console.log(\'err\' + error)    let { message } = error    if (message == \"Network Error\") {      message = \"后端接口连接异常\"   } else if (message.includes(\"timeout\")) {      message = \"系统接口请求超时\"   } else if (message.includes(\"Request failed with status code\")) {      message = \"系统接口\" + message.substr(message.length - 3) + \"异常\"   }    Message({      message: message,      type: \'error\',      duration: 5 * 1000   })    return Promise.reject(error) })​export default service
考试管理API接口
// api/system/exam.jsimport request from \'@/utils/request\'​// 查询考试列表export function listExams(query) {  return request({    url: \'/exam/list\',    method: \'get\',    params: query })}​// 查询考试详细export function getExamById(examId) {  return request({    url: \'/exam/\' + examId,    method: \'get\' })}​// 新增考试export function addExam(data) {  return request({    url: \'/exam/add\',    method: \'post\',    data: data })}​// 修改考试export function updateExam(data) {  return request({    url: \'/exam/update\',    method: \'put\',    data: data })}​// 删除考试export function deleteExam(examId) {  return request({    url: \'/exam/delete/\' + examId,    method: \'delete\' })}​// 查询试卷列表export function listPapers(query) {  return request({    url: \'/paper/list\',    method: \'get\',    params: query })}

3. 用户体验优化

加载状态处理
  
       
         
                                 
         
 
​.loading-container {  padding: 20px;}​.empty-container {  padding: 60px 0;  text-align: center;}
错误处理与提示
// 统一错误处理methods: {  async handleApiCall() {    try {      this.loading = true      const response = await someApiCall()            if (response.code === 200) {        this.$message.success(\'操作成功\')        this.refreshData()     } else {        this.$message.error(response.message || \'操作失败\')     }   } catch (error) {      console.error(\'API调用失败:\', error)            if (error.response) {        // 服务器响应错误        const status = error.response.status        if (status === 401) {          this.$message.error(\'登录已过期,请重新登录\')          this.$router.push(\'/login\')       } else if (status === 403) {          this.$message.error(\'权限不足\')       } else if (status === 500) {          this.$message.error(\'服务器内部错误\')       } else {          this.$message.error(\'请求失败:\' + status)       }     } else if (error.request) {        // 网络错误        this.$message.error(\'网络连接失败,请检查网络设置\')     } else {        // 其他错误        this.$message.error(\'操作失败:\' + error.message)     }   } finally {      this.loading = false   } }}
表单验证优化
// 自定义验证规则const validatePhone = (rule, value, callback) => {  if (value && !/^1[3-9]\\d{9}$/.test(value)) {    callback(new Error(\'请输入正确的手机号\')) } else {    callback() }}​const validateEmail = (rule, value, callback) => {  if (value && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {    callback(new Error(\'请输入正确的邮箱地址\')) } else {    callback() }}​// 表单验证规则rules: {  username: [   { required: true, message: \'用户名不能为空\', trigger: \'blur\' },   { min: 3, max: 20, message: \'用户名长度在 3 到 20 个字符\', trigger: \'blur\' } ],  phone: [   { validator: validatePhone, trigger: \'blur\' } ],  email: [   { validator: validateEmail, trigger: \'blur\' } ]}

4. 响应式设计

移动端适配
// 响应式样式.app-container {  padding: 20px;    @media (max-width: 768px) {    padding: 10px;        .el-form--inline .el-form-item {      display: block;      margin-right: 0;      margin-bottom: 15px;   }        .el-table {      font-size: 12px;            .el-table__header th,      .el-table__body td {        padding: 8px 0;     }   }        .el-button--mini {      padding: 5px 8px;      font-size: 11px;   } }    @media (max-width: 480px) {    .el-table {      .el-table-column--selection {        display: none;     }            .operation-column {        .el-button {          display: block;          margin: 2px 0;          width: 100%;       }     }   } }}
弹性布局应用
  
                 
         
                     
         
           
{{ userCount }}
           
用户总数
         
       
                 
​.dashboard-container {  padding: 20px;}​.stat-card {  display: flex;  align-items: center;  padding: 20px;  background: #fff;  border-radius: 8px;  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);  transition: all 0.3s;   &:hover {    transform: translateY(-2px);    box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15); }}​.stat-icon {  width: 60px;  height: 60px;  display: flex;  align-items: center;  justify-content: center;  border-radius: 50%;  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);  color: white;  font-size: 24px;  margin-right: 15px;}​.stat-content {  flex: 1;}​.stat-number {  font-size: 28px;  font-weight: bold;  color: #303133;  line-height: 1;}​.stat-label {  font-size: 14px;  color: #909399;  margin-top: 5px;}

性能优化策略

1. 组件懒加载

// router/index.jsconst routes = [ {    path: \'/exam\',    component: Layout,    children: [     {        path: \'list\',        name: \'ExamList\',        component: () => import(\'@/views/exam/Exam/index\'), // 懒加载        meta: { title: \'考试管理\', icon: \'exam\' }     }   ] }]

2. 图片懒加载

  
     

3. 虚拟滚动

  
     

4. 防抖与节流

import { debounce, throttle } from \'lodash\'​export default {  data() {    return {      searchKeyword: \'\'   } },  watch: {    searchKeyword: {      handler: debounce(function(newVal) {        this.handleSearch(newVal)     }, 300),      immediate: false   } },  methods: {    // 搜索处理(防抖)    handleSearch(keyword) {      console.log(\'执行搜索:\', keyword)      // 执行搜索逻辑   },        // 滚动处理(节流)    handleScroll: throttle(function(event) {      console.log(\'滚动事件:\', event)      // 执行滚动逻辑   }, 100) }}

主题定制与样式管理

1. Element UI主题定制

// styles/element-variables.scss/* 改变主题色变量 */$--color-primary: #409EFF;$--color-success: #67C23A;$--color-warning: #E6A23C;$--color-danger: #F56C6C;$--color-info: #909399;​/* 改变 icon 字体路径变量,必需 */$--font-path: \'~element-ui/lib/theme-chalk/fonts\';​@import \"~element-ui/packages/theme-chalk/src/index\";

2. 全局样式管理

// styles/index.scss// 全局样式​// 重置样式* {  box-sizing: border-box;}​body {  margin: 0;  padding: 0;  font-family: \'Helvetica Neue\', Helvetica, \'PingFang SC\', \'Hiragino Sans GB\', \'Microsoft YaHei\', \'微软雅黑\', Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;}​// 通用类.clearfix::after {  content: \"\";  display: table;  clear: both;}​.text-center {  text-align: center;}​.text-right {  text-align: right;}​.pull-left {  float: left;}​.pull-right {  float: right;}​// 间距类.mt-10 { margin-top: 10px; }.mt-20 { margin-top: 20px; }.mb-10 { margin-bottom: 10px; }.mb-20 { margin-bottom: 20px; }​// 动画类.fade-enter-active, .fade-leave-active {  transition: opacity 0.3s;}​.fade-enter, .fade-leave-to {  opacity: 0;}​// 卡片样式.custom-card {  background: #fff;  border-radius: 8px;  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);  padding: 20px;  margin-bottom: 20px;  transition: all 0.3s;   &:hover {    box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15); }}

国际化支持

1. 多语言配置

// i18n/index.jsimport Vue from \'vue\'import VueI18n from \'vue-i18n\'import elementEnLocale from \'element-ui/lib/locale/lang/en\'import elementZhLocale from \'element-ui/lib/locale/lang/zh-CN\'import enLocale from \'./en\'import zhLocale from \'./zh\'​Vue.use(VueI18n)​const messages = {  en: {    ...enLocale,    ...elementEnLocale },  zh: {    ...zhLocale,    ...elementZhLocale }}​const i18n = new VueI18n({  locale: \'zh\', // 默认语言  messages})​export default i18n

2. 语言包定义

// i18n/zh.jsexport default {  route: {    dashboard: \'首页\',    exam: \'考试管理\',    student: \'学生管理\',    teacher: \'教师管理\' },  navbar: {    logOut: \'退出登录\',    profile: \'个人中心\',    theme: \'换肤\',    size: \'布局大小\' },  exam: {    title: \'考试管理\',    subject: \'考试科目\',    status: \'状态\',    startTime: \'开始时间\',    endTime: \'结束时间\',    duration: \'考试时长\',    participants: \'参与对象\' }}

小结

本文详细介绍了在线教育系统前端界面的设计与实现,包括Vue组件开发、Element UI的使用、用户体验优化、性能优化等方面。良好的前端设计不仅能提升用户体验,还能提高系统的易用性和维护性。

下一篇文章将介绍《在线教育系统开发实战(五):系统部署与运维实践》,详细讲解项目的部署、监控、维护等运维相关内容。

购物导航