如果没有环境,自行安装。
node --version
vue --version
vue create file-web
步骤:
选择预设,这里我们采用 Manually select features
来手动安装一些依赖。
选择需要的功能,选择 Vue 版本,安装 Babel、Router、Vuex、CSS 预编译器和格式化检查
选择 Vue.js 版本,采用 2.x
。
路由是否采用 history 模式,选择是。输入 Y,回车键。
CSS 预编译器,采用 Stylus
。
选择一个插件化的 JavaScript 代码检测和格式化工具 ,结合 Visual Studio Code 提供的扩展程序 ESLint、Prettier 等,你可以选择自己习惯用的代码检测工具和格式化工具,采用 ESLint width error prevention only
。
何时检测代码,此实验采用 Lint on save
保存文件时测试。
如何存放配置,选择保存到专用的配置文件中。
是否保存本次配置(y:记录本次配置,然后需要给配置命名,N:不记录本次配置),这里选择 N。
项目根目录下创建文件vue.config.js
module.exports = {
publicPath: "/",
devServer: {
host: "0.0.0.0",
open: true,
disableHostCheck: true,
},
};
操作和命令行差不多
vue ui
node_modules
:存放项目的各种依赖。
public
:存放静态资源,其中的 index.html 是项目的入口文件,浏览器访问项目的时候默认打开的是生成后的 index.html。
src
:存放项目主体文件,具体介绍如下:
assets
:存放各种静态文件,包括图片、CSS 文件、JavaScript 文件、各类数据文件等。components
:存放公共组件,比如此课程后续将会用到的顶部导航栏 Header.vue。router/index.js
:vue-router 安装时自动生成的路由相关文件,主要用来为每个路由设置路径、名称和对应页面的 .vue 文件等。store/index.js
:vuex 安装时自动生成的状态相关文件,后续章节会详细介绍,用来让多个页面或组件共享数据。views
:存放页面文件,比如默认生成的 Home.vue 首页、About.vue 关于页面。App.vue
:是主 vue 模块,主要是使用 router-link 引入其他模块,所有的页面都是在 App.vue 下切换的。main.js
:是入口文件,主要作用是初始化 vue 示例、引用某些组件库或挂载一些变量。.eslintrc
:配置代码校验 ESLint 规则。
.gitignore
:配置 git 上传时想要忽略的文件。
babel.config.js
:一个工具链,主要用于兼容低版本的浏览器。
package.json
:配置项目名称、版本号,记录项目开发所安装的依赖的名称、版本号等。
package-lock.json
:记录项目安装依赖时,各个依赖的具体来源和版本号。
为了保持代码整洁,我们需要对编辑器做一定的设置。
安装插件prettier,创建 .prettierrc.json 文件
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleAttributePerLine": false,
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"printWidth": 120,
"tabWidth": 4
}
npm run serve
-S选项表示将包添加到您的项目依赖项中,–legacy-peer-deps选项表示使用旧版本的npm对等依赖项算法。
npm i element-ui -S --legacy-peer-deps
采用完整引入 Element UI 的方式,在 src/main.js
中补充以下内容(注意是vue2,不是vue3):
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
Vue Router 是 Vue.js 官方的路由管理器(Vue Router),和 Vue.js 的核心深度集成,可以帮我们快速创建、配置路由,例如路由和页面的对应、路由参数获取和修改、HTML5 历史模式或 hash 模式等。
自动生成的路由文件 src/router/index.js
,首先需要引入 vue-router
模块(创建项目时,已经安装了 Vue Router 依赖)、挂载在 Vue 上,接着需要创建路由列表,包含路径、路由名称、页面文件等配置,之后创建路由实例,并导出,然后在 src/main.js
中引入并使用即可。
这里特别要说明的是 HTML5 History 模式。如果不使用 history 模式,例如现有的关于页面,路由路径将会是 http://oursite.com/#/about
这种形式。如果不想要就很丑的 hash, 可以用路由的 history 模式,那么路由路径就会是 http://oursite.com/about
。这种模式充分利用 history.pushState
API 来完成 URL 跳转而无须重新加载页面。如果使用了这种模式,在部署时需要后台配置支持,因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/about
就会返回 404。所以需要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html
页面,这个页面就是你 app 依赖的页面。
我们可以在 *.vue
文件中通过 $router
访问路由实例,调用 this.$router
来操作路由(编程式的导航 | Vue Router),例如后续实验将会用到的 this.$router.push
,用于向 history 栈添加新的记录;使用 this.$route
获取路由信息,例如接下来将会用到的获取当前页面路由路径 this.$route.path
。
首先来做一点清洁工作:删除 src/views/about.vue
文件,删除 src/router/index.js
中的路由 About,在 src/App.vue
的
* {
margin: 0;
padding: 0;
}
src/main.js
中引入此样式文件:import "./assets/style/base.styl";
我们将沿用 src/router/index.js
中的路由 Home 来作为首页,可以看到此路由的 component 中引入的页面文件为 src/views/Home.vue
,先来清空下 src/views/Home.vue
中的代码,以便进行后续的代码编写,并删除 src/components/HelloWorld.vue
文件。
清空后,键入以下内容:
这是网盘主页
src/views
中新建登录页面文件 Login.vue
,键入以下内容:
这是登录页面
src/router/index.js
创建登录页面路由:const routes = [
...{
path: "/login", // 登录页面
name: "Login",
component: () =>
import(/* webpackChunkName: "login" */ "../views/Login.vue"),
},
];
views
目录下新建注册页面文件 Register.vue
并添加路由:
这是注册页面
router/index.js
文件下添加路由:const routes = [
...{
path: "/register", // 注册页面
name: "Register",
component: () =>
import(/* webpackChunkName: "register" */ "../views/Register.vue"),
},
];
"rules": {
"vue/multi-word-component-names": "off"
}
为了防止用户在地址栏输入错误的路径而导致页面加载出错,我们需要用一个 404 页面来拦截,页面添加方式与登录、注册页面相同,在 src/views
目录下新建 Error_404.vue
文件:
此页面不存在……
重点是 404 页面路由的添加位置,需要添加在所有路由之后(404 Not found 路由):
const routes = [
...{
path: "/register", // 注册页面
name: "Register",
component: () =>
import(/* webpackChunkName: "register" */ "../views/Register.vue"),
},
{
path: "*", // 404页面
name: "Error_404",
component: () =>
import(/* webpackChunkName: "error_404" */ "../views/Error_404.vue"),
},
];
前面我们已经成功的添加了页面,接下来我们将使用 NavMenu 导航菜单 添加顶部导航栏,帮助我们快速构建页面,免去不必要的样式修改、事件添加等工作。
在 src/components
下创建文件 Header.vue
,键入以下内容:
首页
登录
注册
其中的 el-menu 即为 Element UI 中的 NavMenu 导航菜单,:router="true"
表示使用 vue-router 的模式, index 是每个导航菜单的唯一标志,这里配置为各个页面对应的路由名称 name,default-active 为当前激活菜单的 index,为了刷新页面时也可以保证停留在当前页面,这里采用计算属性的方式给 activeIndex 赋值。 中的属性 route 为 Vue Router 路径对象,即要跳转到的页面的路由对象,这里依次配置为首页、登录、注册页面的路由对象。
在 src/App.vue
中引入、注册并使用此组件:
Axios(axios 中文网|axios API 中文文档 | axios)是易用、简洁且高效的 http 库, 使用 Promise 管理异步,支持请求和响应拦截器,自动转换 JSON 数据等高级配置,与 Vue.js 有很好的融合。
终端中键入以下命令,以安装 Axios:
npm install axios
为了便于后续接口管理,一般都将所有的接口单独放在同一目录下统一管理。在 src
下新建文件夹request
,并创建文件 src/request/http.js
,后续对接口的 baseURL、超时时间、请求和响应拦截、接口类型封装等都将在此文件中。
在 http.js
中先来引入 Axios,设置请求超时时间,基础 URL,并自定义 POST 请求头:
import axios from "axios";
// 请求超时时间
axios.defaults.timeout = 10000 * 5;
// 请求基础URL,对应后台服务接口地址
axios.defaults.baseURL = "http://localhost:8081";
// 自定义post请求头
axios.defaults.headers.post["Content-Type"] =
"application/x-www-form-urlencoded";
设置请求拦截器和响应拦截器,对接口的请求头、响应结果做统一处理,例如自定义请求头,对接口响应的 HTTP 状态码非 200 的情况做处理等:
import { Message } from "element-ui";
// 请求拦截器
axios.interceptors.request.use(
(config) => {
// 自定义请求头
return config;
},
(error) => {
// 请求错误时
console.log(error); // 打印错误信息
return Promise.reject(error);
}
);
// 响应拦截器
axios.interceptors.response.use(
(response) => {
if (response.status === 200) {
// 接口HTTP状态码为200时
return Promise.resolve(response);
}
},
// HTTP状态码非200的情况
(error) => {
if (error.response.status) {
switch (error.response.status) {
case 500: // HTTP状态码500
Message.error("后台服务发生错误");
break;
case 401: // HTTP状态码401
Message.error("无权限");
break;
case 404: // HTTP状态码404
Message.error("当前接口不存在");
break;
default: // 页面显示接口返回的错误信息
this.$message.error(error.response.message);
return Promise.reject(error.response);
}
}
}
);
get 请求中的参数分为 params 和 info,其中 params 是查询参数,接口中的表现形式为 & 符号连接的 key=value 形式的字符串,统一用?符号拼接在接口后,例如常用的分页查询接口 getFileList?page=1&pageSize=10
;info 参数直接拼接在 url 中,例如某些查询接口 get 请求,需要把 id 拼接在 url 中。
/**
* get方法,对应get请求
*/
export function get(url, params, info = "") {
return new Promise((resolve, reject) => {
axios
.get(url + info, {
params: params,
})
.then((res) => {
resolve(res.data); // 返回接口响应结果
})
.catch((err) => {
reject(err.data);
});
});
}
post 请求中的参数分为 formData 格式和 json 格式,需要根据后台接口采用不同的传参格式:
/**
* post方法,对应post请求
* info为 true,formData格式;
* info为 undefined或false,是json格式
*/
export function post(url, data = {}, info) {
return new Promise((resolve, reject) => {
let newData = data;
if (info) {
// 转formData格式
newData = new FormData();
for (let i in data) {
newData.append(i, data[i]);
}
}
axios
.post(url, newData)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
put 请求和 delete 请求封装同理:
/**
* 封装put请求
*/
export function put(url, params = {}, info = "") {
return new Promise((resolve, reject) => {
axios.put(url + info, params).then(
(res) => {
resolve(res.data);
},
(err) => {
reject(err.data);
}
);
});
}
/**
* 封装delete请求
*/
export function axiosDelete(url, params = {}, info = "") {
return new Promise((resolve, reject) => {
axios
.delete(url + info, {
params: params,
})
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
在 src/request
下创建新文件 user.js
,所有与用户相关的接口均维护在这个文件中。
首先引入封装好的 get、post 类型的请求:
import { get, post } from "./http";
接下来封装登录接口:
'/user/login'
为后台提供的接口 path。// 登录接口
export const login = (p) => get("/user/login", p);
封装注册接口:
// 注册接口
export const addUser = (p) => post("/user/register", p);
我们需要对 Axios 中的设置和封装做些改动,以便在本地开发环境中也可以调用接口。
在 public
中新建 config.json
文件,存放后台接口,这里必须配置完整的接口 baseURL,包括协议、IP、端口,有时候后台会有后缀 /backend
等:
{
"baseUrl": "http://localhost:8081"
}
在 vue.config.js
中配置代理:
const productConfig = require("./public/config.json"); // 引入config.json文件
module.exports = {
publicPath: "/",
devServer: {
host: "0.0.0.0",
open: true,
disableHostCheck: true,
proxy: {
//配置代理,解决跨域请求后台数据的问题
"/api": {
target: productConfig.baseUrl, //后台接口,连接本地服务
ws: true, //是否跨域
changeOrigin: true,
pathRewrite: {
"^/api": "/",
},
},
},
},
};
对 src/request/http.js
中的 Axios 的 baseURL 做修改:
// 请求基础URL
axios.defaults.baseURL = "/api";
现在我们重新启动项目。
先来采用 Element UI 的表单 Form 组件等编写页面,在 src/views/Register.vue
中键入以下内容:
注册
注册
继续编辑 Register.vue
文件,引入封装好的注册接口:
import { addUser } from "@/request/user.js";
在注册按钮的点击事件中调用注册接口:
在 src/views/Login.vue
中编写登录页面:
登录
登录
在登录按钮的点击事件中调用登录接口,这里我们需要在登录之后在接口的自定义请求头中添加 token。
键入以下命令安装 js-cookie
:
npm install js-cookie
在 src/request/http.js
和 src/views/Login.vue
中引入 js-cookie
,并自定义请求头:
http.js
中使用 js-cookie
:
import Cookies from "js-cookie";
// 请求拦截器
axios.interceptors.request.use(
(config) => {
// 自定义请求头
config.headers["token"] = Cookies.get("token");
return config;
},
(error) => {
console.log(error);
return Promise.reject(error);
}
);
登录页面使用 js-cookie
,引入封装好的登录接口,编辑 src/views/Login.vue
文件:
...
在 src/request/user.js
中添加获取用户登录信息接口:
// 获取登录状态及用户信息
export const checkUserLoginInfo = (p) => get("/user/checkuserlogininfo", p);
先来使用 Vuex 把状态保存实现,在 src/store
下新建文件夹 module
,并新建文件 src/store/module/user.js
,键入以下内容(下节实验会介绍 Vuex 的使用):
import { checkUserLoginInfo } from "@/request/user.js"; // 引入获取用户登录信息接口
export default {
state: {
isLogin: false, // 初始时候给一个 isLogin = false 表示用户未登录
username: "",
userId: 0,
userImgUrl: "",
userInfoObj: {},
},
mutations: {
changeLogin(state, data) {
state.isLogin = data;
},
changeUsername(state, data) {
state.username = data;
},
changeUserId(state, data) {
state.userId = data;
},
changeUserInfoObj(state, data) {
state.userInfoObj = Object.assign({}, state.userInfoObj, data);
},
},
actions: {
getUserInfo(context) {
return checkUserLoginInfo().then((res) => {
if (res.success) {
context.commit("changeLogin", res.success);
context.commit("changeUsername", res.data.username);
context.commit("changeUserId", res.data.userId);
context.commit("changeUserInfoObj", res.data);
} else {
context.commit("changeLogin", res.success);
}
});
},
},
};
在 src/store/index.js
中引入刚才创建好的 user.js
,并将相关数据导出:
import Vue from "vue";
import Vuex from "vuex";
import user from "./module/user"; // 引入user.js
Vue.use(Vuex);
export default new Vuex.Store({
state: {
//
},
getters: {
isLogin: (state) => state.user.isLogin,
username: (state) => state.user.username,
userId: (state) => state.user.userId,
userInfoObj: (state) => state.user.userInfoObj,
},
mutations: {
//
},
actions: {
//
},
modules: {
user,
},
});
之后就可以在 *.vue
文件中使用 this.$store.getters.isLogin
来获取用户的登录状态了。
为了判断哪些路由需要登录之后才可进入,需要在路由上添加一些信息。在 src/router/index.js
中给首页路由添加 meta 属性,并添加参数 requireAuth
,值为 true:
{
path: '/', // 路由路径,即浏览器地址栏中显示的URL
name: 'Home', // 路由名称
component: Home, // 路由所使用的页面
meta: {
requireAuth: true
}
}
在 src/router
下新建文件 before.js
,引入 Vue Router 和状态保存文件 src/store/index.js
:
import router from "./index.js";
import store from "@/store/index.js";
// 路由全局前置守卫
router.beforeEach((to, from, next) => {
// 调用接口,判断当前登录状态
store.dispatch("getUserInfo").then(() => {
if (to.matched.some((m) => m.meta.requireAuth)) {
if (!store.getters.isLogin) {
// 没有登录
next({
path: "/login",
query: { Rurl: to.fullPath },
});
} else {
next(); // 正常跳转到你设置好的页面
}
} else {
next(); // 正常跳转到你设置好的页面
}
});
});
添加全局前置守卫,可以在触发导航之前进行一些处理,当处理完成后才会执行导航:
meta.requireAuth
是否为 true,若为 true,表示需要登录后才可进入;若没有设置当前参数,或参数值为 false,表示无需登录也可进入。meta.requireAuth
为 true 时,判断在 Vuex 中保存的 isLogin 为 true 还是 false,为 true 表示已登录,那么执行 next()
即可正常导航;为 false 表示未登录,按照之前的说明,将跳转到登录页面。全局前置守卫有三个参数 to、from、next:
to: Route
:即将要进入的路由对象,包含路由名称、路径、参数等。from: Route
:当前导航正要离开的路由对象。next: Function
:在全局前置守卫中, 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next
方法的调用参数:next()
无参数时, 进行管道中的下一个钩子;next(false)
参数为 false 时,中断当前的导航;next({ path: '/' })
跳转到一个不同的地址,当前的导航被中断,然后进行一个新的导航。 next
支持传递任意位置对象,且允许设置诸如 replace: true
、name: 'home'
之类的选项以及任何用 router-link 的 to
prop 或 router.push
中的选项。在 src/main.js
中引入刚才创建好的 before.js
:
import "@/router/before.js";
现在我们来直接进入首页,发现接口请求返回的 false,页面直接跳转到了登录页面,并且带了查询参数 Rurl
:
然后来给登录、注册页面在登录状态下添加自动跳转到首页的效果,在 src/views/Login.vue
中的生命周期 created()
中添加登录状态判断:
<script>
import { login } from '@/request/user.js' // 引入登录接口
import Cookies from 'js-cookie'
export default {
name: 'Login',
data() {
return {
// ......
}
},
created() {
if (this.$store.getters.isLogin) {
// 用户若已登录,自动跳转到首页
this.$notify({
title: '成功',
message: '您已登录!已跳转到首页',
type: 'success'
})
this.$router.replace({ name: 'Home' })
}
},
methods: {
...
注册页面同理,在 created
中添加同样的处理。修改 Register.vue
文件,添加如下代码:
来看下登录、跳转到首页、退出登录流程的效果:
根据前面的实验,我们知道了项目启动的完整命令,如下所示:
# 启动后端
sudo service mysql start
cd /home/project/qiwen-file
mvn spring-boot:run
# 新开一个终端,启动前端
cd /home/project/file-web
npm run serve
在之前的实验中,实现顶部导航栏时已经用到过 NavMenu 导航菜单,这次我们主要来实现菜单的收缩展开功能。先来对项目的文件目录做点调整:
src/views
下新建文件夹 Home,将 src/views/Home.vue
重命名为 src/views/index.vue
,并移动到刚才创建好的文件夹 src/views/Home
中。src/router/index.js
中对原有的 Home.vue
的引入,保证路由和页面可以正常匹配:将 import Home from '../views/Home.vue'
改为 import Home from '../views/Home/index.vue'
。刷新首页,可以正常显示。
在 src/views/Home
下创建新的文件夹 components
,存放独属于首页的组件,后续的面包屑导航栏、表格列筛选组件都会放置于此文件夹下。
在 src/views/Home/components
下创建新文件 SideMenu.vue
,在 src/views/Home/index.vue
中添加以下内容来引入、注册并使用此组件:
参考 Element UI 的 NavMenu(NavMenu 垂直菜单),先来实现一个区分文件类型的垂直菜单,在 SideMenu.vue
文件中添加以下内容:
全部
图片
文档
视频
音乐
其他
接着我们来使用 NavMenu 的 collapse 属性(NavMenu 折叠)把左侧菜单的收缩展开功能添加上,并调整样式:
全部
在 `index.vue` 中引入 `BreadCrumb.vue`,调整布局,使菜单居左,右侧内容区域自适应宽度:
刷新首页,看下页面布局,收缩展开左侧菜单时,右侧内容区域可以自适应宽度:

### 3.使用 Table 表格组件实现文件展示
参考官方示例([Table 表格](https://element.eleme.cn/#/zh-CN/component/table))来实现文件展示区域。仍然在 `views/Home/components` 下创建文件 `FileTable.vue`,添加以下内容:
在 `index.vue` 中引入:
…
刷新首页,看下文件展示区域和页面布局:

表格的操作列需要添加对文件操作的按钮:删除、移动、重命名、下载,同时需要支持操作列的展开和收缩两种形态,先来添加下操作列展开状态下的按钮,并为每个按钮先创建好点击事件,写法可以参考官方示例([Table 自定义列模板](https://element.eleme.cn/#/zh-CN/component/table#zi-ding-yi-lie-mo-ban)),继续编辑 `FileTable.vue` 文件:
...
刷新页面,点击操作列的按钮,可以看到控制台打印出相应的信息:

再来添加操作列收缩状态下的按钮,用 Element UI 的下拉菜单([Dropdown 下拉菜单](https://element.eleme.cn/#/zh-CN/component/dropdown))来实现:
... ... 操作
刷新页面,看下效果:
来添加下表格操作列收缩和展开状态的切换,将控制切换的入口添加在表格头上,这里需要用到 Element UI 中 Table 组件的自定义表头(Table 自定义表头),同时将表格列的宽度设置为动态变化,随收缩状态而改变:
...
操作
...
...
刷新首页,点击表格操作列表头的图标,切换收缩状态:
由于网盘中存储的文件会很多,一次性加载所有的文件会降低文件加载速度,且在表格内拖动滚动条的方式在交互上也不够友好,所以需要常见的分页组件来提升查看文件的效率。参考官方示例(Pagination 分页组件的附加功能)中的完整功能,来实现分页组件。仍然在 views/Home/components
下创建新文件 FilePagination.vue
,添加以下内容:
然后在 views/Home/index.vue
文件中添加如下代码:
...
...
然后在 views/Home/index.vue
文件中添加如下代码:
...
...
现在需要在 SelectColumn.vue
中来控制表格列的显隐,点击按钮时,获取 store 中存储的表格显示列,对应的多选框处于勾选状态,点击对话框确定按钮时,提交 mutation 更新表格显示列:
...
刷新首页,表格显示列的显隐已可以控制:
7.页面接口添加并实现数据联动
在 src/request
下新建文件 file.js
,后续与文件有关的接口均可放在此文件中,添加接口:
import { get } from "./http";
// 左侧菜单选择的为 全部 时,根据路径,获取文件列表
export const getFileListByPath = (p) => get("/file/getfilelist", p);
// 左侧菜单选择的为 除全部以外 的类型时,根据类型,获取文件列表
export const getFileListByType = (p) => get("/file/selectfilebyfiletype", p);
当左侧菜单选择全部时,右侧的面包屑导航栏将会显示当前所处的路径,需调用接口——根据路径获取文件列表;当左侧菜单选择除全部以外的菜单时,右侧的面包屑导航栏会显示当前所展示的文件类型,右侧表格区域显示相应类型的文件,需调用接口——根据类型获取文件列表。
8.左侧菜单和右侧表格数据联动
表格数据获取要考虑三点:文件类型、文件路径、分页。在前面实现左侧菜单组件时,已经把文件类型添加到了路由参数上,文件类型可以通过 this.$route.query.fileType
来获取;文件路径在面包屑导航栏组件中;分页数据也在子组件中;查询结果需要在表格中显示。
综合考量,将调用接口函数写在 src/views/Home/index.vue
中比较合适,父子组件通讯使用 props/$emit
,在 index.vue
中添加以下内容:
...
...
在 FilePagination.vue
中添加以下内容:
...
...
在 FileTable.vue
中添加以下内容:
...
刷新首页,左侧菜单选中全部、图片时看下运行结果:
下一节介绍完上传文件后,上述两个接口返回值就会有数据了。
9.左侧菜单和右侧面包屑导航栏数据联动
在 src/views/Home/index.vue
中给面包屑导航栏组件传值 fileType
:
由于要区分当前查看的文件是按类型还是按路径,面包屑导航栏要显示的信息不同,在 BreadCrumb.vue
中添加以下内容:
在下个实验讲解完上传文件和创建文件夹后,将会回到这个文件,完善面包屑导航栏的功能:当点击面包屑导航栏中的某一级时,改变路由,在 index.vue
中监听 filePath 变化重新获取表格数据。
刷新首页,看到左侧菜单切换时,路由变化,右侧面包屑导航栏也会随之变化:
十三、文件夹添加、文件上传实现
1.添加文件上传组件
在 src/views/Home/components
下创建文件 OperationMenu.vue
,将文件夹的添加、文件上传均放在此组件中,文件内容稍后讲解。在 src/views/Home/index.vue
中引入此文件,将 fileType 传递给子组件,以便在不同类型文件页面,判断是否对新建文件夹按钮做禁用:
//删除原有的
...
对所有跳转到首页中全部类型文件页面的路由做修改:
src/components/Header.vue
中跳转到首页的路由修改为:
首页
src/views/Home/components/SideMenu.vue
中跳转到首页的路由修改为:
全部
Login.vue
和 Register.vue
中跳转到首页的路由修改为:
this.$router.replace({
name: "Home",
query: { fileType: 0, filePath: "/" },
});
同时将 SideMenu.vue
中 created()
中的路由跳转代码删除:
现在退出登录,重新登录,跳转到首页的路由参数会带上 fileType 和 filePath。
2.新建文件夹功能
在 src/views/Home/index.vue
中将 filePath 传递给子组件,同时接收子组件向外触发的获取文件列表事件:
filePath 的值通过路由参数获取:
computed: {
...
// 当前所在路径
filePath() {
return this.$route.query.filePath
}
},
在 src/request/file.js
中添加新建文件夹接口:
import { get, post } from "./http";
// 创建文件夹 或 文件
export const createFile = (p) => post("/file/createfile", p);
在 OperationMenu.vue
中使用按钮组来包裹新建文件按钮,点击按钮弹出对话框,用户输入文件夹名称,点击对话框提交按钮,表单校验通过后,调用新建文件夹接口,创建成功后,关闭对话框,并重新获取文件列表:
刷新首页,新建文件夹“实验楼”:
面包屑导航栏文件 BreadCrumb.vue
也需要一些改造:
...
根据路径获取文件列表接口 getFileListByPath 中的请求参数 filePath 也需要改为路由参数中的 filePath,监听路由参数中的 filePath 变化,值改变时,重新获取文件列表。这样点击面包屑导航栏中的某一级就可以获取该路径下的文件列表了。在 src/views/Home/index.vue
中修改:
...
watch: {
filePath() {
// 当左侧菜单选择全部,文件路径发生变化时,再重新获取文件列表
if (this.fileType === 0) {
this.getFileData() // 获取文件列表
}
}
},
methods: {
...
// 根据路径获取文件列表
getFileDataByPath() {
getFileListByPath({
filePath: this.filePath, // 传递当前路径
currentPage: this.pageData.currentPage,
pageCount: this.pageData.pageCount
}).then(
// 已有代码不再赘述
...
)
}
}
3.文件列表数据处理
现在获取文件列表接口的返回值有值了,来处理下这些返回值,以便能更好的在表格中展示:
- 类型:显示当前行的文件类型,若为文件夹就显示“文件夹”;
- 大小:显示当前行的文件的大小,单位转化为 KB、MB、GB;
- 文件名:当前行若为文件夹,点击文件名,进入文件夹内部,获取文件夹内部的文件列表,路由参数中的 filePath 和面包屑导航栏随之改变,表格数据重新渲染。
在 FileTable.vue
中加入以下内容,处理文件类型:
...
{{ scope.row.extendName ? scope.row.extendName : '文件夹' }}
...
处理文件大小:
...
{{ calculateFileSize(scope.row.fileSize) }}
...
处理函数如下:
...
methods: {
// 计算文件大小
calculateFileSize(size) {
const B = 1024
const KB = Math.pow(1024, 2)
const MB = Math.pow(1024, 3)
const GB = Math.pow(1024, 4)
if (!size) {
return '_'
} else if (size < KB) {
return (size / B).toFixed(0) + 'KB'
} else if (size < MB) {
return (size / KB).toFixed(1) + 'MB'
} else if (size < GB) {
return (size / MB).toFixed(2) + 'GB'
} else {
return (size / GB).toFixed(3) + 'TB'
}
},
// 删除按钮 - 点击事件
handleClickDelete(row) {
console.log('删除', row.fileName)
},
...
处理文件名点击事件:
{{ scope.row.fileName }}
添加函数:
methods: {
// 文件名点击事件
handleFileNameClick(row) {
// 若是目录则进入目录
if (row.isDir) {
this.$router.push({
query: {
filePath: `${row.filePath}${row.fileName}/`,
fileType: 0
}
})
}
},
...
}
刷新首页,点击文件夹名称路由参数改变,可以进入到文件夹内部查看文件列表,点击面包屑导航栏也可以切换文件查看路径:
再来调整下表格高度和滚动条样式,使表格高度自适应窗口高度,表格组件添加 height 属性,继续编辑 FileTable.vue
文件:
...
刷新首页,可以看到表格已经可以自适应高度了:
下一步
4.文件上传功能
大多数项目,文件上传可以使用 Element UI 自带的 Upload 组件来实现,但是对于一个文件管理系统来说,要支撑大文件上传,需要使用文件切片,断点续传等技术。
目前有许多优秀的开源插件可以实现此类技术,这里就不再重复造轮子。此次实验使用了开源插件 vue-simple-uploader 来封装文件上传组件,此插件具有文件秒传、切片上传、断点续传的功能,具体参数说明可以去 Github 上查看;同时使用计算文件 MD5 的方式,来让后台判断是否有相同文件已存在于服务器中。
先来安装插件 vue-simple-uploader
:
npm install vue-simple-uploader --save
安装 spark-md5
用于计算文件 MD5:
npm install spark-md5 --save
在 main.js
中引入:
import Vue from 'vue'
import uploader from 'vue-simple-uploader'
...
Vue.use(uploader)
在 src/views/Home/components
中创建上传文件组件 FileUploader.vue
,在 src/views/Home/index.vue
中引入:
...
5.整体流程概览
-
点击上传文件按钮,触发文件上传操作,展示文件上传窗口。 在 OperationMenu.vue
中添加上传文件按钮:
在 src/views/Home/index.vue
中将接收上传文件按钮点击事件,打开上传文件窗口:
// 上传文件 按钮点击事件
handleUploadFile() {
// 触发子组件中的打开文件上传窗口事件
this.$refs.globalUploader.triggerSelectFileClick()
}
在 FileUploader.vue
中添加打开文件上传窗口事件:
// 触发选择文件按钮的点击事件
triggerSelectFileClick() {
this.$refs.uploadBtn.$el.click() // 触发 选择文件按钮 的点击事件
},
-
开始计算文件 MD5,这里需要将组件的 autoStart
设为 false,关闭自动上传功能。
-
计算完毕,开始文件分片上传。
-
上传过程中,会不断触发组件 file-progress
上传进度的回调,可以将上传进度打印出来,在控制台查看上传进度。
-
文件上传成功后,在 file-success
的回调中,刷新文件列表。
6.文件切片
vue-simple-uploader
自动将文件进行分片,在 options
的 chunkSize
中可以设置每个分片的大小。对于大文件来说,会发送多个请求,在设置 testChunks
为 true
后(在插件中默认就是true
),会发送与服务器进行分片校验的请求。
看一下发送给服务端的参数,其中 chunkNumber
表示当前是第几个分片,totalChunks
代表所有的分片数,这两个参数都是插件根据你设置的 chunkSize
来计算的。
skipUpload
表示当前分片已存在于服务器中,无需上传此分片了。
此时 FileUploader.vue
文件的内容如下所示:
选择文件
上传列表
-
暂无待上传文件
7.文件秒传及断点续传
整体思路如下:
- 通过
file.pause()
暂停文件上传。
- 使用
spark-md5
计算文件 MD5 值。
- file 有个属性是
uniqueIdentifier
,代表文件唯一标识,把计算出来的 MD5 赋值给这个属性。
- 服务器判断逻辑如下:
- 服务器发现文件已经完全上传成功,则直接返回文件秒传的标识。
- 服务器发现文件上传过分片信息,则返回这些分片信息,告诉前端继续上传,即断点续传,前端通过
file.resume()
继续上传文件。
- 服务器发现不存在此文件的分片信息,那这就是个全新的文件了,走完整的分片上传逻辑,前端通过
file.resume()
开始上传文件。
最后 FileUploader.vue
文件的完整代码如下:
选择文件
上传列表
-
暂无待上传文件
vue-simple-uploader
基于 simple-uploader.js
开发,示例代码中所有的回调函数及配置项,都可参考 simple-uploader.js,大家可以自己尝试。
刷新首页,测试下文件上传功能:
8.完善文件列表数据回显
文件夹的添加和文件的上传功能介绍完毕了,来完善下我们的表格数据回显:
- 在文件名称列之前,添加上对应类型的图标,可以去iconfont上寻找文件类型图标库,下载为 png 或 svg 格式使用,当图标过多时,可以使用更为常用的 Font class 或 Unicode 方式。为了简便,这里使用 png 格式。
- 文件名后拼接扩展名。
9.文件图标资源上传
本地环境大家可以直接下载并保存到项目的 src/assets/images
中,我们看一下环境中如何操作。
在环境中执行如下命令:
cd /home/project/file-web/src/assets
wget https://labfile.oss-internal.aliyuncs.com/courses/3472/image.zip
unzip image.zip
10.全局函数-回显图片缩略图
在 src
下新建文件夹 libs
,并新建文件 globalFunction.js
,写入以下内容:
//全局函数 ,挂载到Vue实例上
export default function install(Vue) {
// 加载缩略图
Vue.prototype.downloadImgMin = function (row) {
let fileUrl = row.fileUrl;
if (fileUrl) {
let index = fileUrl.lastIndexOf(".");
fileUrl =
"api" + fileUrl.substr(0, index) + "_min" + fileUrl.substr(index);
}
return fileUrl;
};
// 当然,你还可以在这里封装并挂载更多的全局函数在这里,示例同上
}
在 src/main.js
中将全局函数挂载在 Vue 实例上:
...
import all from '@/libs/globalFunction.js'
...
Vue.use(all)
11.其他文件类型图标回显
结合文件扩展名,来对常用的文件类型添加图标,同时在文件名列,拼接上文件扩展名,修改 FileTable.vue
文件:
{{
scope.row.extendName
? `${scope.row.fileName}.${scope.row.extendName}`
: `${scope.row.fileName}`
}}
...
刷新首页,看下文件列表数据回显效果:
大家可以找更多的文件类型图标来做映射。
12.存储空间统计
在左侧菜单底部,添加存储空间大小展示。
在 src/request/file.js
中添加接口:
// 获取存储空间已占用大小
export const getFileStorage = (p) => get("/filetransfer/getstorage", p);
在 src/views/Home/index.vue
中每次获取表格列表数据时,调用此接口,获取存储空间大小,并将值传递给左侧菜单组件:
...
在左侧菜单组件 SideMenu.vue
中添加存储空间展示区,并使用 Element UI 的进度条组件来展示空间已使用占比,数据处理使用过滤器:
...
存储
{{ storageValue | storageTrans }} /
{{ storageMaxValue | storageTrans(true) }}
刷新首页,看下效果:
下一步
十四、文件的基本操作-删除、移动、下载和重命名
1.文件单个操作
对单个文件的操作,包括删除、移动、下载和重命名,操作入口在文件表格操作列的各个按钮。先来把接下来要用到的接口维护在 src/request/file.js
中:
...
// 获取文件夹列表 树状结构
export const getFileTree = (p) => get('/file/getfiletree', p)
// 单文件操作接口
// 文件删除
export const deleteFile = (p) => post('/file/deletefile', p)
// 文件移动
export const moveFile = (p) => post('/file/movefile', p)
// 文件重命名
export const renameFile = (p) => post('/file/renamefile', p)
2.文件删除
在 FileTable.vue
中,已经将文件删除函数添加了,现在来在函数内部调用删除接口,在这之前,需要先提示用户是否确定删除,因此需要用到 Element UI 的 MessageBox 组件。删除文件之后需要刷新文件列表:
在 src/views/Home/index.vue
中接收子组件的刷新文件列表事件 @getTableData="getFileData"
:
刷新首页,看下效果:
3.文件移动
考虑到接下来的文件批量操作,把文件移动封装为公共组件。在 src/views/Home/components
下新建文件 MoveFileDialog.vue
,内容稍后来讲。
在 FileTable.vue
中,已经将文件移动函数添加了,函数内部向父组件 index.vue
触发事件,以打开移动文件对话框,并保存当前行文件数据:
// 移动按钮 - 点击事件
handleClickMove(row) {
this.$emit('handleSelectFile', false, row) // true/false 操作类型:批量移动/单文件操作;row 当前行文件数据
this.$emit('handleMoveFile', true) // true/false 打开/关闭移动文件对话框
},
父组件 index.vue
中接收事件,添加 @handleSelectFile="setOperationFile"
和 @handleMoveFile="setMoveFileDialog"
:
回调函数内部执行操作:打开选择目标路径对话框,并保存当前行文件数据和操作类型,继续编辑 index.vue
文件:
...
接下来添加 MoveFileDialog.vue
的内容:使用 Element UI 的 Tree 树形控件 ,在对话框内以树形结构显示文件夹,并使用树形控件的 node-click
事件,获取当前点击的文件夹路径,向父组件触发事件,在父组件中保存移动文件的目标路径。对话框点击确定按钮时,向父组件触发事件,在回调函数中调用移动文件接口;对话框点击取消按钮时,关闭对话框:
在父组件 index.vue
中引入 MoveFileDialog.vue
,并按照上述说明,在回调函数中添加相应操作:
...
...
刷新首页,看下效果:
下一步
4.文件下载
下载按钮添加显隐控制 v-if="scope.row.isDir === 0"
,当前行的文件类型不是文件夹时才可下载,同时在
标签的 href 属性中添加下载链接,修改 FileTable.vue
文件:
下载
...
下载
来处理下
标签的下划线,在 src/assets/style/base.styl
中添加样式:
a {
text-decoration: none;
}
刷新首页,看下效果:
5.文件重命名
在 FileTable.vue
中,文件重命名的函数已添加,函数内部执行以下操作:先打开消息弹框让用户输入新文件名,点击确定按钮后,调用重命名接口。
刷新首页,看下效果:
6.文件批量操作
对文件的批量操作,包括删除、移动、下载,文件的选择需要在表格列的每行添加勾选框,操作按钮将会放置在顶部的操作按钮组中。操作流程如下:
- 勾选需要批量操作的表格行,保存用户已勾选的表格行;
- 点击批量操作按钮(删除/移动/下载);
- 执行批量操作;
- 批量操作结束,刷新文件列表。
先来把接下来要用到的接口维护在 src/request/file.js
中:
// 批量文件操作接口
// 批量删除文件
export const batchDeleteFile = (p) => post("/file/batchdeletefile", p);
// 批量移动文件
export const batchMoveFile = (p) => post("/file/batchmovefile", p);
7.文件列表勾选框添加
在 FileTable.vue
中,参考 Element UI 的多选表格官方示例在表格第一列前添加 type="selection"
的列,并在表格上添加 @selection-change="handleSelectRow"
,当选择项发生变化时会触发该事件,并将已选项通过事件抛出,在父组件中保存:
...
在父组件 index.vue
中将保存的已选项数据 operationFileList
,传递给操作按钮组:
在操作按钮组组件 OperationMenu.vue
中,接收该值,这样批量删除、批量下载的事件均在此组件中执行即可:
props: {
...
// 表格行 已选项
operationFileList: {
type: Array,
required: true
}
},
8.文件批量删除
在操作按钮组组件 OperationMenu.vue
中添加删除按钮,并添加按钮点击事件:
...
删除
在按钮点击事件中,和删除单文件步骤类似,询问用户是否确定删除,确定时调用批量删除文件接口,删除成功后刷新文件列表,继续编辑 OperationMenu.vue
文件:
刷新首页,看下效果:
下一步
9.文件批量移动
在实现单文件移动时,已经将文件夹列表的展示封装为了公共组件,这里还是使用该组件选择目标路径,在OperationMenu.vue
中向父组件中抛出事件,打开路径选择对话框。
先在 OperationMenu.vue
中添加移动按钮,这里需要做个显隐控制:只有左侧菜单选择全部时,才显示移动按钮;并给移动按钮添加点击事件:
...
移动
// 移动文件按钮 - 点击事件
handleMoveFileClick() {
// true/false 批量移动/单文件操作 | this.operationFileList 当前行文件数据
this.$emit('handleSelectFile', true, this.operationFileList)
this.$emit('handleMoveFile', true) // true/false 打开/关闭移动文件对话框
}
在父组件 index.vue
中接收事件 @handleSelectFile
和 @handleMoveFile
:
在回调函数 setMoveFileDialog()
内打开目标路径选择对话框,对话框点击确定按钮时,需要在回调函数 confirmMoveFile()
中调用批量移动文件接口:
import { batchMoveFile } from '@/request/file.js'
// 移动文件模态框-确定按钮事件
confirmMoveFile() {
if (this.isBatch) {
// 批量移动
let data = {
filePath: this.selectFilePath,
files: JSON.stringify(this.operationFileList)
}
batchMoveFile(data).then((res) => {
if (res.success) {
this.$message.success(res.data)
this.getFileData() // 刷新文件列表
this.dialogMoveFile.visible = false // 关闭对话框
this.operationFileList = []
} else {
this.$message.error(res.message)
}
})
} else {
// 单文件移动
// ......已有代码不再赘述
...
}
}
刷新页面,看下效果:
下一步
10.文件批量下载
文件下载仍然采用
标签来实现,点击下载按钮时,触发
的点击事件,开始下载文件。在 OperationMenu.vue
中添加以下内容,同样需要做按钮禁用处理:
刷新首页,来看下效果:
11.完善文件表格数据回显
当左侧菜单点击图片/文档/视频/音乐/其他时,在右侧表格中添加一列——文件所在路径;点击路径,可以直接跳转到该路径下,右侧表格显示该路径下的文件列表。
把 fileType 传递给文件表格组件,在 index.vue
中添加 :fileType="fileType"
:
在 FileTable.vue
中接收 fileType:
props: {
// 文件类型
fileType: {
type: Number,
required: true
}
}
在 FileTable.vue
中实现跳转功能:
...
{{ scope.row.filePath }}
...
刷新首页,看下效果:
十五、图片三种展示方式和在线图片查看实现
点击左侧菜单的图片,右侧现在是以列表模式来展示图片的,接下来添加两种查看模式:网格模式和时间线模式。当切换网格模式时,图片将平铺显示,切换时间线模式时,图片将按照时间分组展示,类似于时间轴。同时,要实现刷新页面时保存刚才选择的图片展示方式。
1.结合 Vuex 实现图片展示方式保存
之前的实验已经结合 Vuex 实现过保存表格列显隐状态,这次同理。在 src/views/Home/components
下新建文件 ShowModel.vue
,内容稍后来讲。
在 src/views/Home/index.vue
中引入该组件,将 fileType 传递给该组件,并对样式做些调整:
.operation-wrapper {
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
// 左侧菜单按钮组 样式调整
>>> .operation-menu-wrapper {
flex: 1;
}
}
在 ShowModel
使用 Element UI 的单选按钮组来实现图片展示方式的切换,编辑 ShowModel.vue
文件:
列表
网格
时间线
结合 Vuex 保存图片展示方式,在 src/store/module/file.js
中添加 showModel
:
export default {
state: {
...
showModel: sessionStorage.getItem('showModel') // 查看模式 - 0 列表 | 1 网格 | 2 时间线
},
mutations: {
...
// 切换查看模式
changeShowModel(state, data) {
sessionStorage.setItem('showModel', data)
state.showModel = data
}
}
}
在 src/store/index.js
中的 getters 中添加:
getters: {
...
// 查看模式 - 0 列表 | 1 网格 | 2 时间线
showModel: (state) => (state.file.showModel === null ? 0 : Number(state.file.showModel))
}
刷新首页,可以看到无论是切换左侧菜单,还是刷新页面,图片展示方式都会被保存:
2.网格模式
在 src/views/Home/components
下新建文件 FileGrid.vue
,内容稍后添加。
在 index.vue
中引入此组件,并对现有的文件表格组件和此组件做显隐控制:
...
...
在 FileGrid.vue
中添加以下内容:
-
{{ item.fileName }}.{{ item.extendName }}
在 src/assets/style/mixins.styl
中添加以下样式:
// 文字过长显示省略号
setEllipsis(line)
display: -webkit-box;
overflow: hidden;
white-space: wrap;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: line;
刷新首页,看下效果:
3.时间线模式
在 src/views/Home/components
下新建文件 FileTimeLine.vue
,内容稍后添加。
在 index.vue
中引入此组件,并对现有的文件表格组件和此组件做显隐控制:
...
...
在 FileTimeLine.vue
中使用 Element UI 的时间线组件来模仿时间轴:
排序:
倒序
正序
-
{{ image.fileName }}.{{ image.extendName }}
刷新首页,查看效果:
4.图片在线查看
图片在线查看功能,包括查看、放大、缩小、旋转和上下张切换。在点击图片时,全屏显示大图,通过鼠标滚轮或底部滑块放大缩小图片,可以切换上一张和下一张图片。考虑到列表、网格、时间线模式都需要用到此功能,因此封装为公共组件。
在 src/components
下新建文件 ImgReview.vue
,内容稍后添加,用 Vuex 来共享图片列表数据,在图片点击事件中分发事件,打开图片预览。
5.图片数据共享
在 src/store/module
下新建文件 imgReview.js
,添加以下内容:
export default {
state: {
imgReviewVisible: false, // 图片查看组件显隐状态
imgReviewList: [], // 图片列表
defaultActiveIndex: 0, // 默认当前打开的图片的索引
},
mutations: {
setImgReviewData(state, data) {
if (data.imgReviewVisible) {
state.imgReviewVisible = data.imgReviewVisible;
state.imgReviewList = data.imgReviewList;
state.defaultActiveIndex = data.activeIndex;
} else {
state.imgReviewVisible = data.false;
state.imgReviewList = [];
state.defaultActiveIndex = 0;
}
},
},
};
在 src/store/index.js
中引入此模块:
import Vue from "vue";
import Vuex from "vuex";
import imgReview from "./module/imgReview"; // 1. 引入 imgReview 模块
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
user,
file,
imgReview, // 2. 注册模块
},
});
6.图片在线查看组件添加
在 src/views/Home/index.vue
中引入图片在线查看组件:
在 ImgReview.vue
中添加以下内容:
7.图片在线查看事件添加
当左侧菜单选择图片时,右侧显示图片列表,在图片点击事件中触发图片在线查看。
在 FileGrid.vue
中添加图片点击事件 handleFileClick()
:
-
{{ item.fileName }}.{{ item.extendName }}
在 FileTimeLine.vue
中添加图片点击事件 handleFileClick()
:
{{ image.fileName }}.{{ image.extendName }}
在 FileTable.vue
中,对表格当前行是图片类型的文件,在原有的文件名点击事件 handleFileNameClick
中触发图片在线查看:
// 文件名点击事件
handleFileNameClick(row) {
// 若是目录则进入目录
if (row.isDir) {
this.$router.push({
query: {
filePath: `${row.filePath}${row.fileName}/`,
fileType: 0
}
})
} else {
// 若当前点击项是图片
const PIC = ['png', 'jpg', 'jpeg', 'gif', 'svg']
if (PIC.includes(row.extendName)) {
let data = {
imgReviewVisible: true,
imgReviewList: [{
fileUrl: `/api${row.fileUrl}`,
downloadLink: `/api/filetransfer/downloadfile?userFileId=${row.userFileId}`,
fileName: row.fileName,
extendName: row.extendName
}],
activeIndex: 0
}
this.$store.commit('setImgReviewData', data) // 触发图片在线查看
}
}
}
刷新首页,看下图片在线查看效果: