
Vue.js框架入门与实战
大约 13 分钟
Vue.js框架入门与实战
Vue.js就像是前端开发的"瑞士军刀"——简单易学、功能强大、使用灵活。如果说原生JavaScript是"手工制作",那么Vue.js就是"智能工厂"——让你用更少的代码实现更多的功能。作为测试开发工程师,掌握Vue.js就像拥有了"超级助手",能够快速构建美观、交互丰富的测试工具界面。
一、Vue.js简介:渐进式框架的魅力
什么是Vue.js?
Vue.js是一个用于构建用户界面的渐进式JavaScript框架。它的核心特点:
- 渐进式:可以逐步采用,不需要重写整个应用
- 组件化:将界面拆分成可复用的组件
- 响应式:数据变化自动更新界面
- 易学易用:学习曲线平缓,文档友好
Vue.js vs 原生JavaScript
// 原生JavaScript:繁琐的DOM操作
const button = document.getElementById('test-button');
const resultDiv = document.getElementById('result');
let testCount = 0;
button.addEventListener('click', () => {
testCount++;
resultDiv.textContent = `已执行 ${testCount} 次测试`;
});
// Vue.js:声明式的简洁写法
const { createApp } = Vue;
createApp({
data() {
return {
testCount: 0
}
},
methods: {
runTest() {
this.testCount++;
}
}
}).mount('#app');<!-- Vue.js模板:直观易懂 -->
<div id="app">
<button @click="runTest">执行测试</button>
<p>已执行 {{ testCount }} 次测试</p>
</div>二、Vue.js基础语法
1. 创建Vue应用
// Vue 3 创建应用的标准方式
import { createApp } from 'vue'
const app = createApp({
// 数据
data() {
return {
message: '欢迎使用测试平台',
testCases: [],
isLoading: false
}
},
// 方法
methods: {
async loadTestCases() {
this.isLoading = true;
try {
const response = await fetch('/api/testcases');
this.testCases = await response.json();
} catch (error) {
console.error('加载测试用例失败:', error);
} finally {
this.isLoading = false;
}
}
},
// 生命周期钩子
mounted() {
this.loadTestCases();
}
});
app.mount('#app');2. 模板语法
<div id="app">
<!-- 文本插值 -->
<h1>{{ message }}</h1>
<!-- 属性绑定 -->
<input :value="searchKeyword" :disabled="isLoading">
<img :src="userAvatar" :alt="userName">
<!-- 条件渲染 -->
<div v-if="isLoading">
<p>正在加载测试用例...</p>
</div>
<div v-else-if="testCases.length === 0">
<p>暂无测试用例</p>
</div>
<div v-else>
<p>共找到 {{ testCases.length }} 个测试用例</p>
</div>
<!-- 列表渲染 -->
<ul>
<li v-for="testCase in testCases" :key="testCase.id">
{{ testCase.name }} - {{ testCase.status }}
</li>
</ul>
<!-- 事件监听 -->
<button @click="runAllTests" :disabled="isLoading">
{{ isLoading ? '执行中...' : '执行所有测试' }}
</button>
<!-- 双向数据绑定 -->
<input v-model="searchKeyword" placeholder="搜索测试用例">
<!-- 样式绑定 -->
<div :class="{ 'test-passed': testResult.success, 'test-failed': !testResult.success }">
测试结果: {{ testResult.message }}
</div>
</div>3. 计算属性和侦听器
const app = createApp({
data() {
return {
testCases: [
{ id: 1, name: '登录测试', status: 'passed', priority: 'high' },
{ id: 2, name: '搜索测试', status: 'failed', priority: 'medium' },
{ id: 3, name: '支付测试', status: 'pending', priority: 'high' }
],
searchKeyword: '',
selectedPriority: 'all'
}
},
// 计算属性:基于其他数据计算得出
computed: {
// 过滤后的测试用例
filteredTestCases() {
let filtered = this.testCases;
// 按关键词过滤
if (this.searchKeyword) {
filtered = filtered.filter(testCase =>
testCase.name.toLowerCase().includes(this.searchKeyword.toLowerCase())
);
}
// 按优先级过滤
if (this.selectedPriority !== 'all') {
filtered = filtered.filter(testCase =>
testCase.priority === this.selectedPriority
);
}
return filtered;
},
// 测试统计
testStats() {
const total = this.testCases.length;
const passed = this.testCases.filter(t => t.status === 'passed').length;
const failed = this.testCases.filter(t => t.status === 'failed').length;
const pending = this.testCases.filter(t => t.status === 'pending').length;
return { total, passed, failed, pending };
},
// 通过率
passRate() {
if (this.testStats.total === 0) return 0;
return Math.round((this.testStats.passed / this.testStats.total) * 100);
}
},
// 侦听器:监听数据变化
watch: {
// 监听搜索关键词变化
searchKeyword(newValue, oldValue) {
console.log(`搜索关键词从 "${oldValue}" 变为 "${newValue}"`);
// 可以在这里添加防抖逻辑
},
// 深度监听测试用例数组
testCases: {
handler(newTestCases) {
console.log('测试用例数据发生变化');
// 自动保存到本地存储
localStorage.setItem('testCases', JSON.stringify(newTestCases));
},
deep: true // 深度监听
}
}
});三、组件化开发
1. 组件基础
// 定义测试用例卡片组件
const TestCaseCard = {
props: {
testCase: {
type: Object,
required: true
}
},
emits: ['run-test', 'edit-test', 'delete-test'],
template: `
<div class="test-case-card" :class="statusClass">
<div class="card-header">
<h3>{{ testCase.name }}</h3>
<span class="priority-badge" :class="priorityClass">
{{ testCase.priority }}
</span>
</div>
<div class="card-body">
<p>{{ testCase.description }}</p>
<div class="test-info">
<span>状态: {{ statusText }}</span>
<span v-if="testCase.lastRun">
最后执行: {{ formatDate(testCase.lastRun) }}
</span>
</div>
</div>
<div class="card-actions">
<button @click="$emit('run-test', testCase.id)"
class="btn btn-primary">
执行测试
</button>
<button @click="$emit('edit-test', testCase.id)"
class="btn btn-secondary">
编辑
</button>
<button @click="$emit('delete-test', testCase.id)"
class="btn btn-danger">
删除
</button>
</div>
</div>
`,
computed: {
statusClass() {
return `status-${this.testCase.status}`;
},
priorityClass() {
return `priority-${this.testCase.priority}`;
},
statusText() {
const statusMap = {
'passed': '✅ 通过',
'failed': '❌ 失败',
'pending': '⏳ 待执行'
};
return statusMap[this.testCase.status] || '未知';
}
},
methods: {
formatDate(date) {
return new Date(date).toLocaleString();
}
}
};
// 使用组件
const app = createApp({
components: {
TestCaseCard
},
data() {
return {
testCases: [
{
id: 1,
name: '用户登录测试',
description: '验证用户登录功能是否正常',
status: 'passed',
priority: 'high',
lastRun: '2024-01-15T10:30:00'
}
]
}
},
methods: {
handleRunTest(testId) {
console.log(`执行测试: ${testId}`);
// 执行测试逻辑
},
handleEditTest(testId) {
console.log(`编辑测试: ${testId}`);
// 打开编辑对话框
},
handleDeleteTest(testId) {
if (confirm('确定要删除这个测试用例吗?')) {
this.testCases = this.testCases.filter(t => t.id !== testId);
}
}
},
template: `
<div class="test-case-list">
<test-case-card
v-for="testCase in testCases"
:key="testCase.id"
:test-case="testCase"
@run-test="handleRunTest"
@edit-test="handleEditTest"
@delete-test="handleDeleteTest">
</test-case-card>
</div>
`
});2. 单文件组件(SFC)
<!-- TestCaseForm.vue -->
<template>
<div class="test-case-form">
<h2>{{ isEdit ? '编辑测试用例' : '新建测试用例' }}</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="name">用例名称</label>
<input
id="name"
v-model="form.name"
type="text"
required
placeholder="请输入测试用例名称">
</div>
<div class="form-group">
<label for="description">用例描述</label>
<textarea
id="description"
v-model="form.description"
rows="3"
placeholder="请输入测试用例描述">
</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="priority">优先级</label>
<select id="priority" v-model="form.priority">
<option value="low">低</option>
<option value="medium">中</option>
<option value="high">高</option>
</select>
</div>
<div class="form-group">
<label for="category">分类</label>
<select id="category" v-model="form.category">
<option value="functional">功能测试</option>
<option value="performance">性能测试</option>
<option value="security">安全测试</option>
</select>
</div>
</div>
<div class="form-group">
<label>测试步骤</label>
<div v-for="(step, index) in form.steps" :key="index" class="step-item">
<input
v-model="step.description"
type="text"
placeholder="请输入测试步骤">
<button type="button" @click="removeStep(index)" class="btn-remove">
删除
</button>
</div>
<button type="button" @click="addStep" class="btn btn-secondary">
添加步骤
</button>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="!isFormValid">
{{ isEdit ? '更新' : '创建' }}
</button>
<button type="button" @click="$emit('cancel')" class="btn btn-secondary">
取消
</button>
</div>
</form>
</div>
</template>
<script>
export default {
name: 'TestCaseForm',
props: {
testCase: {
type: Object,
default: null
}
},
emits: ['submit', 'cancel'],
data() {
return {
form: {
name: '',
description: '',
priority: 'medium',
category: 'functional',
steps: [{ description: '' }]
}
}
},
computed: {
isEdit() {
return !!this.testCase;
},
isFormValid() {
return this.form.name.trim() &&
this.form.steps.some(step => step.description.trim());
}
},
watch: {
testCase: {
handler(newTestCase) {
if (newTestCase) {
this.form = { ...newTestCase };
} else {
this.resetForm();
}
},
immediate: true
}
},
methods: {
addStep() {
this.form.steps.push({ description: '' });
},
removeStep(index) {
if (this.form.steps.length > 1) {
this.form.steps.splice(index, 1);
}
},
handleSubmit() {
if (!this.isFormValid) return;
const testCaseData = {
...this.form,
steps: this.form.steps.filter(step => step.description.trim())
};
this.$emit('submit', testCaseData);
},
resetForm() {
this.form = {
name: '',
description: '',
priority: 'medium',
category: 'functional',
steps: [{ description: '' }]
};
}
}
}
</script>
<style scoped>
.test-case-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 20px;
}
.form-row .form-group {
flex: 1;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
input, textarea, select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #007bff;
}
.step-item {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.step-item input {
flex: 1;
}
.btn-remove {
padding: 5px 10px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 30px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>四、状态管理
1. 组件间通信
// 父子组件通信示例
const TestManager = {
data() {
return {
testCases: [],
showForm: false,
editingTestCase: null
}
},
methods: {
// 处理子组件事件
handleCreateTest() {
this.editingTestCase = null;
this.showForm = true;
},
handleEditTest(testCase) {
this.editingTestCase = testCase;
this.showForm = true;
},
handleFormSubmit(testCaseData) {
if (this.editingTestCase) {
// 更新现有测试用例
const index = this.testCases.findIndex(t => t.id === this.editingTestCase.id);
this.testCases[index] = { ...this.editingTestCase, ...testCaseData };
} else {
// 创建新测试用例
const newTestCase = {
id: Date.now(),
...testCaseData,
status: 'pending',
createdAt: new Date().toISOString()
};
this.testCases.push(newTestCase);
}
this.showForm = false;
this.editingTestCase = null;
},
handleFormCancel() {
this.showForm = false;
this.editingTestCase = null;
}
},
template: `
<div class="test-manager">
<div class="toolbar">
<button @click="handleCreateTest" class="btn btn-primary">
新建测试用例
</button>
</div>
<!-- 测试用例列表 -->
<test-case-list
:test-cases="testCases"
@edit-test="handleEditTest">
</test-case-list>
<!-- 表单对话框 -->
<div v-if="showForm" class="modal-overlay">
<test-case-form
:test-case="editingTestCase"
@submit="handleFormSubmit"
@cancel="handleFormCancel">
</test-case-form>
</div>
</div>
`
};2. Provide/Inject:跨层级通信
// 祖先组件提供数据
const TestPlatform = {
data() {
return {
currentUser: {
id: 1,
name: '张三',
role: 'admin'
},
apiConfig: {
baseUrl: 'https://api.test.com',
timeout: 5000
}
}
},
provide() {
return {
currentUser: this.currentUser,
apiConfig: this.apiConfig,
// 提供方法
showNotification: this.showNotification
}
},
methods: {
showNotification(message, type = 'info') {
// 显示通知的逻辑
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
};
// 后代组件注入数据
const TestCaseExecutor = {
inject: ['currentUser', 'apiConfig', 'showNotification'],
methods: {
async executeTest(testCase) {
try {
this.showNotification(`开始执行测试: ${testCase.name}`, 'info');
const response = await fetch(`${this.apiConfig.baseUrl}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
testCase,
executor: this.currentUser.id
})
});
const result = await response.json();
if (result.success) {
this.showNotification('测试执行成功', 'success');
} else {
this.showNotification('测试执行失败', 'error');
}
return result;
} catch (error) {
this.showNotification(`测试执行出错: ${error.message}`, 'error');
throw error;
}
}
}
};五、Vue Router:单页应用路由
1. 路由基础配置
import { createRouter, createWebHistory } from 'vue-router'
import TestCaseList from './components/TestCaseList.vue'
import TestCaseDetail from './components/TestCaseDetail.vue'
import TestExecution from './components/TestExecution.vue'
import Dashboard from './components/Dashboard.vue'
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard
},
{
path: '/testcases',
name: 'TestCaseList',
component: TestCaseList
},
{
path: '/testcases/:id',
name: 'TestCaseDetail',
component: TestCaseDetail,
props: true // 将路由参数作为props传递给组件
},
{
path: '/execution',
name: 'TestExecution',
component: TestExecution,
meta: { requiresAuth: true } // 路由元信息
},
{
path: '/login',
name: 'Login',
component: () => import('./components/Login.vue') // 懒加载
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 路由守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('authToken');
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login');
} else {
next();
}
});
export default router;2. 在组件中使用路由
<!-- Navigation.vue -->
<template>
<nav class="navbar">
<div class="nav-brand">
<router-link to="/" class="brand-link">
🧪 测试平台
</router-link>
</div>
<ul class="nav-menu">
<li class="nav-item">
<router-link to="/" class="nav-link" exact-active-class="active">
仪表盘
</router-link>
</li>
<li class="nav-item">
<router-link to="/testcases" class="nav-link" active-class="active">
测试用例
</router-link>
</li>
<li class="nav-item">
<router-link to="/execution" class="nav-link" active-class="active">
测试执行
</router-link>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: 'Navigation'
}
</script><!-- TestCaseDetail.vue -->
<template>
<div class="test-case-detail">
<div class="breadcrumb">
<router-link to="/testcases">测试用例</router-link>
<span> / </span>
<span>{{ testCase?.name }}</span>
</div>
<div v-if="testCase" class="case-content">
<h1>{{ testCase.name }}</h1>
<p>{{ testCase.description }}</p>
<div class="actions">
<button @click="executeTest" class="btn btn-primary">
执行测试
</button>
<button @click="editTest" class="btn btn-secondary">
编辑
</button>
<button @click="goBack" class="btn btn-outline">
返回列表
</button>
</div>
</div>
<div v-else class="loading">
加载中...
</div>
</div>
</template>
<script>
export default {
name: 'TestCaseDetail',
props: {
id: {
type: String,
required: true
}
},
data() {
return {
testCase: null
}
},
async created() {
await this.loadTestCase();
},
methods: {
async loadTestCase() {
try {
const response = await fetch(`/api/testcases/${this.id}`);
this.testCase = await response.json();
} catch (error) {
console.error('加载测试用例失败:', error);
this.$router.push('/testcases');
}
},
executeTest() {
// 跳转到执行页面,传递测试用例ID
this.$router.push({
name: 'TestExecution',
query: { testCaseId: this.id }
});
},
editTest() {
// 跳转到编辑页面
this.$router.push(`/testcases/${this.id}/edit`);
},
goBack() {
// 返回上一页或测试用例列表
this.$router.go(-1);
}
},
// 监听路由参数变化
watch: {
id(newId) {
this.loadTestCase();
}
}
}
</script>六、实战项目:完整的测试用例管理系统
让我们整合所有学到的Vue.js知识,创建一个完整的测试用例管理系统:
<!-- App.vue -->
<template>
<div id="app">
<Navigation />
<main class="main-content">
<router-view />
</main>
<!-- 全局通知组件 -->
<Notification
v-if="notification.show"
:message="notification.message"
:type="notification.type"
@close="hideNotification" />
</div>
</template>
<script>
import Navigation from './components/Navigation.vue'
import Notification from './components/Notification.vue'
export default {
name: 'App',
components: {
Navigation,
Notification
},
data() {
return {
notification: {
show: false,
message: '',
type: 'info'
}
}
},
provide() {
return {
showNotification: this.showNotification
}
},
methods: {
showNotification(message, type = 'info') {
this.notification = {
show: true,
message,
type
};
// 3秒后自动隐藏
setTimeout(() => {
this.hideNotification();
}, 3000);
},
hideNotification() {
this.notification.show = false;
}
}
}
</script><!-- TestCaseManager.vue -->
<template>
<div class="test-case-manager">
<div class="page-header">
<h1>测试用例管理</h1>
<button @click="showCreateForm" class="btn btn-primary">
➕ 新建用例
</button>
</div>
<!-- 搜索和过滤 -->
<div class="filters">
<div class="search-box">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索测试用例..."
class="search-input">
</div>
<div class="filter-controls">
<select v-model="selectedStatus" class="filter-select">
<option value="">所有状态</option>
<option value="passed">通过</option>
<option value="failed">失败</option>
<option value="pending">待执行</option>
</select>
<select v-model="selectedPriority" class="filter-select">
<option value="">所有优先级</option>
<option value="high">高</option>
<option value="medium">中</option>
<option value="low">低</option>
</select>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-number">{{ testStats.total }}</div>
<div class="stat-label">总用例数</div>
</div>
<div class="stat-card success">
<div class="stat-number">{{ testStats.passed }}</div>
<div class="stat-label">通过</div>
</div>
<div class="stat-card danger">
<div class="stat-number">{{ testStats.failed }}</div>
<div class="stat-label">失败</div>
</div>
<div class="stat-card warning">
<div class="stat-number">{{ testStats.pending }}</div>
<div class="stat-label">待执行</div>
</div>
</div>
<!-- 测试用例列表 -->
<div class="test-case-grid">
<TestCaseCard
v-for="testCase in filteredTestCases"
:key="testCase.id"
:test-case="testCase"
@run-test="handleRunTest"
@edit-test="handleEditTest"
@delete-test="handleDeleteTest" />
</div>
<!-- 空状态 -->
<div v-if="filteredTestCases.length === 0" class="empty-state">
<div class="empty-icon">📝</div>
<h3>暂无测试用例</h3>
<p>{{ searchKeyword ? '没有找到匹配的测试用例' : '开始创建你的第一个测试用例吧' }}</p>
<button v-if="!searchKeyword" @click="showCreateForm" class="btn btn-primary">
创建测试用例
</button>
</div>
<!-- 表单对话框 -->
<Modal v-if="showForm" @close="hideForm">
<TestCaseForm
:test-case="editingTestCase"
@submit="handleFormSubmit"
@cancel="hideForm" />
</Modal>
</div>
</template>
<script>
import TestCaseCard from './TestCaseCard.vue'
import TestCaseForm from './TestCaseForm.vue'
import Modal from './Modal.vue'
export default {
name: 'TestCaseManager',
components: {
TestCaseCard,
TestCaseForm,
Modal
},
inject: ['showNotification'],
data() {
return {
testCases: [],
searchKeyword: '',
selectedStatus: '',
selectedPriority: '',
showForm: false,
editingTestCase: null,
loading: false
}
},
computed: {
filteredTestCases() {
let filtered = this.testCases;
// 搜索过滤
if (this.searchKeyword) {
const keyword = this.searchKeyword.toLowerCase();
filtered = filtered.filter(testCase =>
testCase.name.toLowerCase().includes(keyword) ||
testCase.description.toLowerCase().includes(keyword)
);
}
// 状态过滤
if (this.selectedStatus) {
filtered = filtered.filter(testCase => testCase.status === this.selectedStatus);
}
// 优先级过滤
if (this.selectedPriority) {
filtered = filtered.filter(testCase => testCase.priority === this.selectedPriority);
}
return filtered;
},
testStats() {
const total = this.testCases.length;
const passed = this.testCases.filter(t => t.status === 'passed').length;
const failed = this.testCases.filter(t => t.status === 'failed').length;
const pending = this.testCases.filter(t => t.status === 'pending').length;
return { total, passed, failed, pending };
}
},
async created() {
await this.loadTestCases();
},
methods: {
async loadTestCases() {
this.loading = true;
try {
const response = await fetch('/api/testcases');
this.testCases = await response.json();
} catch (error) {
this.showNotification('加载测试用例失败', 'error');
console.error('加载测试用例失败:', error);
} finally {
this.loading = false;
}
},
showCreateForm() {
this.editingTestCase = null;
this.showForm = true;
},
hideForm() {
this.showForm = false;
this.editingTestCase = null;
},
async handleFormSubmit(testCaseData) {
try {
if (this.editingTestCase) {
// 更新测试用例
const response = await fetch(`/api/testcases/${this.editingTestCase.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testCaseData)
});
if (response.ok) {
const updatedTestCase = await response.json();
const index = this.testCases.findIndex(t => t.id === this.editingTestCase.id);
this.testCases[index] = updatedTestCase;
this.showNotification('测试用例更新成功', 'success');
}
} else {
// 创建新测试用例
const response = await fetch('/api/testcases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testCaseData)
});
if (response.ok) {
const newTestCase = await response.json();
this.testCases.unshift(newTestCase);
this.showNotification('测试用例创建成功', 'success');
}
}
this.hideForm();
} catch (error) {
this.showNotification('操作失败', 'error');
console.error('操作失败:', error);
}
},
handleEditTest(testCase) {
this.editingTestCase = testCase;
this.showForm = true;
},
async handleDeleteTest(testCase) {
if (!confirm(`确定要删除测试用例"${testCase.name}"吗?`)) {
return;
}
try {
const response = await fetch(`/api/testcases/${testCase.id}`, {
method: 'DELETE'
});
if (response.ok) {
this.testCases = this.testCases.filter(t => t.id !== testCase.id);
this.showNotification('测试用例删除成功', 'success');
}
} catch (error) {
this.showNotification('删除失败', 'error');
console.error('删除失败:', error);
}
},
async handleRunTest(testCase) {
try {
this.showNotification(`开始执行测试: ${testCase.name}`, 'info');
const response = await fetch(`/api/testcases/${testCase.id}/execute`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
// 更新测试用例状态
const index = this.testCases.findIndex(t => t.id === testCase.id);
this.testCases[index] = { ...this.testCases[index], ...result };
const message = result.status === 'passed' ? '测试执行成功' : '测试执行失败';
const type = result.status === 'passed' ? 'success' : 'error';
this.showNotification(message, type);
}
} catch (error) {
this.showNotification('测试执行出错', 'error');
console.error('测试执行出错:', error);
}
}
}
}
</script>
<style scoped>
.test-case-manager {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.filters {
display: flex;
gap: 20px;
margin-bottom: 20px;
align-items: center;
}
.search-box {
flex: 1;
max-width: 400px;
}
.search-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.filter-controls {
display: flex;
gap: 10px;
}
.filter-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
border-left: 4px solid #007bff;
}
.stat-card.success {
border-left-color: #28a745;
}
.stat-card.danger {
border-left-color: #dc3545;
}
.stat-card.warning {
border-left-color: #ffc107;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #333;
}
.stat-label {
color: #666;
margin-top: 5px;
}
.test-case-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
</style>七、总结与下一步
通过这篇文章,我们全面学习了Vue.js的核心概念和实战应用:
✅ Vue.js基础语法:模板语法、指令、计算属性 ✅ 组件化开发:组件定义、通信、单文件组件 ✅ 状态管理:数据流、组件通信模式 ✅ 路由管理:Vue Router的使用 ✅ 完整项目实战:测试用例管理系统
关键知识点回顾
- 响应式数据:Vue的核心特性,数据变化自动更新视图
- 组件化思维:将复杂界面拆分成可复用的组件
- 声明式编程:关注"是什么"而不是"怎么做"
- 单向数据流:父组件向子组件传递数据,子组件通过事件通知父组件
- 生命周期:理解组件的创建、更新、销毁过程
Vue.js的优势
- 学习曲线平缓:渐进式框架,可以逐步采用
- 开发效率高:模板语法直观,组件化开发
- 生态系统完善:Vue Router、Vuex/Pinia、Vue CLI等
- 文档友好:中文文档完善,社区活跃
- 性能优秀:虚拟DOM和响应式系统优化
实践建议
- 多写组件:培养组件化思维
- 理解响应式:掌握Vue的数据响应原理
- 学习生态:Vue Router、状态管理等
- 关注最佳实践:代码组织、性能优化
🎯 下一步预告:掌握了Vue.js,我们已经可以构建现代化的单页应用了!下一篇将学习React框架,对比两大主流前端框架的异同,让你在技术选型时更有底气!
Vue.js为我们打开了现代前端开发的大门,让我们继续探索更广阔的前端世界!🚀
