教育项目之前端模块(vue+nuxt+webpack)介绍
1.项目结构2.目录和代码分析build的文件夹:主要放置webpack的打包文件的,webpack自动生成不用管。config目录,用于配置请求地址前缀的。peod.env.js:'use strict'module.exports = {NODE_ENV: '"production"',BASE_API: '"https://easy-mock.com/mock/5950a2419adc231
·
0.前言
vue项目的快速搭建
搭建链接
1.项目结构
2.目录和代码分析
- build的文件夹:主要放置webpack的打包文件的,webpack自动生成不用管。
- config目录,用于配置请求地址前缀的。
peod.env.js:
'use strict'
module.exports = {
NODE_ENV: '"production"',
BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
}
dev.env.js:
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
// BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"'
BASE_API: '"http://localhost:9000"',
OSS_PATH: '"https://grain-online-education.oss-cn-shenzhen.aliyuncs.com"'
})
- src目录主要放置js,css,.vue页面文件。
import request from '@/utils/request'
const api_name = '/chapter'
export default {
getChapterAndVideoByCourseId(courseId) {
return request({
url: `${api_name}/${courseId}`,
method: 'get'
})
},
save(chapter) {
return request({
url: `${api_name}/save`,
method: 'post',
data: chapter
})
},
getChapterById(id) {
return request({
url: `${api_name}/get/${id}`,
method: 'get'
})
},
updateById(chapter) {
return request({
url: `${api_name}/update`,
method: 'put',
data: chapter
})
},
removeById(id) {
return request({
url: `${api_name}/${id}`,
method: 'delete'
})
}
}
组件模块,组件就相当于java里面的封装,将内容封装到组件标签中,然后要使用时,引入组件标签即可,被引入的组件为子组件,外围为父组件,父子组件之间可以进行传值和取值。
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" v-if="item.meta.title" :key="item.path">
<span v-if="item.redirect==='noredirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
let matched = this.$route.matched.filter(item => {
if (item.name) {
return true
}
})
const first = matched[0]
if (first && first.name !== 'dashboard') {
matched = [{ path: '/dashboard', meta: { title: '首页' }}].concat(matched)
}
this.levelList = matched
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 10px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
放图标文件夹。
import Vue from 'vue'
import Router from 'vue-router'
// in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading;
// detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading
Vue.use(Router)
/* Layout */
import Layout from '../views/layout/Layout'
/**
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu, whatever its child routes length
* if not set alwaysShow, only more than one route under the children
* it will becomes nested mode, otherwise not show the root menu
* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
title: 'title' the name show in submenu and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar,
}
**/
export const constantRouterMap = [
{ path: '/login', component: () => import('@/views/login/index'), hidden: true },
{ path: '/404', component: () => import('@/views/404'), hidden: true },
// 首页
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: 'Dashboard',
children: [{
path: 'dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '谷粒学院后台首页', icon: 'dashboard' }
}]
},
// 讲师管理
{
path: '/teacher',
component: Layout,
redirect: '/teacher/list',
name: '讲师管理',
meta: { title: '讲师管理', icon: 'users' },
children: [
{
path: 'list',
name: '讲师列表',
component: () => import('@/views/teacher/list'),
meta: { title: '讲师列表', icon: 'list' }
},
{
path: 'add',
name: '讲师新增',
component: () => import('@/views/teacher/form'),
meta: { title: '讲师新增', icon: 'addUser' }
},
{
path: 'edit/:id', // :用来传递参数
name: '讲师修改',
component: () => import('@/views/teacher/form'),
meta: { title: '讲师修改', icon: 'tree' },
hidden: true
}
]
},
// 课程分类管理
{
path: '/subject',
component: Layout,
redirect: '/subject/list',
name: '课程分类管理',
meta: { title: '课程分类管理', icon: 'classify_select' },
children: [
{
path: 'list',
name: '课程分类列表',
component: () => import('@/views/subject/list'),
meta: { title: '课程分类列表', icon: 'list' }
},
{
path: 'import',
name: '课程分类导入',
component: () => import('@/views/subject/import'),
meta: { title: '课程分类导入', icon: 'import' }
}
]
},
// 课程管理
{
path: '/course',
component: Layout,
redirect: '/course/list',
name: '课程管理',
meta: { title: '课程管理', icon: 'manage' },
children: [
{
path: 'list',
name: '课程列表',
component: () => import('@/views/course/list'),
meta: { title: '课程列表', icon: 'list' }
},
{
path: 'info',
name: '发布课程',
component: () => import('@/views/course/info'),
meta: { title: '发布课程', icon: 'publish' }
},
{
path: 'info/:id',
name: 'EduCourseInfoEdit',
component: () => import('@/views/course/info'),
meta: { title: '编辑课程基本信息', noCache: true },
hidden: true
},
{
path: 'chapter/:id',
name: 'EduCourseChapterEdit',
component: () => import('@/views/course/chapter'),
meta: { title: '编辑课程大纲', noCache: true },
hidden: true
},
{
path: 'publish/:id',
name: 'EduCoursePublishEdit',
component: () => import('@/views/course/publish'),
meta: { title: '发布课程', noCache: true },
hidden: true
}
]
},
// 统计分析
{
path: '/statistics',
component: Layout,
redirect: '/statistics/chart',
name: '统计分析',
meta: { title: '统计分析', icon: 'chart' },
children: [
{
path: 'create',
name: '生成数据',
component: () => import('@/views/statistics/create'),
meta: { title: '生成数据', icon: 'data' }
},
{
path: 'chart',
name: '图表显示',
component: () => import('@/views/statistics/chart'),
meta: { title: '图表显示', icon: 'report' }
}
]
},
{ path: '*', redirect: '/404', hidden: true }
]
export default new Router({
// mode: 'history', //后端支持可开
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
index.js(总引用js):
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
user
},
getters
})
export default store
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
roles: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
return new Promise((resolve, reject) => {
login(username, userInfo.password).then(response => {
const data = response.data
setToken(data.token)
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const data = response.data
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', data.roles)
} else {
reject('getInfo: roles must be a non-null array !')
}
commit('SET_NAME', data.name)
commit('SET_AVATAR', data.avatar)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 登出
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
其实就是触发事件,当state为某个值时,先在mutations(同步)中赋值,然后最后执行下面的commit事件(actions异步)。
这是样式包。
//to reset element-ui default css
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
//element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
这是工具包。
validate.js(校验js):
/**
* Created by jiachenpan on 16/11/18.
*/
export function isvalidUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}
/* 合法uri*/
export function validateURL(textval) {
const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return urlregex.test(textval)
}
/* 小写字母*/
export function validateLowerCase(str) {
const reg = /^[a-z]+$/
return reg.test(str)
}
/* 大写字母*/
export function validateUpperCase(str) {
const reg = /^[A-Z]+$/
return reg.test(str)
}
/* 大小写字母*/
export function validatAlphabets(str) {
const reg = /^[A-Za-z]+$/
return reg.test(str)
}
auth.js(获取token的js):
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
request.js:
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
response => {
/**
* code为非20000是抛错 可结合自己业务进行修改
*/
const res = response.data
if (res.code !== 20000) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm(
'你已被登出,可以取消继续留在该页面,或者重新登录',
'确定登出',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
return response.data
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<el-button type="text" @click="dialogChapterFormVisible = true">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li
v-for="chapter in chapterNestedList"
:key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text" @click="dialogVideoFormVisible = true; chapterId = chapter.id">添加课时</el-button>
<el-button type="text" @click="editChapter(chapter.id)">编辑</el-button>
<el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li
v-for="video in chapter.children"
:key="video.id">
<p>{{ video.title }}
<span class="acts">
<el-button type="text" @click="editVideo(video.id)">编辑</el-button>
<el-button type="text" @click="removeVideo(video.id)">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</div>
<!-- 添加和修改章节表单 -->
<el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节">
<el-form :model="chapter" label-width="120px">
<el-form-item label="章节标题">
<el-input v-model="chapter.title"/>
</el-form-item>
<el-form-item label="章节排序">
<el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogChapterFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</div>
</el-dialog>
<!-- 添加和修改课时表单 -->
<el-dialog :visible.sync="dialogVideoFormVisible" title="添加课时">
<el-form :model="video" label-width="120px">
<el-form-item label="课时标题">
<el-input v-model="video.title"/>
</el-form-item>
<el-form-item label="课时排序">
<el-input-number v-model="video.sort" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item label="是否免费">
<el-radio-group v-model="video.isFree">
<el-radio :label="true">免费</el-radio>
<el-radio :label="false">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="上传视频">
<el-upload
:on-success="handleVodUploadSuccess"
:on-remove="handleVodRemove"
:before-remove="beforeVodRemove"
:on-exceed="handleUploadExceed"
:file-list="fileList"
:action="BASE_API+'/vod/upload'"
:limit="1"
class="upload-demo">
<el-button size="small" type="primary">上传视频</el-button>
<el-tooltip placement="right-end">
<div slot="content">最大支持1G,<br>
支持3GP、ASF、AVI、DAT、DV、FLV、F4V、<br>
GIF、M2T、M4V、MJ2、MJPEG、MKV、MOV、MP4、<br>
MPE、MPG、MPEG、MTS、OGG、QT、RM、RMVB、<br>
SWF、TS、VOB、WMV、WEBM 等视频格式上传</div>
<i class="el-icon-question"/>
</el-tooltip>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVideoFormVisible = false;helpSaveVideo()">取 消</el-button>
<el-button :disabled="saveVideoBtnDisabled" type="primary" @click="saveOrUpdateVideo">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
import video from '@/api/edu/video'
import vod from '@/api/edu/vod'
export default {
data() {
return {
saveBtnDisabled: false, // 保存按钮是否禁用
courseId: '', // 所属课程
chapterNestedList: [], // 章节嵌套视频列表
dialogChapterFormVisible: false, // 是否显示章节表单
chapter: {// 章节对象
title: '',
courseId: '',
sort: 0
},
saveVideoBtnDisabled: false, // 课时按钮是否禁用
dialogVideoFormVisible: false, // 是否显示课时表单
chapterId: '', // 课时所在的章节id
video: {// 课时对象
title: '',
sort: 0,
isFree: 0,
chapterId: '',
courseId: '',
videoSourceId: '',
videoOriginalName: ''
},
fileList: [],
BASE_API: process.env.BASE_API
}
},
created() {
console.log('chapter created')
this.init()
},
methods: {
init() {
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
// 根据id获取课程基本信息
this.getChapterAndVideoByCourseId(this.courseId)
}
},
saveOrUpdate() {
// 判断保存还是修改
if (this.chapter.id) {
this.updateChapter()
} else {
this.saveChapter()
}
},
saveChapter() {
this.chapter.courseId = this.courseId
chapter.save(this.chapter)
.then(response => {
this.$message({
type: 'success',
message: '保存成功!'
})
this.helpSave()
})
},
updateChapter() {
chapter.updateById(this.chapter)
.then(response => {
this.$message({
type: 'success',
message: '修改成功!'
})
this.helpSave()
})
},
editChapter(chapterId) {
this.dialogChapterFormVisible = true
chapter.getChapterById(chapterId)
.then(response => {
this.chapter = response.data.chapter
})
},
removeChapter(id) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return chapter.removeById(id)
}).then(() => {
this.getChapterAndVideoByCourseId(this.courseId)// 刷新列表
this.$message({
type: 'success',
message: '删除成功!'
})
}).catch((response) => { // 失败
if (response === 'cancel') {
this.$message({
type: 'info',
message: '已取消删除'
})
} else {
this.$message({
type: 'error',
message: response.message
})
}
})
},
saveOrUpdateVideo() {
this.saveVideoBtnDisabled = true
if (this.video.id) {
this.updateDataVideo()
} else {
this.saveDataVideo()
}
},
saveDataVideo() {
this.video.courseId = this.courseId
this.video.chapterId = this.chapterId
video.saveVideo(this.video)
.then(response => {
this.$message({
type: 'success',
message: '保存成功!'
})
this.helpSaveVideo()
})
},
editVideo(videoId) {
this.dialogVideoFormVisible = true
video.getVideoById(videoId)
.then(response => {
this.video = response.data.eduVideo
if (this.video.videoOriginalName) {
this.fileList = [{ 'name': this.video.videoOriginalName }]
}
})
},
updateDataVideo() {
video.updateVideo(this.video)
.then(response => {
this.$message({
type: 'success',
message: '修改成功!'
})
this.helpSaveVideo()
})
.catch(() => {
this.helpSaveVideo()
})
},
removeVideo(videoId) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return video.removeVideoById(videoId)
}).then(() => {
this.getChapterAndVideoByCourseId(this.courseId)// 刷新列表
this.$message({
type: 'success',
message: '删除成功!'
})
}).catch((response) => { // 失败
if (response === 'cancel') {
this.$message({
type: 'info',
message: '已取消删除'
})
}
})
},
getChapterAndVideoByCourseId(id) {
chapter.getChapterAndVideoByCourseId(id)
.then(response => {
this.chapterNestedList = response.data.list
})
},
// 视频上传成功后赋值
handleVodUploadSuccess(response, file, fileList) {
this.video.videoSourceId = response.data.videoSourceId
this.video.videoOriginalName = file.name
},
// 视图上传多于一个视频
handleUploadExceed(files, fileList) {
this.$message.warning('想要重新上传视频,请先删除已上传的视频')
},
// 删除视频
beforeVodRemove(file, fileList) {
return this.$confirm(`确定移除 ${file.name}?`)
},
handleVodRemove(file, fileList) {
console.log(file)
vod.removeById(this.video.videoSourceId)
.then(response => {
this.video.videoSourceId = ''
this.video.videoOriginalName = ''
this.fileList = []
this.$message({
type: 'success',
message: response.message
})
})
},
previous() {
console.log('previous')
this.$router.push({ path: '/course/info/' + this.courseId })
},
next() {
console.log('next')
this.$router.push({ path: '/course/publish/' + this.courseId })
},
helpSave() {
// 关闭文本框
this.dialogChapterFormVisible = false
// 刷新页面
this.getChapterAndVideoByCourseId(this.courseId)
// 重置章节标题
this.chapter.title = ''
// 重置章节排序
this.chapter.sort = 0
this.saveBtnDisabled = false
},
helpSaveVideo() {
this.dialogVideoFormVisible = false// 如果保存成功则关闭对话框
this.getChapterAndVideoByCourseId(this.courseId)// 刷新列表
this.video.id = ''
this.video.title = ''// 重置章节标题
this.video.sort = 0// 重置章节标题
this.video.isFree = 0 // 重置小节是否免费
this.video.videoSourceId = ''// 重置视频资源id
this.video.videoOriginalName = ''// 重置视频资源名称
this.fileList = []
this.saveVideoBtnDisabled = false
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
package.json(版本号管理文件):
{
"name": "grain_admin",
"version": "3.8.0",
"license": "MIT",
"description": "谷粒学院后台管理系统",
"author": "Dragon Wen <18475536452@163.com>",
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"build": "node build/build.js",
"build:report": "npm_config_report=true npm run build",
"lint": "eslint --ext .js,.vue src",
"test": "npm run lint",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
},
"dependencies": {
"axios": "0.18.0",
"echarts": "^4.1.0",
"element-ui": "2.4.6",
"js-cookie": "2.2.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"vue": "2.5.17",
"vue-router": "3.0.1",
"vuex": "3.0.1"
},
"devDependencies": {
"autoprefixer": "8.5.0",
"babel-core": "6.26.0",
"babel-eslint": "8.2.6",
"babel-helper-vue-jsx-merge-props": "2.0.3",
"babel-loader": "7.1.5",
"babel-plugin-syntax-jsx": "6.18.0",
"babel-plugin-transform-runtime": "6.23.0",
"babel-plugin-transform-vue-jsx": "3.7.0",
"babel-preset-env": "1.7.0",
"babel-preset-stage-2": "6.24.1",
"chalk": "2.4.1",
"copy-webpack-plugin": "4.5.2",
"css-loader": "1.0.0",
"eslint": "4.19.1",
"eslint-friendly-formatter": "4.0.1",
"eslint-loader": "2.0.0",
"eslint-plugin-vue": "4.7.1",
"eventsource-polyfill": "0.9.6",
"file-loader": "1.1.11",
"friendly-errors-webpack-plugin": "1.7.0",
"html-webpack-plugin": "4.0.0-alpha",
"mini-css-extract-plugin": "0.4.1",
"node-notifier": "5.2.1",
"node-sass": "^4.7.2",
"optimize-css-assets-webpack-plugin": "5.0.0",
"ora": "3.0.0",
"path-to-regexp": "2.4.0",
"portfinder": "1.0.16",
"postcss-import": "12.0.0",
"postcss-loader": "2.1.6",
"postcss-url": "7.3.2",
"rimraf": "2.6.2",
"sass-loader": "7.0.3",
"script-ext-html-webpack-plugin": "2.0.1",
"semver": "5.5.0",
"shelljs": "0.8.2",
"svg-sprite-loader": "3.8.0",
"svgo": "1.0.5",
"uglifyjs-webpack-plugin": "1.2.7",
"url-loader": "1.0.1",
"vue-loader": "15.3.0",
"vue-style-loader": "4.1.2",
"vue-template-compiler": "2.5.17",
"webpack": "4.16.5",
"webpack-bundle-analyzer": "2.13.1",
"webpack-cli": "3.1.0",
"webpack-dev-server": "3.1.5",
"webpack-merge": "4.1.4"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
此项目分析可以用于大部分vue项目的套用,内容换成自己的即可接私活了。
更多推荐
已为社区贡献5条内容
所有评论(0)