项目学习记录 (一)

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 adminVue 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 3m94 packages are looking for fundingrun `npm fund` for detailsfound 0 vulnerabilities
🚀  Invoking generators...
📦  Installing additional dependencies...added 92 packages, and audited 951 packages in 13s107 packages are looking for fundingrun `npm fund` for detailsfound 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 webVue 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 2m94 packages are looking for fundingrun `npm fund` for detailsfound 0 vulnerabilities
🚀  Invoking generators...
📦  Installing additional dependencies...added 92 packages, and audited 951 packages in 15s107 packages are looking for fundingrun `npm fund` for detailsfound 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 corsadded 79 packages, and audited 80 packages in 26s3 packages are looking for fundingrun `npm fund` for details3 high severity vulnerabilitiesTo address issues that do not require attention, run:npm audit fixTo address all issues (including breaking changes), run:npm audit fix --forceRun `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 serveINFO  Starting development server...DONE  Compiled successfully in 29989ms                                                                                                                                                                                   10:03:20App 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 11s107 packages are looking for fundingrun `npm fund` for detailsfound 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 24s108 packages are looking for fundingrun `npm fund` for detailsfound 0 vulnerabilities
⚓  Running completion hooks...✔  Successfully invoked generator for plugin: vue-cli-plugin-element-plus

在浏览器打开 http://localhost:8080/ 发现页面发生了变化
在这里插入图片描述

admin 添加 router

admin % vue add routerWARN  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 4s108 packages are looking for fundingrun `npm fund` for detailsfound 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 4s109 packages are looking for fundingrun `npm fund` for detailsfound 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/ 发现页面发生了变化
![在这里插入图片描述](https://img-blog.csdnimg.cn/910b2d4e21be4c2bb8e950cf95306a4d.png

在这里插入图片描述

新建文件

在 admin/src/views 新建文件 AdEdit.vue, AdList.vue, AdminUserEdit.vue, AdminUserList.vue, ArticleEdit.vue, ArticleList.vue, CategoryEdit.vue, CategoryList.vue, HeroEdit.vue, HeroList.vue, ItemEdit.vue, ItemList.vue, Login.vue

<template><h1>This is an Category Edit page</h1>
</template>

修改 admin/src/router/index.js

import { createRouter, createWebHashHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
import MainView from '../views/MainVue.vue'import LoginView from '../views/LoginView.vue'import CategoryEdit from '../views/CategoryEdit.vue'
import CategoryList from '../views/CategoryList.vue'import ItemEdit from '../views/ItemEdit.vue'
import ItemList from '../views/ItemList.vue'import HeroEdit from '../views/HeroEdit.vue'
import HeroList from '../views/HeroList.vue'import ArticleEdit from '../views/ArticleEdit.vue'
import ArticleList from '../views/ArticleList.vue'import AdEdit from '../views/AdEdit.vue'
import AdList from '../views/AdList.vue'import AdminUserEdit from '../views/AdminUserEdit.vue'
import AdminUserList from '../views/AdminUserList.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: '/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-depsadded 6 packages, and audited 968 packages in 9s109 packages are looking for fundingrun `npm fund` for detailsfound 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 sentif (localStorage.token) {config.headers.Authorization = 'Bearer ' + localStorage.token}return config;
}, function (error) {// Do something with request errorreturn 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 = httpconst 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 23s4 packages are looking for fundingrun `npm fund` for details3 high severity vulnerabilitiesTo address issues that do not require attention, run:npm audit fixTo address all issues (including breaking changes), run:npm audit fix --forceRun `npm audit` for details.
npm i bcrypt                                         added 53 packages, and audited 165 packages in 32s7 packages are looking for fundingrun `npm fund` for details3 high severity vulnerabilitiesTo address issues that do not require attention, run:npm audit fixTo address all issues (including breaking changes), run:npm audit fix --forceRun `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.返回tokenconst 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.namereturn 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.返回tokenconst 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-depsadded 16 packages, and audited 984 packages in 17s117 packages are looking for fundingrun `npm fund` for details2 moderate severity vulnerabilitiesSome 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-buttontype="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 resif (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-buttontype="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-optionv-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-buttontype="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 resif (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-buttontype="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-uploadclass="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-uploadclass="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-optionv-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-uploadclass="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-buttontype="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-uploadclass="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.tokenlocalStorage.token = res.data.tokenthis.$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 7s107 packages are looking for fundingrun `npm fund` for detailsfound 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 3s108 packages are looking for fundingrun `npm fund` for detailsfound 0 vulnerabilities
⚓  Running completion hooks...✔  Successfully invoked generator for plugin: @vue/cli-plugin-router
npm i axios dayjs swiperadded 8 packages, and audited 961 packages in 13s109 packages are looking for fundingrun `npm fund` for detailsfound 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/fsadded 133 packages, and audited 1094 packages in 29m118 packages are looking for fundingrun `npm fund` for detailsfound 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">更多英雄 &gt;</router-link> --><router-link to="/" custom v-slot="{ navigate }"><div @click="navigate" @keypress.enter="navigate" role="link">更多英雄 &gt;</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 &gt;</router-link> --><router-link to="/" class="text-grey fs-sm" custom v-slot="{ navigate }"><span @click="navigate" @keypress.enter="navigate" role="link">皮肤: 2 &gt;</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%;" customv-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.vue59:39  error  Named slots must use '