Node.js + Vue.js 全栈开发王者荣耀手机端官网和管理后台
创建项目
创建 npm 项目 server
新建文件夹 node-vue-moba,在此文件夹新建 server 文件夹
node-vue-moba % mkdir server
进入 server 目录,创建 npm 项目
node-vue-moba % cd server
server % npm init -y
Wrote to /node-vue-moba/server/package.json:
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
创建 vue 项目 admin
node-vue-moba % vue create admin
Vue CLI v5.0.8
? Please pick a preset: Default ([Vue 3] babel, eslint)
Vue CLI v5.0.8
✨ Creating project in /node-vue-moba/admin.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
added 858 packages, and audited 859 packages in 3m
94 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
🚀 Invoking generators...
📦 Installing additional dependencies...
added 92 packages, and audited 951 packages in 13s
107 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project admin.
👉 Get started with the following commands:
$ cd admin
$ npm run serve
创建 vue 项目 web
node-vue-moba % vue create web
Vue CLI v5.0.8
? Please pick a preset: Default ([Vue 3] babel, eslint)
Vue CLI v5.0.8
✨ Creating project in /node-vue-moba/web.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
added 858 packages, and audited 859 packages in 2m
94 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
🚀 Invoking generators...
📦 Installing additional dependencies...
added 92 packages, and audited 951 packages in 15s
107 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project web.
👉 Get started with the following commands:
$ cd web
$ npm run serve
server package.json 修改
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"serve": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
若是没有安装过nodemon ,先全局安装 nodemon
npm i -g nodemon
server 安装 express, mongoose, cors
node-vue-moba % cd server
server % npm i express@next mongoose cors
added 79 packages, and audited 80 packages in 26s
3 packages are looking for funding
run `npm fund` for details
3 high severity vulnerabilities
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
在 server 新建文件 index.js
const express = require("express")
const app = express()
app.use(require('cors')())
app.use(express.json())
app.use('/uploads', express.static(__dirname + '/uploads'))
app.listen(3000, ()=>{
console.log('http://localhost:3000');
});
启用服务
npm run serve
在浏览器打开 http://localhost:3000/
启动 admin
node-vue-moba % cd admin
admin % npm run serve
> admin@0.1.0 serve
> vue-cli-service serve
INFO Starting development server...
DONE Compiled successfully in 29989ms 10:03:20
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.50.81:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.
在浏览器打开 http://localhost:8080/
admin 安装 element-plus
node-vue-moba % cd admin
admin % vue add element-plus
📦 Installing vue-cli-plugin-element-plus...
added 1 package, and audited 952 packages in 11s
107 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
✔ Successfully installed plugin: vue-cli-plugin-element-plus
? How do you want to import Element Plus? Fully import
? Do you want to overwrite the SCSS variables of Element Plus? No
? Choose the locale you want to load, the default locale is English (en) zh-cn
🚀 Invoking generator for vue-cli-plugin-element-plus...
📦 Installing additional dependencies...
added 8 packages, and audited 960 packages in 24s
108 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
⚓ Running completion hooks...
✔ Successfully invoked generator for plugin: vue-cli-plugin-element-plus
在浏览器打开 http://localhost:8080/ 发现页面发生了变化
admin 添加 router
admin % vue add router
WARN There are uncommitted changes in the current repository, it's recommended to commit or stash them first.
? Still proceed? Yes
📦 Installing @vue/cli-plugin-router...
up to date, audited 960 packages in 4s
108 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
✔ Successfully installed plugin: @vue/cli-plugin-router
? Use history mode for router? (Requires proper server setup for index fallback
in production) No
🚀 Invoking generator for @vue/cli-plugin-router...
📦 Installing additional dependencies...
added 2 packages, and audited 962 packages in 4s
109 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
⚓ Running completion hooks...
✔ Successfully invoked generator for plugin: @vue/cli-plugin-router
在浏览器打开 http://localhost:8080/ 发现页面发生了变化
在 admin/src/views 新建文件 MainVue.vue
<template>
<el-container style="height: 100vh;">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu router :default-openeds="['1']" unique-opened :default-active="$route.path">
<el-submenu index="1">
<template #title>
<i class="el-icon-message"></i>内容管理
</template>
<el-menu-item-group>
<template #title>物品</template>
<el-menu-item index="/items/create">新建物品</el-menu-item>
<el-menu-item index="/items/list">物品列表</el-menu-item>
</el-menu-item-group>
<el-menu-item-group>
<template #title>英雄</template>
<el-menu-item index="/heroes/create">新建英雄</el-menu-item>
<el-menu-item index="/heroes/list">英雄列表</el-menu-item>
</el-menu-item-group>
<el-menu-item-group>
<template #title>文章</template>
<el-menu-item index="/articles/create">新建文章</el-menu-item>
<el-menu-item index="/articles/list">文章列表</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template #title>
<i class="el-icon-message"></i>运营管理
</template>
<el-menu-item-group>
<template #title>广告位</template>
<el-menu-item index="/ads/create">新建广告位</el-menu-item>
<el-menu-item index="/ads/list">广告位列表</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="3">
<template #title>
<i class="el-icon-message"></i>系统设置
</template>
<el-menu-item-group>
<template #title>分类</template>
<el-menu-item index="/categories/create">新建分类</el-menu-item>
<el-menu-item index="/categories/list">分类列表</el-menu-item>
</el-menu-item-group>
<el-menu-item-group>
<template #title>管理员</template>
<el-menu-item index="/admin_users/create">新建管理员</el-menu-item>
<el-menu-item index="/admin_users/list">管理员列表</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<template v-slot:dropdown>
<el-dropdown-menu>
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
<router-view :key="$route.path"></router-view>
</el-main>
</el-container>
</el-container>
</template>
<style>
.el-header {
background-color: #b3c0d1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
<script>
export default {
data() {
const item = {
date: "2016-05-02",
name: "王小虎",
address: "上海市普陀区金沙江路 1518 弄"
};
return {
tableData: Array(20).fill(item)
};
}
};
</script>
修改 admin/src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
import MainView from '../views/MainVue.vue'
const routes = [
// {
// path: '/',
// name: 'home',
// component: HomeView
// },
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (about.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
// }
{
path: '/',
name: 'main',
component: MainView
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
修改 admin/src/App.vue
<template>
<!-- <nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav> -->
<router-view/>
</template>
<style>
html,body{
margin: 0;
padding: 0;
}
/* #app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
} */
</style>
在浏览器打开 http://localhost:8080/ 发现页面发生了变化
 for this route
// // which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
// }
{ path: '/login', name: 'login', component: LoginView, meta: { isPublic: true } },
{
path: '/',
name: 'main',
component: MainView,
children: [
{ path: '/categories/create', component: CategoryEdit },
{ path: '/categories/edit/:id', component: CategoryEdit, props: true },
{ path: '/categories/list', component: CategoryList },
{ path: '/items/create', component: ItemEdit },
{ path: '/items/edit/:id', component: ItemEdit, props: true },
{ path: '/items/list', component: ItemList },
{ path: '/heroes/create', component: HeroEdit },
{ path: '/heroes/edit/:id', component: HeroEdit, props: true },
{ path: '/heroes/list', component: HeroList },
{ path: '/articles/create', component: ArticleEdit },
{ path: '/articles/edit/:id', component: ArticleEdit, props: true },
{ path: '/articles/list', component: ArticleList },
{ path: '/ads/create', component: AdEdit },
{ path: '/ads/edit/:id', component: AdEdit, props: true },
{ path: '/ads/list', component: AdList },
{ path: '/admin_users/create', component: AdminUserEdit },
{ path: '/admin_users/edit/:id', component: AdminUserEdit, props: true },
{ path: '/admin_users/list', component: AdminUserList },
]
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
router.beforeEach((to, from ,next) => {
if (!to.meta.isPublic && !localStorage.token) {
return next('/login')
}
next()
})
export default router
admin 添加 axios
npm i axios --legacy-peer-deps
added 6 packages, and audited 968 packages in 9s
109 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
在 admin 新建文件 .env.development
VUE_APP_API_URL=http://localhost:3000/admin/api
-
.env 全局默认配置文件
-
.env.development 开发环境下的配置文件
-
.env.production 生产环境下的配置文件
如果我们运行 npm run serve 就会先加载 .env 文件,之后加载 .env.development 文件,如果两个文件有同一项,则后加载的文件就会覆盖掉第一个文件,即 .env.development 文件覆盖掉了 .env 文件的选项
同理,如果执行了 npm run build ,则就是加载了 .env 和 .env.production 文件
属性名必须以 VUE_APP 开头,比如 VUE_APP_API_URL
直接调用 process.env 属性(全局属性,任何地方都可以使用)比如 process.env.VUE_APP_API_URL
在 admin 新建文件 http.js
import axios from 'axios'
import router from './router'
const http = axios.create({
baseURL: process.env.VUE_APP_API_URL || '/admin/api'
// baseURL: 'http://localhost:3000/admin/api'
})
http.interceptors.request.use(function (config) {
// Do something before request is sent
if (localStorage.token) {
config.headers.Authorization = 'Bearer ' + localStorage.token
}
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
http.interceptors.response.use(res => {
return res
}, err => {
if (err.response.data.message) {
this.$message({
type: 'error',
message: err.response.data.message
})
if (err.response.status === 401) {
router.push('/login')
}
}
return Promise.reject(err)
})
export default http
$ 是在 Vue 所有实例中都可用的 property 的一个简单约定。这样做会避免和已被定义的数据、方法、计算属性产生冲突。
- 如果 vue 原型参数和组件中定义的参数相同,则会被覆盖,有冲突,建议使用 $ 定义原型参数
- 如果 vue 原型参数和组件中定义的参数不相同,那么可以不使用 $ 定义
修改 admin/src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import installElementPlus from './plugins/element'
import router from './router'
import http from './http'
Vue.prototype.$http = http
const app = createApp(App).use(router)
installElementPlus(app)
app.mount('#app')
vue3.x vs vue2.x
//=======vue3.x
//使用createApp函数来实例化vue,
//该函数接收一个根组件选项对象作为第一个参数
//使用第二个参数,我们可以将根 prop 传递给应用程序
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App,{ userName: "blackLieo" })
.use(store)
.use(router)
.mount('#app')
//由于 createApp 方法返回应用实例本身,因此可以在其后链式调用其它方法,这些方法可以在以下部分中找到。
//=======vue2.x
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
server 添加 bcrypt, http-assert, inflection, jsonwebtoken, multer, require-all
npm i http-assert inflection jsonwebtoken multer require-all
added 32 packages, and audited 112 packages in 23s
4 packages are looking for funding
run `npm fund` for details
3 high severity vulnerabilities
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
npm i bcrypt
added 53 packages, and audited 165 packages in 32s
7 packages are looking for funding
run `npm fund` for details
3 high severity vulnerabilities
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
在 server 新建文件夹 models, plugins, routes, middleware
在 plugins 新建 db.js 文件
module.exports = app => {
const mongoose = require("mongoose")
mongoose.connect('mongodb://127.0.0.1:27017/node-vue-moba', {
useNewUrlParser: true
})
require('require-all')(__dirname + '/../models')
}
在 models 新建 Ad.js 文件
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
name: { type: String },
items: [{
image: { type: String },
url: { type: String },
}]
})
module.exports = mongoose.model('Ad', schema)
在 models 新建 AdminUser.js 文件
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
username: { type: String },
password: {
type: String,
select: false,
set(val) {
return require('bcrypt').hashSync(val, 10)
}
},
})
module.exports = mongoose.model('AdminUser', schema)
在 models 新建 Article.js 文件
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
categories: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Category' }],
title: { type: String },
body: { type: String },
}, {
timestamps: true
})
module.exports = mongoose.model('Article', schema)
在 models 新建 Category.js 文件
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
name: { type: String },
parent: { type: mongoose.SchemaTypes.ObjectId, ref: 'Category' },
})
schema.virtual('children', {
localField: '_id',
foreignField: 'parent',
justOne: false,
ref: 'Category'
})
schema.virtual('newsList', {
localField: '_id',
foreignField: 'categories',
justOne: false,
ref: 'Article'
})
module.exports = mongoose.model('Category', schema)
在 models 新建 Hero.js 文件
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
name: { type: String },
avatar: { type: String },
banner: { type: String },
title: { type: String },
categories: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Category' }],
scores: {
difficult: { type: Number },
skills: { type: Number },
attack: { type: Number },
survive: { type: Number },
},
skills: [{
icon: { type: String },
name: { type: String },
delay: { type: String },
cost: { type: String },
description: { type: String },
tips: { type: String },
}],
items1: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
items2: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
usageTips: { type: String },
battleTips: { type: String },
teamTips: { type: String },
partners: [{
hero: { type: mongoose.SchemaTypes.ObjectId, ref: 'Hero' },
description: { type: String },
}],
})
module.exports = mongoose.model('Hero', schema, 'heroes')
在 models 新建 Item.js 文件
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
name: { type: String },
icon: { type: String },
})
module.exports = mongoose.model('Item', schema)
在 middleware 新建 auth.js 文件
module.exports = options => {
const assert = require('http-assert')
const jwt = require('jsonwebtoken')
const AdminUser = require('../models/AdminUser')
return async (req, res, next) => {
const token = String(req.headers.authorization || '').split(' ').pop()
assert(token, 401, '请先登录')
const { id } = jwt.verify(token, req.app.get('secret'))
assert(id, 401, '请先登录')
req.user = await AdminUser.findById(id)
assert(req.user, 401, '请先登录')
await next()
}
}
在 middleware 新建 resource.js 文件
module.exports = options => {
return async (req, res, next) => {
const modelName = require('inflection').classify(req.params.resource)
req.Model = require(`../models/${modelName}`)
next()
}
}
在 routes 新建文件夹 admin, web
在 routes/admin 新建 index.js 文件
module.exports = app => {
const express = require('express')
const assert = require('http-assert')
const jwt = require('jsonwebtoken')
const AdminUser = require('../../models/AdminUser')
const router = express.Router({
mergeParams: true
})
// 创建资源
router.post('/', async (req, res) => {
const model = await req.Model.create(req.body)
res.send(model)
})
// 更新资源
router.put('/:id', async (req, res) => {
const model = await req.Model.findByIdAndUpdate(req.params.id, req.body)
res.send(model)
})
// 删除资源
router.delete('/:id', async (req, res) => {
await req.Model.findByIdAndDelete(req.params.id)
res.send({
success: true
})
})
// 资源列表
router.get('/', async (req, res) => {
const queryOptions = {}
if (req.Model.modelName === 'Category') {
queryOptions.populate = 'parent'
}
const items = await req.Model.find().setOptions(queryOptions).limit(100)
res.send(items)
})
// 资源详情
router.get('/:id', async (req, res) => {
const model = await req.Model.findById(req.params.id)
res.send(model)
})
// 登录校验中间件
const authMiddleware = require('../../middleware/auth')
const resourceMiddleware = require('../../middleware/resource')
app.use('/admin/api/rest/:resource', authMiddleware(), resourceMiddleware(), router)
const multer = require('multer')
// const MAO = require('multer-aliyun-oss');
const upload = multer({
dest: __dirname + '/../../uploads',
// storage: MAO({
// config: {
// region: 'oss-cn-zhangjiakou',
// accessKeyId: '替换为你的真实id',
// accessKeySecret: '替换为你的真实secret',
// bucket: 'node-vue-moba'
// }
// })
})
app.post('/admin/api/upload', authMiddleware(), upload.single('file'), async (req, res) => {
const file = req.file
// file.url = `http://test.topfullstack.com/uploads/${file.filename}`
file.url = `http://localhost:3000/uploads/${file.filename}`
res.send(file)
})
app.post('/admin/api/login', async (req, res) => {
const { username, password } = req.body
// 1.根据用户名找用户
const user = await AdminUser.findOne({ username }).select('+password')
assert(user, 422, '用户不存在')
// 2.校验密码
const isValid = require('bcrypt').compareSync(password, user.password)
assert(isValid, 422, '密码错误')
// 3.返回token
const token = jwt.sign({ id: user._id }, app.get('secret'))
res.send({ token })
})
// 错误处理函数
app.use(async (err, req, res, next) => {
// console.log(err)
res.status(err.statusCode || 500).send({
message: err.message
})
})
}
在 routes/web 新建 index.js 文件
module.exports = app => {
const router = require('express').Router()
const mongoose = require('mongoose')
// const Article = require('../../models/Article')
const Category = mongoose.model('Category')
const Article = mongoose.model('Article')
const Hero = mongoose.model('Hero')
// 导入新闻数据
router.get('/news/init', async (req, res) => {
const parent = await Category.findOne({
name: '新闻分类'
})
const cats = await Category.find().where({
parent: parent
}).lean()
const newsTitles = ["夏日新版本“稷下星之队”即将6月上线", "王者荣耀携手两大博物馆 走进稷下学宫", "王者大陆第一学院【稷下】档案", "跨界合作丨控油神装登场,唤醒无限护肤力量!", "像素游戏时代“老四强”重聚《魂斗罗:归来》,新版本、新英雄燃爆两周年庆", "6月11日全服不停机更新公告", "【已修复】王者大陆的端午宝藏活动页面异常问题说明", "6月7日体验服停机更新公告", "6月4日全服不停机更新公告", "关于2019年KPL春季赛总决赛 RNG.M vs eStarPro 补赛、赛果及世界冠军杯安排公告", "活力夏日活动周 王者峡谷好礼多", "王者大陆的端午宝藏活动公告", "峡谷庆端午 惊喜礼不断", "【场里场外,一起开黑】感恩礼包放送", "KPL总决赛来临之际 场里场外一起开黑/观赛活动开启!", "【6月15日 再战西安 · 2019年KPL春季赛总决赛重启公告】", "王者荣耀世界冠军杯荣耀来袭,KPL赛区选拔赛谁能突围而出?", "【关于2019年KPL春季赛总决赛门票退换及异地用户现场观赛补贴公告】", "KRKPL:还在用庄周打辅助?JY边路庄周带你越塔莽!", "世冠KPL赛区战队出征名单公布 王者,无惧挑战!"]
const newsList = newsTitles.map(title => {
const randomCats = cats.slice(0).sort((a, b) => Math.random() - 0.5)
return {
categories: randomCats.slice(0, 2),
title: title
}
})
await Article.deleteMany({})
await Article.insertMany(newsList)
res.send(newsList)
})
// 新闻列表接口
router.get('/news/list', async (req, res) => {
// const parent = await Category.findOne({
// name: '新闻分类'
// }).populate({
// path: 'children',
// populate: {
// path: 'newsList'
// }
// }).lean()
const parent = await Category.findOne({
name: '新闻分类'
})
const cats = await Category.aggregate([
{ $match: { parent: parent._id } },
{
$lookup: {
from: 'articles',
localField: '_id',
foreignField: 'categories',
as: 'newsList'
}
},
{
$addFields: {
newsList: { $slice: ['$newsList', 5] }
}
}
])
const subCats = cats.map(v => v._id)
cats.unshift({
name: '热门',
newsList: await Article.find().where({
categories: { $in: subCats }
}).populate('categories').limit(5).lean()
})
cats.map(cat => {
cat.newsList.map(news => {
news.categoryName = (cat.name === '热门')
? news.categories[0].name : cat.name
return news
})
return cat
})
res.send(cats)
})
// 导入英雄数据
router.get('/heroes/init', async (req, res) => {
await Hero.deleteMany({})
const rawData = [{ "name": "热门", "heroes": [{ "name": "后羿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/169/169.jpg" }, { "name": "孙悟空", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/167/167.jpg" }, { "name": "铠", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/193/193.jpg" }, { "name": "鲁班七号", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/112/112.jpg" }, { "name": "亚瑟", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/166/166.jpg" }, { "name": "甄姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/127/127.jpg" }, { "name": "孙尚香", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/111/111.jpg" }, { "name": "典韦", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/129/129.jpg" }, { "name": "韩信", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/150/150.jpg" }, { "name": "庄周", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/113/113.jpg" }] }, { "name": "战士", "heroes": [{ "name": "赵云", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/107/107.jpg" }, { "name": "钟无艳", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/117/117.jpg" }, { "name": "吕布", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/123/123.jpg" }, { "name": "曹操", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/128/128.jpg" }, { "name": "典韦", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/129/129.jpg" }, { "name": "宫本武藏", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/130/130.jpg" }, { "name": "达摩", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/134/134.jpg" }, { "name": "老夫子", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/139/139.jpg" }, { "name": "关羽", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/140/140.jpg" }, { "name": "露娜", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/146/146.jpg" }, { "name": "花木兰", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/154/154.jpg" }, { "name": "亚瑟", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/166/166.jpg" }, { "name": "孙悟空", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/167/167.jpg" }, { "name": "刘备", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/170/170.jpg" }, { "name": "杨戬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/178/178.jpg" }, { "name": "雅典娜", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/183/183.jpg" }, { "name": "哪吒", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/180/180.jpg" }, { "name": "铠", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/193/193.jpg" }, { "name": "狂铁", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/503/503.jpg" }, { "name": "李信", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/507/507.jpg" }, { "name": "盘古", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/529/529.jpg" }] }, { "name": "法师", "heroes": [{ "name": "小乔", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/106/106.jpg" }, { "name": "墨子", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/108/108.jpg" }, { "name": "妲己", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/109/109.jpg" }, { "name": "嬴政", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/110/110.jpg" }, { "name": "高渐离", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/115/115.jpg" }, { "name": "扁鹊", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/119/119.jpg" }, { "name": "芈月", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/121/121.jpg" }, { "name": "周瑜", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/124/124.jpg" }, { "name": "甄姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/127/127.jpg" }, { "name": "武则天", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/136/136.jpg" }, { "name": "貂蝉", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/141/141.jpg" }, { "name": "安琪拉", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/142/142.jpg" }, { "name": "姜子牙", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/148/148.jpg" }, { "name": "王昭君", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152.jpg" }, { "name": "张良", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/156/156.jpg" }, { "name": "不知火舞", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/157/157.jpg" }, { "name": "钟馗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/175/175.jpg" }, { "name": "诸葛亮", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/190/190.jpg" }, { "name": "干将莫邪", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/182/182.jpg" }, { "name": "女娲", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/179/179.jpg" }, { "name": "杨玉环", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/176/176.jpg" }, { "name": "弈星", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/197/197.jpg" }, { "name": "米莱狄", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/504/504.jpg" }, { "name": "沈梦溪", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/312/312.jpg" }, { "name": "上官婉儿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/513/513.jpg" }, { "name": "嫦娥", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/515/515.jpg" }] }, { "name": "坦克", "heroes": [{ "name": "廉颇", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/105/105.jpg" }, { "name": "刘禅", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/114/114.jpg" }, { "name": "白起", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/120/120.jpg" }, { "name": "夏侯惇", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/126/126.jpg" }, { "name": "项羽", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/135/135.jpg" }, { "name": "程咬金", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/144/144.jpg" }, { "name": "刘邦", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/149/149.jpg" }, { "name": "牛魔", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/168/168.jpg" }, { "name": "张飞", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/171/171.jpg" }, { "name": "东皇太一", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/187/187.jpg" }, { "name": "苏烈", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/194/194.jpg" }, { "name": "梦奇", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/198/198.jpg" }, { "name": "孙策", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/510/510.jpg" }, { "name": "猪八戒", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/511/511.jpg" }] }, { "name": "刺客", "heroes": [{ "name": "阿轲", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/116/116.jpg" }, { "name": "李白", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/131/131.jpg" }, { "name": "韩信", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/150/150.jpg" }, { "name": "兰陵王", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/153/153.jpg" }, { "name": "娜可露露", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/162/162.jpg" }, { "name": "橘右京", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/163/163.jpg" }, { "name": "百里玄策", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/195/195.jpg" }, { "name": "裴擒虎", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/502/502.jpg" }, { "name": "元歌", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/125/125.jpg" }, { "name": "司马懿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/137/137.jpg" }, { "name": "云中君", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/506/506.jpg" }] }, { "name": "射手", "heroes": [{ "name": "孙尚香", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/111/111.jpg" }, { "name": "鲁班七号", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/112/112.jpg" }, { "name": "马可波罗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/132/132.jpg" }, { "name": "狄仁杰", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/133/133.jpg" }, { "name": "后羿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/169/169.jpg" }, { "name": "李元芳", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/173/173.jpg" }, { "name": "虞姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/174/174.jpg" }, { "name": "成吉思汗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/177/177.jpg" }, { "name": "黄忠", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/192/192.jpg" }, { "name": "百里守约", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/196/196.jpg" }, { "name": "公孙离", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/199/199.jpg" }, { "name": "伽罗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/508/508.jpg" }] }, { "name": "辅助", "heroes": [{ "name": "庄周", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/113/113.jpg" }, { "name": "孙膑", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/118/118.jpg" }, { "name": "蔡文姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/184/184.jpg" }, { "name": "太乙真人", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/186/186.jpg" }, { "name": "大乔", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/191/191.jpg" }, { "name": "鬼谷子", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/189/189.jpg" }, { "name": "明世隐", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/501/501.jpg" }, { "name": "盾山", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/509/509.jpg" }, { "name": "瑶", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/505/505.jpg" }] }]
for (let cat of rawData) {
if (cat.name === '热门') {
continue
}
// 找到当前分类在数据库中对应的数据
const category = await Category.findOne({
name: cat.name
})
cat.heroes = cat.heroes.map(hero => {
hero.categories = [category]
return hero
})
// 录入英雄
await Hero.insertMany(cat.heroes)
}
res.send(await Hero.find())
})
// 英雄列表接口
router.get('/heroes/list', async (req, res) => {
const parent = await Category.findOne({
name: '英雄分类'
})
const cats = await Category.aggregate([
{ $match: { parent: parent._id } },
{
$lookup: {
from: 'heroes',
localField: '_id',
foreignField: 'categories',
as: 'heroList'
}
}
])
const subCats = cats.map(v => v._id)
cats.unshift({
name: '热门',
heroList: await Hero.find().where({
categories: { $in: subCats }
}).limit(10).lean()
})
res.send(cats)
});
// 文章详情
router.get('/articles/:id', async (req, res) => {
const data = await Article.findById(req.params.id).lean()
data.related = await Article.find().where({
categories: { $in: data.categories }
}).limit(2)
res.send(data)
})
router.get('/heroes/:id', async (req, res) => {
const data = await Hero
.findById(req.params.id)
.populate('categories items1 items2 partners.hero')
.lean()
res.send(data)
})
app.use('/web/api', router)
}
修改 server/index.js
const express = require("express")
const app = express()
app.set('secret', 'i2u34y12oi3u4y8')
app.use(require('cors')())
app.use(express.json())
app.use('/', express.static(__dirname + '/web'))
app.use('/admin', express.static(__dirname + '/admin'))
app.use('/uploads', express.static(__dirname + '/uploads'))
require('./plugins/db')(app)
require('./routes/admin')(app)
require('./routes/web')(app)
app.listen(3000, ()=>{
console.log('http://localhost:3000');
});
修改 server/routes/admin/index.js
app.post('/admin/api/login', async (req, res) => {
const { username, password } = req.body
// 1.根据用户名找用户
AdminUser.create(req.body)
const user = await AdminUser.findOne({ username }).select('+password')
assert(user, 422, '用户不存在')
// 2.校验密码
const isValid = require('bcrypt').compareSync(password, user.password)
assert(isValid, 422, '密码错误')
// 3.返回token
const token = jwt.sign({ id: user._id }, app.get('secret'))
res.send({ token })
})
// 错误处理函数
app.use(async (err, req, res, next) => {
// console.log(err)
res.status(err.statusCode || 500).send({
message: err.message
})
})
通过 AdminUser.create(req.body) 添加用户,以便登陆,添加需要的用户名密码后删除该行
修改 admin/src/http.js
import {ElMessage} from 'element-plus'
// this.$message({
// type: 'error',
// message: err.response.data.message
// })
ElMessage({
type: 'error',
message: err.response.data.message
})
在 admin/src 新建 style.css 文件
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
min-width: 5rem;
height: 5rem;
line-height: 5rem;
text-align: center;
}
.avatar {
min-width: 5rem;
height: 5rem;
display: block;
}
修改 admin/src/main.js
import './style.css'
app.mixin({
computed: {
uploadUrl(){
return this.$http.defaults.baseURL + '/upload'
}
},
methods: {
getAuthHeaders(){
return {
Authorization: `Bearer ${localStorage.token || ''}`
}
}
}
})
admin 添加 vue3-editor
npm i vue3-editor --legacy-peer-deps
added 16 packages, and audited 984 packages in 17s
117 packages are looking for funding
run `npm fund` for details
2 moderate severity vulnerabilities
Some issues need review, and may require choosing
a different dependency.
Run `npm audit` for details.
修改 admin/src/views
AdEdit.vue
<template>
<div class="about">
<h1>{{ id ? '编辑' : '新建' }}广告位</h1>
<el-form label-width="120px" @submit.prevent="save">
<el-form-item label="名称">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item label="广告">
<el-button size="small" @click="model.items.push({})">
<i class="el-icon-plus"></i> 添加广告
</el-button>
<el-row type="flex" style="flex-wrap: wrap">
<el-col :md="24" v-for="(item, i) in model.items" :key="i">
<el-form-item label="跳转链接 (URL)">
<el-input v-model="item.url"></el-input>
</el-form-item>
<el-form-item label="图片" style="margin-top: 0.5rem;">
<el-upload class="avatar-uploader" :action="uploadUrl" :headers="getAuthHeaders()"
:show-file-list="false" :on-success="res => item.image = res.url">
<img v-if="item.image" :src="item.image" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item>
<el-button size="small" type="danger" @click="model.items.splice(i, 1)">删除</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
props: {
id: {}
},
data() {
return {
model: {
items: []
}
};
},
methods: {
async save() {
// let res;
if (this.id) {
await this.$http.put(`rest/ads/${this.id}`, this.model);//res =
} else {
await this.$http.post("rest/ads", this.model);//res =
}
this.$router.push("/ads/list");
this.$message({
type: "success",
message: "保存成功"
});
},
async fetch() {
const res = await this.$http.get(`rest/ads/${this.id}`);
this.model = Object.assign({}, this.model, res.data);
}
},
created() {
this.id && this.fetch();
}
};
</script>
AdList.vue
<template>
<div>
<h1>广告位列表</h1>
<el-table :data="items">
<el-table-column prop="_id" label="ID" width="240"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template v-slot="scope">
<el-button
type="text"
size="small"
@click="$router.push(`/ads/edit/${scope.row._id}`)"
>编辑</el-button>
<el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
items: []
};
},
methods: {
async fetch() {
const res = await this.$http.get("rest/ads");
this.items = res.data;
},
remove(row) {
this.$confirm(`是否确定要删除 "${row.name}"`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.$http.delete(`rest/ads/${row._id}`);//const res =
this.$message({
type: "success",
message: "删除成功!"
});
this.fetch();
});
}
},
created() {
this.fetch();
}
};
</script>
AdminUserEdit.vue
<template>
<div class="about">
<h1>{{id ? '编辑' : '新建'}}管理员</h1>
<el-form label-width="120px" @submit.prevent="save">
<el-form-item label="用户名">
<el-input v-model="model.username"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input type="text" v-model="model.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
props: {
id: {}
},
data(){
return {
model: {},
}
},
methods: {
async save(){
// let res
if (this.id) {
await this.$http.put(`rest/admin_users/${this.id}`, this.model)//res =
} else {
await this.$http.post('rest/admin_users', this.model)//res =
}
this.$router.push('/admin_users/list')
this.$message({
type: 'success',
message: '保存成功'
})
},
async fetch(){
const res = await this.$http.get(`rest/admin_users/${this.id}`)
this.model = res.data
},
},
created(){
this.id && this.fetch()
}
}
</script>
AdminUserList.vue
<template>
<div>
<h1>管理员列表</h1>
<el-table :data="items">
<el-table-column prop="_id" label="ID" width="240"></el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template v-slot="scope">
<el-button
type="text"
size="small"
@click="$router.push(`/admin_users/edit/${scope.row._id}`)"
>编辑</el-button>
<el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
items: []
};
},
methods: {
async fetch() {
const res = await this.$http.get("rest/admin_users");
this.items = res.data;
},
remove(row) {
this.$confirm(`是否确定要删除 "${row.name}"`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.$http.delete(`rest/admin_users/${row._id}`);//const res =
this.$message({
type: "success",
message: "删除成功!"
});
this.fetch();
});
}
},
created() {
this.fetch();
}
};
</script>
ArticleEdit.vue
<template>
<div class="about">
<h1>{{id ? '编辑' : '新建'}}文章</h1>
<el-form label-width="120px" @submit.prevent="save">
<el-form-item label="所属分类">
<el-select v-model="model.categories" multiple>
<el-option
v-for="item in categories"
:key="item._id"
:label="item.name"
:value="item._id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="标题">
<el-input v-model="model.title"></el-input>
</el-form-item>
<el-form-item label="详情">
<vue-editor v-model="model.body" useCustomImageHandler @imageAdded="handleImageAdded"></vue-editor>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { VueEditor } from "vue3-editor";
export default {
props: {
id: {}
},
components: {
VueEditor
},
data() {
return {
model: {},
categories: []
};
},
methods: {
async handleImageAdded(file, Editor, cursorLocation, resetUploader) {
const formData = new FormData();
formData.append("file", file);
const res = await this.$http.post("upload", formData);
Editor.insertEmbed(cursorLocation, "image", res.data.url);
resetUploader();
},
async save() {
// let res;
if (this.id) {
await this.$http.put(`rest/articles/${this.id}`, this.model);//res =
} else {
await this.$http.post("rest/articles", this.model);//res =
}
this.$router.push("/articles/list");
this.$message({
type: "success",
message: "保存成功"
});
},
async fetch() {
const res = await this.$http.get(`rest/articles/${this.id}`);
this.model = res.data;
},
async fetchCatgories() {
const res = await this.$http.get(`rest/categories`);
this.categories = res.data;
}
},
created() {
this.fetchCatgories();
this.id && this.fetch();
}
};
</script>
ArticleList.vue
<template>
<div>
<h1>文章列表</h1>
<el-table :data="items">
<el-table-column prop="_id" label="ID" width="240"></el-table-column>
<el-table-column prop="title" label="标题"></el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template v-slot="scope">
<el-button
type="text"
size="small"
@click="$router.push(`/articles/edit/${scope.row._id}`)"
>编辑</el-button>
<el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
items: []
};
},
methods: {
async fetch() {
const res = await this.$http.get("rest/articles");
this.items = res.data;
},
remove(row) {
this.$confirm(`是否确定要删除文章 "${row.title}"`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.$http.delete(`rest/articles/${row._id}`);//const res =
this.$message({
type: "success",
message: "删除成功!"
});
this.fetch();
});
}
},
created() {
this.fetch();
}
};
</script>
CategoryEdit.vue
<template>
<div class="about">
<h1>{{id ? '编辑' : '新建'}}分类</h1>
<el-form label-width="120px" @submit.prevent="save">
<el-form-item label="上级分类">
<el-select v-model="model.parent">
<el-option v-for="item in parents" :key="item._id"
:label="item.name" :value="item._id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="名称">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
props: {
id: {}
},
data(){
return {
model: {},
parents: [],
}
},
methods: {
async save(){
// let res
if (this.id) {
await this.$http.put(`rest/categories/${this.id}`, this.model)//res =
} else {
await this.$http.post('rest/categories', this.model)//res =
}
this.$router.push('/categories/list')
this.$message({
type: 'success',
message: '保存成功'
})
},
async fetch(){
const res = await this.$http.get(`rest/categories/${this.id}`)
this.model = res.data
},
async fetchParents(){
const res = await this.$http.get(`rest/categories`)
this.parents = res.data
},
},
created(){
this.fetchParents()
this.id && this.fetch()
}
}
</script>
CategoryList.vue
<template>
<div>
<h1>分类列表</h1>
<el-table :data="items">
<el-table-column prop="_id" label="ID" width="240"></el-table-column>
<el-table-column prop="parent.name" label="上级分类"></el-table-column>
<el-table-column prop="name" label="分类名称"></el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template v-slot="scope">
<el-button
type="text"
size="small"
@click="$router.push(`/categories/edit/${scope.row._id}`)"
>编辑</el-button>
<el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
items: []
};
},
methods: {
async fetch() {
const res = await this.$http.get("rest/categories");
this.items = res.data;
},
remove(row) {
this.$confirm(`是否确定要删除分类 "${row.name}"`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.$http.delete(`rest/categories/${row._id}`);//const res =
this.$message({
type: "success",
message: "删除成功!"
});
this.fetch();
});
}
},
created() {
this.fetch();
}
};
</script>
HeroEdit.vue
<template>
<div class="about">
<h1>{{id ? '编辑' : '新建'}}英雄</h1>
<el-form label-width="120px" @submit.prevent="save">
<el-tabs value="basic" type="border-card">
<el-tab-pane label="基础信息" name="basic">
<el-form-item label="名称">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item label="称号">
<el-input v-model="model.title"></el-input>
</el-form-item>
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
:action="uploadUrl"
:headers="getAuthHeaders()"
:show-file-list="false"
:on-success="res => model.avatar=res.url"
>
<img v-if="model.avatar" :src="model.avatar" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item label="Banner">
<el-upload
class="avatar-uploader"
:action="uploadUrl"
:headers="getAuthHeaders()"
:show-file-list="false"
:on-success="res => model.banner=res.url"
>
<img v-if="model.banner" :src="model.banner" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item label="类型">
<el-select v-model="model.categories" multiple>
<el-option
v-for="item of categories"
:key="item._id"
:label="item.name"
:value="item._id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="难度">
<el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.difficult"></el-rate>
</el-form-item>
<el-form-item label="技能">
<el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.skills"></el-rate>
</el-form-item>
<el-form-item label="攻击">
<el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.attack"></el-rate>
</el-form-item>
<el-form-item label="生存">
<el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.survive"></el-rate>
</el-form-item>
<el-form-item label="顺风出装">
<el-select v-model="model.items1" multiple>
<el-option v-for="item of items" :key="item._id" :label="item.name" :value="item._id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="逆风出装">
<el-select v-model="model.items2" multiple>
<el-option v-for="item of items" :key="item._id" :label="item.name" :value="item._id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="使用技巧">
<el-input type="textarea" v-model="model.usageTips"></el-input>
</el-form-item>
<el-form-item label="对抗技巧">
<el-input type="textarea" v-model="model.battleTips"></el-input>
</el-form-item>
<el-form-item label="团战思路">
<el-input type="textarea" v-model="model.teamTips"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="技能" name="skills">
<el-button size="small" @click="model.skills.push({})">
<i class="el-icon-plus"></i> 添加技能
</el-button>
<el-row type="flex" style="flex-wrap: wrap">
<el-col :md="12" v-for="(item, i) in model.skills" :key="i">
<el-form-item label="名称">
<el-input v-model="item.name"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-upload
class="avatar-uploader"
:action="uploadUrl"
:headers="getAuthHeaders()"
:show-file-list="false"
:on-success="res => item.icon=res.url"
>
<img v-if="item.icon" :src="item.icon" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item label="冷却值">
<el-input v-model="item.delay"></el-input>
</el-form-item>
<el-form-item label="消耗">
<el-input v-model="item.cost"></el-input>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="item.description" type="textarea"></el-input>
</el-form-item>
<el-form-item label="小提示">
<el-input v-model="item.tips" type="textarea"></el-input>
</el-form-item>
<el-form-item>
<el-button size="small" type="danger" @click="model.skills.splice(i, 1)">删除</el-button>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="最佳搭档" name="partners">
<el-button size="small" @click="model.partners.push({})">
<i class="el-icon-plus"></i> 添加英雄
</el-button>
<el-row type="flex" style="flex-wrap: wrap">
<el-col :md="12" v-for="(item, i) in model.partners" :key="i">
<el-form-item label="英雄">
<el-select filterable v-model="item.hero">
<el-option
v-for="hero in heroes"
:key="hero._id"
:value="hero._id"
:label="hero.name"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="item.description" type="textarea"></el-input>
</el-form-item>
<el-form-item>
<el-button size="small" type="danger" @click="model.partners.splice(i, 1)">删除</el-button>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
<el-form-item style="margin-top: 1rem;">
<el-button type="primary" native-type="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
props: {
id: {}
},
data() {
return {
categories: [],
items: [],
heroes: [],
model: {
name: "",
avatar: "",
skills: [],
partners: [],
scores: {
difficult: 0
}
}
};
},
methods: {
async save() {
// let res;
if (this.id) {
await this.$http.put(`rest/heroes/${this.id}`, this.model);//res =
} else {
await this.$http.post("rest/heroes", this.model);//res =
}
// this.$router.push("/heroes/list");
this.$message({
type: "success",
message: "保存成功"
});
},
async fetch() {
const res = await this.$http.get(`rest/heroes/${this.id}`);
this.model = Object.assign({}, this.model, res.data);
},
async fetchCategories() {
const res = await this.$http.get(`rest/categories`);
this.categories = res.data;
},
async fetchItems() {
const res = await this.$http.get(`rest/items`);
this.items = res.data;
},
async fetchHeroes() {
const res = await this.$http.get(`rest/heroes`);
this.heroes = res.data;
}
},
created() {
this.fetchItems();
this.fetchCategories();
this.fetchHeroes();
this.id && this.fetch();
}
};
</script>
<style>
</style>
HeroList.vue
<template>
<div>
<h1>英雄列表</h1>
<el-table :data="items">
<el-table-column prop="_id" label="ID" width="240"></el-table-column>
<el-table-column prop="name" label="英雄名称"></el-table-column>
<el-table-column prop="title" label="称号"></el-table-column>
<el-table-column prop="avatar" label="头像">
<template v-slot="scope">
<img :src="scope.row.avatar" style="height:3rem;">
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template v-slot="scope">
<el-button
type="text"
size="small"
@click="$router.push(`/heroes/edit/${scope.row._id}`)"
>编辑</el-button>
<el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
items: []
};
},
methods: {
async fetch() {
const res = await this.$http.get("rest/heroes");
this.items = res.data;
},
remove(row) {
this.$confirm(`是否确定要删除分类 "${row.name}"`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.$http.delete(`rest/heroes/${row._id}`);//const res =
this.$message({
type: "success",
message: "删除成功!"
});
this.fetch();
});
}
},
created() {
this.fetch();
}
};
</script>
ItemEdit.vue
<template>
<div class="about">
<h1>{{id ? '编辑' : '新建'}}物品</h1>
<el-form label-width="120px" @submit.prevent="save">
<el-form-item label="名称">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-upload
class="avatar-uploader"
:action="uploadUrl"
:headers="getAuthHeaders()"
:show-file-list="false"
:on-success="afterUpload"
>
<img v-if="model.icon" :src="model.icon" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
props: {
id: {}
},
data() {
return {
model: {}
};
},
methods: {
afterUpload(res){
// this.$set(this.model, 'icon', res.url)
this.model.icon = res.url
},
async save() {
// let res;
if (this.id) {
await this.$http.put(`rest/items/${this.id}`, this.model);//res =
} else {
await this.$http.post("rest/items", this.model);//res =
}
this.$router.push("/items/list");
this.$message({
type: "success",
message: "保存成功"
});
},
async fetch() {
const res = await this.$http.get(`rest/items/${this.id}`);
this.model = res.data;
}
},
created() {
this.id && this.fetch();
}
};
</script>
ItemList.vue
<template>
<div>
<h1>物品列表</h1>
<el-table :data="items">
<el-table-column prop="_id" label="ID" width="240"></el-table-column>
<el-table-column prop="name" label="物品名称"></el-table-column>
<el-table-column prop="icon" label="图标">
<template v-slot="scope">
<img :src="scope.row.icon" style="height:3rem;">
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template v-slot="scope">
<el-button type="text" size="small"
@click="$router.push(`/items/edit/${scope.row._id}`)">编辑</el-button>
<el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
items: []
};
},
methods: {
async fetch() {
const res = await this.$http.get("rest/items");
this.items = res.data;
},
remove(row) {
this.$confirm(`是否确定要删除分类 "${row.name}"`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.$http.delete(`rest/items/${row._id}`);//const res =
this.$message({
type: "success",
message: "删除成功!"
});
this.fetch();
});
}
},
created() {
this.fetch();
}
};
</script>
Login.vue
<template>
<div class="login-container">
<el-card header="请先登录" class="login-card">
<el-form @submit.prevent="login">
<el-form-item label="用户名">
<el-input v-model="model.username"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input type="password" v-model="model.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
model: {}
}
},
methods: {
async login() {
const res = await this.$http.post('login', this.model)
// sessionStorage.token = res.data.token
localStorage.token = res.data.token
this.$router.push('/')
this.$message({
type: 'success',
message: '登录成功'
})
}
}
}
</script>
<style>
.login-card {
width: 25rem;
margin: 5rem auto;
}
</style>
web 添加 axios, dayjs, vue-router,swiper, node-sass, sass-loader
web % vue add router
📦 Installing @vue/cli-plugin-router...
up to date, audited 951 packages in 7s
107 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
✔ Successfully installed plugin: @vue/cli-plugin-router
? Use history mode for router? (Requires proper server setup for index fallback in production) No
🚀 Invoking generator for @vue/cli-plugin-router...
📦 Installing additional dependencies...
added 2 packages, and audited 953 packages in 3s
108 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
⚓ Running completion hooks...
✔ Successfully invoked generator for plugin: @vue/cli-plugin-router
npm i axios dayjs swiper
added 8 packages, and audited 961 packages in 13s
109 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
npm install --save-dev node-sass sass-loader
npm WARN deprecated @npmcli/move-file@2.0.1: This functionality has been moved to @npmcli/fs
npm WARN deprecated @npmcli/move-file@1.1.2: This functionality has been moved to @npmcli/fs
added 133 packages, and audited 1094 packages in 29m
118 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
web 新建 .env.development
VUE_APP_API_URL=http://localhost:3000/web/api
web/src/assets 添加 iconfont, images, scss
修改 web/src/components
新建 Card.vue
<template>
<div class="card bg-white p-3 mt-3">
<div class="card-header d-flex ai-center"
:class="{'border-bottom': !plain, 'pb-3': !plain}">
<i class="iconfont" :class="`icon-${icon}`"></i>
<div class="fs-xl flex-1 px-2">
<strong>{{title}}</strong>
</div>
<i class="iconfont icon-menu" v-if="!plain"></i>
</div>
<div class="card-body pt-3">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true },
icon: { type: String, required: true },
plain: { type: Boolean }
}
};
</script>
<style lang="scss">
@import "../assets/scss/_variables.scss";
.card {
border-bottom: 1px solid $border-color;
}
</style>
新建 ListCard.vue
<template>
<m-card :icon="icon" :title="title">
<div class="nav jc-between">
<!-- @click="$refs.list.swiper.slideTo(i)" -->
<div class="nav-item" :class="{ active: active === i }" v-for="(category, i) in categories" :key="i">
<div class="nav-link">{{ category.name }}</div>
</div>
</div>
<div class="pt-3">
<swiper ref="list" :options="{ autoHeight: true }" @slide-change="() => active = $refs.list.swiper.realIndex">
<swiper-slide v-for="(category, i) in categories" :key="i">
<slot name="items" :category="category"></slot>
</swiper-slide>
</swiper>
</div>
</m-card>
</template>
<script>
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from 'swiper/vue';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';
// import './style.css';
// import required modules
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
export default {
components: {
Swiper,
SwiperSlide,
},
setup() {
return {
modules: [Autoplay, Pagination, Navigation],
};
},
props: {
icon: { type: String, required: true },
title: { type: String, required: true },
categories: { type: Array, required: true }
},
data() {
return {
active: 0
}
}
};
</script>
<style></style>
修改 web/src/views
新建 ArticleView.vue
<template>
<div class="page-article" v-if="model">
<div class="d-flex py-3 px-2 border-bottom">
<div class="iconfont icon-Back text-blue"></div>
<strong class="flex-1 text-blue pl-2">{{ model.title }}</strong>
<div class="text-grey fs-xs">2019-06-19</div>
</div>
<div v-html="model.body" class="px-3 body fs-lg"></div>
<div class="px-3 border-top py-3">
<div class="d-flex ai-center">
<i class="iconfont icon-menu1"></i>
<strong class="text-blue fs-lg ml-1">相关资讯</strong>
</div>
<div class="pt-2">
<!-- <router-link class="py-1" tag="div" :to="`/articles/${item._id}`" v-for="item in model.related"
:key="item._id">{{ item.title }}</router-link> -->
<router-link class="py-1" custom v-slot="{ navigate }" :to="`/articles/${item._id}`"
v-for="item in model.related" :key="item._id">
<div @click="navigate" @keypress.enter="navigate" role="link">{{ item.title }}</div>
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
id: { required: true }
},
data() {
return {
model: null
};
},
watch: {
id: 'fetch',
// id(){
// this.fetch()
// }
},
methods: {
async fetch() {
const res = await this.$http.get(`articles/${this.id}`);
this.model = res.data;
}
},
created() {
this.fetch();
}
};
</script>
<style lang="scss">
.page-article {
.icon-Back {
font-size: 1.6923rem;
}
.body {
img {
max-width: 100%;
height: auto;
}
iframe {
width: 100%;
height: auto;
}
}
}
</style>
新建 HeroView.vue
<template>
<div class="page-hero" v-if="model">
<div class="topbar bg-black py-2 px-3 d-flex ai-center text-white">
<img src="../assets/logo.png" height="30" />
<div class="px-2 flex-1">
<span class="text-white">王者荣耀</span>
<span class="ml-2">攻略站</span>
</div>
<!-- <router-link to="/" tag="div">更多英雄 ></router-link> -->
<router-link to="/" custom v-slot="{ navigate }">
<div @click="navigate" @keypress.enter="navigate" role="link">更多英雄 ></div>
</router-link>
</div>
<div class="top" :style="{ 'background-image': `url(${model.banner})` }">
<div class="info text-white p-3 h-100 d-flex flex-column jc-end">
<div class="fs-sm">{{ model.title }}</div>
<h2 class="my-2">{{ model.name }}</h2>
<div class="fs-sm">{{ model.categories.map(v => v.name).join('/') }}</div>
<div class="d-flex jc-between pt-2">
<div class="scores d-flex ai-center" v-if="model.scores">
<span>难度</span>
<span class="badge bg-primary">{{ model.scores.difficult }}</span>
<span>技能</span>
<span class="badge bg-blue-1">{{ model.scores.skills }}</span>
<span>攻击</span>
<span class="badge bg-danger">{{ model.scores.attack }}</span>
<span>生存</span>
<span class="badge bg-dark">{{ model.scores.survive }}</span>
</div>
<!-- <router-link to="/" tag="span" class="text-grey fs-sm">皮肤: 2 ></router-link> -->
<router-link to="/" class="text-grey fs-sm" custom v-slot="{ navigate }">
<span @click="navigate" @keypress.enter="navigate" role="link">皮肤: 2 ></span>
</router-link>
</div>
</div>
</div>
<!-- end of top -->
<div>
<div class="bg-white px-3">
<div class="nav d-flex jc-around pt-3 pb-2 border-bottom">
<div class="nav-item active">
<div class="nav-link">英雄初识</div>
</div>
<div class="nav-item">
<div class="nav-link">进阶攻略</div>
</div>
</div>
</div>
<swiper>
<swiper-slide>
<div>
<div class="p-3 bg-white border-bottom">
<div class="d-flex">
<!-- <router-link tag="button" to="/" class="btn btn-lg flex-1">
<i class="iconfont icon-menu1"></i>
英雄介绍视频
</router-link> -->
<router-link to="/" class="btn btn-lg flex-1" custom v-slot="{ navigate }">
<button @click="navigate" @keypress.enter="navigate" role="link">
<i class="iconfont icon-menu1"></i>
英雄介绍视频
</button>
</router-link>
<!-- <router-link tag="button" to="/" class="btn btn-lg flex-1 ml-2">
<i class="iconfont icon-menu1"></i>
英雄介绍视频
</router-link> -->
<router-link to="/" class="btn btn-lg flex-1 ml-2" custom v-slot="{ navigate }">
<button @click="navigate" @keypress.enter="navigate" role="link">
<i class="iconfont icon-menu1"></i>
英雄介绍视频
</button>
</router-link>
</div>
<!-- skills -->
<div class="skills bg-white mt-4">
<div class="d-flex jc-around">
<img class="icon" @click="currentSkillIndex = i"
:class="{ active: currentSkillIndex === i }" :src="item.icon"
v-for="(item, i) in model.skills" :key="item.name" />
</div>
<div v-if="currentSkill">
<div class="d-flex pt-4 pb-3">
<h3 class="m-0">{{ currentSkill.name }}</h3>
<span class="text-grey-1 ml-4">
(冷却值: {{ currentSkill.delay }}
消耗: {{ currentSkill.cost }})
</span>
</div>
<p>{{ currentSkill.description }}</p>
<div class="border-bottom"></div>
<p class="text-grey-1">小提示: {{ currentSkill.tips }}</p>
</div>
</div>
</div>
<m-card plain icon="menu1" title="出装推荐" class="hero-items">
<div class="fs-xl">顺风出装</div>
<div class="d-flex jc-around text-center mt-3">
<div v-for="item in model.items1" :key="item.name">
<img :src="item.icon" class="icon">
<div class="fs-xs">{{ item.name }}</div>
</div>
</div>
<div class="border-bottom mt-3"></div>
<div class="fs-xl mt-3">逆风出装</div>
<div class="d-flex jc-around text-center mt-3">
<div v-for="item in model.items2" :key="item.name">
<img :src="item.icon" class="icon">
<div class="fs-xs">{{ item.name }}</div>
</div>
</div>
</m-card>
<m-card plain icon="menu1" title="使用技巧">
<p class="m-0">{{ model.usageTips }}</p>
</m-card>
<m-card plain icon="menu1" title="对抗技巧">
<p class="m-0">{{ model.battleTips }}</p>
</m-card>
<m-card plain icon="menu1" title="团战思路">
<p class="m-0">{{ model.teamTips }}</p>
</m-card>
<m-card plain icon="menu1" title="英雄关系">
<div class="fs-xl">最佳搭档</div>
<div v-for="item in model.partners" :key="item.name" class="d-flex pt-3">
<img :src="item.hero.avatar" alt="" height="50">
<p class="flex-1 m-0 ml-3">
{{ item.description }}
</p>
</div>
<div class="border-bottom mt-3"></div>
</m-card>
</div>
</swiper-slide>
<swiper-slide></swiper-slide>
</swiper>
</div>
</div>
</template>
<script>
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from 'swiper/vue';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';
// import './style.css';
// import required modules
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
export default {
components: {
Swiper,
SwiperSlide,
},
setup() {
return {
modules: [Autoplay, Pagination, Navigation],
};
},
props: {
id: { required: true }
},
data() {
return {
model: null,
currentSkillIndex: 0
};
},
computed: {
currentSkill() {
return this.model.skills[this.currentSkillIndex];
}
},
methods: {
async fetch() {
const res = await this.$http.get(`heroes/${this.id}`);
this.model = res.data;
}
},
created() {
this.fetch();
}
};
</script>
<style lang="scss">
@import '../assets/scss/_variables.scss';
.page-hero {
.top {
height: 50vw;
background: #fff no-repeat top center;
background-size: auto 100%;
}
.info {
background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 1));
.scores {
.badge {
margin: 0 0.25rem;
display: inline-block;
width: 1rem;
height: 1rem;
line-height: 0.9rem;
text-align: center;
border-radius: 50%;
font-size: 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.2);
}
}
}
.skills {
img.icon {
width: 70px;
height: 70px;
border: 3px solid map-get($colors, 'white');
&.active {
border-color: map-get($colors, 'primary');
}
border-radius: 50%;
}
}
.hero-items {
img.icon {
width: 45px;
height: 45px;
border-radius: 50%;
}
}
}
</style>
新建 HomeView.vue
<!-- <template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'HomeView',
components: {
HelloWorld
}
}
</script> -->
<template>
<div>
<swiper :spaceBetween="30" :centeredSlides="true" :autoplay="{ delay: 2500, disableOnInteraction: false, }" :pagination="{ clickable: true, }" :navigation="true" :modules="modules" class="mySwiper" :options="swiperOption">
<swiper-slide>
<img class="w-100" src="../assets/images/210794580bb9303653804bb7b482f2a4.jpeg" alt>
</swiper-slide>
<swiper-slide>
<img class="w-100" src="../assets/images/210794580bb9303653804bb7b482f2a4.jpeg" alt>
</swiper-slide>
<swiper-slide>
<img class="w-100" src="../assets/images/210794580bb9303653804bb7b482f2a4.jpeg" alt>
</swiper-slide>
<template #pagination>
<div class="swiper-pagination pagination-home text-right px-3 pb-1"></div>
</template>
</swiper>
<!-- end of swiper -->
<div class="nav-icons bg-white mt-3 text-center pt-3 text-dark-1">
<div class="d-flex flex-wrap">
<div class="nav-item mb-3" v-for="n in 10" :key="n">
<i class="sprite sprite-news"></i>
<div class="py-2">爆料站</div>
</div>
</div>
<div class="bg-light py-2 fs-sm">
<i class="sprite sprite-arrow mr-1"></i>
<span>收起</span>
</div>
</div>
<!-- end of nav icons -->
<m-list-card icon="menu1" title="新闻资讯" :categories="newsCats">
<template #items="{ category }">
<!-- <router-link tag="div" :to="`/articles/${news._id}`" class="py-2 fs-lg d-flex"
v-for="(news, i) in category.newsList" :key="i">
<span class="text-info">[{{ news.categoryName }}]</span>
<span class="px-2">|</span>
<span class="flex-1 text-dark-1 text-ellipsis pr-2">{{ news.title }}</span>
<span class="text-grey-1 fs-sm">{{ news.createdAt | date }}</span>
</router-link> -->
<router-link :to="`/articles/${news._id}`" class="py-2 fs-lg d-flex" v-for="(news, i) in category.newsList"
:key="i" custom v-slot="{ navigate }">
<div @click="navigate" @keypress.enter="navigate" role="link">
<span class="text-info">[{{ news.categoryName }}]</span>
<span class="px-2">|</span>
<span class="flex-1 text-dark-1 text-ellipsis pr-2">{{ news.title }}</span>
<!-- news.createdAt | date -->
<span class="text-grey-1 fs-sm">{{ (news.createdAt) }}</span>
</div>
</router-link>
</template>
</m-list-card>
<m-list-card icon="card-hero" title="英雄列表" :categories="heroCats">
<template #items="{ category }">
<div class="d-flex flex-wrap" style="margin: 0 -0.5rem;">
<!-- <router-link tag="div" :to="`/heroes/${hero._id}`" class="p-2 text-center" style="width: 20%;"
v-for="(hero, i) in category.heroList" :key="i">
<img :src="hero.avatar" class="w-100">
<div>{{ hero.name }}</div>
</router-link> -->
<router-link :to="`/heroes/${hero._id}`" class="p-2 text-center" style="width: 20%;" custom
v-slot="{ navigate }" v-for="(hero, i) in category.heroList" :key="i">
<div @click="navigate" @keypress.enter="navigate" role="link">
<img :src="hero.avatar" class="w-100">
<div>{{ hero.name }}</div>
</div>
</router-link>
</div>
</template>
</m-list-card>
<m-card icon="menu1" title="精彩视频"></m-card>
<m-card icon="menu1" title="图文攻略"></m-card>
</div>
</template>
<script>
import dayjs from "dayjs";
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from 'swiper/vue';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';
// import './style.css';
// import required modules
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
export default {
// filters: {
// date(val) {
// return dayjs(val).format("MM/DD");
// }
// },
components: {
Swiper,
SwiperSlide,
},
setup() {
return {
modules: [Autoplay, Pagination, Navigation],
};
},
data() {
return {
swiperOption: {
pagination: {
el: ".pagination-home"
}
},
newsCats: [],
heroCats: []
};
},
methods: {
async fetchNewsCats() {
const res = await this.$http.get("news/list");
this.newsCats = res.data;
},
async fetchHeroCats() {
const res = await this.$http.get("heroes/list");
this.heroCats = res.data;
}
}, computed: {
newsDate(theNewsDate) {
if (theNewsDate) {
// return date(theNewsDate)
return dayjs(theNewsDate).format("MM/DD");
}
return theNewsDate
}
},
created() {
this.fetchNewsCats();
this.fetchHeroCats();
}
};
</script>
<style lang="scss">
@import "../assets/scss/variables";
.pagination-home {
.swiper-pagination-bullet {
opacity: 1;
border-radius: 0.1538rem;
background: map-get($colors, "white");
&.swiper-pagination-bullet-active {
background: map-get($colors, "info");
}
}
}
.nav-icons {
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
.nav-item {
width: 25%;
border-right: 1px solid $border-color;
&:nth-child(4n) {
border-right: none;
}
}
}
</style>
新建 MainView.vue
<template>
<div>
<div class="topbar bg-black py-2 px-3 d-flex ai-center">
<img src="../assets/logo.png" height="30">
<div class="px-2 flex-1">
<div class="text-white">王者荣耀</div>
<div class="text-grey-1 fs-xxs">团队成就更多</div>
</div>
<button type="button" class="btn bg-primary">立即下载</button>
</div>
<div class="bg-primary pt-3 pb-2">
<div class="nav nav-inverse pb-1 jc-around">
<div class="nav-item active">
<!-- <router-link class="nav-link" tag="div" to="/">首页</router-link> -->
<router-link to="/" class="nav-link" custom v-slot="{ navigate }">
<div @click="navigate" @keypress.enter="navigate" role="link">首页</div>
</router-link>
</div>
<div class="nav-item">
<!-- <router-link class="nav-link" tag="div" to="/">攻略中心</router-link> -->
<router-link to="/" class="nav-link" custom v-slot="{ navigate }">
<div @click="navigate" @keypress.enter="navigate" role="link">攻略中心</div>
</router-link>
</div>
<div class="nav-item">
<!-- <router-link class="nav-link" tag="div" to="/">赛事中心</router-link> -->
<router-link to="/" class="nav-link" custom v-slot="{ navigate }">
<div @click="navigate" @keypress.enter="navigate" role="link">赛事中心</div>
</router-link>
</div>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss">
.topbar {
position: sticky;
top: 0;
z-index: 999;
}
</style>
修改 web/src/App.vue
<template>
<!-- <nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/> -->
<div id="app">
<router-view/>
</div>
</template>
<style>
#app {
width: 50%;
margin:0 auto;
}
/* #app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
} */
</style>
修改 web/src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App).use(router)
app.mount('#app')
// createApp(App).use(router).mount('#app')
import './assets/iconfont/iconfont.css'
import './assets/scss/style.scss'
import Card from './components/CardView.vue'
app.component('m-card', Card)
import ListCard from './components/ListCard.vue'
app.component('m-list-card', ListCard)
import axios from 'axios'
app.config.globalProperties.$http = axios.create({
baseURL: process.env.VUE_APP_API_URL || '/web/api'
// baseURL: 'http://localhost:3000/web/api'
})
修改 web/src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
import Main from '../views/MainView.vue'
import Home from '../views/HomeView.vue'
import Article from '../views/ArticleView.vue'
import Hero from '../views/HeroView.vue'
const routes = [
// {
// path: '/',
// name: 'home',
// component: HomeView
// },
{
path: '/',
component: Main,
children: [
{ path: '/', name: 'home', component: Home },
{ path: '/articles/:id', name: 'article', component: Article, props: true }
]
},
{path: '/heroes/:id', name: 'hero', component: Hero, props: true},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
web 显示
代码资源
代码资源: https://download.csdn.net/download/weixin_42350100/88048998
错误处理
ERROR Failed to compile with 1 error 10:59:33
[eslint]
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/MainVue.vue
59:39 error Named slots must use '<template>' on a custom element vue/valid-v-slot
✖ 1 problem (1 error, 0 warnings)
You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
ERROR in [eslint]
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/MainVue.vue
59:39 error Named slots must use '<template>' on a custom element vue/valid-v-slot
✖ 1 problem (1 error, 0 warnings)
webpack compiled with 1 error
v-slot 应位于 <template> 标签上:
<el-dropdown-menu v-slot:dropdown>
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
==>
<template v-slot:dropdown>
<el-dropdown-menu>
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</template>
===================================================================================
node-vue-moba\admin\src\views\MainVue.vue
6:21 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
8:23 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
16:23 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
21:21 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
23:23 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
31:23 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
36:21 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
38:23 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
46:23 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
57:29 error `slot` attributes are deprecated vue/no-deprecated-slot-attribute
✖ 10 problems (10 errors, 0 warnings)
9 errors and 0 warnings potentially fixable with the `--fix` option.
webpack compiled with 1 error
vue 3.x 增加了v-slot的指令,去掉了原来的slot,slot-scope属性。
-
slot=“title” ==> #title
-
slot-scope=“scope” ==> v-slot=“scope”
-
slot-scope ==> v-slot
===================================================================================
ERROR Failed to compile with 1 error 13:27:40
[eslint]
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/Login.vue
1:1 error Component name "Login" should always be multi-word vue/multi-word-component-names
✖ 1 problem (1 error, 0 warnings)
You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
ERROR in [eslint]
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/Login.vue
1:1 error Component name "Login" should always be multi-word vue/multi-word-component-names
✖ 1 problem (1 error, 0 warnings)
webpack compiled with 1 error
更改组件名, 使其符合命名规范, 如: StudentName 或者 student-name
===================================================================================
admin % npm i axios
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: element-plus@1.0.2-beta.71
npm ERR! Found: vue@3.3.4
npm ERR! node_modules/vue
npm ERR! peerOptional vue@"^2 || ^3.2.13" from @vue/babel-preset-app@5.0.8
npm ERR! node_modules/@vue/babel-preset-app
npm ERR! @vue/babel-preset-app@"^5.0.8" from @vue/cli-plugin-babel@5.0.8
npm ERR! node_modules/@vue/cli-plugin-babel
npm ERR! dev @vue/cli-plugin-babel@"~5.0.0" from the root project
npm ERR! peerOptional vue@"*" from @vue/babel-preset-jsx@1.4.0
npm ERR! node_modules/@vue/babel-preset-jsx
npm ERR! @vue/babel-preset-jsx@"^1.1.2" from @vue/babel-preset-app@5.0.8
npm ERR! node_modules/@vue/babel-preset-app
npm ERR! @vue/babel-preset-app@"^5.0.8" from @vue/cli-plugin-babel@5.0.8
npm ERR! node_modules/@vue/cli-plugin-babel
npm ERR! dev @vue/cli-plugin-babel@"~5.0.0" from the root project
npm ERR! 3 more (@vue/server-renderer, vue-router, the root project)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer vue@"3.1.x" from element-plus@1.0.2-beta.71
npm ERR! node_modules/element-plus
npm ERR! element-plus@"^1.0.2-beta.28" from the root project
npm ERR!
npm ERR! Conflicting peer dependency: vue@3.1.5
npm ERR! node_modules/vue
npm ERR! peer vue@"3.1.x" from element-plus@1.0.2-beta.71
npm ERR! node_modules/element-plus
npm ERR! element-plus@"^1.0.2-beta.28" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR!
npm ERR! For a full report see:
npm ERR! /.npm/_logs/2023-07-11T05_47_34_901Z-eresolve-report.txt
npm ERR! A complete log of this run can be found in:
npm ERR! /.npm/_logs/2023-07-11T05_47_34_901Z-debug-0.log
在新版本的npm中,默认情况下,npm install遇到冲突的peerDependencies时将失败。
使用 --force 或 --legacy-peer-deps 可解决这种情况。
–force 会无视冲突,并强制获取远端npm库资源,当有资源冲突时覆盖掉原先的版本。
–legacy-peer-deps:安装时忽略所有peerDependencies,忽视依赖冲突,采用npm版本4到版本6的样式去安装依赖,已有的依赖不会覆盖。
建议用–legacy-peer-deps 比较保险一点
npm install --legacy-peer-deps
===================================================================================
[eslint]
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/main.js
7:1 error 'Vue' is not defined no-undef
✖ 1 problem (1 error, 0 warnings)
Vue 2.x 有许多全局 API 和配置。Vue3.0中对这些API做出了调整:
将全局的API,即:Vue.xxx调整到应用实例(app)上
2.x 全局 API | 3.x全局 API |
---|---|
Vue.config | app.config |
Vue.config.productionTip | 移除 |
Vue.config.ignoredElements | app.config.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Vue.prototype | app.config.globalProperties |
productionTip:在开发环境下,Vue.js 会在控制台输出一些有用的提示信息,可以通过将其设置为 false 来禁用这些提示,默认为 true。在 Vue 3.x 中移除。
===================================================================================
error in ./src/main.js
Module not found: Error: Can't resolve './http' in '/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src'
ERROR in ./src/main.js 5:0-26
Module not found: Error: Can't resolve './http' in '/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src'
webpack compiled with 1 error
检查 http.js 文件路径
===================================================================================
ERROR
_ctx.$set is not a function
TypeError: _ctx.$set is not a function
at on-success (webpack-internal:///./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/views/AdEdit.vue?vue&type=template&id=099dedf2:80:43)
at Proxy.handleSuccess (webpack-internal:///./node_modules/element-plus/es/el-upload/index.js:605:13)
at Object.onSuccess (webpack-internal:///./node_modules/element-plus/es/el-upload/index.js:481:17)
at XMLHttpRequest.onload (webpack-internal:///./node_modules/element-plus/es/el-upload/index.js:92:12)
:on-success="res => $set(item, ‘image’, res.url)
==>
:on-success="res => item.image=res.url
===================================================================================
Module not found: Error: Can't resolve 'sass-loader' in '/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/web'
@ ./src/main.js 11:0-41 12:24-28
确认项目中是否已安装 sass-loader 包。可以在项目根目录下运行以下命令进行确认:
npm ls sass-loader
npm ls sass-loader
web@0.1.0 /Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/web
└── (empty)
确认项目中是否已安装 node-sass 包。sass-loader 是依赖于 node-sass 包的,如果没有安装 node-sass 包,也会导致无法找到 sass-loader 包。可以在项目根目录下运行以下命令进行确认:
npm ls node-sass
npm ls node-sass
web@0.1.0 /Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/web
└── (empty)
安装 sass-loader
npm install --save-dev sass-loader
安装 node-sass
npm install --save-dev node-sass
npm install --save-dev node-sass sass-loader
npm WARN deprecated @npmcli/move-file@2.0.1: This functionality has been moved to @npmcli/fs
npm WARN deprecated @npmcli/move-file@1.1.2: This functionality has been moved to @npmcli/fs
added 133 packages, and audited 1094 packages in 29m
118 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
===================================================================================
'tag' property on 'router-link' component is deprecated. Use scoped slots instead
vue-routerv3.1.x以上版本,新增“v-slot”,推荐使用‘custom v-slot’代替‘tag=“li”’
Vue Router3.1.0以下 以前
<router-link tag='li' to='/about' class="customer">联系客服</router-link>
Vue Router3.1.0以上 现在
<router-link to="/about" custom v-slot="{ navigate }">
<li @click="navigate" @keypress.enter="navigate" role="link">联系客服</li>
</router-link>
===================================================================================
error Filters are deprecated vue/no-deprecated-filter
从vue3.0开始,过滤器就被移除了
//vue2.x中的过滤器:
<span>{{ curTime | Time }}</span>
filters: {
Time(time) {
//如果time存在
if(time) {
var m = Math.floor(time / 60)
var s = Math.round(time % 60)
return `${m < 10 ? "0" + m : m}:${s < 10 ? "0" + s : s}`
}
}
}
//vue.3.x把它写到计算属性里:
<span>{{ TimeCurTime }}</span>
computed: {
TimeCurTime() {
if(this.curTime) {
var m = Math.floor(this.curTime / 60)
var s = Math.round(this.curTime % 60)
return `${m < 10 ? "0" + m : m}:${s < 10 ? "0" + s : s}`
}
}
}
用于自定义组件时, v-model prop和事件默认名称已更改:
- prop : value -> modelValue
- 事件: input -> update: modelValue;
v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替
新增:
现在可以在同一个组件上使用多个 v-model 绑定;
现在可以自定义 v-mdoel 修饰符;