1.创建项目 #

vue create task-front
cd task-front
npm run serve

2.使用element-ui #

2.1 安装 #

npm install element-ui axios --save
npm install babel-plugin-component --save-dev

2.2 配置babel #

babel.config.js

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

2.3 main.js #

src\main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
+import './element-ui'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

2.4 src\element-ui.js #

src\element-ui.js

import Vue from 'vue'
import {
    Button, Tag, Table, TableColumn, Link, Popconfirm, Dialog, Form,
    FormItem, Input, DatePicker, Message,Loading
} from 'element-ui';


Vue.use(Button);
Vue.use(Tag);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(Link);
Vue.use(Popconfirm);
Vue.use(Dialog);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);
Vue.use(DatePicker);
Vue.use(Loading);
Vue.prototype.$message = Message;

2.5 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <el-button type="primary">主要按钮</el-button>
  </div>
</template>

<script>

export default {
  name: 'HomeView'
}
</script>

3.头部布局 #

3.1 HomeView.vue #

src\views\HomeView.vue

<template>
+ <div class="home">
+   <div class="header">
+     <h3>TASK OA任务管理系统</h3>
+     <el-button type="primary">新增任务</el-button>
+   </div>
+ </div>
</template>

<script>

export default {
  name: 'HomeView'
}
</script>

<style lang="less" scoped>
+.home {
+  width: 850px;
+  margin: 0px auto;
+
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #CCC;
+  }
+}
</style>

4.筛选标签 #

4.1 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <div class="header">
      <h3>TASK OA任务管理系统</h3>
      <el-button type="primary">新增任务</el-button>
    </div>
+   <div class="tags">
+     <el-tag 
+       v-for="status in taskStatus"
+       style="cursor:pointer"
+       :key="status.value"
+       type="info"
+     >
+       {{ status.label }}
+     </el-tag>
    </div>
  </div>
</template>

<script>

export default {
  name: 'HomeView',
+ data() {
+   return {
+     taskStatus: [
+       { label: '全部', value: 'all' },
+       { label: '未完成', value: 1 },
+       { label: '已完成', value: 2 },
+     ]
+   };
+ },
}
</script>

<style lang="less" scoped>
.home {
  width: 850px;
  margin: 0px auto;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #CCC;
  }

+  .tags {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 850px;
+    span {
+      margin: 10px;
+    }
+  }
}
</style>

5.数据表格 #

5.1 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <div class="header">
      <h3>TASK OA任务管理系统</h3>
      <el-button type="primary">新增任务</el-button>
    </div>
    <div class="tags">
      <el-tag 
        v-for="status in taskStatus"
        :key="status.value"
        type="info"
      >
        {{ status.label }}
      </el-tag>
    </div>
+   <el-table style="width: 100%">
+     <el-table-column prop="id" label="编号" width="80"></el-table-column>
+     <el-table-column prop="description" label="任务描述" width="180"></el-table-column>
+     <el-table-column prop="status" label="状态">
+        <template slot-scope="scopeProps">
+         {{scopeProps.row.status===1?`未完成`:`已完成`}}
+        </template> 
+     </el-table-column>
+     <el-table-column prop="expected_completion_time" label="预期完成时间"></el-table-column>
+     <el-table-column prop="actual_completion_time" label="实际完成时间"></el-table-column>
+     <el-table-column fixed="right" label="操作" width="100">
+       <template v-slot:default="scope">
+         <el-popconfirm title="确定删除吗?">
+           <el-link slot="reference" type="danger">删除</el-link>
+         </el-popconfirm>
+         <el-popconfirm title="确定完成吗?">
+           <el-link slot="reference" type="success">完成</el-link>
+         </el-popconfirm>
+       </template>
+     </el-table-column>
+   </el-table>
  </div>
</template>

<script>

export default {
  name: 'HomeView',
  data() {
    return {
      taskStatus: [
        { label: '全部', value: 'all' },
        { label: '未完成', value: 1 },
        { label: '已完成', value: 2 },
      ]
    };
  },
}
</script>

<style lang="less" scoped>
.home {
  width: 850px;
  margin: 0px auto;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #CCC;
  }

  .tags {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 850px;

    span {
      margin: 10px;
    }
  }
}
</style>

这段代码使用了Element UI的<el-table><el-table-column>组件来创建一个表格。让我们分别解析这个代码的每一部分:

  1. <el-table style="width: 100%">:创建一个el-table,设置其宽度为100%。这个表格会占据其父元素的全部宽度。

  2. <el-table-column prop="id" label="编号" width="80"></el-table-column>:这定义了一个表格列,列的标签(即表头)是“编号”,这个列的内容来自于每行数据的"id"属性,列的宽度设置为80px。

  3. <el-table-column prop="description" label="任务描述" width="180"></el-table-column>:定义了一个标签为“任务描述”的表格列,内容来自于每行数据的"description"属性,列的宽度设置为180px。

  4. <el-table-column prop="status" label="状态"></el-table-column>:定义了一个标签为“状态”的表格列,内容来自于每行数据的"status"属性。

  5. <el-table-column prop="expected_completion_time" label="预期完成时间"></el-table-column>:定义了一个标签为“预期完成时间”的表格列,内容来自于每行数据的"expected_completion_time"属性。

  6. <el-table-column prop="actual_completion_time" label="实际完成时间"></el-table-column>:定义了一个标签为“实际完成时间”的表格列,内容来自于每行数据的"actual_completion_time"属性。

  7. 最后的<el-table-column>则定义了一个固定在右侧的表格列,标签为"操作",宽度为100px。在这个列中,定义了一个使用了v-slot:default="scope"的模板,这是一个作用域插槽,用于自定义列的内容。这个插槽内包含两个el-popconfirm组件,它们分别用于显示“删除”和“完成”链接,当这两个链接被点击时,会显示一个确认对话框。其中,slot="reference"是用来标记触发这个确认对话框的元素的。

6.增加任务 #

6.1 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <div class="header">
      <h3>TASK OA任务管理系统</h3>
+     <el-button type="primary" @click="openDialog">新增任务</el-button>
    </div>
    <div class="tags">
      <el-tag 
        v-for="status in taskStatus"
        :key="status.value"
        type="info"
      >
        {{ status.label }}
      </el-tag>
    </div>
    <el-table style="width: 100%">
      <el-table-column prop="id" label="编号" width="80"></el-table-column>
      <el-table-column prop="description" label="任务描述" width="180"></el-table-column>
      <el-table-column prop="status" label="状态"></el-table-column>
      <el-table-column prop="expected_completion_time" label="预期完成时间"></el-table-column>
      <el-table-column prop="actual_completion_time" label="实际完成时间"></el-table-column>
      <el-table-column fixed="right" label="操作" width="100">
        <template v-slot:default="scope">
          <el-popconfirm title="确定删除吗?">
            <el-link slot="reference" type="danger">删除</el-link>
          </el-popconfirm>
          <el-popconfirm title="确定完成吗?">
            <el-link slot="reference" type="success">完成</el-link>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
+   <el-dialog title="新增任务" width="50%" :visible.sync="dialogVisible">
+     <el-form>
+       <el-form-item label="任务描述">
+         <el-input type="textarea" :rows="5"></el-input>
+       </el-form-item>
+       <el-form-item label="预计完成时间">
+         <el-date-picker type="datetime" placeholder="请选择日期时间"></el-date-picker>
+       </el-form-item>
+     </el-form>
+     <template v-slot:footer>
+       <span class="dialog-footer">
+         <el-button type="primary">确 定</el-button>
+         <el-button @click="dialogVisible = false">取 消</el-button>
+       </span>
+     </template>
+   </el-dialog>
  </div>
</template>

<script>

export default {
  name: 'HomeView',
  data() {
    return {
      taskStatus: [
        { label: '全部', value: 'all' },
        { label: '未完成', value: 1 },
        { label: '已完成', value: 2 },
      ],
+     dialogVisible: false,
    };
  },
+ methods: {
+   openDialog() {
+     this.dialogVisible = true;
+   }
+ }
}
</script>

<style lang="less" scoped>
.home {
  width: 850px;
  margin: 0px auto;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #CCC;
  }

  .tags {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 850px;

    span {
      margin: 10px;
    }
  }

+  ::v-deep(.cell .el-link) {
+    margin-left: 5px;
+  }

+  ::v-deep(.el-dialog__header) {
+    text-align: left;
+  }

+  ::v-deep(.el-form-item__content) {
+    text-align: left;
+  }
}
</style>

7.添加任务 #

7.1 api\index.js #

src\api\index.js

import axios from 'axios';
import { Message } from 'element-ui';

// 创建 axios 实例
const service = axios.create({
  baseURL: 'http://localhost:3000', // API 的 base URL
  timeout: 5000, // 请求超时时间
});

// 请求拦截器
service.interceptors.request.use(
  config => {
    return config;
  },
  error => {
    console.error(error); 
    return Promise.reject(error);
  },
);

// 响应拦截器
service.interceptors.response.use(
  response => {
     if (!(response.status >=200&&response.status <300)) {
      Message.error('API error: ' + response.data.message);
      throw new Error('API error: ' + response.data.message);
    }
    return response.data;
  },
  error => {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      Message.error('Error: ' + error.response.status);
    } else if (error.request) {
      // The request was made but no response was received
      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
      // http.ClientRequest in Node.js
      Message.error('Network error');
    } else {
      // Something happened in setting up the request that triggered an Error
      Message.error('Error: ' + error.message);
    }
    return Promise.reject(error);
  },
);

export default service;


7.2 taskService.js #

src\api\taskService.js

import service from './index';

export default {
  // 获取任务列表
  getTasks(status) {
    return service.get('/tasks', { params: { status } });
  },

  // 创建新任务
  createTask(description, expected_completion_time) {
    return service.post('/tasks', { description, expected_completion_time });
  },

  // 删除任务
  deleteTask(id) {
    return service.delete(`/tasks/${id}`);
  },

  // 更新任务
  updateTask(id) {
    return service.put(`/tasks/${id}`);
  },
};

7.3 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <div class="header">
      <h3>TASK OA任务管理系统</h3>
      <el-button type="primary" @click="openDialog">新增任务</el-button>
    </div>
    <div class="tags">
      <el-tag 
        v-for="status in taskStatus"
        :key="status.value"
        type="info"
      >
        {{ status.label }}
      </el-tag>
    </div>
+   <el-table style="width: 100%" v-loading="isLoading">
      <el-table-column prop="id" label="编号" width="80"></el-table-column>
      <el-table-column prop="description" label="任务描述" width="180"></el-table-column>
      <el-table-column prop="status" label="状态"></el-table-column>
      <el-table-column prop="expected_completion_time" label="预期完成时间"></el-table-column>
      <el-table-column prop="actual_completion_time" label="实际完成时间"></el-table-column>
      <el-table-column fixed="right" label="操作" width="100">
        <template v-slot:default="scope">
          <el-popconfirm title="确定删除吗?">
            <el-link slot="reference" type="danger">删除</el-link>
          </el-popconfirm>
          <el-popconfirm title="确定完成吗?">
            <el-link slot="reference" type="success">完成</el-link>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <el-dialog title="新增任务" width="50%" :visible.sync="dialogVisible">
+     <el-form ref="taskForm" :rules="taskRules" >
        <el-form-item label="任务描述" prop="description">
+         <el-input type="textarea" :rows="5" v-model="taskForm.description"></el-input>
        </el-form-item>
        <el-form-item label="预计完成时间" prop="expected_completion_time">
+        <el-date-picker type="datetime" placeholder="请选择日期时间" v-model="taskForm.expected_completion_time"></el-date-picker>
        </el-form-item>
      </el-form>
      <template v-slot:footer>
        <span class="dialog-footer">
+         <el-button type="primary" @click="handleNewTaskConfirm">确 定</el-button>
          <el-button @click="dialogVisible = false">取 消</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
+import taskService from '@/api/taskService';
export default {
  name: 'HomeView',
  data() {
    return {
      taskStatus: [
        { label: '全部', value: 'all' },
        { label: '未完成', value: 1 },
        { label: '已完成', value: 2 },
      ],
      dialogVisible: false,
+     taskForm: {
+       description: '',
+       expected_completion_time: ''
+     },
+     taskRules: {
+       description: [
+         { required: true, message: '请输入任务名称', trigger: 'blur' },
+         { min: 1, max: 200, message: '长度在1到200个字符之间', trigger: 'blur' }
+       ],
+       expected_completion_time: [
+         { required: true, message: '请输入预计完成时间', trigger: 'blur' }
+       ]
+     },
+     isLoading: false
    };
  },
  methods: {
    openDialog() {
      this.dialogVisible = true;
+     this.taskForm = {
+       description: '',
+       expected_completion_time: ''
+     };
    },
+   async handleNewTaskConfirm() {
+       this.$refs.taskForm.validate(async (valid) => {
+       if (valid) {
+         try {
+           this.isLoading = true;
+           const { description, expected_completion_time } = this.taskForm;
+           const formattedExpectedCompletionTime = expected_completion_time.toLocaleString('zh-CN', { hour12: false });
+           await taskService.createTask(description, formattedExpectedCompletionTime);
+           this.dialogVisible = false;
+           this.taskForm = {
+            description: '',
+            expected_completion_time: ''
+           };
+          this.$message.success('任务创建成功');
+        } catch (error) {
+          this.$message.error('创建新任务失败');
+        }finally{
+          this.isLoading = false;
+        }
+       } else {
+         this.$message({
+           type: 'error',
+           message: '表单验证失败!'
+         });
+         return false;
+       }
+     })
+   }
  }
}
</script>

<style lang="less" scoped>
.home {
  width: 850px;
  margin: 0px auto;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #CCC;
  }

  .tags {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 850px;

    span {
      margin: 10px;
    }
  }

  ::v-deep(.cell .el-link) {
    margin-left: 5px;
  }

  ::v-deep(.el-dialog__header) {
  text-align: left;
}

  ::v-deep(.el-form-item__content) {
    text-align: left;
  }
}
</style>

8.查询任务 #

8.1 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <div class="header">
      <h3>TASK OA任务管理系统</h3>
      <el-button type="primary" @click="openDialog">新增任务</el-button>
    </div>
    <div class="tags">
      <el-tag 
        v-for="status in taskStatus"
        :key="status.value"
+       style="cursor:pointer"
+       :type="currentFilter === status.value ? 'primary' : 'info'"
+       @click="filterTasks(status.value)"
      >
        {{ status.label }}
      </el-tag>
    </div>
+   <el-table style="width: 100%" v-loading="isLoading" :data="filteredTasks">
      <el-table-column prop="id" label="编号" width="80"></el-table-column>
      <el-table-column prop="description" label="任务描述" width="180"></el-table-column>
      <el-table-column prop="status" label="状态"></el-table-column>
      <el-table-column prop="expected_completion_time" label="预期完成时间"></el-table-column>
      <el-table-column prop="actual_completion_time" label="实际完成时间"></el-table-column>
      <el-table-column fixed="right" label="操作" width="100">
        <template v-slot:default="scope">
          <el-popconfirm title="确定删除吗?">
            <el-link slot="reference" type="danger">删除</el-link>
          </el-popconfirm>
          <el-popconfirm title="确定完成吗?">
            <el-link slot="reference" type="success">完成</el-link>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <el-dialog title="新增任务" width="50%" :visible.sync="dialogVisible">
      <el-form>
        <el-form-item label="任务描述">
          <el-input type="textarea" :rows="5" v-model="taskForm.description"></el-input>
        </el-form-item>
        <el-form-item label="预计完成时间">
          <el-date-picker type="datetime" placeholder="请选择日期时间" v-model="taskForm.expected_completion_time"></el-date-picker>
        </el-form-item>
      </el-form>
      <template v-slot:footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="handleNewTaskConfirm">确 定</el-button>
          <el-button @click="dialogVisible = false">取 消</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import taskService from '@/api/taskService';
export default {
  name: 'HomeView',
  data() {
    return {
      taskStatus: [
        { label: '全部', value: 'all' },
        { label: '未完成', value: 1 },
        { label: '已完成', value: 2 },
      ],
      dialogVisible: false,
      taskForm: {
        description: '',
        expected_completion_time: ''
      },
      taskRules: {
        description: [
          { required: true, message: '请输入任务名称', trigger: 'blur' },
          { min: 1, max: 200, message: '长度在1到200个字符之间', trigger: 'blur' }
        ],
        expected_completion_time: [
          { required: true, message: '请输入预计完成时间', trigger: 'blur' }
        ]
      },
      isLoading: false,
+     currentFilter: 'all',
+     tasks: [],
    };
  },
+ computed: {
+   filteredTasks() {
+     return this.tasks.filter((task) => this.currentFilter === 'all' || task.status === this.currentFilter);
+   }
+ },
+ created() {
+   this.retrieveTasks();
+ },
  methods: {
    openDialog() {
      this.dialogVisible = true;
      this.taskForm = {
        description: '',
        expected_completion_time: ''
      };
    },
    async handleNewTaskConfirm() {
      try {
        this.isLoading = true;
        const { description, expected_completion_time } = this.taskForm;
        const formattedExpectedCompletionTime = expected_completion_time.toLocaleString('zh-CN', { hour12: false });
        await taskService.createTask(description, formattedExpectedCompletionTime);
        this.dialogVisible = false;
        this.taskForm = {
          description: '',
          expected_completion_time: ''
        };
+       this.retrieveTasks();
        this.$message.success('任务创建成功');
      } catch (error) {
        this.$message.error('创建新任务失败');
      }finally{
        this.isLoading = false;
      }
    },
+   async retrieveTasks() {
+     try {
+       this.isLoading = true;
+       const tasks = await taskService.getTasks();
+       this.tasks = tasks;
+     } catch (error) {
+       this.$message.error('获取任务列表失败');
+     }finally{
+       this.isLoading = false;
+     }
+   },
+   filterTasks(status) {
+     this.currentFilter = status;
+   },
  }
}
</script>

<style lang="less" scoped>
.home {
  width: 850px;
  margin: 0px auto;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #CCC;
  }

  .tags {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 850px;

    span {
      margin: 10px;
    }
  }

  .cell .el-link {
    margin-left: 5px;
  }

  ::v-deep(.el-dialog__header) {
  text-align: left;
}

  ::v-deep(.el-form-item__content) {
    text-align: left;
  }
}
</style>

9.删除和完成 #

9.1 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <div class="header">
      <h3>TASK OA任务管理系统</h3>
      <el-button type="primary" @click="openDialog">新增任务</el-button>
    </div>
    <div class="tags">
      <el-tag 
        v-for="status in taskStatus"
        :key="status.value"
        style="cursor:pointer"
        :type="currentFilter === status.value ? 'primary' : 'info'"
        @click="filterTasks(status.value)"
      >
        {{ status.label }}
      </el-tag>
    </div>
    <el-table style="width: 100%" v-loading="isLoading" :data="filteredTasks">
      <el-table-column prop="id" label="编号" width="80"></el-table-column>
      <el-table-column prop="description" label="任务描述" width="180"></el-table-column>
      <el-table-column prop="status" label="状态"></el-table-column>
      <el-table-column prop="expected_completion_time" label="预期完成时间"></el-table-column>
      <el-table-column prop="actual_completion_time" label="实际完成时间"></el-table-column>
      <el-table-column fixed="right" label="操作" width="100">
+       <template v-slot:default="scope">
+         <el-popconfirm title="确定删除吗?" @confirm="() => handleDeleteClick(scope.row)">
+           <el-link slot="reference" type="danger">删除</el-link>
+         </el-popconfirm>
+         <el-popconfirm title="确定完成吗?"  @confirm="() => handleCompleteClick(scope.row)">
+           <el-link slot="reference" type="success">完成</el-link>
+         </el-popconfirm>
+       </template>
      </el-table-column>
    </el-table>
    <el-dialog title="新增任务" width="50%" :visible.sync="dialogVisible">
      <el-form>
        <el-form-item label="任务描述">
          <el-input type="textarea" :rows="5" v-model="taskForm.description"></el-input>
        </el-form-item>
        <el-form-item label="预计完成时间">
          <el-date-picker type="datetime" placeholder="请选择日期时间" v-model="taskForm.expected_completion_time"></el-date-picker>
        </el-form-item>
      </el-form>
      <template v-slot:footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="handleNewTaskConfirm">确 定</el-button>
          <el-button @click="dialogVisible = false">取 消</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import taskService from '@/api/taskService';
export default {
  name: 'HomeView',
  data() {
    return {
      taskStatus: [
        { label: '全部', value: 'all' },
        { label: '未完成', value: 1 },
        { label: '已完成', value: 2 },
      ],
      dialogVisible: false,
      taskForm: {
        description: '',
        expected_completion_time: ''
      },
      taskRules: {
        description: [
          { required: true, message: '请输入任务名称', trigger: 'blur' },
          { min: 1, max: 200, message: '长度在1到200个字符之间', trigger: 'blur' }
        ],
        expected_completion_time: [
          { required: true, message: '请输入预计完成时间', trigger: 'blur' }
        ]
      },
      isLoading: false,
      currentFilter: 'all',
      tasks: [],
    };
  },
  computed: {
    filteredTasks() {
      return this.tasks.filter((task) => this.currentFilter === 'all' || task.status === this.currentFilter);
    }
  },
  created() {
    this.retrieveTasks();
  },
  methods: {
    openDialog() {
      this.dialogVisible = true;
      this.taskForm = {
        description: '',
        expected_completion_time: ''
      };
    },
    async handleNewTaskConfirm() {
      try {
        this.isLoading = true;
        const { description, expected_completion_time } = this.taskForm;
        const formattedExpectedCompletionTime = expected_completion_time.toLocaleString('zh-CN', { hour12: false });
        await taskService.createTask(description, formattedExpectedCompletionTime);
        this.dialogVisible = false;
        this.taskForm = {
          description: '',
          expected_completion_time: ''
        };
        this.retrieveTasks();
        this.$message.success('任务创建成功');
      } catch (error) {
        this.$message.error('创建新任务失败');
      }finally{
        this.isLoading = false;
      }
    },
    async retrieveTasks() {
      try {
        this.isLoading = true;
        const tasks = await taskService.getTasks();
        this.tasks = tasks;
      } catch (error) {
        this.$message.error('获取任务列表失败');
      }finally{
        this.isLoading = false;
      }
    },
    filterTasks(status) {
      this.currentFilter = status;
    },
+   async handleDeleteClick(task) {
+     try {
+       this.isLoading = true;
+       await taskService.deleteTask(task.id);
+       this.retrieveTasks();
+       this.$message.success('任务删除成功');
+     } catch (error) {
+       this.$message.error('删除任务失败');
+     }finally{
+       this.isLoading = false;
+     }
+   },
+   async handleCompleteClick(task) {
+     try {
+       this.isLoading = true;
+       await taskService.updateTask(task.id, { status: 2 });
+       this.retrieveTasks();
+       this.$message.success('任务状态更新成功');
+     } catch (error) {
+       this.$message.error('更新任务状态失败');
+     }finally{
+       this.isLoading = false;
+     }
+   },
+ }
}
</script>

<style lang="less" scoped>
.home {
  width: 850px;
  margin: 0px auto;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #CCC;
  }

  .tags {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 850px;

    span {
      margin: 10px;
    }
  }

  .cell .el-link {
    margin-left: 5px;
  }

  ::v-deep(.el-dialog__header) {
  text-align: left;
}

  ::v-deep(.el-form-item__content) {
    text-align: left;
  }
}
</style>

10.vuex #

10.1 task.js #

src\store\task.js

import taskService from '@/api/taskService';

const state = {
  isLoading: false,
  currentFilter: 'all',
  tasks: [],
};

const getters = {
  filteredTasks: (state) => {
    return state.tasks.filter((task) => state.currentFilter === 'all' || task.status === state.currentFilter);
  },
};

const actions = {
  async retrieveTasks({ commit }) {
    commit('SET_LOADING', true);
    try {
      const tasks = await taskService.getTasks();
      commit('SET_TASKS', tasks);
    } catch (error) {
      this._vm.$message.error('获取任务列表失败');
    } finally {
      commit('SET_LOADING', false);
    }
  },
  async deleteTask({ dispatch }, taskId) {
    try {
      await taskService.deleteTask(taskId);
      dispatch('retrieveTasks');
      this._vm.$message.success('任务删除成功');
    } catch (error) {
      this._vm.$message.error('删除任务失败');
    }
  },
  async completeTask({ dispatch }, taskId) {
    try {
      await taskService.updateTask(taskId, { status: 2 });
      dispatch('retrieveTasks');
      this._vm.$message.success('任务状态更新成功');
    } catch (error) {
      this._vm.$message.error('更新任务状态失败');
    }
  },
  async createTask({ dispatch }, { description, expected_completion_time }) {
    try {
      await taskService.createTask(description, expected_completion_time);
      dispatch('retrieveTasks');
      this._vm.$message.success('任务创建成功');
    } catch (error) {
      this._vm.$message.error('创建新任务失败');
    }
  },
};

const mutations = {
  SET_LOADING: (state, isLoading) => {
    state.isLoading = isLoading;
  },
  SET_TASKS: (state, tasks) => {
    state.tasks = tasks;
  },
  SET_FILTER: (state, filter) => {
    state.currentFilter = filter;
  },
};

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
};

10.2 store\index.js #

src\store\index.js

import Vue from 'vue'
import Vuex from 'vuex'
+import task from './task'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
+   task
  }
})

10.3 HomeView.vue #

src\views\HomeView.vue

<template>
  <div class="home">
    <div class="header">
      <h3>TASK OA任务管理系统</h3>
      <el-button type="primary" @click="openDialog">新增任务</el-button>
    </div>
    <div class="tags">
      <el-tag v-for="status in taskStatus" :key="status.value" style="cursor:pointer"
        :type="currentFilter === status.value ? 'primary' : 'info'" @click="filterTasks(status.value)">
        {{ status.label }}
      </el-tag>
    </div>
    <el-table style="width: 100%" v-loading="isLoading" :data="filteredTasks">
      <el-table-column prop="id" label="编号" width="80"></el-table-column>
      <el-table-column prop="description" label="任务描述" width="180"></el-table-column>
      <el-table-column prop="status" label="状态"></el-table-column>
      <el-table-column prop="expected_completion_time" label="预期完成时间"></el-table-column>
      <el-table-column prop="actual_completion_time" label="实际完成时间"></el-table-column>
      <el-table-column fixed="right" label="操作" width="100">
        <template v-slot:default="scope">
          <el-popconfirm title="确定删除吗?" @confirm="() => handleDeleteClick(scope.row)">
            <el-link slot="reference" type="danger">删除</el-link>
          </el-popconfirm>
          <el-popconfirm title="确定完成吗?" @confirm="() => handleCompleteClick(scope.row)">
            <el-link slot="reference" type="success">完成</el-link>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <el-dialog title="新增任务" width="50%" :visible.sync="dialogVisible">
      <el-form :model="taskForm" ref="taskForm" :rules="taskRules">
        <el-form-item label="任务描述" prop="description">
          <el-input type="textarea" :rows="5" v-model="taskForm.description" ></el-input>
        </el-form-item>
        <el-form-item label="预计完成时间" prop="expected_completion_time">
          <el-date-picker type="datetime" placeholder="请选择日期时间"
            v-model="taskForm.expected_completion_time"></el-date-picker>
        </el-form-item>
      </el-form>
      <template v-slot:footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="handleNewTaskConfirm">确 定</el-button>
          <el-button @click="dialogVisible = false">取 消</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import { mapState, mapActions, mapGetters } from 'vuex';
export default {
  name: 'HomeView',
  data() {
    return {
      dialogVisible: false,
      taskForm: {
        description: '',
        expected_completion_time: ''
      },
      taskRules: {
        description: [
          { required: true, message: '请输入任务名称', trigger: 'blur' },
          { min: 1, max: 200, message: '长度在1到200个字符之间', trigger: 'blur' }
        ],
        expected_completion_time: [
          { required: true, message: '请输入预计完成时间', trigger: 'blur' }
        ]
      },
      taskStatus: [
        { label: '全部', value: 'all' },
        { label: '未完成', value: 1 },
        { label: '已完成', value: 2 },
      ]
    };
  },
  computed: {
+   ...mapState('task', [ 'isLoading', 'currentFilter']),
+   ...mapGetters('task', ['filteredTasks']),
  },
  created() {
+   if (!this.filteredTasks.length)
+     this.retrieveTasks();
  },
  methods: {
+   ...mapActions('task', ['retrieveTasks', 'deleteTask', 'completeTask']),
    openDialog() {
      this.dialogVisible = true;
      this.taskForm = {
        description: '',
        expected_completion_time: '',
      };
    },
    async handleNewTaskConfirm() {
      this.$refs.taskForm.validate(async (valid) => {
        if (valid) {
          const { description, expected_completion_time } = this.taskForm;
          const formattedExpectedCompletionTime = expected_completion_time.toLocaleString('zh-CN', { hour12: false });
+         await this.$store.dispatch('task/createTask', { description, expected_completion_time: formattedExpectedCompletionTime });
          this.dialogVisible = false;
          this.taskForm = {
            description: '',
            expected_completion_time: '',
          };
        } else {
          this.$message({
            type: 'error',
            message: '表单验证失败!'
          });
          return false;
        }
      })

    },
    filterTasks(status) {
+     this.$store.commit('task/SET_FILTER', status);
    },
    handleDeleteClick(task) {
+     this.deleteTask(task.id);
    },
    handleCompleteClick(task) {
+    this.completeTask(task.id);
    },
  }
}
</script>

<style lang="less" scoped>
.home {
  width: 850px;
  margin: 0px auto;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #CCC;
  }

  .tags {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 850px;

    span {
      margin: 10px;
    }
  }

  .cell .el-link {
    margin-left: 5px;
  }

  ::v-deep(.el-dialog__header) {
    text-align: left;
  }

  ::v-deep(.el-form-item__content) {
    text-align: left;
  }
}
</style>

11.参考 #

11.1 element-ui #

Element UI 是一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的组件库。它内置了丰富的基础组件和实用的业务组件,可以帮助开发者快速搭建出功能丰富、界面美观的 Web 应用。

以下是 Element UI 的一些主要特点:

  1. 基础组件丰富:Element UI 内置了一系列的基础组件,比如 Button(按钮)、Icon(图标)、Input(输入框)、Radio(单选框)、Checkbox(多选框)、Select(选择器)、Switch(开关)、Table(表格)、Dialog(对话框)、Tooltip(文字提示)等等。

  2. 实用的业务组件:Element UI 还提供了一些实用的业务组件,比如 DatePicker(日期选择器)、TimePicker(时间选择器)、DateTimePicker(日期时间选择器)、Upload(上传)、Form(表单)等。

  3. 支持国际化:Element UI 支持国际化,可以通过设置来切换语言。

  4. 响应式设计:Element UI 的组件都支持响应式设计,可以适应不同尺寸的屏幕和设备。

  5. 主题定制:Element UI 提供了主题定制的功能,你可以通过它来定制自己的主题颜色。

要开始使用 Element UI,你可以通过 npm 或 yarn 来进行安装:

npm install element-ui

yarn add element-ui

然后,在你的 Vue 项目中,可以全局引入 Element UI:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

现在,你就可以在你的 Vue 组件中使用 Element UI 的组件了。例如:

<template>
  <el-button type="primary">Primary Button</el-button>
</template>

如果你不希望全局引入所有的组件,Element UI 也支持按需引入,你可以只引入你需要的组件。你可以使用官方提供的 Element UI 按需引入插件 来进行按需引入。

11.2 components #

在 Element UI 中,每个组件都是基于 Vue 实现的,它们可以很好地和 Vue 的数据驱动视图进行集成。

  1. Button(按钮):Button 组件用于触发一个操作或者一个动作。你可以通过 type 属性来定义按钮的样式,比如 "primary"、"success"、"warning"、"danger" 和 "info"。plain 属性可以让按钮变成朴素按钮。roundcircle 属性可以使按钮变成圆角或者圆形按钮。

  2. Tag(标签):Tag 组件用来表示标签,比如分类标签。你可以通过 type 属性来定义标签的样式,也可以设置 closable 属性来让标签可关闭。

  3. Table(表格):Table 组件用于显示表格数据,它的数据源是一个对象数组。每一列的配置通过 TableColumn 组件进行。可以通过 data 属性来绑定数据源。

  4. TableColumn(表格列):TableColumn 组件用于定义表格的列,它需要配合 Table 组件使用。你可以使用 prop 属性来指定该列对应数据对象的属性,使用 label 属性来指定该列的标题。

  5. Link(链接):Link 组件用于表示一个链接。你可以通过 type 属性来定义链接的样式,也可以设置 underline 属性来控制是否显示下划线。

  6. Popconfirm(气泡确认框):Popconfirm 组件用于在用户进行某些操作时进行确认。比如,你可以在用户点击删除按钮时,先弹出 Popconfirm 来让用户确认是否真的要删除。

  7. Dialog(对话框):Dialog 组件用于显示对话框。你可以通过 visible 属性来控制对话框是否显示,通过 title 属性来设置对话框的标题。

  8. Form(表单):Form 组件用于创建表单。Form 组件可以配合 FormItem 组件以及各种表单控件(如 Input)一起使用,创建各种复杂的表单。

  9. FormItem(表单项):FormItem 组件用于创建表单项,它需要配合 Form 组件使用。每一个 FormItem 对应一个表单字段,你可以通过 label 属性来设置表单字段的标签。

  10. Input(输入框):Input 组件用于获取用户的输入。你可以使用 v-model 指令来双向绑定输入框的值。

  11. DatePicker(日期选择器):DatePicker 组件用于让用户选择日期或日期范围。你可以通过 type 属性来控制日期选择器的类型("date"、"datetime"、"datetimerange" 等)。

  12. Message(消息提示):Message 是一个全局的消息提示函数,你可以通过调用 this.$message 来显示消息提示。可以通过参数来控制消息

11.3 axios #

Axios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 中使用。它提供了一个 API 来处理 XMLHttpRequests 和 HTTP 请求。

以下是 Axios 的主要特点:

  1. 浏览器兼容性:Axios 可以在现代浏览器和 Node.js 中使用。

  2. Promise API:Axios 使用 Promise API,提供了一种更优雅的方式来处理异步操作。

  3. 请求和响应拦截:你可以在请求或响应被 then 或 catch 处理之前对它们进行修改。

  4. 自动转换 JSON 数据:当请求或响应包含 JSON 数据时,Axios 会自动将其转换为 JavaScript 对象。

  5. 客户端支持防御 XSRF:当在浏览器中使用时,可以设置一个值,该值将在请求头中被自动包含,以防御跨站请求伪造(XSRF)。

以下是如何在 JavaScript 中使用 Axios 发起 GET 请求的示例:

axios.get('/user?ID=12345')
  .then(function (response) {
    // 处理成功的响应
    console.log(response);
  })
  .catch(function (error) {
    // 处理发生的错误
    console.log(error);
  })
  .then(function () {
    // 总是会被执行
  });

你也可以使用 async/await 语法来处理 Promise:

async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

你可以使用 axios.create 方法创建一个新的 Axios 实例,并配置默认的请求选项:

const instance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

这个新的实例具有和全局 axios 对象一样的方法,但可以有自己的配置。

在 Node.js 中使用 Axios 也非常简单。你只需要通过 npm 或 yarn 安装 Axios,然后在你的代码中引入它:

npm install axios

然后在你的代码中:

const axios = require('axios');

axios.get('https://api.example.com/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.error(error);
  });

总的来说,Axios 是一个功能强大、灵活、易用的 HTTP 库,无论在浏览器还是 Node.js 环境中都是一个很好的选择。

11.4 babel-plugin-component #

babel-plugin-component 是一个 Babel 插件,它用于按需引入 Element UI 组件,以减少项目的最终构建体积。Element UI 是一个为 Vue.js 构建的开源 UI 组件库,通过 babel-plugin-component,你可以只引入你实际使用到的 Element UI 组件,而不是整个 Element UI 库。

下面是如何在项目中安装并使用 babel-plugin-component

  1. 安装 babel-plugin-component

在你的项目中,通过 npm 或 yarn 安装 babel-plugin-component

使用 npm:

npm install babel-plugin-component --save-dev

或者使用 yarn:

yarn add babel-plugin-component --dev
  1. 配置 .babelrcbabel.config.js

在你的 .babelrcbabel.config.js 文件中,需要配置 babel-plugin-component 插件。一个常见的配置如下:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

在上面的配置中,libraryName 设置为 "element-ui",这意味着插件会处理所有引入自 Element UI 的模块。styleLibraryName 设置为 "theme-chalk",这意味着插件会自动引入对应组件的样式。

  1. 在代码中按需引入 Element UI 组件

有了上述配置后,你就可以在代码中按需引入 Element UI 组件了。例如:

import { Button } from 'element-ui';

通过上述代码,Babel 在构建时会利用 babel-plugin-component 插件,只引入 Button 组件及其样式,而不是整个 Element UI 库。

11.5 v-loading #

v-loading 是一个由 Element UI 提供的指令,它用于在某个元素(在你的例子中,这个元素是 <el-table>)上显示或隐藏加载提示(即一个 loading spinner)。

v-loading 指令接受一个布尔值。当这个值为 true 时,加载提示会显示;当这个值为 false 时,加载提示会隐藏。

v-loading 指令的值是 isLoading。这个 isLoading 应该是在 Vue 组件的 data 选项中定义的一个布尔值。这意味着,当 isLoading 的值改变时,加载提示的显示状态也会相应地改变。例如,你可能会在发送 AJAX 请求的时候将 isLoading 设置为 true,然后在请求完成后将 isLoading 设置为 false,这样就可以给用户显示一个加载提示,让他们知道表格数据正在加载。

11.6 .sync #

在Vue.js中,.sync修饰符是一个语法糖,它使我们能够双向绑定父子组件之间的属性。这使得在子组件中修改一个传入的属性值时,可以自动更新父组件中相应的数据。

.sync修饰符实际上是使用了一个名为update:myPropName的事件来实现的,当子组件需要更新一个属性时,它会触发这个事件。然后,父组件会监听这个事件,并根据事件的参数来更新一个本地的数据。注意,myPropName是需要更新的属性名。

下面是一个简单的示例:

子组件(子组件接收一个title属性,并在用户输入时触发update:title事件):

<template>
  <div>
    <input v-model="localTitle" @input="updateTitle" />
  </div>
</template>

<script>
export default {
  props: ['title'],
  data() {
    return {
      localTitle: this.title
    };
  },
  methods: {
    updateTitle(e) {
      this.$emit('update:title', e.target.value);
    }
  }
}
</script>

父组件(父组件传入一个title属性,并监听update:title事件):

<template>
  <div>
    <ChildComponent :title.sync="title" />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      title: 'Hello, Vue!'
    };
  }
}
</script>

在这个示例中,当用户在子组件的输入框中输入内容时,updateTitle方法会被触发,从而触发了update:title事件并传入新的值。然后,父组件监听这个事件,并使用传入的新值更新title属性。由于title属性和ChildComponenttitle属性进行了双向绑定,所以ChildComponenttitle属性也会更新。

需要注意的是,虽然.sync可以使我们更容易地在父子组件间同步数据,但它并不是一个真正的双向数据绑定。实际上,Vue的官方建议是尽可能避免改变一个传入的属性的值,而.sync修饰符是一种妥协和例外。在大多数情况下,使用事件来告知父组件状态的变化,然后由父组件决定如何处理,是一种更好的模式。

11.7 this.$message #

在 Vue 中,this.$message 是 Element UI 库提供的一个全局方法,用于展示提示信息。这是一种全局注册的方式,让我们可以在任何一个组件中通过 this.$message 来调用这个方法。

this.$message 接收一个字符串或一个对象作为参数,当接收一个字符串时,它会直接显示这个字符串作为提示信息。当接收一个对象时,这个对象的属性会被用来配置提示信息的各种参数。下面是一些可用的配置选项:

例如,下面的代码会显示一个带有成功图标,持续 2 秒的提示信息:

this.$message({
  message: '恭喜你,这是一条成功消息',
  type: 'success',
  duration: 2000
});

同时,this.$message 也提供了一些快捷方法,如 this.$message.success('恭喜你,这是一条成功消息'),这样也可以显示一个带有成功图标的提示信息。

需要注意的是,要使用 this.$message,我们需要先在我们的 Vue 应用中安装并引入 Element UI 库。

11.8 this._vm #

在 Vuex 的 actions 中,this._vm 可以理解为 Vuex 当前的 Vue 实例。因此,this._vm.$message 实际上就是在调用 Vue 实例上的 $message 方法,这个方法是 Element UI 提供的一个全局方法,用于展示提示信息。这是一种全局注册的方式,让我们可以在任何一个 Vue 组件或 Vuex action 中通过 this.$messagethis._vm.$message 来调用这个方法。

当你在 action 中执行 this._vm.$message.success('任务删除成功'),你实际上在执行一个带有成功类型 ('success') 的消息提示,消息内容为 '任务删除成功'。

需要注意的是,Vuex 的 actions 是专门处理异步操作的,例如 API 请求等,actions 可以包含任何异步操作。当这些异步操作完成后,我们通常需要给用户一些反馈,这时我们可以利用 Element UI 的 this._vm.$message 来给出相应的提示信息。

在 Vuex 中使用 Element UI 的 this._vm.$message 提示信息的一个好处是,你可以将 UI 层(Vue 组件)和业务逻辑(Vuex)解耦,这使得代码更加清晰和易于管理。

11.9 ::v-deep #

::v-deep()是一个Vue.js应用程序中特别的深度选择器,用于穿透Scoped CSS。该选择器在Vue 3中取代了 /deep/::v-deep,和 >>>

在Vue.js应用程序中,当我们使用scoped样式时,Vue会在元素上添加一个唯一的属性以实现样式的封装。然而,有时我们需要改变子组件的样式,这个时候我们就需要使用深度选择器。

::v-deep的使用方法是:

<style scoped>
.parent ::v-deep .child {
  color: blue;
}
</style>

上述代码将会应用于拥有 .child 类的元素,无论这个元素是在 .parent 里面的什么深度。

需要注意的是,::v-deep 不仅可以应用于子组件,还可以应用于 slot 中的元素,以及动态添加的内容。

虽然 ::v-deep 是一个强大的工具,但请谨慎使用,因为它可能会破坏组件样式的封装性。

11.10 slot="reference" #

slot="reference" 是 Element UI 的 Popconfirm(气泡确认框)组件中的一个特定插槽名。插槽(slot)是 Vue.js 中一种非常有用的特性,用于复用和重写模板中的 HTML 结构。这可以让你自定义组件模板中的一部分内容,以满足更多的使用场景。

在这个示例中,<el-link slot="reference" type="danger">删除</el-link> 的作用是在 Popconfirm 组件中定义一个点击触发气泡确认框的元素,即作为触发点(参考元素)。当你点击这个带有 "danger" 类型的链接时,就会弹出 Popconfirm 组件要求确认。

你可以将任何内容放入这个插槽中,比如按钮、链接或者其他元素,以便在点击这个元素时显示气泡确认框。

以下是 Popconfirm 组件的示例代码:

<el-popconfirm title="确定删除吗?">
  <template #reference>
    <el-link type="danger">删除</el-link>
  </template>
</el-popconfirm>

在这段代码中,#reference 是 Vue.js 2.6.0+ 版本中新增的动态插槽语法,和 slot="reference" 是等效的。

v-slot:reference应用于<template>元素,而不是直接应用于<el-link>。在Vue的插槽语法中,v-slot只能添加在<template>上。

这是因为v-slot的设计目的是允许我们在父组件中定义一个可供子组件内部使用的模板。而这个模板需要一个容器来包裹它,这个容器就是<template>标签。

如果你希望使用新的v-slot语法,可以这样做:

<template v-slot:reference>
  <el-link type="danger">删除</el-link>
</template>

注意,上面的代码只有在<el-link>所在的组件提供了名为"reference"的插槽时才会工作。

然而,对于slot="reference",它是旧语法,可以直接应用于任何元素(包括组件),用于指定这个元素(或组件)应该被放在哪个插槽中。所以在你的原代码中,<el-link slot="reference" type="danger">删除</el-link>的含义是,将<el-link>放入名为"reference"的插槽中。