测试开发前端篇之项目列表页

项目列表页构建

vue_base
├─ .gitignore
├─ auto-imports.d.ts
├─ components.d.ts
├─ index.html
├─ package-lock.json
├─ package.json
├─ public
│  └─ favicon.ico
├─ README.md
├─ src
│  ├─ api
│  │  └─ api.js
│  ├─ App.vue
│  ├─ assets
│  │  ├─ css
│  │  │  └─ main.css
│  │  ├─ logo6.png
│  │  ├─ logoA.png
│  ├─ components
│  │  └─ LoginBack.vue
│  ├─ main.js
│  ├─ router
│  │  └─ index.js
│  ├─ store
│  │  └─ index.js
│  └─ views
│     ├─ AllProject.vue
│     ├─ Login.vue
└─ vite.config.js

这里增加了一个项目列表页AllProject.vue

路由文件修改

// 1.导入createRouter、createWebHashHistory
import {
	createRouter, 
	createWebHashHistory,
	createWebHistory,
} from 'vue-router'

// 2.配置路由规则
const routes = [
	{
		// 指定页面访问的路径
		// 必须指定
		path: '/',
		// 指定该路由的名称
		name: 'root',
		// 定义当返回路径时,显示的组件
		// component: () => import('../views/Home.vue')
		// redirect: '/login'
		redirect: { name: 'login' } // 将 /home 重定向到名为 dashboard 的路由
	},
	{
		path: '/login',
		name: 'login',
		component: () => import('../views/Login.vue')
 	},
	 {
		path: '/allProject',
		name: 'allProject',
		component: () => import('../views/AllProject.vue'),
		meta: {
			requiresAuth: true // 需要登录权限
		  }
 	},
]

// 3.创建路由实例
const router = createRouter({
	// 设置路由模式
	// a.hash模式:会在url中带一个#,无需服务器进行额外配置、兼容性好、不利于SEO,搜索引擎不会抓取#后面的内容
	history: createWebHashHistory(),
	// b.HTML5模式:url中不带#,需要服务器的额外配置(nginx)、兼容性不好、利于SEO
	// history: createWebHistory(),
	
	// 指定路由规则数组
	// routes: routes
	routes
})

// 4.导出路由实例 
export default router

这里的路由文件需要注意的是meta 字段中的 requiresAuth: true 表示该路由需要登录权限。这意味着用户在访问此路由之前必须先进行身份验证。与路由守卫配合使用。

meta: {
	requiresAuth: true // 需要登录权限
}

封装接口请求增加

import axios from 'axios'

const base_url = 'http://121.4.107.148:18899';
// 创建axios请求实例对象
const httpDev = axios.create({
  baseURL: base_url,	//定义公共的base url
  headers: {
    'Content-Type': 'application/json'
  },
  	// 定义校验响应状态码
  validateStatus: function(status) {
    return status >= 200 && status < 300;
  }
})

// 请求拦截器
httpDev.interceptors.request.use(
	function(config) {
      // 发送请求前判断是否需要鉴权
  console.log('请求拦截器:config', config)
  if (config.url !== '/login/' && config.url !== '/register/') {
    // 添加token
    const token = sessionStorage.getItem('token');
			if (token) {
				// Bearer
				config.headers['Authorization'] = 'JWT ' + token;
			}
  }
  return config;
  },
  function(error) {
    // 请求错误时做一些处理
    return Promise.reject(error)
  }
)
// 响应拦截器
httpDev.interceptors.response.use(
  function(response) {
    console.log('响应拦截器:response', response)
    // 响应数据之前做一些处理
    return response
  },
  function(error) {
    // 响应错误时做一些处理
    return Promise.reject(error)
  }
)
export default {
	// 登录接口
	loginApi(params) {
		return httpDev.post('/login/', params);
	},
	// 获取项目列表接口
	getProjects() {
		return httpDev.get('/projects/');
	},
	// 添加项目接口
	createProject(params) {
		return httpDev.post('/projects/', params)
	},
	// 更新项目
	updateProject(id, params) {
		return httpDev.patch(`/projects/${id}/`, params)
	},
	// 删除
	deleteProject(id,leader) {
		return httpDev.delete(`/projects/${id}/?leader=${leader}`)
	}
},
}

这里需要注意:反引号(`)通常用于创建模板字面量,可以让您在字符串中插入变量或表达式,并在需要时进行字符串插值。${id} 是要插入到字符串中的表达式

(`/projects/${id}/`, params)
(`/projects/${id}/?leader=${leader}`)
  • ${baseUrl}: 这是 API 的基础 URL 地址,用于构建完整的请求 URL。您需要将其替换为您自己的 API 服务器地址。

  • /projects/${id}/: 这部分构成了完整的请求 URL。${id} 是项目的唯一标识符,用于指定要更新的特定项目。

  • params: 这是作为请求体发送的数据,包含要更新的项目参数。根据您的 API 的要求,您可能需要以特定的

项目列表组件

<template>
  <div class="title" style="margin: 40px 220px 5px;">
    <span style="height: 30px; color: #42b983; font: 700 20px/30px microsoft yahei;">项目列表</span>
    <el-button type="success" style="float: right; margin-left:10px" @click="logout">
      <el-icon><UserFilled /></el-icon>退出登录
    </el-button>
    <el-button type="success" style="float: right; "  @click.stop="handleAdd()">
      <el-icon ><Plus /></el-icon>添加项目
    </el-button>
    <div class="divider"></div>
  </div>
    <div class="custom-row">
      <div v-for="project in projects" :key="project.id" class="card-wrapper" @click="goToProjectDetail(project.id)">
        <div style="padding:20px 10px ">
          <div  class="el-card" style="border: none;"> 
            <el-icon class="monitor-icon"><Monitor /></el-icon>
            <el-tooltip :content="project.name" placement="bottom">
              <div class="pro_name">{{ project.name }}</div>
            </el-tooltip>
            <el-tooltip :content="project.leader" placement="top">
              <div class="pro_leader">负责人:{{ project.leader }}</div>
            </el-tooltip>
            <div style="margin-top: 30px;">
              <el-button size="small"  @click.stop="handleEdit(project)"><el-icon ><Edit /></el-icon>编辑</el-button>
              <el-button size="small" type="danger"  @click.stop="openDeleteDialog(project)" style="margin-left: 40px;"><el-icon ><Delete /></el-icon>删除</el-button>
            </div>
          </div>
        </div>
      </div>
      <div class="card-wrapper" @click="handleAdd()">  
        <el-icon class="plus-icon"><Plus /></el-icon>
      </div>
    </div>

  <!-- 编辑弹框 -->
  <el-dialog v-model="showEditDialog" title="修改项目" width="35%">
    <template #default>
      <div class="dialog-content">
        <label for="projectNameInput">项目名</label>
        <input id="projectNameInput" v-model="editForm.name" :placeholder="editForm.name ? editForm.name : ''"/>
        <br />
        <label for="projectLeaderInput">负责人</label>
        <input id="projectLeaderInput" v-model="editForm.leader" :placeholder="editForm.leader ? editForm.leader : ''"/>
      </div>
    </template>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="cancelEdit">取消</el-button>
        <el-button type="primary" @click="confirmEdit">确定</el-button>
      </span>
    </template>
  </el-dialog>

  <!-- 删除弹框 -->
  <el-dialog v-model="showDelDialog" title="提示" width="35%">
  <div style="display: flex; align-items: center;">
  <el-icon style="font-size: 24px; color: rgb(233, 226, 125);"><WarningFilled /></el-icon>
  <span style="margin-left: 10px;">确定要删除该项目吗?</span>
  </div>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="cancelDel">取消</el-button>
        <el-button type="primary" @click="confirmDel">确定</el-button>
      </span>
    </template>
  </el-dialog>

<!-- 添加项目弹框 -->
  <el-dialog v-model="showAddDialog" title="添加项目" width="35%" >
    <template #default>
      <div class="dialog-content">
        <label for="">项目名</label>
        <input v-model="addForm.name" placeholder="" autocomplete="off"/>
        <br />
        <label for="">负责人</label>
        <input v-model="addForm.leader" placeholder="" autocomplete="off" />
      </div>
    </template>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="cancelEdit">取消</el-button>
        <el-button type="primary" @click="confirmAdd">确定</el-button>
      </span>
    </template>
  </el-dialog>
</template>
<script>

export default {
  data() {
    return {
      // 项目列表
      projects: [],
      // 定义项目id
      id:'',
      // 编辑默认弹框
      showEditDialog: false,
      // 添加项目默认弹框
      showAddDialog:false,
      // 删除默认弹框
      showDelDialog: false,
      // 用于接收用户编辑的项目信息
	  editForm: {
		name: '',
		leader: '',
	  },
      // 用于接收用户输入的项目信息
      addForm:{
        name:'',
        leader:'',
      }
    }
  },
  mounted() {
    this.fetchProjectInfo();
    const redirectedFromLogin = sessionStorage.getItem('redirectedFromLogin');
    console.log(redirectedFromLogin)
    if (redirectedFromLogin) {
      // 显示提示框或弹出框
      // 可以使用第三方库如 element-ui、vant 等,或自己实现弹出框逻辑
      this.showLoginSuccessMessage()
      // 清除跳转标记
      sessionStorage.removeItem('redirectedFromLogin');
    }
  },
  methods: {
    // 获取所有项目信息接口
    async fetchProjectInfo() {
      try {
        // const response = await httpDev.get('/projects/'); 
        const response = await this.$api.getProjects();
        this.projects = response.data;
        console.log('项目信息:', this.projects);
        // 在这里将获取到的项目信息保存到组件的data属性中,以便在模板中使用
      } catch (error) {
        console.error('获取项目信息失败', error);
      }
    },

    // 添加项目接口
    async clickaddProject() {
      try {
        // const response = await httpDev.post('/projects/',this.addForm);
        const response = await this.$api.createProject(this.addForm)
        if (response.status == 201){
            ElMessage({
            message: '添加成功!',
            type: 'success',
          })
          this.fetchProjectInfo()
        }
      } catch (error) {
        console.error('添加项目失败', error);
      }
    },

    // 删除项目接口
    async clickdelProject() {
      try {
        const id = this.id;
        const leader = this.editForm.leader;
        const response = await this.$api.deleteProject(id,leader)
        if (response.status == 204){
            ElMessage({
            message: '删除成功!',
            type: 'success',
          })
          this.fetchProjectInfo()
        }
      } catch (error) {
        console.error('删除项目失败', error);
      }
    },

    
    // 修改项目接口
    async clickeditProject() {
      try {
        const id = this.id;
        const response = await this.$api.updateProject(id,this.editForm)
        if (response.status == 200){
            ElMessage({
            message: '更新成功!',
            type: 'success',
          })
          this.fetchProjectInfo()
        }
      } catch (error) {
        console.error('更新项目失败', error);
      }
    },

    handleEdit(project) {
      this.showEditDialog = true;
      this.id = project.id;
      this.editForm.name = project.name
      this.editForm.leader = project.leader
    },
    cancelEdit() {
      this.showEditDialog = false;
      this.showAddDialog = false;
    },
    confirmEdit() {
      // 处理编辑确定按钮逻辑
      this.clickeditProject()
      this.showEditDialog = false;
    },
    
    handleAdd() {
      this.showAddDialog = true;
    },

    confirmAdd() {
      if (this.addForm.name.trim() === '' || this.addForm.leader.trim() === '') {
        ElMessage({
            message: '请填写项目名和负责人',
            type: 'warning',
          })
      } else {
        // 处理添加确定按钮逻辑
        this.clickaddProject();
        this.showAddDialog = false;
        // 清空输入框中的内容
        this.addForm.name = '';
        this.addForm.leader = '';
      }
    },
    
    openDeleteDialog(project) {
      this.showDelDialog = true;
      this.id = project.id;
      this.editForm.leader  = project.leader;
    },
    cancelDel() {
      this.showDelDialog = false;
    },
    confirmDel() {
      // 处理删除确定按钮逻辑
      this.clickdelProject()
      this.showDelDialog = false;
    },

    logout() {
    // 执行退出登录的逻辑
    // 重定向到首页
      sessionStorage.removeItem('token'); // 假设用于存储用户令牌的键名为'Token'
      console.log(sessionStorage.getItem('token'))
      this.$router.push('/');
      // location.reload(true);
    },

    showLoginSuccessMessage() {
      ElMessage({
      message: '登录成功!',
      type: 'success',
    })
    },

    goToProjectDetail(id) {
      console.log('Navigating to Project with ID:', id);
       // 将项目ID保存到会话存储
      sessionStorage.setItem('projectId', id);
       // 调用 setProjectId mutation 来设置项目ID
      // this.$store.commit('setProjectId', id);
      this.$router.push({ name: 'Project' });
    },
  }
}
</script>

<style>
.custom-row  {
  display: flex;
  margin: 10px calc(14.7%) 60px;
  flex-wrap: wrap;
  justify-content:flex-start;
}

.card-wrapper{
  box-shadow: -4px 2px 12px 5px rgb(0 0 0 / 10%);
  width: 200px;
  height: 250px;
  margin: 0 10px 20px 10px;
  color: #fff;
  text-align: center;
}

.pro_leader,
.pro_name{
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.pro_leader{
  line-height: 20px;
  font-size: 10px;
  cursor: pointer;
  color: #000;
}
.pro_name{
  line-height: 60px;
  font-size: 25px;
  cursor: pointer;
}
.monitor-icon {
  margin-top: 10px;
  color: #066a3d;
  font-size: 40px;
  background: #a3c9bb;
  border-radius: 50%;
  width: 70px;
  height: 70px;
  cursor: pointer;
}

.divider {
  height: 6px;
  background-color: #42b983;
  margin-top: 5px;
  margin-bottom: 50px;
}
.plus-icon {
  margin-top: 80px;
  color: rgb(194, 194, 194);
  font-size: 80px;
  cursor: pointer;
}
.el-select {
  width: 300px;
}
.el-input {
  width: 300px;
}
.dialog-footer button:first-child {
  margin-right: 10px;
}
.dialog-footer button:first-child {
  margin-right: 10px;
}

.dialog-content{
  margin-left: 30px;

}
.dialog-content input {
  width: 70%; /* 调整输入框宽度 */
  height: 32px;
  line-height: 32px;
  margin: 10px;
  border-radius: 8px;
  
}
.el-popper.is-customized {
  /* Set padding to ensure the height is 32px */
  padding: 3px 12px;
  background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
}
</style>

这段代码是一个项目列表的前端组件,使用Vue.js和Element UI库进行开发。

该组件包含以下功能:

  1. 展示项目列表,循环渲染每个项目的卡片。

  2. 可以点击项目卡片进入项目详情页。

  3. 可以添加项目,点击“添加项目”按钮会弹出一个对话框,输入项目名和负责人后可以确认添加。

  4. 可以编辑项目,点击“编辑”按钮会弹出一个对话框,可以修改项目名和负责人后确认更新。

  5. 可以删除项目,点击“删除”按钮会弹出一个确认对话框,确认后会删除该项目。

整个组件的样式使用了一些CSS来设置卡片、对话框等的样式。

另外,组件中还包含了一些接口请求的逻辑,通过调用接口获取项目列表、添加项目、删除项目、更新项目等。

需要注意的是,组件中使用了一些第三方库(如element-ui)、全局变量(如this.api)和路由(this.router),请确保相关依赖已经正确引入,并且项目中已经配置好了路由和接口请求相关的配置。

另外,组件中有一些el-开头的标签,这些是Element UI库提供的组件,需要确保已经正确引入Element UI库。

Element UI弹框的使用

Element UI弹框的使用逻辑如下:

  1. 定义弹框的显示状态,通过v-model指令实现双向绑定。

  2. 设置弹框的标题和宽度。

  3. 使用<template>定义弹框的内容区域和底部按钮。

  4. 在按钮的点击事件中调用对应的方法,处理确认和取消操作。

我这里用修改项目弹框来解释说明一下:

首先组件模板中有一个编辑的按钮绑定了一个点击事件,在点击编辑按钮的时候弹出修改项目弹框,弹框中有标题、两个输入框、底部两个按钮:取消和确认。

<div style="margin-top: 30px;">
              <el-button size="small"  @click.stop="handleEdit(project)"><el-icon ><Edit /></el-icon>编辑</el-button>
              <el-button size="small" type="danger"  @click.stop="openDeleteDialog(project)" style="margin-left: 40px;"><el-icon ><Delete /></el-icon>删除</el-button>
</div>

然后是编辑弹框的模板

 <!-- 编辑弹框 -->
 <el-dialog v-model="showEditDialog" title="修改项目" width="35%">
    <template #default>
      <div class="dialog-content">
        <label for="projectNameInput">项目名</label>
        <input id="projectNameInput" v-model="editForm.name" :placeholder="editForm.name ? editForm.name : ''"/>
        <br />
        <label for="projectLeaderInput">负责人</label>
        <input id="projectLeaderInput" v-model="editForm.leader" :placeholder="editForm.leader ? editForm.leader : ''"/>
      </div>
    </template>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="cancelEdit">取消</el-button>
        <el-button type="primary" @click="confirmEdit">确定</el-button>
      </span>
    </template>
  </el-dialog>
  1. 弹框的显示与隐藏:

    • v-model="showEditDialog"showEditDialog与弹框的显示状态进行双向绑定。

  2. 弹框的标题和宽度:

    • title="修改项目" 设置弹框的标题为"修改项目"。

    • width="35%" 设置弹框的宽度为35%。

  3. 弹框的内容:

    • <template #default>...</template> 定义了弹框的内容区域。

    • 在内容区域中,包含两个输入框和对应的标签。其中,项目名输入框使用v-model="editForm.name"进行数据绑定,负责人输入框使用v-model="editForm.leader"进行数据绑定。

    • 输入框的placeholder属性,通过三元表达式动态绑定了editForm.nameeditForm.leader的值。

  4. 弹框的底部按钮:

    • <template #footer>...</template> 定义了弹框的底部按钮区域。

    • 弹框底部的按钮包含两个:取消按钮和确定按钮。

    • 在取消按钮的点击事件中,调用cancelEdit方法进行相应的操作。

    • 在确定按钮的点击事件中,调用confirmEdit方法进行相应的操作。

总结一下,该弹框组件实现了一个用于修改项目信息的弹框,其中包含了项目名和负责人两个输入框,并且可以通过取消和确定按钮进行

初始数据的定义

export default {
  data() {
    return {
      // 项目列表
      projects: [],
      // 定义项目id
      id:'',
      // 编辑默认弹框
      showEditDialog: false,
      // 添加项目默认弹框
      showAddDialog:false,
      // 删除默认弹框
      showDelDialog: false,
      // 用于接收用户编辑的项目信息
	  editForm: {
		name: '',
		leader: '',
		},
      // 用于接收用户输入的项目信息
      addForm:{
        name:'',
        leader:'',
      }
    }
  },

这里定义了编辑弹框的状态一开始是关闭状态的

最后是点击事件中调用对应的方法,处理确认和取消操作。

handleEdit(project) {
      this.showEditDialog = true;
      this.id = project.id;
      this.editForm.name = project.name
      this.editForm.leader = project.leader
    },
cancelEdit() {
      this.showEditDialog = false;
    },
confirmEdit() {
      // 处理编辑确定按钮逻辑
      this.clickeditProject()
      this.showEditDialog = false;
    },
   

handleEdit方法用于打开编辑弹框并展示项目的原始信息,cancelEdit方法用于关闭编辑和添加弹框,confirmEdit方法用于处理编辑弹框中确定按钮的逻辑,并关闭编辑弹框。

接口请求

methods: {
    // 获取所有项目信息接口
    async fetchProjectInfo() {
      try {
        // const response = await httpDev.get('/projects/'); 
        const response = await this.$api.getProjects();
        this.projects = response.data;
        console.log('项目信息:', this.projects);
        // 在这里将获取到的项目信息保存到组件的data属性中,以便在模板中使用
      } catch (error) {
        console.error('获取项目信息失败', error);
      }
    },

    // 添加项目接口
    async clickaddProject() {
      try {
        // const response = await httpDev.post('/projects/',this.addForm);
        const response = await this.$api.createProject(this.addForm)
        if (response.status == 201){
            ElMessage({
            message: '添加成功!',
            type: 'success',
          })
          this.fetchProjectInfo()
        }
      } catch (error) {
        console.error('添加项目失败', error);
      }
    },

    // 删除项目接口
    async clickdelProject() {
      try {
        const id = this.id;
        const leader = this.editForm.leader;
        const response = await this.$api.deleteProject(id,leader)
        if (response.status == 204){
            ElMessage({
            message: '删除成功!',
            type: 'success',
          })
          this.fetchProjectInfo()
        }
      } catch (error) {
        console.error('删除项目失败', error);
      }
    },

    
    // 修改项目接口
    async clickeditProject() {
      try {
        const id = this.id;
        const response = await this.$api.updateProject(id,this.editForm)
        if (response.status == 200){
            ElMessage({
            message: '更新成功!',
            type: 'success',
          })
          this.fetchProjectInfo()
        }
      } catch (error) {
        console.error('更新项目失败', error);
      }
    },
}

这里定义了几个方法:fetchProjectInfoclickaddProjectclickdelProjectclickeditProject,分别用于获取所有项目信息、添加项目、删除项目和修改项目。

  1. fetchProjectInfo 方法:

    • 该方法用于通过调用异步接口获取所有项目的信息。

  2. clickaddProject 方法:

    • 该方法用于添加项目。

  3. clickdelProject 方法:

    • 该方法用于删除项目。

  4. clickeditProject 方法:

    • 该方法用于修改项目。

mounted 钩子函数

mounted 钩子函数是在组件挂载后执行的生命周期钩子。

  mounted() {
    this.fetchProjectInfo();
    const redirectedFromLogin = sessionStorage.getItem('redirectedFromLogin');
    console.log(redirectedFromLogin)
    if (redirectedFromLogin) {
      // 显示提示框或弹出框
      // 可以使用第三方库如 element-ui、vant 等,或自己实现弹出框逻辑
      this.showLoginSuccessMessage()
      // 清除跳转标记
      sessionStorage.removeItem('redirectedFromLogin');
    }
  },

在该钩子函数中,调用了 fetchProjectInfo() 方法来获取项目信息。

此外,还检查了 sessionStorage 中是否存在名为 redirectedFromLogin 的键。如果存在,表示用户刚刚从登录页面重定向到当前页面。在这种情况下,您会显示一个提示框或弹出框来通知用户登录成功。

然后,调用了 showLoginSuccessMessage() 方法,用于显示登录成功的提示。

最后,清除了 sessionStorage 中的 redirectedFromLogin 键,以便在用户刷新页面或导航到其他页面时不再显示登录成功提示。